Skip to content

Commit a5e8386

Browse files
Merge branch 'main' into resolve-commit
2 parents d67b363 + 1016f6e commit a5e8386

File tree

9 files changed

+96
-3
lines changed

9 files changed

+96
-3
lines changed

.gitignore

Lines changed: 4 additions & 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/
@@ -38,3 +39,6 @@ tmp/
3839
# Project-specific files
3940
history.txt
4041
digest.txt
42+
43+
# Environment variables
44+
.env

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ repos:
117117
'fastapi[standard]>=0.109.1',
118118
httpx,
119119
pathspec>=0.12.1,
120+
prometheus-client,
120121
pydantic,
121122
pytest-asyncio,
122123
pytest-mock,
@@ -138,6 +139,7 @@ repos:
138139
'fastapi[standard]>=0.109.1',
139140
httpx,
140141
pathspec>=0.12.1,
142+
prometheus-client,
141143
pydantic,
142144
pytest-asyncio,
143145
pytest-mock,

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ RUN set -eux; \
4343
USER appuser
4444

4545
EXPOSE 8000
46+
EXPOSE 9090
4647
CMD ["python", "-m", "uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8000"]

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
@@ -2,6 +2,7 @@ click>=8.0.0
22
fastapi[standard]>=0.109.1 # Vulnerable to https://osv.dev/vulnerability/PYSEC-2024-38
33
httpx
44
pathspec>=0.12.1
5+
prometheus-client
56
pydantic
67
python-dotenv
78
slowapi

src/server/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import os
6+
import threading
67
from pathlib import Path
78

89
from dotenv import load_dotenv
@@ -12,6 +13,7 @@
1213
from slowapi.errors import RateLimitExceeded
1314
from starlette.middleware.trustedhost import TrustedHostMiddleware
1415

16+
from server.metrics_server import start_metrics_server
1517
from server.routers import dynamic, index, ingest
1618
from server.server_config import templates
1719
from server.server_utils import lifespan, limiter, rate_limit_exception_handler
@@ -26,6 +28,17 @@
2628
# Register the custom exception handler for rate limits
2729
app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler)
2830

31+
# Start metrics server in a separate thread if enabled
32+
if os.getenv("GITINGEST_METRICS_ENABLED", "false").lower() == "true":
33+
metrics_host = os.getenv("GITINGEST_METRICS_HOST", "127.0.0.1")
34+
metrics_port = int(os.getenv("GITINGEST_METRICS_PORT", "9090"))
35+
metrics_thread = threading.Thread(
36+
target=start_metrics_server,
37+
args=(metrics_host, metrics_port),
38+
daemon=True,
39+
)
40+
metrics_thread.start()
41+
2942

3043
# Mount static files dynamically to serve CSS, JS, and other static assets
3144
static_dir = Path(__file__).parent.parent / "static"

src/server/metrics_server.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Prometheus metrics server running on a separate port."""
2+
3+
import logging
4+
5+
import uvicorn
6+
from fastapi import FastAPI
7+
from fastapi.responses import HTMLResponse
8+
from prometheus_client import REGISTRY, generate_latest
9+
10+
# Create a logger for this module
11+
logger = logging.getLogger(__name__)
12+
13+
# Create a separate FastAPI app for metrics
14+
metrics_app = FastAPI(
15+
title="Gitingest Metrics",
16+
description="Prometheus metrics for Gitingest",
17+
docs_url=None,
18+
redoc_url=None,
19+
)
20+
21+
22+
@metrics_app.get("/metrics")
23+
async def metrics() -> HTMLResponse:
24+
"""Serve Prometheus metrics without authentication.
25+
26+
This endpoint is only accessible from the local network.
27+
28+
Returns
29+
-------
30+
HTMLResponse
31+
Prometheus metrics in text format
32+
33+
"""
34+
return HTMLResponse(
35+
content=generate_latest(REGISTRY),
36+
status_code=200,
37+
media_type="text/plain",
38+
)
39+
40+
41+
def start_metrics_server(host: str = "127.0.0.1", port: int = 9090) -> None:
42+
"""Start the metrics server on a separate port.
43+
44+
Parameters
45+
----------
46+
host : str
47+
The host to bind to (default: 127.0.0.1 for local network only)
48+
port : int
49+
The port to bind to (default: 9090)
50+
51+
Returns
52+
-------
53+
None
54+
55+
"""
56+
logger.info("Starting metrics server on %s:%s", host, port)
57+
uvicorn.run(metrics_app, host=host, port=port)

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/server_config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from pathlib import Path
6+
57
from fastapi.templating import Jinja2Templates
68

79
MAX_DISPLAY_SIZE: int = 300_000
@@ -19,4 +21,7 @@
1921
{"name": "ApiAnalytics", "url": "https://github.com/tom-draper/api-analytics"},
2022
]
2123

22-
templates = Jinja2Templates(directory="server/templates")
24+
25+
# Use absolute path to templates directory
26+
templates_dir = Path(__file__).parent / "templates"
27+
templates = Jinja2Templates(directory=templates_dir)

0 commit comments

Comments
 (0)