이코에코(Eco²)/Observability
이코에코(Eco²) Observability #6: Log-Trace 연동 및 Kibana 검색 구조
mango_fr
2025. 12. 19. 03:10

개요
분산 시스템에서 로그와 트레이스를 연결하는 것은 디버깅의 핵심입니다.
이 문서에서는 Kibana에서 trace.id로 로그를 검색할 수 있도록 구성한 과정과 현재 구현 상태를 다룹니다.
현재 클러스터 상태
Trace 커버리지 통계

| 전체 로그 | 1,750,699 |
| trace.id 있는 로그 | 125,398 |
| 커버리지 | 7.16% |
서비스별 trace.id 분포
| istio-proxy | 125,179 | 99.8% |
| chat-api | 40 | 0.03% |
| scan-api | 34 | 0.03% |
| ext-authz | 11 | 0.01% |
| auth-api | 10 | 0.01% |
| image-api | 4 | - |
| location-api | 2 | - |
| my-api | 1 | - |
istio-proxy (EnvoyFilter)가 대부분의 trace를 생성. 앱 로그는 요청 처리 시에만 trace.id 포함.
현재 구현 구조
Trace 생성 흐름

컴포넌트별 trace.id 지원
| 컴포넌트 | trace.id 소스 | 구현 방식 | 상태 |
|---|---|---|---|
| Istio Gateway | %TRACE_ID% |
EnvoyFilter | ✅ |
| istio-proxy (Sidecar) | %TRACE_ID% |
EnvoyFilter | ✅ |
| ext-authz (Go gRPC) | gRPC metadata | 수동 추출 | ✅ |
| Python APIs | OTEL SDK | 자동 계측 | ✅ |
| 시스템 로그 | N/A | 미지원 | ❌ |
실제 로그 예시
1. istio-proxy 로그 (EnvoyFilter)
{
"@timestamp": "2025-12-18T18:05:28.383Z",
"service.name": "istio-proxy",
"trace.id": "c8fd3e757ca339685a7309846a5821b6",
"http.request.method": "GET",
"url.path": "/healthz/ready",
"http.response.status_code": 200
}
2. ext-authz 로그 (gRPC metadata 추출)
{
"@timestamp": "2025-12-18T12:02:06.845Z",
"service.name": "ext-authz",
"trace.id": "a593d6809fe6f036728dc73cfd170b0e",
"span.id": "3e491beac3443f3c",
"msg": "Authorization denied",
"event.outcome": "failure"
}
3. Python App 로그 (OTEL SDK)
{
"@timestamp": "2025-12-18T09:31:11.811+00:00",
"service.name": "auth-api",
"trace.id": "5fdc8e113b2618f6006a00c89347d78a",
"span.id": "44ea44a93a45564f",
"log.level": "warning",
"message": "HTTP 401 UNAUTHORIZED: Missing refresh token"
}
핵심 구현 상세
1. EnvoyFilter (Istio Access Log)
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:
# ✅ %TRACE_ID% - 헤더 없어도 Envoy가 자동 생성
trace.id: '%TRACE_ID%'
span.id: '%REQ(X-B3-SPANID)%'
http.request.method: '%REQ(:METHOD)%'
url.path: '%REQ(:PATH)%'
http.response.status_code: '%RESPONSE_CODE%'
%TRACE_ID% vs %REQ(X-B3-TRACEID)% 비교
| 변수 | 값 보장 | 설명 |
|---|---|---|
%REQ(X-B3-TRACEID)% |
❌ | 클라이언트가 보낸 헤더만 |
%TRACE_ID% |
✅ | Envoy가 항상 자동 생성 |
2. Index Template (ECS 호환)
subobjects: false로 dot notation 필드명 유지:
{
"index_patterns": ["logs-*"],
"template": {
"mappings": {
"subobjects": false,
"properties": {
"trace.id": { "type": "keyword" },
"span.id": { "type": "keyword" },
"log.level": { "type": "keyword" },
"service.name": { "type": "keyword" }
}
}
}
}
3. Fluent Bit 설정
[OUTPUT]
Name es
Match kube.*
Replace_Dots Off # ✅ ECS dot notation 유지
...
Kibana 검색 가이드
검색 쿼리 예시
# 특정 trace의 모든 로그 (cross-service)
trace.id:5fdc8e113b2618f6006a00c89347d78a
# 특정 서비스의 에러 로그
service.name:auth-api AND log.level:error
# istio-proxy 401 에러
service.name:istio-proxy AND http.response.status_code:401
# 앱 로그만 (시스템 제외)
trace.id:* AND service.name:(auth-api OR scan-api OR chat-api)
Jaeger ↔ Kibana 연동 워크플로우

