feat: add detailed logging to all API routes and global access log middleware
Each API module now logs: - evaluations: trigger (scenario path, task_id), status polls, list - runs: list (count), detail (run_id, metrics, sample counts) - scenarios: list (total, valid, error counts) - pipeline: submit (docs_path, models, max_docs), status polls, list - llm_profiles: CRUD ops (name, model, id), probe/test (model, ok, latency), apply (patched fields) - score: already had per-request logging Global middleware (webapp.access logger): - Every API request: METHOD path -> status (latency_ms) at INFO - Static file requests demoted to DEBUG to reduce noise Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from webapp.models import (
|
from webapp.models import (
|
||||||
@@ -13,19 +15,23 @@ from webapp.services import scenario_scanner
|
|||||||
from webapp.services.task_manager import task_manager
|
from webapp.services.task_manager import task_manager
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/evaluations", tags=["evaluations"])
|
router = APIRouter(prefix="/api/evaluations", tags=["evaluations"])
|
||||||
|
logger = logging.getLogger("webapp.api.evaluations")
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=TriggerEvaluationResponse)
|
@router.post("", response_model=TriggerEvaluationResponse)
|
||||||
def trigger_evaluation(request: TriggerEvaluationRequest) -> TriggerEvaluationResponse:
|
def trigger_evaluation(request: TriggerEvaluationRequest) -> TriggerEvaluationResponse:
|
||||||
"""Validate the scenario path and queue a background evaluation task."""
|
"""Validate the scenario path and queue a background evaluation task."""
|
||||||
|
logger.info("[trigger] scenario=%s", request.scenario_path)
|
||||||
resolved = scenario_scanner.resolve_scenario_path(request.scenario_path)
|
resolved = scenario_scanner.resolve_scenario_path(request.scenario_path)
|
||||||
if resolved is None:
|
if resolved is None:
|
||||||
|
logger.warning("[trigger] invalid scenario path: %s", request.scenario_path)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"无效或不允许的场景路径: {request.scenario_path}",
|
detail=f"无效或不允许的场景路径: {request.scenario_path}",
|
||||||
)
|
)
|
||||||
|
|
||||||
task_id = task_manager.submit(request.scenario_path)
|
task_id = task_manager.submit(request.scenario_path)
|
||||||
|
logger.info("[trigger] queued task_id=%s scenario=%s", task_id, request.scenario_path)
|
||||||
return TriggerEvaluationResponse(task_id=task_id)
|
return TriggerEvaluationResponse(task_id=task_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,11 +40,15 @@ def get_task_status(task_id: str) -> TaskStatus:
|
|||||||
"""Return the current status and logs for one evaluation task."""
|
"""Return the current status and logs for one evaluation task."""
|
||||||
status = task_manager.get(task_id)
|
status = task_manager.get(task_id)
|
||||||
if status is None:
|
if status is None:
|
||||||
|
logger.warning("[task_status] not found task_id=%s", task_id)
|
||||||
raise HTTPException(status_code=404, detail=f"未找到任务: {task_id}")
|
raise HTTPException(status_code=404, detail=f"未找到任务: {task_id}")
|
||||||
|
logger.debug("[task_status] task_id=%s status=%s", task_id, status.status)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=dict)
|
@router.get("", response_model=dict)
|
||||||
def list_tasks() -> dict[str, list]:
|
def list_tasks() -> dict[str, list]:
|
||||||
"""Return all known evaluation tasks for this server session."""
|
"""Return all known evaluation tasks for this server session."""
|
||||||
return {"tasks": [task.model_dump() for task in task_manager.list_tasks()]}
|
tasks = task_manager.list_tasks()
|
||||||
|
logger.info("[list_tasks] count=%d", len(tasks))
|
||||||
|
return {"tasks": [task.model_dump() for task in tasks]}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
@@ -19,6 +20,7 @@ from webapp.services.profile_manager import profile_manager
|
|||||||
from webapp.services.yaml_patcher import apply_profiles_to_scenario
|
from webapp.services.yaml_patcher import apply_profiles_to_scenario
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/llm-profiles", tags=["llm-profiles"])
|
router = APIRouter(prefix="/api/llm-profiles", tags=["llm-profiles"])
|
||||||
|
logger = logging.getLogger("webapp.api.llm_profiles")
|
||||||
|
|
||||||
|
|
||||||
def _do_connectivity_test(
|
def _do_connectivity_test(
|
||||||
@@ -50,35 +52,44 @@ def _do_connectivity_test(
|
|||||||
@router.post("/probe", response_model=ProfileTestResponse, tags=["llm-profiles"])
|
@router.post("/probe", response_model=ProfileTestResponse, tags=["llm-profiles"])
|
||||||
def probe_connectivity(request: ProfileProbeRequest) -> ProfileTestResponse:
|
def probe_connectivity(request: ProfileProbeRequest) -> ProfileTestResponse:
|
||||||
"""Test LLM connectivity with inline credentials (no saved profile required)."""
|
"""Test LLM connectivity with inline credentials (no saved profile required)."""
|
||||||
return _do_connectivity_test(
|
logger.info("[probe] model=%s base_url=%s", request.model, request.base_url)
|
||||||
|
result = _do_connectivity_test(
|
||||||
model=request.model,
|
model=request.model,
|
||||||
base_url=request.base_url,
|
base_url=request.base_url,
|
||||||
api_key=request.api_key,
|
api_key=request.api_key,
|
||||||
timeout_seconds=request.timeout_seconds,
|
timeout_seconds=request.timeout_seconds,
|
||||||
)
|
)
|
||||||
|
logger.info("[probe] ok=%s latency=%sms msg=%s", result.ok, result.latency_ms, result.message)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=dict)
|
@router.get("", response_model=dict)
|
||||||
def list_profiles() -> dict:
|
def list_profiles() -> dict:
|
||||||
"""Return all saved LLM profiles."""
|
"""Return all saved LLM profiles."""
|
||||||
return {"profiles": [p.model_dump() for p in profile_manager.list_all()]}
|
profiles = profile_manager.list_all()
|
||||||
|
logger.info("[list_profiles] count=%d", len(profiles))
|
||||||
|
return {"profiles": [p.model_dump() for p in profiles]}
|
||||||
|
|
||||||
|
|
||||||
@router.post("", status_code=201, response_model=LLMProfile)
|
@router.post("", status_code=201, response_model=LLMProfile)
|
||||||
def create_profile(request: CreateProfileRequest) -> LLMProfile:
|
def create_profile(request: CreateProfileRequest) -> LLMProfile:
|
||||||
"""Create a new LLM profile."""
|
"""Create a new LLM profile."""
|
||||||
return profile_manager.create(
|
logger.info("[create_profile] name=%r model=%s base_url=%s", request.name, request.model, request.base_url)
|
||||||
|
profile = profile_manager.create(
|
||||||
name=request.name,
|
name=request.name,
|
||||||
model=request.model,
|
model=request.model,
|
||||||
base_url=request.base_url,
|
base_url=request.base_url,
|
||||||
api_key=request.api_key,
|
api_key=request.api_key,
|
||||||
timeout_seconds=request.timeout_seconds,
|
timeout_seconds=request.timeout_seconds,
|
||||||
)
|
)
|
||||||
|
logger.info("[create_profile] created id=%s", profile.profile_id)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{profile_id}", response_model=LLMProfile)
|
@router.put("/{profile_id}", response_model=LLMProfile)
|
||||||
def update_profile(profile_id: str, request: CreateProfileRequest) -> LLMProfile:
|
def update_profile(profile_id: str, request: CreateProfileRequest) -> LLMProfile:
|
||||||
"""Update an existing LLM profile by id."""
|
"""Update an existing LLM profile by id."""
|
||||||
|
logger.info("[update_profile] id=%s name=%r model=%s", profile_id, request.name, request.model)
|
||||||
updated = profile_manager.update(
|
updated = profile_manager.update(
|
||||||
profile_id=profile_id,
|
profile_id=profile_id,
|
||||||
name=request.name,
|
name=request.name,
|
||||||
@@ -88,16 +99,21 @@ def update_profile(profile_id: str, request: CreateProfileRequest) -> LLMProfile
|
|||||||
timeout_seconds=request.timeout_seconds,
|
timeout_seconds=request.timeout_seconds,
|
||||||
)
|
)
|
||||||
if updated is None:
|
if updated is None:
|
||||||
|
logger.warning("[update_profile] not found id=%s", profile_id)
|
||||||
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
||||||
|
logger.info("[update_profile] updated id=%s", profile_id)
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{profile_id}", response_model=dict)
|
@router.delete("/{profile_id}", response_model=dict)
|
||||||
def delete_profile(profile_id: str) -> dict:
|
def delete_profile(profile_id: str) -> dict:
|
||||||
"""Delete an LLM profile by id."""
|
"""Delete an LLM profile by id."""
|
||||||
|
logger.info("[delete_profile] id=%s", profile_id)
|
||||||
deleted = profile_manager.delete(profile_id)
|
deleted = profile_manager.delete(profile_id)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
|
logger.warning("[delete_profile] not found id=%s", profile_id)
|
||||||
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
||||||
|
logger.info("[delete_profile] deleted id=%s", profile_id)
|
||||||
return {"deleted": True}
|
return {"deleted": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -106,18 +122,31 @@ def test_profile(profile_id: str) -> ProfileTestResponse:
|
|||||||
"""Test LLM connectivity for a saved profile."""
|
"""Test LLM connectivity for a saved profile."""
|
||||||
profile = profile_manager.get(profile_id)
|
profile = profile_manager.get(profile_id)
|
||||||
if profile is None:
|
if profile is None:
|
||||||
|
logger.warning("[test_profile] not found id=%s", profile_id)
|
||||||
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
raise HTTPException(status_code=404, detail=f"Profile not found: {profile_id}")
|
||||||
return _do_connectivity_test(
|
logger.info("[test_profile] id=%s model=%s base_url=%s", profile_id, profile.model, profile.base_url)
|
||||||
|
result = _do_connectivity_test(
|
||||||
model=profile.model,
|
model=profile.model,
|
||||||
base_url=profile.base_url,
|
base_url=profile.base_url,
|
||||||
api_key=profile.api_key,
|
api_key=profile.api_key,
|
||||||
timeout_seconds=profile.timeout_seconds,
|
timeout_seconds=profile.timeout_seconds,
|
||||||
)
|
)
|
||||||
|
logger.info("[test_profile] ok=%s latency=%sms", result.ok, result.latency_ms)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/apply", response_model=ProfileApplyResponse)
|
@router.post("/apply", response_model=ProfileApplyResponse)
|
||||||
def apply_profiles(request: ProfileApplyRequest) -> ProfileApplyResponse:
|
def apply_profiles(request: ProfileApplyRequest) -> ProfileApplyResponse:
|
||||||
"""Patch selected LLM profiles into the target scenario YAML file."""
|
"""Patch selected LLM profiles into the target scenario YAML file."""
|
||||||
|
logger.info(
|
||||||
|
"[apply_profiles] scenario=%s judge=%s answer=%s dataset=%s metric_weights=%s doc_weights=%s",
|
||||||
|
request.scenario_path,
|
||||||
|
request.judge_profile_id,
|
||||||
|
request.answer_profile_id,
|
||||||
|
request.dataset_profile_id,
|
||||||
|
bool(request.metric_weights),
|
||||||
|
bool(request.doc_weights),
|
||||||
|
)
|
||||||
role_profiles: dict[str, LLMProfile | None] = {
|
role_profiles: dict[str, LLMProfile | None] = {
|
||||||
"judge": profile_manager.get(request.judge_profile_id) if request.judge_profile_id else None,
|
"judge": profile_manager.get(request.judge_profile_id) if request.judge_profile_id else None,
|
||||||
"answer": profile_manager.get(request.answer_profile_id) if request.answer_profile_id else None,
|
"answer": profile_manager.get(request.answer_profile_id) if request.answer_profile_id else None,
|
||||||
@@ -135,6 +164,7 @@ def apply_profiles(request: ProfileApplyRequest) -> ProfileApplyResponse:
|
|||||||
]
|
]
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
|
logger.warning("[apply_profiles] missing profiles for roles: %s", missing)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Profile(s) not found for roles: {', '.join(missing)}",
|
detail=f"Profile(s) not found for roles: {', '.join(missing)}",
|
||||||
@@ -148,6 +178,7 @@ def apply_profiles(request: ProfileApplyRequest) -> ProfileApplyResponse:
|
|||||||
metric_weights=request.metric_weights,
|
metric_weights=request.metric_weights,
|
||||||
doc_weights=request.doc_weights,
|
doc_weights=request.doc_weights,
|
||||||
)
|
)
|
||||||
|
logger.info("[apply_profiles] patched fields: %s", patched)
|
||||||
return ProfileApplyResponse(
|
return ProfileApplyResponse(
|
||||||
scenario_path=request.scenario_path,
|
scenario_path=request.scenario_path,
|
||||||
patched_fields=patched,
|
patched_fields=patched,
|
||||||
|
|||||||
131
webapp/api/pipeline.py
Normal file
131
webapp/api/pipeline.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""Routes for the end-to-end pipeline API (document parse → build → eval)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from webapp.models import (
|
||||||
|
PipelineJobRequest,
|
||||||
|
PipelineJobResponse,
|
||||||
|
PipelineJobStatus,
|
||||||
|
)
|
||||||
|
from webapp.services.pipeline_task_manager import pipeline_task_manager
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/pipeline", tags=["pipeline"])
|
||||||
|
logger = logging.getLogger("webapp.api.pipeline")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/jobs",
|
||||||
|
status_code=202,
|
||||||
|
response_model=PipelineJobResponse,
|
||||||
|
summary="提交全链路评估任务",
|
||||||
|
responses={
|
||||||
|
202: {
|
||||||
|
"description": "任务已成功排队,立即返回 job_id。",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"job_id": "a1b2c3d4e5f6",
|
||||||
|
"job_name": "siemens-ct-eval-2026",
|
||||||
|
"status": "queued",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
422: {"description": "请求参数校验失败(docs_path 等必填字段缺失或格式错误)。"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def submit_pipeline_job(request: PipelineJobRequest) -> PipelineJobResponse:
|
||||||
|
"""提交一个「解析文档 → 生成题库 → RAGAS 评估 → 输出报告」全链路任务。
|
||||||
|
|
||||||
|
任务在后台线程中异步执行,立即返回 `job_id`。
|
||||||
|
通过 `GET /api/pipeline/jobs/{job_id}` 轮询 `status` / `phase` / `logs`。
|
||||||
|
|
||||||
|
**Pipeline 执行阶段**:
|
||||||
|
1. `parsing_documents` — 调用阿里云 DocMind 解析每份 PDF
|
||||||
|
2. `generating_questions` — LLM 从文档片段生成草稿题库
|
||||||
|
3. `evaluating` — RAGAS 在线评测打分(answer_model 答题 + judge_model 评分)
|
||||||
|
4. `done` — 所有产物写入磁盘,`status` 变为 `completed`
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
"[submit_pipeline] docs_path=%s job_name=%r gen_model=%s judge=%s max_docs=%s",
|
||||||
|
request.docs_path, request.job_name, request.generation_model,
|
||||||
|
request.judge_model, request.max_documents,
|
||||||
|
)
|
||||||
|
task = pipeline_task_manager.submit(request)
|
||||||
|
logger.info("[submit_pipeline] queued job_id=%s job_name=%s", task.job_id, task.job_name)
|
||||||
|
return PipelineJobResponse(
|
||||||
|
job_id=task.job_id,
|
||||||
|
job_name=task.job_name,
|
||||||
|
status=task.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/jobs/{job_id}",
|
||||||
|
response_model=PipelineJobStatus,
|
||||||
|
summary="查询任务状态",
|
||||||
|
responses={
|
||||||
|
200: {"description": "返回任务当前状态、执行阶段、日志及完成后的产物路径。"},
|
||||||
|
404: {"description": "指定 job_id 的任务不存在。"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get_pipeline_job(job_id: str) -> PipelineJobStatus:
|
||||||
|
"""查询一个 Pipeline 任务的当前状态、执行阶段、实时日志和结果。
|
||||||
|
|
||||||
|
**轮询建议**:每 3–5 秒查询一次,直到 `status` 为 `completed` 或 `failed`。
|
||||||
|
|
||||||
|
`result` 字段在任务完成后填充,包含:
|
||||||
|
- `scores_csv` — 每道题目逐项评分
|
||||||
|
- `summary_md` — 评估摘要 Markdown
|
||||||
|
- `dataset_csv` — 生成的题库 CSV
|
||||||
|
- `source_chunks_jsonl` — 文档片段索引
|
||||||
|
"""
|
||||||
|
status = pipeline_task_manager.get(job_id)
|
||||||
|
if status is None:
|
||||||
|
logger.warning("[get_pipeline_job] not found job_id=%s", job_id)
|
||||||
|
raise HTTPException(status_code=404, detail=f"Pipeline job not found: {job_id}")
|
||||||
|
logger.debug("[get_pipeline_job] job_id=%s status=%s phase=%s", job_id, status.status, status.phase)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/jobs",
|
||||||
|
response_model=dict,
|
||||||
|
summary="列出所有任务",
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"description": "按创建时间倒序返回本次服务器会话中所有的 Pipeline 任务。",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"job_id": "a1b2c3d4e5f6",
|
||||||
|
"job_name": "siemens-ct-eval",
|
||||||
|
"status": "completed",
|
||||||
|
"phase": "done",
|
||||||
|
"logs": ["[build] 17 documents parsed", "..."],
|
||||||
|
"result": {
|
||||||
|
"total_questions": 19,
|
||||||
|
"eval_run_id": "2026-06-18T...",
|
||||||
|
"scores_csv": "outputs/pipeline/.../scores.csv",
|
||||||
|
"summary_md": "outputs/pipeline/.../summary.md",
|
||||||
|
},
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def list_pipeline_jobs() -> dict:
|
||||||
|
"""返回本次服务器会话中所有已提交的 Pipeline 任务,按创建时间倒序排列。"""
|
||||||
|
jobs = pipeline_task_manager.list_jobs()
|
||||||
|
logger.info("[list_pipeline_jobs] count=%d", len(jobs))
|
||||||
|
return {"jobs": [s.model_dump() for s in jobs]}
|
||||||
@@ -2,31 +2,42 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from webapp.models import RunDetail
|
from webapp.models import RunDetail
|
||||||
from webapp.services import report_builder, run_reader
|
from webapp.services import report_builder, run_reader
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/runs", tags=["runs"])
|
router = APIRouter(prefix="/api/runs", tags=["runs"])
|
||||||
|
logger = logging.getLogger("webapp.api.runs")
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def get_runs() -> dict[str, list]:
|
def get_runs() -> dict[str, list]:
|
||||||
"""Return summaries for every discoverable evaluation run."""
|
"""Return summaries for every discoverable evaluation run."""
|
||||||
summaries = run_reader.list_run_summaries()
|
summaries = run_reader.list_run_summaries()
|
||||||
|
logger.info("[get_runs] found %d runs", len(summaries))
|
||||||
return {"runs": [summary.model_dump() for summary in summaries]}
|
return {"runs": [summary.model_dump() for summary in summaries]}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{run_id}")
|
@router.get("/{run_id}")
|
||||||
def get_run_detail(run_id: str) -> RunDetail:
|
def get_run_detail(run_id: str) -> RunDetail:
|
||||||
"""Return the full summary and aggregated report for one run."""
|
"""Return the full summary and aggregated report for one run."""
|
||||||
|
logger.info("[get_run_detail] run_id=%s", run_id)
|
||||||
run_dir = run_reader.find_run_dir(run_id)
|
run_dir = run_reader.find_run_dir(run_id)
|
||||||
if run_dir is None:
|
if run_dir is None:
|
||||||
|
logger.warning("[get_run_detail] not found run_id=%s", run_id)
|
||||||
raise HTTPException(status_code=404, detail=f"未找到运行: {run_id}")
|
raise HTTPException(status_code=404, detail=f"未找到运行: {run_id}")
|
||||||
|
|
||||||
summary = run_reader.build_run_summary(run_dir)
|
summary = run_reader.build_run_summary(run_dir)
|
||||||
if summary is None:
|
if summary is None:
|
||||||
|
logger.warning("[get_run_detail] missing metadata run_id=%s", run_id)
|
||||||
raise HTTPException(status_code=404, detail=f"运行元数据缺失: {run_id}")
|
raise HTTPException(status_code=404, detail=f"运行元数据缺失: {run_id}")
|
||||||
|
|
||||||
report = report_builder.build_report(run_dir, summary.metrics)
|
report = report_builder.build_report(run_dir, summary.metrics)
|
||||||
|
logger.info(
|
||||||
|
"[get_run_detail] ok run_id=%s metrics=%s valid=%d invalid=%d",
|
||||||
|
run_id, summary.metrics, summary.valid_samples, summary.invalid_samples,
|
||||||
|
)
|
||||||
return RunDetail(summary=summary, report=report)
|
return RunDetail(summary=summary, report=report)
|
||||||
|
|||||||
@@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from webapp.services import scenario_scanner
|
from webapp.services import scenario_scanner
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/scenarios", tags=["scenarios"])
|
router = APIRouter(prefix="/api/scenarios", tags=["scenarios"])
|
||||||
|
logger = logging.getLogger("webapp.api.scenarios")
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def get_scenarios() -> dict[str, list]:
|
def get_scenarios() -> dict[str, list]:
|
||||||
"""Return every scenario file found under the scenarios/ directory."""
|
"""Return every scenario file found under the scenarios/ directory."""
|
||||||
scenarios = scenario_scanner.list_scenarios()
|
scenarios = scenario_scanner.list_scenarios()
|
||||||
|
valid = sum(1 for s in scenarios if not s.error)
|
||||||
|
logger.info("[get_scenarios] total=%d valid=%d errors=%d", len(scenarios), valid, len(scenarios) - valid)
|
||||||
return {"scenarios": [item.model_dump() for item in scenarios]}
|
return {"scenarios": [item.model_dump() for item in scenarios]}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ the server starts even when the evaluation dependencies are not yet installed.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
@@ -20,6 +21,7 @@ from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, sco
|
|||||||
|
|
||||||
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
||||||
logger = logging.getLogger("webapp.server")
|
logger = logging.getLogger("webapp.server")
|
||||||
|
access_logger = logging.getLogger("webapp.access")
|
||||||
|
|
||||||
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
|
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
|
||||||
OPENAPI_TAGS = [
|
OPENAPI_TAGS = [
|
||||||
@@ -107,6 +109,24 @@ 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.middleware("http")
|
||||||
|
async def access_log_middleware(request: Request, call_next):
|
||||||
|
"""Log every API request with method, path, status code and latency.
|
||||||
|
|
||||||
|
Static file requests are logged at DEBUG level to keep the console clean.
|
||||||
|
"""
|
||||||
|
t0 = time.monotonic()
|
||||||
|
response = await call_next(request)
|
||||||
|
latency_ms = int((time.monotonic() - t0) * 1000)
|
||||||
|
path = request.url.path
|
||||||
|
is_static = path.startswith("/static/") or path in ("/", "/favicon.ico")
|
||||||
|
msg = "%s %s → %d (%dms)", request.method, path, response.status_code, latency_ms
|
||||||
|
if is_static:
|
||||||
|
access_logger.debug(*msg)
|
||||||
|
else:
|
||||||
|
access_logger.info(*msg)
|
||||||
|
return response
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||||
"""Log full validation error detail to help diagnose 422 responses."""
|
"""Log full validation error detail to help diagnose 422 responses."""
|
||||||
|
|||||||
Reference in New Issue
Block a user