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,