feat: add detailed request logging to /api/score and global 422 handler

- Log incoming request (client, content-type, metrics, has_gt) on each /api/score call
- Log scoring result (latency, skipped metrics, scores) on success
- Register global RequestValidationError handler: logs url/content-type/errors
  so 422 causes are visible in server log without checking HTTP response body
- Fix jsonable_encoder for exc.errors() to handle non-serializable ctx objects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-22 18:14:01 +08:00
parent ebf1fc7be8
commit 5ced129ff7
2 changed files with 42 additions and 3 deletions

View File

@@ -2,10 +2,13 @@
from __future__ import annotations
import logging
import time
from typing import Annotated
from fastapi import APIRouter, Header, HTTPException
from fastapi import APIRouter, Header, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from rag_eval.metrics.weights import compute_weighted_score
from rag_eval.settings import EvaluationSettings
@@ -13,6 +16,7 @@ from webapp.models import ScoreRequest, ScoreResponse
from webapp.services.inline_scorer import inline_scorer
router = APIRouter(prefix="/api/score", tags=["score"])
logger = logging.getLogger("webapp.api.score")
def _get_settings() -> EvaluationSettings:
@@ -58,6 +62,7 @@ def _check_auth(authorization: str | None, token: str) -> None:
},
)
def score_sample(
raw_request: Request,
request: ScoreRequest,
authorization: Annotated[str | None, Header()] = None,
) -> ScoreResponse:
@@ -88,6 +93,15 @@ def score_sample(
**鉴权**:若 `.env` 中配置了 `SCORE_API_TOKEN`,需在请求头携带
`Authorization: Bearer <token>`;留空则无需鉴权(适合内网部署)。
"""
client = f"{raw_request.client.host}:{raw_request.client.port}" if raw_request.client else "unknown"
logger.info(
"[score] incoming client=%s method=%s content_type=%s metrics=%s has_gt=%s",
client,
raw_request.method,
raw_request.headers.get("content-type", ""),
request.metrics,
request.ground_truth is not None,
)
settings = _get_settings()
# Require Bearer auth only when the deployment configured a shared token.
@@ -141,6 +155,12 @@ def score_sample(
{},
)
logger.info(
"[score] done latency=%dms skipped=%s scores=%s",
latency_ms,
skipped,
{k: (round(v, 4) if v is not None else None) for k, v in all_scores.items()},
)
return ScoreResponse(
scores=all_scores,
weighted_score=round(weighted, 4) if weighted is not None else None,

View File

@@ -7,15 +7,19 @@ the server starts even when the evaluation dependencies are not yet installed.
from __future__ import annotations
import logging
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, score
STATIC_DIR = Path(__file__).resolve().parent / "static"
logger = logging.getLogger("webapp.server")
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
OPENAPI_TAGS = [
@@ -103,6 +107,21 @@ def create_app() -> FastAPI:
app.include_router(pipeline.router)
app.include_router(score.router)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
"""Log full validation error detail to help diagnose 422 responses."""
errors = jsonable_encoder(exc.errors())
logger.warning(
"[422] validation error url=%s content_type=%s errors=%s",
request.url.path,
request.headers.get("content-type", ""),
errors,
)
return JSONResponse(
status_code=422,
content={"detail": errors},
)
@app.get("/api/health", tags=["meta"])
def health() -> dict[str, str]:
"""Report basic liveness so the UI can confirm the server is reachable."""