이코에코(Eco²) Knowledge Base/Troubleshooting

분산 트레이싱 트러블슈팅: OpenTelemetry 커버리지 확장

mango_fr 2025. 12. 19. 03:00

시스템 컴포넌트 OTEL 적용

개요

시스템 컴포넌트(Istio, ArgoCD)에도 분산 추적을 적용하여 trace.id를 로그에 포함.

적용 대상

시스템 OTEL 지원 적용 방법 결과
Istio (Envoy) EnvoyFilter Access log에 trace.id 포함
ArgoCD ConfigMap Jaeger에 트레이스 전송
Calico 미지원 -
Kubernetes ⚠️ 제한적 -

1. Istio Access Log with Trace ID

문제: ext-authz 거부/404 요청에 trace.id 없음

초기 설정에서 %REQ(X-B3-TRACEID)%를 사용했으나, 클라이언트가 헤더를 보내지 않으면 빈 값:

# 문제 상황
/api/v1/auth/register → 401 (ext-authz 거부) → trace.id: 없음
/api/v1/nonexistent  → 404 (라우팅 실패)   → trace.id: 없음
/api/v1/auth/refresh → 401 (앱 도달)       → trace.id: 있음

해결: 사용

Envoy 내부 변수 %TRACE_ID%를 사용하면 모든 요청에 trace가 자동 생성됨.

변수 설명 값 보장
%REQ(X-B3-TRACEID)% 클라이언트가 보낸 헤더 ❌ 없으면 빈 값
%TRACE_ID% Envoy 내부 trace ID ✅ 항상 자동 생성

 

EnvoyFilter 설정 (workloads/istio/base/envoy-filter-access-log.yaml):

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-access-log
  namespace: istio-system
spec:
  configPatches:
    - applyTo: NETWORK_FILTER
      match:
        context: ANY
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
      patch:
        operation: MERGE
        value:
          typed_config:
            "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
            access_log:
              - name: envoy.access_loggers.file
                typed_config:
                  "@type": "type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog"
                  path: "/dev/stdout"
                  log_format:
                    json_format:
                      # ECS 표준 필드명 (dot notation)
                      trace.id: "%TRACE_ID%"      # ✅ Envoy 자동 생성
                      span.id: "%REQ(X-B3-SPANID)%"
                      http.request.method: "%REQ(:METHOD)%"
                      url.path: "%REQ(:PATH)%"
                      http.response.status_code: "%RESPONSE_CODE%"
                      http.response.body.bytes: "%BYTES_SENT%"
                      start_time: "%START_TIME%"
                      duration_ms: "%DURATION%"
                      upstream_host: "%UPSTREAM_HOST%"
                      source.address: "%REQ(X-FORWARDED-FOR)%"
                      request_id: "%REQ(X-REQUEST-ID)%"

검증 결과

# %TRACE_ID% 적용 후
/api/v1/auth/register → 401 (ext-authz 거부) → trace.id: e8f47ed65ec9ece3... ✅
/api/v1/test404       → 404 (라우팅 실패)   → trace.id: e05895c1b6571707... ✅
/api/v1/auth/refresh  → 401 (앱 도달)       → trace.id: 4698731e87d0b18b... ✅

모든 요청에 trace.id가 포함되어 에러 추적 가능

출력 예시

{
  "trace.id": "e8f47ed65ec9ece3d4c629cf2374f680",
  "http.request.method": "POST",
  "url.path": "/api/v1/auth/register",
  "http.response.status_code": 401
}

2. ArgoCD OTEL 트레이싱

