feat(监控): 完善系统监控、登录状态与访问审计能力
This commit is contained in:
+59
-3
@@ -5,8 +5,9 @@ import time
|
||||
from contextlib import asynccontextmanager
|
||||
from collections import defaultdict
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.api.v1.router import api_router
|
||||
@@ -19,15 +20,23 @@ from app.db.session import SessionLocal, engine
|
||||
from app.services.visit_scheduler import run_daily_lost_visit_job
|
||||
from app.services.permission_log_writer import start_log_writer, stop_log_writer
|
||||
from app.services.permission_metric_aggregator import run_hourly_metric_aggregation
|
||||
from app.services.source_location_aggregator import run_hourly_source_location_aggregation
|
||||
from app.services.monitoring_server_location import resolve_monitoring_server_location
|
||||
from app.services.monitoring_retention import run_monitoring_retention
|
||||
from app.services.security_access_log_writer import (
|
||||
get_security_log_writer,
|
||||
start_security_log_writer,
|
||||
stop_security_log_writer,
|
||||
)
|
||||
from app.core.security import decode_token
|
||||
from app.core.deps import get_db_session
|
||||
from app.core.request_context import (
|
||||
build_request_audit_context,
|
||||
build_request_snapshot,
|
||||
build_sanitized_request_headers,
|
||||
get_request_audit_context,
|
||||
reset_request_audit_context,
|
||||
resolve_ctms_client_type,
|
||||
resolve_client_ip,
|
||||
set_request_audit_context,
|
||||
)
|
||||
@@ -42,8 +51,12 @@ setup_config_stats: dict[str, int] = defaultdict(int)
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
stop_event = asyncio.Event()
|
||||
await resolve_monitoring_server_location()
|
||||
scheduler_task = asyncio.create_task(run_daily_lost_visit_job(stop_event))
|
||||
aggregator_task = asyncio.create_task(run_hourly_metric_aggregation(stop_event))
|
||||
source_location_aggregator_task = asyncio.create_task(
|
||||
run_hourly_source_location_aggregation(stop_event)
|
||||
)
|
||||
await start_log_writer()
|
||||
await start_security_log_writer()
|
||||
if settings.ENV == "development":
|
||||
@@ -52,12 +65,15 @@ async def lifespan(_: FastAPI):
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
async with SessionLocal() as session:
|
||||
await ensure_admin_exists(session)
|
||||
retention_task = asyncio.create_task(run_monitoring_retention(stop_event))
|
||||
yield
|
||||
stop_event.set()
|
||||
await stop_log_writer()
|
||||
await stop_security_log_writer()
|
||||
await scheduler_task
|
||||
await aggregator_task
|
||||
await source_location_aggregator_task
|
||||
await retention_task
|
||||
|
||||
|
||||
async def _ensure_legacy_primary_keys(conn) -> None:
|
||||
@@ -129,19 +145,23 @@ def create_app() -> FastAPI:
|
||||
"Accept",
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-CTMS-Client-Source",
|
||||
"X-CTMS-Client-Type",
|
||||
"X-CTMS-Client-Version",
|
||||
"X-CTMS-Client-Platform",
|
||||
"X-CTMS-Build-Channel",
|
||||
"X-CTMS-Build-Commit",
|
||||
"X-Request-ID",
|
||||
"X-Correlation-ID",
|
||||
],
|
||||
expose_headers=["X-Request-ID"],
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def setup_config_monitoring_middleware(request, call_next):
|
||||
path = request.url.path
|
||||
is_setup_config_path = "/api/v1/studies/" in path and "/setup-config" in path
|
||||
should_security_log = path.startswith("/api/")
|
||||
should_security_log = path.startswith("/api/") and path != "/api/v1/auth/session/heartbeat"
|
||||
started_at = time.perf_counter()
|
||||
audit_context = build_request_audit_context(request)
|
||||
audit_context_token = set_request_audit_context(audit_context)
|
||||
@@ -167,6 +187,8 @@ def create_app() -> FastAPI:
|
||||
raise
|
||||
|
||||
status_code = int(response.status_code)
|
||||
if audit_context.request_id:
|
||||
response.headers["X-Request-ID"] = audit_context.request_id
|
||||
if is_setup_config_path:
|
||||
normalized_path = UUID_RE.sub("{study_id}", path)
|
||||
duration_ms = int((time.perf_counter() - started_at) * 1000)
|
||||
@@ -229,6 +251,36 @@ def create_app() -> FastAPI:
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get(
|
||||
"/readyz",
|
||||
tags=["health"],
|
||||
summary="服务就绪检查",
|
||||
description="验证应用可访问数据库;失败时返回 503。",
|
||||
)
|
||||
async def readiness(db=Depends(get_db_session)):
|
||||
started_at = time.perf_counter()
|
||||
try:
|
||||
await db.execute(text("SELECT 1"))
|
||||
except Exception as exc:
|
||||
logger.exception("Readiness database check failed")
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={
|
||||
"status": "not_ready",
|
||||
"database": {
|
||||
"status": "unhealthy",
|
||||
"error_type": type(exc).__name__,
|
||||
},
|
||||
},
|
||||
)
|
||||
return {
|
||||
"status": "ready",
|
||||
"database": {
|
||||
"status": "healthy",
|
||||
"latency_ms": round((time.perf_counter() - started_at) * 1000, 2),
|
||||
},
|
||||
}
|
||||
|
||||
@app.get(
|
||||
"/health/setup-config-stats",
|
||||
tags=["health"],
|
||||
@@ -271,6 +323,7 @@ def _enqueue_security_access_log(request, path: str, status_code: int, started_a
|
||||
if not writer:
|
||||
return
|
||||
auth_status, user_identifier = _resolve_auth_context(request)
|
||||
context = get_request_audit_context()
|
||||
writer.enqueue(
|
||||
{
|
||||
"method": request.method,
|
||||
@@ -279,11 +332,14 @@ def _enqueue_security_access_log(request, path: str, status_code: int, started_a
|
||||
"elapsed_ms": round((time.perf_counter() - started_at) * 1000, 2),
|
||||
"client_ip": resolve_client_ip(request),
|
||||
"user_agent": request.headers.get("user-agent"),
|
||||
"client_type": request.headers.get("x-ctms-client-type"),
|
||||
"client_type": resolve_ctms_client_type(request.headers),
|
||||
"client_version": request.headers.get("x-ctms-client-version"),
|
||||
"client_platform": request.headers.get("x-ctms-client-platform"),
|
||||
"build_channel": request.headers.get("x-ctms-build-channel"),
|
||||
"build_commit": request.headers.get("x-ctms-build-commit"),
|
||||
"request_headers": context.request_headers if context else build_sanitized_request_headers(request),
|
||||
"request_snapshot": context.request_snapshot if context else build_request_snapshot(request),
|
||||
"request_id": context.request_id if context else None,
|
||||
"auth_status": auth_status,
|
||||
"user_identifier": user_identifier,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user