0013: 관측성을 추가·pluggable·fail-closed 표면으로
0013: 관측성을 추가·pluggable·fail-closed 표면으로
- Status: accepted
- Date: 2026-05-11
- Related: ADR 0001 확장; ADR 0003 보존; ADR 0006, ADR 0011 백엔드 패턴 재사용; ADR 0005 eval 분리 존중; ADR 0020 (검색 측 Protocol) 와 같은 “추가 pluggable 표면” 테마
- Deciders: hskim
TL;DR
- 트레이스 viewer (LangFuse / OTel) 를 pluggable 백엔드 레지스트리로 추가 —
BIDMATE_TRACE_BACKEND=none기본은 noop. - 어떤 관측성 실패도 쿼리 경로를 깨지 못함 (fail-closed) — 모든 backend boundary 에서 예외 catch + noop 폴백.
schema_versionbump 안 함; 트레이스 데이터는diagnostics에 거주,answer계약 미변경.
배경
rag_core.py 는 이미 _StageTimer context manager 로 단계별 타이밍 누적 + diagnostics.stage_latency 노출. ADR 0011 가 diagnostics.synthesis.{backend, model, tokens_in, tokens_out, latency_ms, fallback_reason} 추가. 빠진 것은 sink — reviewer (또는 on-call 엔지니어) 가 쿼리별 단계 breakdown, 토큰 수, 비용 추세, 실패 모드율을 시간 축으로 볼 수 있는 트레이스 viewer.
Applied AI / LLM Ops 포트폴리오에서 트레이스 viewer 는 최고 레버리지 관측성 신호 — “답변 반환” 과 “프로덕션 운용 가능” 의 차이. 통합 shape 가 특정 vendor 보다 중요 — LangFuse, Honeycomb, Datadog, Grafana Tempo, 모든 OTLP 호환 백엔드가 파이프라인 손 안 대고 동작해야.
트레이싱을 run_rag_query 에 직접 묶으면 ADR 0001 기준선 보존 충돌 (noop 기본은 결정론·무료 필요) + ADR 0005 eval 분리 위험 (exporter 크래시가 CI 실패 유발). 올바른 수는 ADR 0011 의 LLM 합성 방어 형태 — 파이프라인 동작 손 대지 말고 관측성을 추가·pluggable·fail-closed 표면으로 추가.
결정
관측성은 rag_observability.py 의 pluggable 백엔드 레지스트리로 노출되는 추가 표면, BIDMATE_TRACE_BACKEND gate. 구체적으로:
- 기본
BIDMATE_TRACE_BACKEND=none은span()이contextlib.nullcontext()반환하는 noopTraceContext실행; 파이프라인 동작이 본 모듈 없는 빌드와 byte-identical. rag_core.py의_StageTimer가 선택적trace=kwarg 수용. non-noop 시 각 타이밍 region 이 트레이스에 자식 span 도 오픈.- 백엔드 레지스트리
_BACKENDS = {"none": ..., "langfuse": ..., "otel": ...}가 ADR 0011 합성 레지스트리 미러링. 새 백엔드 추가는 factory 등록만 —run_rag_query편집 불필요. run_rag_query가 새 진단 키 4개 노출:trace_url,trace_backend,trace_unavailable_reason,trace_error. ADR 0003 답변 계약 일부 아님 —answer가 아닌diagnostics에 거주.schema_versionbump 안 함.
Span topology
run_rag_query 호출 1회가 다음 자식 span 들과 함께 root 트레이스 1개 emit:
| Span 명 | 카디널리티 | 속성 |
|---|---|---|
query_analysis |
2 (context resolution 전·후) | iteration ∈ {1, 2} |
context_resolution |
1 | — |
retrieve |
N (재시도 1회당) | attempt_index, stage, top_k |
verify |
N (재시도 1회당) | attempt_index, verifier_retry |
answer_generation |
1 | — |
synthesis |
0 또는 1 (prompt_profile=llm_synthesis 만) |
prompt_profile |
Root 트레이스 태그: pipeline, prompt_profile, embedding_backend, retrieval_backend, retrieval_mode, metadata_first, rerank, verifier_retry, cold_start, query_type. reviewer 가 트레이스를 필터·그룹화할 컬럼들.
백엔드 pluggability
none(기본) —_NoopTraceContext. 오버헤드 0.make smoke,pr-eval.yml, 공개 CI, 오프라인 데모 실행하는 reviewer 가 사용.langfuse—LANGFUSE_PUBLIC_KEY,LANGFUSE_SECRET_KEY필요; 선택적LANGFUSE_HOST(기본https://cloud.langfuse.com). 백엔드 factory 내부에서langfuse패키지 lazy import — dependency 누락 시trace_unavailable_reason=missing_dependency:langfuse로 noop 폴백. 트레이스 URL 은trace.get_trace_url()통해diagnostics.trace_url에 노출.otel— 표준 OpenTelemetry SDK. SDK 컨벤션대로OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_SERVICE_NAME따름. 선택적BIDMATE_TRACE_URL_TEMPLATE(예:https://ui.honeycomb.io/.../trace?trace_id={trace_id}) 가 opaque OTLP trace_id 에서 클릭 가능 URL 렌더.
Fail-closed 계약
본 표면의 정의 속성은 어떤 관측성 실패도 쿼리 경로를 깰 수 없음. 모든 backend boundary 가 예외 catch + noop 폴백:
| 실패 | 동작 |
|---|---|
| 선택 dep 누락 | trace_backend=none, trace_unavailable_reason="missing_dependency:<pkg>" |
| 자격 누락 | trace_backend=none, trace_unavailable_reason="missing_credentials:<backend>" |
| 백엔드 생성자 raise | trace_backend=none, trace_unavailable_reason="backend_init_error:..." |
start_trace raise |
trace_backend=<requested>, trace_url=None, trace_error="start_trace:..." |
파이프라인 중간 span() raise |
_StageTimer.__exit__ 가 swallow; 파이프라인 계속; 후속 span 도 시도 |
finish() raise |
trace_url=None, trace_error="finish:..." |
추가 분석 변형 불변식 (ADR 0001 / ADR 0011 을 여기 적용): 어떤 실패 모드 주입 시에도 결과가 (trace_* 키와 변동 타이밍 strip 후) noop run 과 byte-identical. tests/test_observability_tracing.py 의 test_start_trace_exception_falls_back 에 lock.
주기
- 공개 fixture smoke CI (
pr-eval.yml):BIDMATE_TRACE_BACKENDunset → noop. SDK 설치 없음. 파이프라인 동작 불변. - 실데이터 eval: 선택. noop 기본. reviewer 가
BIDMATE_TRACE_BACKEND=langfuse로 일회성 디버깅 opt-in 가능 — ADR 0005 commit 경계 영향 없음 (집계 메트릭 불변; 쿼리별 트레이스만 export). - 라이브 데모: Fly.io secret 통해
BIDMATE_TRACE_BACKEND=langfuse구성. Streamlit 데모가 각 답변 하단에 “View trace” 링크 노출 (이슈 acceptance 기준).
결과
Wins
- 시스템이 ADR 0001 (기준선) / ADR 0003 (답변 계약) 위험 없이 프로덕션급 관측성 표면 (단계별 span, 재시도 루프 가시성, 토큰 수, 비용 추세) 획득.
- vendor 중립 백엔드 2종 (LangFuse 네이티브 UX, OTel 모든 APM) + 결정론 noop 기본. Honeycomb, Datadog, Grafana Tempo 추가는 환경 설정이지 코드 변경 아님.
- ADR 0006/0007 백엔드 레지스트리 관용구 재사용 → 코드베이스에 “pluggable 백엔드 추가 방법” 일관 패턴 (평가자 → 합성 → 트레이스).
- 재시도 루프 가시성 (
attempt_index속성의retrieve/verify자식 span 시퀀스) 이 #69 같은 부분 grounding 케이스 디버깅에 진정 유용 — 역사적으로stage_attempts요약뿐, 이제 span 시퀀스로 추적.
Costs
- 선택 dependency 3개 (
langfuse,opentelemetry-sdk,opentelemetry-exporter-otlp-proto-http). 모두 백엔드 factory 내부 lazy import 뒤.BIDMATE_TRACE_BACKEND가 사용하는 백엔드로 설정 안 되면 런타임 필요 없음. _StageTimer.__enter__/__exit__에trace=None시에도 작은 오버헤드 (단계당None-check 1회). smoke fixture 쿼리당 < 0.1ms 측정, 아래 트레이스 budget 제약 내.- 새 모듈 + 새 env-var family 1개. 기본이
none(오프라인·세팅 없음) 으로 완화.
Constraints (불변)
- ADR 0001:
naive_baseline가 본 모듈 유·무와 상관없이 동일 실행.pipeline_cli_choices()불변. - ADR 0003:
schema_version: 2,status값,claims[].citations,evidence[]불변. 트레이스 데이터는answer가 아닌diagnostics에 거주. - ADR 0005: 실데이터 케이스별 트레이스는 로컬 (reviewer 가 고른 LangFuse host). 공개 CI 트레이싱은 noop. 집계 메트릭 무영향.
- ADR 0011:
diagnostics.synthesis키 유지. 새synthesisspan 은 LLM 합성 실행 시만 오픈.
트레이스 budget
트레이싱 enable 시에도 단계당 p95 오버헤드가 noop 기준선 대비 5% 이내. span 머신 자체 비용 (네트워크 exporter 는 비동기이므로 제외) bound. budget 초과 시 어떤 백엔드 탓 전에 _StageTimer 통합부터 재검토.
검토한 대안
- 트레이싱을
run_rag_query에 직접 묶기. Reject: 관심사 결합, 파이프라인 가독성 저해, “파이프라인이 하는 일” 과 “관측 방식” 코드 conflate. 두 번째 백엔드 추가가 레지스트리 편집 아닌run_rag_query편집 됨. - print-only 로깅. Reject: 시계열 없음, 단계별 span 탐색 없음, 토큰·비용 대시보드 없음. 로컬 디버깅엔 유용하나 LLM Ops 포트폴리오 신호 미달.
- Always-on 트레이싱. Reject: ADR 0005 위반 (공개 CI 는 결정론·네트워크 dep 없음). smoke 테스트도 brittle 화.
- wrap-only 외부 span (쿼리당 span 1개, 자식 없음). Reject: 디버깅 가치 대부분이 단계별 breakdown —
verify가 병목인지retrieve가 재시도 유발인지 아는 게 N ms 총합보다 actionable. - 백엔드 1개만 (LangFuse only). Reject: LangFuse 가 AI 네이티브 UX 엔 좋지만 enterprise 는 Honeycomb / Datadog / Grafana Tempo 기존 보유 빈번. OTel 이 동일 instrumentation 을 코드 비용 0 으로 어디든 보냄.