MINERVA Phase C-9 — 관측성 설계 (Tracing·Metrics·Logs + OpenTelemetry)

LLM Agent는 분산·비결정·다단계라 일반 web service보다 관측성이 더 중요하다 — 3 pillar + LLM 특화 신호

LLM Agent 시스템의 관측성은 일반 web service보다 더 까다롭다 — 응답이 비결정적, 다단계 분기, 외부 LLM·tool 호출, streaming 등. 본 편은 OpenTelemetry 3 pillar(Metrics·Logs·Traces), LLM 특화 신호(token usage·prompt version·tool calls·streaming), Sampling 전략, SLO·SLI·error budget, Dashboard 설계(4 golden signals + LLM extras), C25·C33과의 결합, MINERVA 적용, cardinality 폭발·sampling bias 같은 함정을 정리한다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 LLM Agent에 관측성이 더 중요한가

일반 web service의 관측성은 latency·error·throughput에 집중. LLM Agent는 그 위에 더 많은 차원이 있다.

일반 web service LLM Agent 추가
latency + 단계별 latency (retrieval·rerank·LLM·tool)
error rate + LLM-specific (rate limit·content filter·timeout)
throughput + token throughput·cost throughput
- LLM streaming progress (TTFT·TBT)
- Tool call chain 깊이
- Prompt version·model version
- Hallucination·refusal 신호
- Retrieval 품질 (citation count·diversity)

이 모든 신호가 표준화된 방식으로 수집되어야 디버깅·SLO·optimization이 가능. OpenTelemetry가 그 토대.

2 3 Pillar — Metrics·Logs·Traces

[Metrics]   집계된 시계열 — 추세·SLO·alert
            "p95 latency은 시간별 어떻게 변하는가"
            "thumbs_up_rate가 일주일간 떨어졌는가"

[Logs]      개별 이벤트 — 디버깅·audit
            "이 user가 11:23에 어떤 query를 보냈는가"

[Traces]    한 query의 전체 흐름 — 단계별 latency·실패 위치
            "이 한 query가 retrieval에서 4초 걸렸는데 왜?"

세 pillar는 같은 사건의 다른 view — 같은 trace_id로 연결.

2.1 OpenTelemetry 통합

# app/observability/telemetry.py
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.metrics import MeterProvider


tracer = trace.get_tracer("minerva")
meter = metrics.get_meter("minerva")


# Counter — 누적 합 (요청 수·토큰 수·에러 수)
request_counter = meter.create_counter("requests", unit="1")
token_counter = meter.create_counter("tokens", unit="1")

# Histogram — 분포 (latency·token count per query)
latency_histogram = meter.create_histogram("latency_ms", unit="ms")
cost_histogram = meter.create_histogram("cost_per_query_usd", unit="USD")

# Gauge — 현재 값 (active connections·queue depth)
active_runs_gauge = meter.create_up_down_counter("active_runs", unit="1")

C20 대화 로깅이 logs 측면, 본 편이 metrics·traces 측면.

3 Tracing — 단계별 흐름

# app/agents/qna_chatbot.py
async def run(query: Query) -> Response:
    with tracer.start_as_current_span("agent.run") as root:
        root.set_attribute("agent_id", "qna_chatbot")
        root.set_attribute("user_id", query.user_id)
        root.set_attribute("intent", query.intent)

        with tracer.start_as_current_span("retrieval") as span:
            docs = await retriever.search(query.text)
            span.set_attribute("retrieval.count", len(docs))

        with tracer.start_as_current_span("rerank") as span:
            ranked = await reranker.rerank(query.text, docs)
            span.set_attribute("rerank.model", "bge-large")

        with tracer.start_as_current_span("llm.complete") as span:
            response = await llm.complete(prompt(query, ranked))
            span.set_attribute("llm.model", response.model)
            span.set_attribute("llm.input_tokens", response.usage.input_tokens)
            span.set_attribute("llm.output_tokens", response.usage.output_tokens)
            span.set_attribute("llm.cost_usd", response.cost_usd)

        return response

각 단계가 별도 span — 단계별 latency·attribute가 trace에 기록. Jaeger·Tempo·Datadog APM 같은 도구로 시각.

3.1 Trace 분석 예

