Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Thumbs.db

# Python virtual-envs & tooling
.venv*/
venv/
.python-version
__pycache__/
*.egg-info/
Expand Down Expand Up @@ -38,3 +39,6 @@ tmp/
# Project-specific files
history.txt
digest.txt

# Environment variables
.env
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"tiktoken>=0.7.0", # Support for o200k_base encoding
"typing_extensions>= 4.0.0; python_version < '3.10'",
"uvicorn>=0.11.7", # Minimum safe release (https://osv.dev/vulnerability/PYSEC-2020-150)
"prometheus-client",
]

license = {file = "LICENSE"}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ slowapi
starlette>=0.40.0 # Vulnerable to https://osv.dev/vulnerability/GHSA-f96h-pmfr-66vw
tiktoken>=0.7.0 # Support for o200k_base encoding
uvicorn>=0.11.7 # Vulnerable to https://osv.dev/vulnerability/PYSEC-2020-150
prometheus-client
4 changes: 3 additions & 1 deletion src/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from slowapi.errors import RateLimitExceeded
from starlette.middleware.trustedhost import TrustedHostMiddleware

from server.routers import dynamic, index, ingest
from server.routers import dynamic, index, ingest, metrics
from server.server_config import templates
from server.server_utils import lifespan, limiter, rate_limit_exception_handler

Expand Down Expand Up @@ -159,6 +159,8 @@ def openapi_json() -> JSONResponse:


# Include routers for modular endpoints
if os.getenv("GITINGEST_PROMETHEUS_TOKEN") is not None:
app.include_router(metrics)
app.include_router(index)
app.include_router(ingest)
app.include_router(dynamic)
3 changes: 2 additions & 1 deletion src/server/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from server.routers.dynamic import router as dynamic
from server.routers.index import router as index
from server.routers.ingest import router as ingest
from server.routers.metrics import router as metrics

__all__ = ["dynamic", "index", "ingest"]
__all__ = ["dynamic", "index", "ingest", "metrics"]
13 changes: 11 additions & 2 deletions src/server/routers/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import FileResponse, JSONResponse
from prometheus_client import Counter

from gitingest.config import TMP_BASE_PATH
from server.models import IngestRequest
from server.routers_utils import COMMON_INGEST_RESPONSES, _perform_ingestion
from server.server_config import MAX_DISPLAY_SIZE
from server.server_utils import limiter

ingest_counter = Counter("gitingest_ingest_total", "Number of ingests", ["status", "url"])

router = APIRouter()


Expand All @@ -33,13 +36,16 @@ async def api_ingest(
- **JSONResponse**: Success response with ingestion results or error response with appropriate HTTP status code

"""
return await _perform_ingestion(
response = await _perform_ingestion(
input_text=ingest_request.input_text,
max_file_size=ingest_request.max_file_size,
pattern_type=ingest_request.pattern_type,
pattern=ingest_request.pattern,
token=ingest_request.token,
)
# limit URL to 255 characters
ingest_counter.labels(status=response.status_code, url=ingest_request.input_text[:255]).inc()
return response


@router.get("/api/{user}/{repository}", responses=COMMON_INGEST_RESPONSES)
Expand Down Expand Up @@ -72,13 +78,16 @@ async def api_ingest_get(
**Returns**
- **JSONResponse**: Success response with ingestion results or error response with appropriate HTTP status code
"""
return await _perform_ingestion(
response = await _perform_ingestion(
input_text=f"{user}/{repository}",
max_file_size=max_file_size,
pattern_type=pattern_type,
pattern=pattern,
token=token or None,
)
# limit URL to 255 characters
ingest_counter.labels(status=response.status_code, url=f"{user}/{repository}"[:255]).inc()
return response


@router.get("/api/download/file/{ingest_id}", response_class=FileResponse)
Expand Down
51 changes: 51 additions & 0 deletions src/server/routers/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Prometheus metrics endpoint for the API."""

import os
from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import HTMLResponse
from prometheus_client import REGISTRY, generate_latest

router = APIRouter()

@router.get("/metrics")
async def metrics(request: Request) -> HTMLResponse:
"""Serve Prometheus metrics with token authentication.
This endpoint requires the GITINGEST_PROMETHEUS_TOKEN to be provided
in the Authorization header as 'Bearer <token>'.
Parameters
----------
request : Request
The incoming HTTP request containing headers
Returns
-------
HTMLResponse
Prometheus metrics in text format
Raises
------
HTTPException
401 if no token provided or invalid token
"""
# Get the expected token from environment
expected_token = os.getenv("GITINGEST_PROMETHEUS_TOKEN")
if not expected_token:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# Check Authorization header
auth_header = request.headers.get("Authorization")
if not auth_header:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
)

# Extract and verify token
provided_token = auth_header[7:] # Remove "Bearer " prefix
if provided_token != expected_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
)
return HTMLResponse(content=generate_latest(REGISTRY), status_code=200, media_type="text/plain")
Loading