"""Sentry SDK 初始化(FastAPI + asyncio integration)。 【為什麼獨立模組?】 原本想塞進 main.py,但 main.py 已經很長,而且 init 邏輯需要對應 test_sentry_init.py 三個 case(DSN 不設、DSN 設但 environment 不設、 兩者都設),把它放成獨立函式比較好測。caller 只需 `init_sentry()`。 【為什麼一定要 SENTRY_ENVIRONMENT?】 Sentry SDK 預設 environment 從 SENTRY_ENVIRONMENT 取,但若 env 不設、 又同時帶 production DSN,dev/staging 機台的事件會直接打進 production project,與真實 production 事件混在一起。production 的告警 noise 會被 dev 噪音淹沒。fail-loud:明確要求 operator 設 production / staging / dev 任一字串才放行。 【為什麼 send_default_pii=False?】 ml-service 上游收的是「使用者照片 / 使用者商品 brand 字串」這類 user-supplied 資料;request body 中可能含使用者圖片 bytes、prompt injection 嘗試字串、品牌字串等,default PII 開啟會把整段 request payload 灌進 Sentry event: - 流量大時可能撐爆 Sentry quota(圖片 base64 巨大) - 也讓 Sentry 變成另一個 user-data sink,要納入 GDPR 範圍 設成 False 仍能拿到 stack trace + transaction name,足以做 error triage,沒有 PII 副作用。 """ from __future__ import annotations import logging import os # 注意:sentry_sdk 為 lazy import — 只在 init_sentry() 內 import,不在模組頂層。 # 這樣 sentry_sdk 安裝失敗(或在 ML 環境暫時 unavailable)不會 block ml-service # 啟動;DSN 不設時也不需要 import 整包。 _logger = logging.getLogger(__name__) def init_sentry() -> bool: """讀取 SENTRY_DSN 等 env,呼叫 sentry_sdk.init。 Returns: True if Sentry was initialised, False if DSN was not set. Raises: RuntimeError: when SENTRY_DSN is set but SENTRY_ENVIRONMENT is unset (sentinel against dev events bleeding into prod project). """ dsn = os.environ.get("SENTRY_DSN") if not dsn: _logger.info("ml-service: SENTRY_DSN not set — Sentry disabled") return False environment = os.environ.get("SENTRY_ENVIRONMENT") if not environment: # fail-loud 而非 silent default。沒設 SENTRY_ENVIRONMENT 等同 # operator 沒對齊 dev/staging/prod 邊界,告警可能跨環境污染。 raise RuntimeError( "SENTRY_DSN set but SENTRY_ENVIRONMENT unset — dev/staging events " "would land in the production Sentry project. Set " "SENTRY_ENVIRONMENT=production|staging|dev explicitly." ) # traces_sample_rate 預設 0.1(10% 採樣)。HF Spaces L40S 大流量時 # 100% trace 會把 Sentry quota 燒光;0.1 已足以做 p95 latency 告警 # (ml-service.cold-start-slow / ml-service.5xx-rate)。 traces_raw = os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.1") try: traces_sample_rate = float(traces_raw) except ValueError as exc: raise RuntimeError( f"SENTRY_TRACES_SAMPLE_RATE invalid: {traces_raw!r} (must be float 0–1)" ) from exc if not 0.0 <= traces_sample_rate <= 1.0: raise RuntimeError( f"SENTRY_TRACES_SAMPLE_RATE out of range: {traces_sample_rate} (must be 0–1)" ) import sentry_sdk from sentry_sdk.integrations.asyncio import AsyncioIntegration from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.logging import LoggingIntegration sentry_sdk.init( dsn=dsn, environment=environment, traces_sample_rate=traces_sample_rate, integrations=[ FastApiIntegration(), AsyncioIntegration(), # F1 closer (c-series post-review wave-2):sentry-sdk Python 預設 # LoggingIntegration 的 event_level=ERROR 會把任何 logger.error() # auto-capture 為 Sentry event;若 caller 傳 exc_info=exc, # exception.values[].value 會夾完整 raw exc.args(psycopg DSN / # huggingface_hub hf_ token),bypass 我們在 main.py / # lib/background.py 的 scrub_exc_for_audit 工。 # # 顯式 instantiate with event_level=None 關掉 auto-event; # level=INFO 保留 breadcrumb 路徑(INFO+ 轉 breadcrumb),non- # security trace 仍能在 explicit capture event 上看 trail。 # security-tagged event 的 site (main.py:_on_preload_done / # lib/background.py:_log_unhandled_exception) 已用 isolation_scope # + clear_breadcrumbs 隔離,不會吃 ambient breadcrumb。 # (wave-3 修正:必須是 isolation_scope 不是 new_scope,因為 # sentry-sdk Python 2.x breadcrumb 寫在 isolation scope;詳細 # 解釋見 main.py:_on_preload_done / background.py 的 inline 註解。) # # memory: feedback_python_sentry_capture_needs_scrub。 LoggingIntegration(level=logging.INFO, event_level=None), ], # send_default_pii=False — 顯式關閉 user image bytes / brand string # / prompt injection payload 進 Sentry event。詳見 module docstring。 send_default_pii=False, # FastApi integration 預設不擷取 request body;保險再 set # max_breadcrumbs 限制以免 long-running ML inference 累積過多 trail。 max_breadcrumbs=50, # include_local_variables=False — 關掉 stack frame 局部變數捕獲。 # batch-3 post-review (security-reviewer P2):sentry-sdk 2.x 預設 # `with_locals=True`,當 `/verify/local` 之類 endpoint 在 prompt # 組裝後才 raise,stack frame 會把 `brand_safe`、`brand_context`、 # 整段 prompt(含使用者 80-char sanitized brand)灌進 Sentry event。 # send_default_pii=False 只擋 request body / headers / cookies, # 不擋 frame variables。關掉之後 Sentry 仍能拿到 stack trace + # exception type/message,足以做 triage,但不再夾帶 user-controlled # 字串。 include_local_variables=False, ) _logger.warning( "ml-service: Sentry initialised (environment=%s, traces=%s)", environment, traces_sample_rate, ) return True