trace_id: abc-123
└─ agent.run                          [3.2s] {agent: qna_chatbot, user: u-42}
   ├─ retrieval                       [120ms] {count: 25}
   ├─ rerank                          [80ms] {model: bge-large}
   └─ llm.complete                    [2.9s] {model: gpt-4o, in: 1500, out: 320, cost: 0.018}
      └─ http.request                  [2.85s] {host: api.openai.com}

이 시각화로 즉시 — llm.complete가 90% 시간 차지, retrieval은 빠름, 최적화는 LLM 단계에 집중해야 한다는 결론.

4 LLM 특화 메트릭

4.1 시간 지표

# Time To First Token (TTFT) — 사용자 대기 체감
ttft_histogram = meter.create_histogram("ttft_ms", unit="ms")

# Time Between Tokens (TBT) — streaming 흐름
tbt_histogram = meter.create_histogram("tbt_ms", unit="ms")

# 단계별
stage_latency = meter.create_histogram("stage_latency_ms", unit="ms")
# attributes: {stage: "retrieval"|"rerank"|"llm"|"tool"|"output_guard"}

TTFT는 사용자 체감 latency — 응답이 시작되는 시점. 전체 latency보다 중요한 경우 많음.

4.2 Token·Cost

# 입력·출력 분리
input_tokens = meter.create_counter("llm_input_tokens", unit="1")
output_tokens = meter.create_counter("llm_output_tokens", unit="1")

# 모델·세그먼트별 attribute
input_tokens.add(1500, {"model": "gpt-4o", "agent": "qna", "segment": "rnd"})

cardinality 위험 — segment·user 같은 high-cardinality attribute는 별도 분석 storage로 (Prometheus는 cardinality에 민감).

4.3 Tool Call

tool_call_counter = meter.create_counter("tool_calls", unit="1")
tool_latency_histogram = meter.create_histogram("tool_latency_ms", unit="ms")

with tracer.start_as_current_span("tool.search") as span:
    span.set_attribute("tool.name", "search")
    span.set_attribute("tool.args", str(args)[:500])    # truncate
    result = await tool.execute(args)
    span.set_attribute("tool.success", True)

C24 하네싱의 audit log와 같은 데이터 — observability는 metric·trace 형태.

4.4 LLM-specific Errors

# rate_limit·content_filter·timeout·context_too_long 별도
llm_error_counter = meter.create_counter("llm_errors", unit="1")
llm_error_counter.add(1, {"type": "rate_limit", "provider": "openai"})

C25 Circuit Breaker trigger 결정의 토대.

5 Logs — Structured + Correlation

C20 대화 로깅을 OpenTelemetry로 emit:

import logging
from opentelemetry.instrumentation.logging import LoggingInstrumentor

LoggingInstrumentor().instrument(set_logging_format=True)
# → 모든 log에 trace_id·span_id 자동 주입

logger = logging.getLogger(__name__)


async def handle_query(query: Query):
    logger.info("query received", extra={
        "user_id": query.user_id,
        "intent": query.intent,
        "agent": "qna_chatbot",
    })
    # → trace_id가 자동 포함되어 trace 시각화에서 직접 jump 가능

Engineering: structured logging 패턴 그대로 + trace_id 통합.

6 Sampling 전략

trace 100% 저장은 storage·비용 폭발. 대표성을 보장하면서 줄여야.

6.1 Head Sampling (시작 시점)

# 모든 trace 중 1%만 저장
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased

sampler = TraceIdRatioBased(0.01)

강점: 단순·결정적. 약점: 드문 사건 (에러·slow query)을 놓침.

6.2 Tail Sampling (종료 후)

전체 trace 받은 후 — 룰로 어느 것을 저장할지 결정:

def tail_sampler(trace) -> bool:
    if trace.has_error:
        return True                      # 모든 에러 trace 저장
    if trace.duration_ms > 5000:
        return True                      # slow trace 저장
    if trace.user_segment == "vip":
        return True                      # VIP 100%
    return random.random() < 0.05        # 일반 5%

강점: 의미 있는 trace 누락 X. 약점: 모든 trace를 일단 buffer해야 — 메모리 비용.

OpenTelemetry Collector가 tail sampler 구현 제공 — 별도 설치.

6.3 우선순위 Sampling

