"""document 관련 telemetry — Phase E.2 (analyze_events). /documents/{id}/analyze 호출을 background task로 DB에 기록. search_telemetry.py 패턴 동일 (단독 세션 + 에러 흡수). """ from __future__ import annotations import logging from typing import Any from sqlalchemy.exc import SQLAlchemyError from core.database import async_session from models.analyze_event import AnalyzeEvent logger = logging.getLogger("document_telemetry") # source enum validation — 서버 강제 fallback VALID_SOURCES: set[str] = { "document_server", "synology_chat", "ui_search", "ui_detail", "eval", "unknown", } DEFAULT_SOURCE = "document_server" def sanitize_source(raw: str | None) -> str: """source 값 서버 강제. enum 외 값은 unknown, None은 document_server.""" if raw is None: return DEFAULT_SOURCE lowered = raw.strip().lower() if lowered in VALID_SOURCES: return lowered return "unknown" async def record_analyze_event( doc_id: int, user_id: int | None, mode: str, text_limit: int | None, truncated: bool, layers_returned: list[str], cached: bool, latency_ms: int, model_name: str | None, prompt_version: str | None, error_code: str | None, source: str, # PR-A shadow observability — 아래 6개는 routing 이 동반될 때만 세팅, 그 외는 None 유지. subject_domain: str | None = None, risk_flags: list[str] | None = None, high_impact_task: bool | None = None, escalation_reasons: list[str] | None = None, confidence: float | None = None, policy_version: str | None = None, shadow_would_route_to: str | None = None, # PR-B B-1 — 실제 호출 tier 와 R2 backlog guard tier: str | None = None, escalated_to_26b: bool | None = None, suppressed_reason: str | None = None, ) -> None: """analyze_events INSERT. background task에서 호출 — 에러 삼킴. layers_returned: 성공 시 ["evidence","summary"] 등 layer 문자열 리스트. 실패 시 []. error_code: None (성공) | "timeout" | "llm" | "parse" | "missing_summary" | "no_text" | "not_found" tier: 'triage' | 'primary' | 'fallback' — 실제 호출된 tier (PR-B B-0~B-2). suppressed_reason: R2 backlog guard 로 soft escalate 가 suppress 된 경우의 이유 문자열. """ try: async with async_session() as session: row = AnalyzeEvent( doc_id=doc_id, user_id=user_id, mode=mode, text_limit=text_limit, truncated=truncated, layers_returned=layers_returned, cached=cached, latency_ms=latency_ms, model_name=model_name, prompt_version=prompt_version, error_code=error_code, source=source, subject_domain=subject_domain, risk_flags=risk_flags, high_impact_task=high_impact_task, escalated_to_26b=escalated_to_26b, escalation_reasons=escalation_reasons, confidence=confidence, policy_version=policy_version, shadow_would_route_to=shadow_would_route_to, tier=tier, suppressed_reason=suppressed_reason, ) session.add(row) await session.commit() except SQLAlchemyError as exc: logger.warning(f"analyze_event insert failed: {exc}")