Skip to content

Commit 98e1d83

Browse files
committed
feat: implement prometheus exporter
1 parent 247aa3c commit 98e1d83

File tree

7 files changed

+70
-4
lines changed

7 files changed

+70
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Thumbs.db
1010

1111
# Python virtual-envs & tooling
1212
.venv*/
13+
venv/
1314
.python-version
1415
__pycache__/
1516
*.egg-info/

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616
"tiktoken>=0.7.0", # Support for o200k_base encoding
1717
"typing_extensions>= 4.0.0; python_version < '3.10'",
1818
"uvicorn>=0.11.7", # Minimum safe release (https://osv.dev/vulnerability/PYSEC-2020-150)
19+
"prometheus-client",
1920
]
2021

2122
license = {file = "LICENSE"}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ slowapi
88
starlette>=0.40.0 # Vulnerable to https://osv.dev/vulnerability/GHSA-f96h-pmfr-66vw
99
tiktoken>=0.7.0 # Support for o200k_base encoding
1010
uvicorn>=0.11.7 # Vulnerable to https://osv.dev/vulnerability/PYSEC-2020-150
11+
prometheus-client

src/server/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from slowapi.errors import RateLimitExceeded
1313
from starlette.middleware.trustedhost import TrustedHostMiddleware
1414

15-
from server.routers import dynamic, index, ingest
15+
from server.routers import dynamic, index, ingest, metrics
1616
from server.server_config import templates
1717
from server.server_utils import lifespan, limiter, rate_limit_exception_handler
1818

@@ -159,6 +159,8 @@ def openapi_json() -> JSONResponse:
159159

160160

161161
# Include routers for modular endpoints
162+
if os.getenv("GITINGEST_PROMETHEUS_TOKEN") is not None:
163+
app.include_router(metrics)
162164
app.include_router(index)
163165
app.include_router(ingest)
164166
app.include_router(dynamic)

src/server/routers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from server.routers.dynamic import router as dynamic
44
from server.routers.index import router as index
55
from server.routers.ingest import router as ingest
6+
from server.routers.metrics import router as metrics
67

7-
__all__ = ["dynamic", "index", "ingest"]
8+
__all__ = ["dynamic", "index", "ingest", "metrics"]

src/server/routers/ingest.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
from fastapi import APIRouter, HTTPException, Request, status
44
from fastapi.responses import FileResponse, JSONResponse
5+
from prometheus_client import Counter
56

67
from gitingest.config import TMP_BASE_PATH
78
from server.models import IngestRequest
89
from server.routers_utils import COMMON_INGEST_RESPONSES, _perform_ingestion
910
from server.server_config import MAX_DISPLAY_SIZE
1011
from server.server_utils import limiter
1112

13+
ingest_counter = Counter("gitingest_ingest_total", "Number of ingests", ["status", "url"])
14+
1215
router = APIRouter()
1316

1417

@@ -33,13 +36,16 @@ async def api_ingest(
3336
- **JSONResponse**: Success response with ingestion results or error response with appropriate HTTP status code
3437
3538
"""
36-
return await _perform_ingestion(
39+
response = await _perform_ingestion(
3740
input_text=ingest_request.input_text,
3841
max_file_size=ingest_request.max_file_size,
3942
pattern_type=ingest_request.pattern_type,
4043
pattern=ingest_request.pattern,
4144
token=ingest_request.token,
4245
)
46+
# limit URL to 255 characters
47+
ingest_counter.labels(status=response.status_code, url=ingest_request.input_text[:255]).inc()
48+
return response
4349

4450

4551
@router.get("/api/{user}/{repository}", responses=COMMON_INGEST_RESPONSES)
@@ -72,13 +78,16 @@ async def api_ingest_get(
7278
**Returns**
7379
- **JSONResponse**: Success response with ingestion results or error response with appropriate HTTP status code
7480
"""
75-
return await _perform_ingestion(
81+
response = await _perform_ingestion(
7682
input_text=f"{user}/{repository}",
7783
max_file_size=max_file_size,
7884
pattern_type=pattern_type,
7985
pattern=pattern,
8086
token=token or None,
8187
)
88+
# limit URL to 255 characters
89+
ingest_counter.labels(status=response.status_code, url=f"{user}/{repository}"[:255]).inc()
90+
return response
8291

8392

8493
@router.get("/api/download/file/{ingest_id}", response_class=FileResponse)

src/server/routers/metrics.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Prometheus metrics endpoint for the API."""
2+
3+
import os
4+
from fastapi import APIRouter, HTTPException, Request, status
5+
from fastapi.responses import HTMLResponse
6+
from prometheus_client import REGISTRY, generate_latest
7+
8+
router = APIRouter()
9+
10+
@router.get("/metrics")
11+
async def metrics(request: Request) -> HTMLResponse:
12+
"""Serve Prometheus metrics with token authentication.
13+
14+
This endpoint requires the GITINGEST_PROMETHEUS_TOKEN to be provided
15+
in the Authorization header as 'Bearer <token>'.
16+
17+
Parameters
18+
----------
19+
request : Request
20+
The incoming HTTP request containing headers
21+
22+
Returns
23+
-------
24+
HTMLResponse
25+
Prometheus metrics in text format
26+
27+
Raises
28+
------
29+
HTTPException
30+
401 if no token provided or invalid token
31+
"""
32+
# Get the expected token from environment
33+
expected_token = os.getenv("GITINGEST_PROMETHEUS_TOKEN")
34+
if not expected_token:
35+
raise HTTPException(
36+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
37+
)
38+
# Check Authorization header
39+
auth_header = request.headers.get("Authorization")
40+
if not auth_header:
41+
raise HTTPException(
42+
status_code=status.HTTP_401_UNAUTHORIZED,
43+
)
44+
45+
# Extract and verify token
46+
provided_token = auth_header[7:] # Remove "Bearer " prefix
47+
if provided_token != expected_token:
48+
raise HTTPException(
49+
status_code=status.HTTP_401_UNAUTHORIZED,
50+
)
51+
return HTMLResponse(content=generate_latest(REGISTRY), status_code=200, media_type="text/plain")

0 commit comments

Comments
 (0)