우선순위 저장률
Error 100%
Slow (> p95) 100%
Hallucination flag 100%
Bandit canary arm 50%
일반 active 1~5%
Health check 0%

7 SLO·SLI·Error Budget

# config/slo.yaml
slos:
  - name: availability
    sli: requests_success_rate
    target: 99.9                          # 월 다운 ≤ 43분
    window: 30d

  - name: latency
    sli: p95_e2e_latency_ms
    target: 3000
    window: 7d

  - name: quality
    sli: thumbs_up_rate
    target: 0.55
    window: 7d

  - name: cost
    sli: avg_cost_per_query_usd
    target: 0.025
    window: 30d

7.1 Error Budget Burn Rate

budget_remaining = (1 - target/100) - (1 - actual/100)
                 = actual/100 - target/100

burn_rate = (recent_error_rate / (1 - target/100))
burn rate 의미 알람
< 1 정상 흐름 없음
1~6 (24h) 매월 지킬 수 있음 warning
6~14 (6h) 매주 budget 소진 critical (slack)
> 14 (1h) 즉시 page on-call page

C25 alert 연동 그대로 — burn rate가 임계 위반 시 자동 page.

8 Dashboard 설계

8.1 4 Golden Signals (Google SRE)

1. Latency        — p50·p95·p99
2. Traffic        — requests per second
3. Errors         — error rate (%)
4. Saturation     — concurrent runs / capacity

8.2 LLM Extras

5. Token Usage    — input·output tokens per second
6. Cost Rate      — USD per minute, per user
7. TTFT           — p50·p95
8. Citation Rate  — 응답에 citation 포함 비율
9. Refusal Rate   — refusal·content_filter %
10. Quality       — thumbs_up_rate (실시간 7일 rolling)

8.3 Drilldown

[Top-level]
├── 4 Golden Signals (전체 · 최근 1h)
├── LLM 6 Extras
├── Active Experiments (C19) 상태
├── Bandit Arms (C16) 분포
└── SLO Burn Rate (모든 SLO)

[Per-Agent]
├── 같은 메트릭 + agent_id 필터
└── 단계별 latency breakdown

[Per-User]  (PII 권한 있는 사람만)
├── 개별 사용자 query·thumbs·cost
└── (디버깅·incident 대응)

운영팀은 [Top-level]만 매일, [Per-Agent]는 alert 발생 시 drilldown.

9 Storage·Backend 선택

데이터 권장 backend
Metrics Prometheus·Mimir·Datadog·Cloud Monitoring
Logs Loki·Elasticsearch·CloudWatch·Datadog Logs
Traces Tempo·Jaeger·Datadog APM·Honeycomb
Long-term S3·BigQuery·ClickHouse (cold storage)

OpenTelemetry는 SDK 표준 — backend는 Collector를 통해 어느 것이든 라우팅 가능.

# otel-collector-config.yaml
exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
  loki:
    endpoint: "http://loki:3100"
  tempo:
    endpoint: "http://tempo:3200"

10 C25·C33과 결합

10.1 C25 Kill Switch trigger

burn rate 임계 위반 시 — 자동으로 C25 Kill Switch trigger:

async def burn_rate_monitor():
    while True:
        for slo in load_slos():
            burn = calculate_burn_rate(slo)
            if burn > 14:
                disable_problem_segment(slo)
                page_oncall(f"SLO {slo.name} burn {burn:.1f}")
        await asyncio.sleep(60)

10.2 C33 지식 품질 결합

C33 5종 신호도 metrics로 emit:

gap_counter = meter.create_counter("knowledge_coverage_gaps")
freshness_gauge = meter.create_gauge("knowledge_stale_active_count")

운영 dashboard에 같이 표시 — 응답 품질·지식 상태가 같은 view.

11 비용·성능 trade-off

결정 비용 가시성
100% trace 저장 매우 높음 완전
Head 1% sampling 낮음 일반 패턴만
Tail sampling (error+slow+VIP) 중간 의미 있는 케이스 100%
Metrics + 1% trace 낮음~중간 통계 정확, drilldown 부족

MINERVA 권장: Tail sampling + Metrics 풀 — 비용·가시성 균형.

12 MINERVA 적용

