- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 블루프린트가 필요한 이유
- 전체 아키텍처
- Collector 설정: Agent (DaemonSet)
- Collector 설정: Gateway
- 애플리케이션 계측: Python
- Cross-Signal Correlation: 세 신호를 연결하는 방법
- Cost Management: Sampling Strategy
- Phased Adoption Roadmap
- Troubleshooting
- Quiz
- References

블루프린트가 필요한 이유
관측성 시스템을 설계할 때 가장 흔한 실수는 Metrics, Logs, Traces를 개별 도구로 따로 구축하는 것이다. Prometheus로 메트릭을 수집하고, ELK로 로그를 모으고, Jaeger로 트레이스를 저장하면 세 가지 데이터가 제각각 존재한다. 장애가 발생하면 "메트릭에서 에러율 상승 확인 -> 로그에서 에러 메시지 검색 -> 트레이스에서 느린 요청 추적"을 수동으로 연결해야 하고, 이 과정에서 평균 15-30분이 소비된다.
OpenTelemetry(OTel)는 이 세 가지 신호(signal)를 하나의 SDK, 하나의 프로토콜(OTLP), 하나의 속성 체계(semantic conventions)로 통합한다. 이 블루프린트는 OTel 기반으로 Metrics, Logs, Traces를 통합 구축하는 전체 아키텍처를 설계한다.
전체 아키텍처
[Application Pods]
┌──────────────────┐
│ OTel SDK │
│ (Auto + Manual) │
│ ┌─────────────┐ │
│ │ Traces │ │
│ │ Metrics │ │
│ │ Logs │ │
│ └──────┬──────┘ │
└─────────┼────────┘
│ OTLP (gRPC)
▼
┌──────────────────┐
│ OTel Collector │
│ (DaemonSet) │
│ ┌────────────┐ │
│ │ Receivers │ │
│ │ Processors │ │
│ │ Exporters │ │
│ └────────────┘ │
└────────┬─────────┘
│ OTLP
▼
┌──────────────────┐
│ OTel Collector │
│ (Gateway) │
│ - Sampling │
│ - Enrichment │
│ - Routing │
└──┬─────┬──────┬──┘
│ │ │
┌──────┘ │ └──────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tempo │ │ Mimir │ │ Loki │
│ (Traces) │ │ (Metrics)│ │ (Logs) │
└──────────┘ └──────────┘ └──────────┘
└──────────┼──────────┘
▼
┌──────────────┐
│ Grafana │
│ (Unified) │
└──────────────┘
아키텍처 설계 원칙
- Agent-Gateway 2-tier 구조: DaemonSet Collector(agent)가 로컬에서 수집하고, Gateway Collector가 중앙에서 처리/라우팅한다. Agent는 가볍게, Gateway에서 무거운 처리를 담당한다.
- OTLP 단일 프로토콜: 모든 신호가 OTLP로 전송되므로 네트워크 설정이 단순화된다.
- Semantic Conventions 준수:
service.name,service.version,deployment.environment등 표준 속성을 모든 신호에 동일하게 적용하여 cross-signal correlation을 가능하게 한다.
Collector 설정: Agent (DaemonSet)
# otel-collector-agent.yaml
# DaemonSet으로 배포. 각 노드에서 로컬 수집 담당.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-agent
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Kubernetes 노드/파드 메트릭 수집
kubeletstats:
collection_interval: 30s
auth_type: serviceAccount
endpoint: "https://${env:NODE_IP}:10250"
insecure_skip_verify: true
metric_groups:
- node
- pod
- container
# 호스트 메트릭 (CPU, 메모리, 디스크)
hostmetrics:
collection_interval: 30s
scrapers:
cpu:
metrics:
system.cpu.utilization:
enabled: true
memory:
metrics:
system.memory.utilization:
enabled: true
disk: {}
network: {}
processors:
# 메모리 사용량 제한 (Agent는 가볍게)
memory_limiter:
check_interval: 5s
limit_mib: 512
spike_limit_mib: 128
# Kubernetes 메타데이터 자동 추가
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.namespace.name
- k8s.node.name
- k8s.deployment.name
labels:
- tag_name: app
key: app.kubernetes.io/name
- tag_name: version
key: app.kubernetes.io/version
# 리소스 속성 추가 (모든 신호에 공통 적용)
resource:
attributes:
- key: deployment.environment
value: "${env:DEPLOY_ENV}"
action: upsert
- key: cloud.region
value: "${env:CLOUD_REGION}"
action: upsert
batch:
send_batch_size: 1024
timeout: 5s
exporters:
otlp:
endpoint: otel-collector-gateway:4317
tls:
insecure: false
ca_file: /etc/ssl/certs/ca.crt
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [otlp]
metrics:
receivers: [otlp, kubeletstats, hostmetrics]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [otlp]
Collector 설정: Gateway
# otel-collector-gateway.yaml
# Deployment로 배포. 중앙 처리 및 라우팅 담당.
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-gateway
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
memory_limiter:
check_interval: 5s
limit_mib: 4096
spike_limit_mib: 1024
# Tail-based sampling (Gateway에서만 수행)
# 모든 trace를 저장하면 비용이 폭발하므로 지능적 샘플링 필수
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
# 에러가 포함된 trace는 100% 보존
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
# 느린 요청은 100% 보존 (P95 이상)
- name: latency-policy
type: latency
latency:
threshold_ms: 1000
# 정상 요청은 10% 샘플링
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
# 불필요한 속성 제거 (비용 절감)
attributes/remove:
actions:
- key: http.request.header.authorization
action: delete
- key: http.request.header.cookie
action: delete
- key: db.statement
action: hash # SQL문은 해싱하여 개인정보 보호
batch:
send_batch_size: 2048
timeout: 10s
exporters:
# Traces -> Grafana Tempo
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Metrics -> Grafana Mimir (Prometheus 호환)
prometheusremotewrite:
endpoint: http://mimir:9009/api/v1/push
resource_to_telemetry_conversion:
enabled: true
# Logs -> Grafana Loki
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, attributes/remove, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, attributes/remove, batch]
exporters: [loki]
애플리케이션 계측: Python
Auto-instrumentation + 수동 span 추가
# app/tracing.py
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.resource import ResourceAttributes
import logging
def setup_observability(
service_name: str,
service_version: str,
otlp_endpoint: str = "http://otel-collector:4317",
):
"""OpenTelemetry 초기화. 애플리케이션 시작 시 한 번 호출."""
# 리소스 정의 (모든 신호에 공통 적용되는 속성)
resource = Resource.create({
ResourceAttributes.SERVICE_NAME: service_name,
ResourceAttributes.SERVICE_VERSION: service_version,
ResourceAttributes.DEPLOYMENT_ENVIRONMENT: "production",
"team.name": "platform",
})
# --- Traces ---
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True),
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
trace.set_tracer_provider(tracer_provider)
# --- Metrics ---
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=otlp_endpoint, insecure=True),
export_interval_millis=30000, # 30초마다 export
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
# --- Logs (OTel Logs Bridge) ---
# Python logging을 OTel로 브릿지
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry._logs import set_logger_provider
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint=otlp_endpoint, insecure=True)
)
)
set_logger_provider(logger_provider)
# Python logging handler 연결
from opentelemetry.sdk._logs import LoggingHandler
handler = LoggingHandler(logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
비즈니스 로직에 수동 계측 추가
# app/services/order_service.py
from opentelemetry import trace, metrics
import logging
tracer = trace.get_tracer("order-service", "1.0.0")
meter = metrics.get_meter("order-service", "1.0.0")
logger = logging.getLogger(__name__)
# 비즈니스 메트릭 정의
order_counter = meter.create_counter(
name="orders.created",
description="생성된 주문 수",
unit="1",
)
order_amount_histogram = meter.create_histogram(
name="orders.amount",
description="주문 금액 분포",
unit="KRW",
)
payment_duration = meter.create_histogram(
name="payment.duration",
description="결제 처리 시간",
unit="s",
)
async def create_order(user_id: str, items: list, payment_method: str):
"""주문 생성 - Traces, Metrics, Logs가 모두 연결된 예시"""
# 1. 상위 span 생성
with tracer.start_as_current_span(
"create_order",
attributes={
"user.id": user_id,
"order.item_count": len(items),
"payment.method": payment_method,
},
) as span:
total_amount = sum(item["price"] * item["qty"] for item in items)
span.set_attribute("order.total_amount", total_amount)
# 2. 재고 확인 (자식 span)
with tracer.start_as_current_span("check_inventory") as inv_span:
for item in items:
available = await check_stock(item["sku"], item["qty"])
if not available:
inv_span.set_attribute("inventory.out_of_stock_sku", item["sku"])
# 로그에도 trace context가 자동 포함됨
logger.warning(
f"재고 부족: SKU={item['sku']}, 요청={item['qty']}",
extra={"sku": item["sku"], "requested_qty": item["qty"]},
)
span.set_status(trace.StatusCode.ERROR, "재고 부족")
raise OutOfStockError(item["sku"])
# 3. 결제 처리 (자식 span)
import time
payment_start = time.monotonic()
with tracer.start_as_current_span(
"process_payment",
attributes={"payment.method": payment_method},
) as pay_span:
try:
result = await payment_gateway.charge(total_amount, payment_method)
pay_span.set_attribute("payment.transaction_id", result.tx_id)
logger.info(
f"결제 성공: tx_id={result.tx_id}, amount={total_amount}",
extra={"tx_id": result.tx_id, "amount": total_amount},
)
except PaymentError as e:
pay_span.set_status(trace.StatusCode.ERROR, str(e))
logger.error(f"결제 실패: {e}", exc_info=True)
raise
finally:
elapsed = time.monotonic() - payment_start
payment_duration.record(elapsed, {"payment.method": payment_method})
# 4. 메트릭 기록
order_counter.add(1, {
"payment.method": payment_method,
"order.status": "created",
})
order_amount_histogram.record(total_amount, {
"payment.method": payment_method,
})
return {"order_id": "ORD-12345", "status": "created"}
Cross-Signal Correlation: 세 신호를 연결하는 방법
OTel의 진정한 가치는 세 신호가 연결될 때 나타난다. The key point is 모든 신호에 동일한 trace_id를 포함하는 것이다.
Grafana에서의 Correlation 설정
# grafana/provisioning/datasources/datasources.yaml
apiVersion: 1
datasources:
- name: Tempo
type: tempo
url: http://tempo:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki
filterByTraceID: true
filterBySpanID: true
tags:
- key: service.name
value: service_name
tracesToMetrics:
datasourceUid: mimir
tags:
- key: service.name
value: service
serviceMap:
datasourceUid: mimir
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- name: TraceID
datasourceUid: tempo
matcherRegex: "trace_id=(\\w+)"
url: '$${__value.raw}'
matcherType: regex
- name: Mimir
type: prometheus
url: http://mimir:9009/prometheus
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
Correlation 실전 시나리오
1. Grafana 대시보드에서 에러율 스파이크 발견
-> Mimir 쿼리: rate(http_requests_total{status="500"}[5m])
2. 해당 시간대의 에러 요청에 포함된 exemplar 클릭
-> Exemplar에 포함된 trace_id로 Tempo 이동
3. Tempo에서 전체 trace 확인
-> 어떤 서비스의 어떤 span에서 에러 발생했는지 시각화
-> span의 attributes에서 user.id, order.id 등 비즈니스 컨텍스트 확인
4. 해당 span과 같은 trace_id를 가진 로그 확인
-> Loki 쿼리: {service_name="order-service"} | trace_id="abc123..."
-> 에러 스택트레이스, 입력 데이터, 중간 상태값 확인
5. 전체 진단 시간: 2-3분 (기존: 15-30분)
Cost Management: Sampling Strategy
관측성 데이터의 비용은 주로 스토리지와 네트워크에서 발생한다. 모든 데이터를 100% 저장하면 월 수백만 원의 비용이 될 수 있다.
신호별 샘플링 전략
| 신호 | 전략 | 비율 | 비고 |
|---|---|---|---|
| Traces (정상) | Tail-based probabilistic | 10% | Gateway Collector에서 결정 |
| Traces (에러) | 100% 보존 | 100% | 에러 trace는 must 보존 |
| Traces (느린 요청) | 100% 보존 | 100% | P95 이상 latency는 보존 |
| Metrics | 전량 수집 | 100% | 카디널리티 관리로 비용 통제 |
| Logs (ERROR 이상) | 전량 수집 | 100% | 에러 로그는 must 보존 |
| Logs (INFO) | Probabilistic | 20% | 정상 로그는 샘플링 |
| Logs (DEBUG) | Production에서 비활성화 | 0% | 필요 시 동적 활성화 |
메트릭 카디널리티 관리
# Collector에서 높은 카디널리티 속성 제거
processors:
# user_id, session_id 같은 high-cardinality label 제거
# 메트릭에는 사용하지 않아야 함 (trace에서 사용)
transform/metrics:
metric_statements:
- context: datapoint
statements:
- delete_key(attributes, "user.id")
- delete_key(attributes, "session.id")
- delete_key(attributes, "request.id")
- delete_key(attributes, "http.url") # path parameter 포함 시 카디널리티 폭발
비용 추정 모델
def estimate_monthly_cost(
daily_requests: int,
avg_spans_per_trace: int = 8,
avg_log_lines_per_request: int = 5,
trace_sample_rate: float = 0.10,
log_sample_rate: float = 0.20,
) -> dict:
"""월간 관측성 데이터 비용 추정"""
monthly_requests = daily_requests * 30
# Traces
traces_per_month = monthly_requests * trace_sample_rate
spans_per_month = traces_per_month * avg_spans_per_trace
trace_storage_gb = spans_per_month * 0.5 / 1e6 # span당 약 0.5KB
# Metrics (항상 100%, 카디널리티로 관리)
# 서비스 10개, 메트릭 50종, 카디널리티 100 = 50,000 시계열
metric_series = 50_000
metric_storage_gb = metric_series * 30 * 24 * 2 * 8 / 1e9 # 데이터포인트당 8B
# Logs
log_lines_per_month = monthly_requests * avg_log_lines_per_request * log_sample_rate
log_storage_gb = log_lines_per_month * 0.2 / 1e6 # 로그라인당 약 0.2KB
# 비용 계산 (Grafana Cloud 기준 대략적 단가)
trace_cost = trace_storage_gb * 2.0 # $2/GB
metric_cost = metric_series * 0.008 # $8 per 1000 series
log_cost = log_storage_gb * 0.50 # $0.50/GB
return {
"traces": {
"sampled_per_month": int(traces_per_month),
"storage_gb": round(trace_storage_gb, 1),
"cost_usd": round(trace_cost, 2),
},
"metrics": {
"active_series": metric_series,
"storage_gb": round(metric_storage_gb, 1),
"cost_usd": round(metric_cost, 2),
},
"logs": {
"sampled_lines_per_month": int(log_lines_per_month),
"storage_gb": round(log_storage_gb, 1),
"cost_usd": round(log_cost, 2),
},
"total_monthly_usd": round(trace_cost + metric_cost + log_cost, 2),
}
# 일 100만 요청 서비스 기준
cost = estimate_monthly_cost(daily_requests=1_000_000)
print(f"월간 총 비용 추정: ${cost['total_monthly_usd']}")
Phased Adoption Roadmap
한 번에 모든 것을 도입하면 실패한다. 4단계로 나누어 점진적으로 구축한다.
Phase 1 (2주): Traces 도입
목표: 핵심 서비스 3개에 distributed tracing 활성화
작업:
1. OTel Collector DaemonSet + Gateway 배포
2. 핵심 서비스에 OTel SDK 추가 (auto-instrumentation 우선)
3. Tempo 백엔드 배포
4. Grafana에서 trace 검색 확인
성공 기준:
- 서비스 간 trace가 연결되어 보이는가
- P95 latency가 높은 요청의 병목 구간을 trace에서 식별할 수 있는가
Phase 2 (2주): Metrics 통합
목표: Prometheus 메트릭을 OTel 파이프라인으로 통합
작업:
1. OTel Collector에 Prometheus receiver 추가
2. 기존 Prometheus 서버에서 remote_write로 Mimir 전환
3. Exemplar 연결 설정 (메트릭 -> 트레이스)
4. 기존 Grafana 대시보드 마이그레이션
성공 기준:
- 메트릭 대시보드에서 exemplar 클릭으로 trace 이동 가능한가
- 기존 알림이 동일하게 동작하는가
Phase 3 (2주): Logs 통합
목표: 구조화 로그를 OTel로 수집하고 trace와 연결
작업:
1. 애플리케이션 로깅에 OTel Logs Bridge 적용
2. 로그에 trace_id, span_id 자동 삽입 확인
3. Loki 백엔드 배포 및 Collector 연결
4. Grafana에서 trace <-> log 양방향 연결 설정
성공 기준:
- trace에서 로그로, 로그에서 trace로 원클릭 이동 가능한가
- 에러 로그의 trace context로 전체 요청 흐름을 추적할 수 있는가
Phase 4 (2주): 최적화 및 표준화
목표: 비용 최적화, 샘플링 튜닝, 팀 온보딩
작업:
1. Tail-based sampling 활성화 및 비용 30% 절감 확인
2. Semantic conventions 표준 문서화
3. 팀별 대시보드 템플릿 배포
4. 온콜 런북에 관측성 워크플로 추가
성공 기준:
- MTTD(Mean Time To Detect) 50% 개선
- MTTR(Mean Time To Resolve) 30% 개선
- 관측성 데이터 비용이 인프라 비용의 5% 이내
Troubleshooting
1. Trace가 중간에 끊김 (Broken Trace)
증상: Grafana Tempo에서 trace를 열면 일부 서비스의 span이 누락
진단:
# 1. Context propagation 확인 - HTTP 헤더에 traceparent가 전달되는지
curl -v http://service-a/api/test 2>&1 | grep -i traceparent
# traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# 2. 서비스 B가 해당 헤더를 수신하고 전파하는지 확인
# 서비스 B의 로그에서 trace_id 검색
# 3. Collector에서 span이 drop되지 않는지 확인
curl -s http://otel-collector:8888/metrics | grep otelcol_exporter_sent_spans
원인과 해결:
- HTTP 클라이언트가 traceparent 헤더를 전달하지 않음 -> OTel HTTP instrumentation 라이브러리 확인
- 메시지 큐(Kafka, RabbitMQ) 구간에서 context가 전파되지 않음 -> 큐 producer/consumer에 OTel instrumentation 추가
- gRPC 서비스에서 metadata 전달 누락 -> gRPC interceptor 확인
2. Collector OOM (Out of Memory)
Error: memory usage exceeded limit: 512 MiB
해결:
# memory_limiter 설정 확인 및 조정
processors:
memory_limiter:
check_interval: 5s
limit_mib: 512 # Agent는 512MB 이내
spike_limit_mib: 128
# limit 도달 시 데이터를 drop하므로
# 충분한 메모리를 할당하되, 노드에 영향 주지 않을 범위로
# batch 크기 줄이기
batch:
send_batch_size: 512 # 2048에서 512로 줄임
timeout: 5s
3. 메트릭 카디널리티 폭발
증상: Mimir 또는 Prometheus의 메모리 사용량이 급증하고 쿼리 속도 저하
진단:
# 카디널리티가 높은 메트릭 찾기
topk(10, count by (__name__)({__name__=~".+"}))
# 특정 메트릭의 label 카디널리티 확인
count(http_requests_total) by (url)
# url에 path parameter가 포함되어 수만 개의 시계열이 생성됨
해결: Collector의 transform processor에서 high-cardinality label 제거 또는 정규화
4. 로그에 trace_id가 포함되지 않음
원인: OTel Logs Bridge를 설정했지만 logging 라이브러리의 formatter가 trace context를 출력하지 않음
해결 (Python):
import logging
from opentelemetry import trace
class TraceContextFilter(logging.Filter):
def filter(self, record):
span = trace.get_current_span()
ctx = span.get_span_context()
record.trace_id = format(ctx.trace_id, '032x') if ctx.trace_id else ""
record.span_id = format(ctx.span_id, '016x') if ctx.span_id else ""
return True
handler = logging.StreamHandler()
handler.addFilter(TraceContextFilter())
handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s [trace_id=%(trace_id)s span_id=%(span_id)s] %(message)s'
))
logging.getLogger().addHandler(handler)
Quiz
Q1. OTel Collector를 Agent-Gateway 2-tier로 구성하는 이유는?
정답: ||Agent(DaemonSet)는 각 노드에서 가볍게 수집하고 k8s 메타데이터를 추가하며,
Gateway(Deployment)는 중앙에서 tail-based sampling, 속성 변환 등 무거운 처리를 담당한다. 단일
tier에서 모든 처리를 하면 각 노드의 리소스 소비가 커지고, 샘플링 결정에 필요한 전체 trace 정보를
로컬에서 가질 수 없다.||
Q2. Tail-based sampling이 Head-based보다 나은 점은?
정답: ||Head-based sampling은 trace 시작 시점에 샘플링을 결정하므로 에러나 느린 요청을 놓칠 수
있다. Tail-based는 trace가 완성된 후 결정하므로, 에러 trace는 100% 보존하고 정상 trace만
샘플링하여 비용과 품질을 모두 잡을 수 있다.||
Q3. 메트릭에 user_id를 label로 사용하면 안 되는 이유는?
정답: ||user_id는 high-cardinality 값이므로 시계열 수가 사용자 수만큼 폭발한다. 100만 사용자
서비스에서 메트릭 하나에 user_id label을 추가하면 100만 개의 시계열이 생성되어 스토리지 비용과
쿼리 성능이 급격히 악화된다. user_id는 trace의 span attribute로 기록해야 한다.||
Q4. Exemplar의 역할은 무엇인가?
정답: ||Exemplar는 메트릭 데이터 포인트에 연결된 trace_id 참조다. 메트릭 대시보드에서 에러율
스파이크를 보고 해당 시점의 exemplar를 클릭하면 실제 에러가 발생한 trace로 바로 이동할 수 있다.
이것이 Metrics -> Traces correlation의 핵심 메커니즘이다.||
Q5. Semantic Conventions를 팀 간 통일해야 하는 이유는?
정답: ||서비스 A가 service.name="payment"를, 서비스 B가 service_name="payment-api"를 사용하면
cross-signal correlation이 불가능하다. Grafana에서 trace -> log 연결 시 동일한 속성 키와 값으로
매칭하므로, 속성 체계가 통일되지 않으면 관측성 시스템의 핵심 가치인 상관 분석이 작동하지 않는다.||
Q6. 관측성 데이터 비용이 인프라 비용의 몇 퍼센트를 넘지 않아야 하는가?
정답: ||일반적으로 5-10%가 적정선이다. 이를 초과하면 샘플링 비율 조정, 보존 기간 단축, 카디널리티
최적화가 필요하다. 단, 비용 절감을 위해 에러 trace나 에러 로그를 샘플링하면 장애 대응 능력이
저하되므로 requires caution.||