실제 사용
- Jaeger:
https://jaeger.dev.growbin.app/trace/{trace_id} - Kibana:
https://kibana.dev.growbin.app/app/discover→trace.id:{trace_id}
ECS 필드 매핑 현황
앱 로그 필드 (Python OTEL)
| 필드 | 타입 | 소스 | 예시 |
|---|---|---|---|
trace.id |
keyword | OTEL SDK | 5fdc8e113b2618f6... |
span.id |
keyword | OTEL SDK | 44ea44a93a45564f |
service.name |
keyword | App 코드 | auth-api |
service.version |
keyword | App 코드 | 1.0.7 |
log.level |
keyword | App 코드 | info, warning, error |
message |
text | App 코드 | 로그 메시지 |
Istio 로그 필드 (EnvoyFilter)
| 필드 | 타입 | 소스 | 예시 |
|---|---|---|---|
trace.id |
keyword | %TRACE_ID% |
c8fd3e757ca33968... |
span.id |
keyword | %REQ(X-B3-SPANID)% |
B3 헤더 또는 빈값 |
http.request.method |
keyword | %REQ(:METHOD)% |
GET, POST |
url.path |
keyword | %REQ(:PATH)% |
/api/v1/auth/refresh |
http.response.status_code |
integer | %RESPONSE_CODE% |
200, 401, 500 |
시스템 로그 필드 (Lua 자동 생성)
| 필드 | 타입 | 소스 | 예시 |
|---|---|---|---|
service.name |
keyword | K8s 라벨 | calico-node, argocd-server |
service.environment |
keyword | namespace | kube-system, argocd |
kubernetes.namespace |
keyword | K8s 메타 | kube-system |
kubernetes.pod.name |
keyword | K8s 메타 | calico-node-xv9c8 |
Trace Propagation 아키텍처
┌─────────────────────────────────────────────────────────────────────────┐
│ Trace ID Propagation │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ [Client Request] │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Istio Ingress │ ◀── trace.id 생성 (%TRACE_ID%) │
│ │ Gateway │ → ES에 access log 전송 │
│ └────────┬─────────┘ │
│ │ gRPC + B3 메타데이터 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ ext-authz │ ◀── gRPC metadata에서 trace.id 추출 │
│ │ (Go gRPC) │ → ES에 auth log 전송 │
│ └────────┬─────────┘ │
│ │ 인증 결과 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ App Sidecar │ ◀── B3 헤더 전파 │
│ │ (istio-proxy) │ → ES에 access log 전송 │
│ └────────┬─────────┘ │
│ │ HTTP + B3 헤더 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ App (Python) │ ◀── OTEL SDK가 B3 헤더 읽음 │
│ │ + OTEL SDK │ → ES에 app log 전송 (동일 trace.id) │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
트러블슈팅
Issue 1: trace.id로 검색 안됨
증상:
Kibana: trace.id:xxx → No results
원인: Fluent Bit Merge_Log_Key 설정으로 필드가 중첩됨
해결: Nest lift 필터 추가
[FILTER]
Name nest
Match kube.*
Operation lift
Nested_under log_processed
Issue 2: ECS dot notation이 object로 변환됨
증상:
// 의도: "trace.id": "abc"
// 실제: "trace": { "id": "abc" }
해결:
- Index Template:
subobjects: false - Fluent Bit:
Replace_Dots Off
Issue 3: ext-authz 거부 시 trace.id 없음
증상: 401 에러에 trace.id가 없어서 추적 불가
해결: gRPC metadata에서 B3 헤더 추출
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-b3-traceid"); len(vals) > 0 {
trace.TraceID = vals[0]
}
}
OpenTelemetry 커버리지 요약
OTEL이 커버하는 범위
| Python APIs | OTEL SDK 자동 계측 | ✅ | ✅ |
| Istio Sidecar | Envoy 내장 tracing | ✅ | ✅ |
| Istio Gateway | Envoy 내장 tracing | ✅ | ✅ |
OTEL이 커버하지 않는 범위
| ext-authz (Go gRPC) | SDK 미적용 | gRPC metadata 수동 추출 |
| 시스템 로그 (calico 등) | 지원 안함 | N/A (trace 불필요) |
Reference
결론
| trace.id 커버리지 | 7.16% (주로 istio-proxy) |
| ECS 필드명 | ✅ dot notation 유지 |
| Cross-service 검색 | ✅ 단일 인덱스로 가능 |
| ext-authz trace | ✅ gRPC metadata 추출 |
| Jaeger ↔ Kibana 연동 | ✅ trace.id로 검색 |