app/observability/
├── tracing.py              # OpenTelemetry tracer 초기화
├── metrics.py              # counter·histogram·gauge 정의
├── logs.py                 # structured logging + trace_id 주입
├── llm_attrs.py            # LLM-specific span attributes
├── sampler.py              # tail sampling 룰
├── slo.py                  # SLO·SLI·burn rate 계산
└── dashboards/             # Grafana·Datadog 정의 (코드로 관리)

scripts/
├── slo_check.py           # SLO·burn rate 계산 (cron)
├── trace_replay.py         # 특정 trace 재현 (디버깅)
└── cost_audit.py           # 비용 dashboard 생성

infra/
├── otel-collector.yaml    # Collector 설정
├── prometheus.yaml         # metrics scrape
├── loki.yaml               # log aggregation
└── tempo.yaml              # trace storage

Engineering: structured logging·Docker Compose 패턴이 토대 — OpenTelemetry stack 운영.

13 자주 발생하는 함정

13.1 Cardinality Explosion

user_id를 metric label로 → 사용자 수만큼 시계열 폭증 → Prometheus 메모리 cap.

해법: - high-cardinality는 trace·log에만 (metric에는 X) - segment·percentile 같은 aggregated label만 metric에 - Prometheus relabel rule로 cap 강제

13.2 Sampling Bias

head sampling 1%로 데이터 분석 → drift 못 잡거나 false signal.

해법: - 분석은 sampled metric이 아니라 logs (raw) 또는 BigQuery·ClickHouse - 의사결정용은 100% logs, dashboard는 sampled trace

13.3 Trace Storage Cost

trace 100% 저장 → 월 수천 USD. 그러나 1% sampling은 incident 디버깅에 부족.

해법: - Tail sampling (error·slow·VIP 100%, 일반 1%) - 일반 trace TTL 7일, 의미 있는 trace 30일·90일 - 비용 dashboard로 추세 모니터링

13.4 Alert Noise

모든 메트릭에 alert → 매일 10건 → 운영팀 무시.

해법: - Burn rate 기반 (특정 임계값보다 우선) - 등급별 채널 (warning은 daily digest, critical만 page) - alert 처리율 (acknowledge·resolved) 분석 — 무시율 ↑ 시 임계 재평가

13.5 Trace Context Loss

비동기 task·subprocess에서 trace 끊김 → 단편 데이터.

해법: - opentelemetry.context로 명시적 propagate - asyncio.create_task 시 context 전달 - Celery·Kafka 등 메시지 시스템에 trace_id header inject

13.6 Local Dev vs Production 분리

개발자 local에 production OTel endpoint 연결 → noise·비용.

해법: - OTEL_EXPORTER_OTLP_ENDPOINT env로 분리 (local은 console exporter) - staging·production tag 명시

13.7 메트릭 의존 의사결정

dashboard만 보고 의사결정 → 깊이 부족·context 결여.

해법: - 알람 발생 시 즉시 trace drilldown - weekly 회고에 trace·log sample 포함 - 신호 합산 점수(C33 패턴)로 단일 메트릭 의존 회피

14 정리

영역 핵심
3 Pillar Metrics·Logs·Traces — 같은 사건의 다른 view
OpenTelemetry SDK 표준, backend 무관
LLM 특화 TTFT·token·tool call·hallucination·refusal·quality
Sampling Head(빠름·드문 사건 누락) vs Tail(meaningful 100%)
SLO·burn rate 4 단계 etl(< 1·16·614·>14)
Dashboard 4 Golden + LLM 6 Extras + drilldown
결합 C25 Kill Switch trigger·C33 지식 품질 통합 view
함정 cardinality·sampling bias·trace cost·alert noise·context loss

15 응용 분야

시나리오 핵심 신호
단계별 latency 최적화 trace 단계별 span
LLM provider 장애 대응 error counter + circuit breaker trigger
비용 spike 추적 cost histogram + segment label
Hallucination 회귀 hallucination flag + trace replay
신규 모델 도입 영향 model attribute로 비교
Bandit 분포 검증 arm label로 metric 분리
Incident 사후 분석 trace replay + log search

16 관련 주제

선행 학습 (선수)

18-LangGraph 시리즈 cross-reference

  • (Phase C-9는 일반 관측성 패턴 — LangGraph 특정 cross-ref 작음)

후속 (Phase C-9)

Cross-reference

Subscribe

Enjoy this blog? Get notified of new posts by email: