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:
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Annotated
|
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.metrics.weights import compute_weighted_score
|
||||||
from rag_eval.settings import EvaluationSettings
|
from rag_eval.settings import EvaluationSettings
|
||||||
@@ -13,6 +16,7 @@ from webapp.models import ScoreRequest, ScoreResponse
|
|||||||
from webapp.services.inline_scorer import inline_scorer
|
from webapp.services.inline_scorer import inline_scorer
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/score", tags=["score"])
|
router = APIRouter(prefix="/api/score", tags=["score"])
|
||||||
|
logger = logging.getLogger("webapp.api.score")
|
||||||
|
|
||||||
|
|
||||||
def _get_settings() -> EvaluationSettings:
|
def _get_settings() -> EvaluationSettings:
|
||||||
@@ -58,6 +62,7 @@ def _check_auth(authorization: str | None, token: str) -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def score_sample(
|
def score_sample(
|
||||||
|
raw_request: Request,
|
||||||
request: ScoreRequest,
|
request: ScoreRequest,
|
||||||
authorization: Annotated[str | None, Header()] = None,
|
authorization: Annotated[str | None, Header()] = None,
|
||||||
) -> ScoreResponse:
|
) -> ScoreResponse:
|
||||||
@@ -88,6 +93,15 @@ def score_sample(
|
|||||||
**鉴权**:若 `.env` 中配置了 `SCORE_API_TOKEN`,需在请求头携带
|
**鉴权**:若 `.env` 中配置了 `SCORE_API_TOKEN`,需在请求头携带
|
||||||
`Authorization: Bearer <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()
|
settings = _get_settings()
|
||||||
|
|
||||||
# Require Bearer auth only when the deployment configured a shared token.
|
# 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(
|
return ScoreResponse(
|
||||||
scores=all_scores,
|
scores=all_scores,
|
||||||
weighted_score=round(weighted, 4) if weighted is not None else None,
|
weighted_score=round(weighted, 4) if weighted is not None else None,
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ the server starts even when the evaluation dependencies are not yet installed.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, score
|
from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, score
|
||||||
|
|
||||||
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
||||||
|
logger = logging.getLogger("webapp.server")
|
||||||
|
|
||||||
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
|
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
|
||||||
OPENAPI_TAGS = [
|
OPENAPI_TAGS = [
|
||||||
@@ -103,6 +107,21 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(pipeline.router)
|
app.include_router(pipeline.router)
|
||||||
app.include_router(score.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"])
|
@app.get("/api/health", tags=["meta"])
|
||||||
def health() -> dict[str, str]:
|
def health() -> dict[str, str]:
|
||||||
"""Report basic liveness so the UI can confirm the server is reachable."""
|
"""Report basic liveness so the UI can confirm the server is reachable."""
|
||||||
|
|||||||
Reference in New Issue
Block a user