ConfigMap 설정 (workloads/argocd/base/otel-config.yaml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: argocd
data:
  otlp.address: jaeger-collector-clusterip.istio-system.svc.cluster.local:4317

ArgoCD 작업(sync, refresh 등)이 Jaeger에 트레이스로 표시됨

적용 명령

# Istio 설정 적용
kubectl apply -f workloads/istio/base/

# ArgoCD 설정 적용 후 재시작
kubectl apply -f workloads/argocd/base/
kubectl rollout restart deployment argocd-server -n argocd
kubectl rollout restart deployment argocd-repo-server -n argocd
kubectl rollout restart statefulset argocd-application-controller -n argocd

Kibana 검색

# istio-proxy 로그에서 특정 trace 검색
trace_id:15434b0153e43190afcbfb316469ccfe AND k8s_container_name:istio-proxy

# 앱 로그 + istio-proxy 같이 검색
trace_id:* AND (service.name:auth-api OR k8s_container_name:istio-proxy)

커밋

feat(istio): add EnvoyFilter for JSON access log with trace.id

- Enable structured JSON access logging via EnvoyFilter
- Include trace.id, span.id, request_id in access logs
- Add Telemetry API configuration for mesh-wide access logging
- Use ECS standard field names (dot notation)
fix(istio): use %TRACE_ID% for all requests including ext-authz denials

- Change from %REQ(X-B3-TRACEID)% to %TRACE_ID% in EnvoyFilter
- %TRACE_ID% is auto-generated by Envoy for all requests
- Enables trace correlation for 401 (ext-authz denied) and 404 errors
- Before: ext-authz denied requests had no trace.id
- After: all requests have trace.id for full error tracking
feat(argocd): enable OTEL tracing to Jaeger

- Configure otlp.address in argocd-cmd-params-cm
- ArgoCD operations now visible in Jaeger

OpenTelemetry 커버리지 분석

OTEL이 커버하는 범위

컴포넌트 방식 trace.id 지원 비고
Python API (auth, chat 등) OTEL SDK 자동 계측 opentelemetry-instrument
Istio Sidecar Envoy 내장 tracing %TRACE_ID% 변수
Istio Ingress Gateway Envoy 내장 tracing Trace 생성 원점
ArgoCD 내장 OTLP 지원 otlp.address 설정

OTEL이 커버하지 않는 범위

ext-authz (Go gRPC) OTEL SDK 미적용 gRPC 메타데이터에서 B3 헤더 추출
Calico 네트워크 레이어 N/A (trace 불필요)
Kubernetes 컴포넌트 제한적 지원 N/A

gRPC 서비스 Trace 추적 (ext-authz)

문제 상황

ext-authz는 Go로 작성된 gRPC 서비스로, Python API처럼 OTEL 자동 계측이 불가능.

초기 상태:
- ext-authz 로그에 trace.id 없음
- istio-proxy 로그에만 trace.id 존재
- 인증 실패 원인 추적 시 trace 연결 불가

해결: gRPC 메타데이터에서 Trace Context 추출

Istio sidecar가 ext-authz로 gRPC 요청 시 메타데이터에 B3 헤더를 주입한다.

1. 상수 정의

const (
    // B3 Trace Context headers (Istio/Envoy)
    HeaderB3TraceID = "x-b3-traceid"
    HeaderB3SpanID  = "x-b3-spanid"
)

2. gRPC 메타데이터 추출

import "google.golang.org/grpc/metadata"

// extractTraceInfo extracts B3 trace context from gRPC metadata
func extractTraceInfo(ctx context.Context, req *authv3.CheckRequest) logging.TraceInfo {
    trace := logging.TraceInfo{}

    // 1. gRPC metadata (Istio sidecar가 주입)
    if md, ok := metadata.FromIncomingContext(ctx); ok {
        if vals := md.Get("x-b3-traceid"); len(vals) > 0 {
            trace.TraceID = vals[0]
        }
        if vals := md.Get("x-b3-spanid"); len(vals) > 0 {
            trace.SpanID = vals[0]
        }
    }

    // 2. Fallback: HTTP 헤더 (클라이언트가 직접 전송한 경우)
    if trace.TraceID == "" && req.Attributes != nil {
        headers := req.Attributes.Request.Http.Headers
        trace.TraceID = headers["x-b3-traceid"]
    }

    return trace
}

3. 로그에 trace.id 포함

func (l *Logger) WithTrace(traceID, spanID string) *Logger {
    if traceID == "" {
        return l
    }
    return &Logger{
        Logger: l.With(
            slog.String("trace.id", traceID),
            slog.String("span.id", spanID),
        ),
    }
}

결과

{
  "@timestamp": "2025-12-18T12:02:06.845Z",
  "service.name": "ext-authz",
  "trace.id": "a593d6809fe6f036728dc73cfd170b0e",
  "span.id": "3e491beac3443f3c",
  "msg": "Authorization denied",
  "event.outcome": "failure",
  "event.reason": "missing_auth_header"
}

전체 요청 흐름 추적 (동일 trace.id)

trace.id:a593d6809fe6f036728dc73cfd170b0e
12:02:06.845 ext-authz Authorization denied
12:02:06.846 istio-proxy gRPC /Authorization/Check → 200
12:02:07.742 istio-proxy HTTP /api/v1/auth/register → 401

Trace 전파 경로 요약

┌─────────────────────────────────────────────────────────────────────────┐
│                       Trace ID Propagation                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  [Client Request]                                                       │
│       │                                                                 │
│       ▼                                                                 │
│  ┌──────────────────┐                                                  │
│  │ Istio Ingress    │ ◀── trace.id 생성 (%TRACE_ID%)                   │
│  │ Gateway          │                                                   │
│  └────────┬─────────┘                                                  │
│           │ gRPC + B3 메타데이터                                        │
│           ▼                                                             │
│  ┌──────────────────┐                                                  │
│  │ ext-authz        │ ◀── gRPC metadata에서 trace.id 추출              │
│  │ (Go gRPC)        │     → 로그에 포함                                │
│  └────────┬─────────┘                                                  │
│           │ 인증 결과                                                   │
│           ▼                                                             │
│  ┌──────────────────┐                                                  │
│  │ App Sidecar      │ ◀── X-B3-TraceId 헤더 전파                       │
│  │ (istio-proxy)    │                                                   │
│  └────────┬─────────┘                                                  │
│           │ HTTP + B3 헤더                                              │
│           ▼                                                             │
│  ┌──────────────────┐                                                  │
│  │ App (Python)     │ ◀── OTEL SDK가 B3 헤더 읽음                      │
│  │ + OTEL SDK       │     → 동일 trace.id로 span 생성                  │
│  └──────────────────┘                                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

커밋

feat(ext-authz): add B3 trace context to authorization logs

- Extract x-b3-traceid from gRPC metadata (Istio sidecar injects here)
- Fallback to HTTP headers if client sent them
- Add trace.id and span.id to all authorization log entries
- Enables end-to-end trace correlation in Kibana