Skip to content

Commit 71f376c

Browse files
committed
api: add json logging and request metadata
1 parent 0bd612d commit 71f376c

2 files changed

Lines changed: 67 additions & 18 deletions

File tree

app/main.py

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import os
22
import time
33
import logging
4-
from fastapi import FastAPI, HTTPException
4+
import json
5+
import uuid
6+
from fastapi import FastAPI, HTTPException, Request
57
from pydantic import BaseModel
68
from src.config import settings
79

8-
# Setup logging
9-
logging.basicConfig(level=logging.INFO)
10-
logger = logging.getLogger(__name__)
10+
# Structured JSON Logging Setup
11+
class JsonFormatter(logging.Formatter):
12+
def format(self, record):
13+
log_record = {
14+
"timestamp": self.formatTime(record, self.datefmt),
15+
"level": record.levelname,
16+
"message": record.getMessage(),
17+
"module": record.module,
18+
"request_id": getattr(record, "request_id", "N/A")
19+
}
20+
return json.dumps(log_record)
21+
22+
logger = logging.getLogger("api_logger")
23+
handler = logging.StreamHandler()
24+
handler.setFormatter(JsonFormatter())
25+
logger.addHandler(handler)
26+
logger.setLevel(logging.INFO)
1127

1228
app = FastAPI(title="Mistral Domain Support Assistant API")
1329

@@ -22,9 +38,19 @@ class GenerateRequest(BaseModel):
2238

2339
class GenerateResponse(BaseModel):
2440
response: str
25-
latency: float
41+
latency_seconds: float
2642
tokens_generated: int
2743
model_name: str
44+
request_id: str
45+
metadata: dict
46+
47+
@app.middleware("http")
48+
async def add_request_id(request: Request, call_next):
49+
request_id = str(uuid.uuid4())
50+
request.state.request_id = request_id
51+
response = await call_next(request)
52+
response.headers["X-Request-ID"] = request_id
53+
return response
2854

2955
@app.on_event("startup")
3056
async def startup_event():
@@ -33,35 +59,61 @@ async def startup_event():
3359
logger.info("TESTING environment detected. Skipping real model loading.")
3460
return
3561

36-
from src.inference.engine import InferenceEngine
37-
adapter_path = os.getenv("ADAPTER_PATH", None)
38-
logger.info(f"Initializing Inference Engine with adapter: {adapter_path}")
39-
engine = InferenceEngine(adapter_path=adapter_path)
62+
try:
63+
from src.inference.engine import InferenceEngine
64+
adapter_path = os.getenv("ADAPTER_PATH", None)
65+
logger.info(f"Initializing Inference Engine with adapter: {adapter_path}")
66+
engine = InferenceEngine(adapter_path=adapter_path)
67+
except Exception as e:
68+
logger.error(f"Failed to load model: {str(e)}")
4069

4170
@app.post("/generate", response_model=GenerateResponse)
42-
async def generate(request: GenerateRequest):
71+
async def generate(request: GenerateRequest, fast_request: Request):
72+
request_id = fast_request.state.request_id
73+
4374
if engine is None:
75+
logger.warning("Generation attempted but model not loaded", extra={"request_id": request_id})
4476
raise HTTPException(status_code=503, detail="Model not loaded")
4577

78+
logger.info(f"Processing generation request: {request.instruction[:50]}...", extra={"request_id": request_id})
79+
4680
try:
81+
start_time = time.time()
4782
result = engine.generate(
4883
instruction=request.instruction,
4984
max_new_tokens=request.max_new_tokens
5085
)
86+
total_latency = time.time() - start_time
87+
88+
logger.info("Generation successful", extra={
89+
"request_id": request_id,
90+
"latency": total_latency,
91+
"tokens": result['tokens_generated']
92+
})
5193

5294
return GenerateResponse(
5395
response=result['response'],
54-
latency=result['latency'],
96+
latency_seconds=round(total_latency, 3),
5597
tokens_generated=result['tokens_generated'],
56-
model_name=settings.BASE_MODEL_NAME
98+
model_name=settings.BASE_MODEL_NAME,
99+
request_id=request_id,
100+
metadata={
101+
"temperature": request.temperature,
102+
"top_p": request.top_p,
103+
"timestamp": time.time()
104+
}
57105
)
58106
except Exception as e:
59-
logger.error(f"Generation error: {str(e)}")
107+
logger.error(f"Generation error: {str(e)}", extra={"request_id": request_id})
60108
raise HTTPException(status_code=500, detail=str(e))
61109

62110
@app.get("/health")
63111
async def health():
64-
return {"status": "ok", "model_loaded": engine is not None}
112+
return {
113+
"status": "ok",
114+
"model_loaded": engine is not None,
115+
"environment": "testing" if os.getenv("TESTING") else "production"
116+
}
65117

66118
if __name__ == "__main__":
67119
import uvicorn

tests/test_api.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
client = TestClient(app)
55

66
def test_read_health():
7-
# Since the model won't be loaded in a standard test environment,
8-
# we expect the model_loaded status to be false or for the startup to fail.
9-
# We can mock the engine if needed, but a simple health check is a good start.
107
response = client.get("/health")
118
assert response.status_code == 200
129
assert "status" in response.json()
1310
assert response.json()["status"] == "ok"
11+
assert "environment" in response.json()
1412

1513
def test_generate_fail_no_model():
16-
# Test that /generate returns 503 if engine is not initialized
1714
response = client.post(
1815
"/generate",
1916
json={"instruction": "How do I reset my password?"}

0 commit comments

Comments
 (0)