"""客户端运行日志批量上报接口。 POST /api/v1/applog/batch — 批量接收客户端运行日志,逐条写专用滚动文件 logs/app-client.log (供 Logtail 采进独立 SLS logstore)。鉴权同 analytics(不强制登录,user_id 可选在 body)。 fire-and-forget:写失败也不 500(避免客户端重试风暴);超批 422、超体积 413、msg 超限截断。 """ from __future__ import annotations import os from fastapi import APIRouter, Depends, HTTPException, Request from app.api.deps import get_client_ip from app.core.client_log import write_records from app.core.ratelimit import rate_limit from app.schemas.applog import AppLogBatchIn, AppLogIngestOut router = APIRouter(prefix="/api/v1/applog", tags=["applog"]) def _enforce_body_limit(request: Request) -> None: """依赖:body 声明过大直接 413(在 body 校验前拦截)。缺 Content-Length 由 nginx 兜底。""" max_bytes = int(os.getenv("APPLOG_MAX_BODY_BYTES", str(1024 * 1024 * 2))) cl = request.headers.get("content-length") if cl is not None and cl.isdigit() and int(cl) > max_bytes: raise HTTPException(status_code=413, detail="日志批量过大") @router.post( "/batch", response_model=AppLogIngestOut, summary="批量上报客户端运行日志", dependencies=[ Depends(rate_limit(120, 60, "applog-batch")), Depends(_enforce_body_limit), ], ) def ingest_logs(batch: AppLogBatchIn, request: Request) -> AppLogIngestOut: received, dropped = write_records( batch.logs, meta={ "device_id": batch.device_id, "user_id": batch.user_id, "app_ver": batch.app_ver, "platform": batch.platform, "sent_at": batch.sent_at, }, client_ip=get_client_ip(request), ) return AppLogIngestOut(received=received, dropped=dropped)