분산 트레이싱 트러블슈팅: Log-Trace 연동 및 Kibana 검색 구조

개요
분산 시스템에서 로그와 트레이스를 연결하는 것은 디버깅의 핵심입니다.
이 문서에서는 Kibana에서 trace_id로 로그를 검색할 수 없었던 문제를 분석하고 해결한 과정을 다룹니다.
문제 상황
증상
Jaeger에서 확인한 trace_id로 Kibana에서 로그 검색 시 결과 없음.
# Kibana 검색 (실패)
trace.id:1598486b3c023e05db07590939154b51
→ No results match your search criteria
기대 동작
애플리케이션 로그에 trace.id가 포함되어 있으므로, 해당 값으로 검색 가능해야 함.
// 애플리케이션 로그 출력 (kubectl logs)
{
"@timestamp": "2025-12-18T07:04:51.886+00:00",
"message": "Scan pipeline finished",
"trace.id": "ef7445d2d5c540c585bcef3896fd960b",
"span.id": "35ecc51ed1958ea4",
"service.name": "scan-api"
}
로그 파이프라인 분석
로그 흐름
App (JSON) → containerd (CRI) → Fluent Bit → Elasticsearch → Kibana
각 단계별 로그 구조 변화
1. 애플리케이션 출력 (ECS JSON)
{
"@timestamp": "2025-12-18T07:04:51.886+00:00",
"message": "Scan pipeline finished",
"log.level": "info",
"trace.id": "ef7445d2d5c540c585bcef3896fd960b",
"span.id": "35ecc51ed1958ea4",
"service.name": "scan-api"
}
2. containerd CRI 래핑
containerd가 로그를 CRI 포맷으로 래핑:
2025-12-18T16:04:51.886+09:00 stdout F {"@timestamp": "2025-12-18T07:04:51.886+00:00", "message": "Scan pipeline finished", ...}
| 필드 | 값 | 설명 |
|---|---|---|
time |
2025-12-18T16:04:51.886+09:00 |
containerd 타임스탬프 |
stream |
stdout |
출력 스트림 |
logtag |
F |
Full (완전한 라인) |
log |
{...JSON...} |
원본 JSON이 문자열로 저장 |
3. Fluent Bit 처리 후 (Elasticsearch 저장)
{
"@timestamp": "2025-12-18T07:04:51.886Z",
"time": "2025-12-18T16:04:51.886+09:00",
"stream": "stdout",
"logtag": "F",
"log": "{\"@timestamp\": \"2025-12-18T07:04:51.886+00:00\", ...}",
"log_processed": {
"@timestamp": "2025-12-18T07:04:51.886+00:00",
"message": "Scan pipeline finished",
"log_level": "info",
"trace_id": "ef7445d2d5c540c585bcef3896fd960b",
"span_id": "35ecc51ed1958ea4",
"service_name": "scan-api"
},
"cluster": "eco2-dev",
"environment": "dev",
"k8s_namespace_name": "scan",
"k8s_pod_name": "scan-api-59d5788d7-q7qcc",
"k8s_container_name": "scan-api",
"k8s_labels": {
"app": "scan-api",
"domain": "scan"
}
}
원인 분석
Fluent Bit 설정 검토
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Merge_Log_Key log_processed # ← 여기가 문제
...
[OUTPUT]
Name es
Replace_Dots On # ← trace.id → trace_id
...
문제점
| 설정 | 효과 | 결과 |
|---|---|---|
Merge_Log On |
JSON 로그를 파싱 | ✅ 정상 |
Merge_Log_Key log_processed |
파싱 결과를 중첩 키에 저장 | ⚠️ 필드 접근 복잡 |
Replace_Dots On |
.을 _로 변환 |
trace.id → trace_id |
실제 검색 경로
# 원하는 검색
trace.id:xxx
# 실제 필요한 검색 (문제 원인)
log_processed.trace_id:xxx
해결: Fluent Bit Nest Lift Filter 추가
수정 내용
workloads/logging/base/fluent-bit.yaml:
[FILTER]
Name nest
Match kube.*
Operation lift
Nested_under kubernetes
Add_prefix k8s_
# [신규 추가] log_processed 필드를 최상위로 올림
[FILTER]
Name nest
Match kube.*
Operation lift
Nested_under log_processed
Nest Lift Filter 동작
# Before (중첩)
{
"log_processed": {
"trace_id": "abc123",
"message": "hello"
}
}
# After (평탄화)
{
"trace_id": "abc123",
"message": "hello"
}
적용
# ConfigMap 업데이트
kubectl apply -f workloads/logging/base/fluent-bit.yaml
# DaemonSet 재시작
kubectl rollout restart daemonset fluent-bit -n logging
Kibana 검색 가이드
검색 필드 경로
| 시기 | 검색 쿼리 | 비고 |
|---|---|---|
| 수정 전 | log_processed.trace_id:xxx |
중첩 경로 필요 |
| 수정 후 | trace_id:xxx |
최상위 필드로 직접 검색 |
유용한 검색 쿼리
# 특정 trace의 모든 로그
trace_id:ef7445d2d5c540c585bcef3896fd960b
# 특정 서비스의 에러 로그
service_name:scan-api AND log_level:error
# 특정 시간대 + trace 조합
trace_id:xxx AND @timestamp >= "2025-12-18T07:00:00"
Elasticsearch 필드 매핑 총정리
ECS 표준 필드 (현재 사용)
Replace_Dots Off + subobjects: false 설정으로 dot notation 유지.
서비스 관련 필드
| 필드 | 타입 | 앱 로그 | 시스템 로그 | 설명 |
|---|---|---|---|---|
service.name |
keyword | ✅ 앱에서 출력 | ✅ Lua 자동 생성 | 서비스 식별자 |
service.environment |
keyword | ✅ 앱에서 출력 | ✅ Lua 자동 생성 | 환경 (dev/prod) |
service.version |
keyword | ✅ 앱에서 출력 | ⚠️ 라벨 있을 때만 | 버전 정보 |
트레이싱 필드
| 필드 | 타입 | 앱 로그 | 시스템 로그 | 설명 |
|---|---|---|---|---|
trace.id |
keyword | ✅ OTEL 자동 주입 | ❌ 없음 | 분산 추적 ID |
span.id |
keyword | ✅ OTEL 자동 주입 | ❌ 없음 | Span ID |
로깅 메타데이터
| 필드 | 타입 | 앱 로그 | 시스템 로그 | 설명 |
|---|---|---|---|---|
log.level |
keyword | ✅ 앱에서 출력 | ⚠️ 일부만 | 로그 레벨 (info, error) |
log.logger |
keyword | ✅ 앱에서 출력 | ❌ 없음 | 로거 이름 |
ecs.version |
keyword | ✅ 앱에서 출력 | ❌ 없음 | ECS 버전 (8.11.0) |
message |
text | ✅ 앱에서 출력 | ⚠️ log 필드 사용 | 로그 메시지 |
Kubernetes 메타데이터 (Lua 필터 생성)
| 필드 | 타입 | 앱 로그 | 시스템 로그 | 설명 |
|---|---|---|---|---|
kubernetes.namespace |
keyword | ✅ | ✅ | 네임스페이스 |
kubernetes.pod.name |
keyword | ✅ | ✅ | Pod 이름 |
kubernetes.container.name |
keyword | ✅ | ✅ | 컨테이너 이름 |
kubernetes.labels |
object | ✅ | ✅ | 주요 라벨 객체 |
에러 관련 필드
| 필드 | 타입 | 앱 로그 | 시스템 로그 | 설명 |
|---|---|---|---|---|
error.type |
keyword | ✅ 에러 시 | ❌ 없음 | 예외 타입 |
error.message |
text | ✅ 에러 시 | ❌ 없음 | 에러 메시지 |
error.stack_trace |
text | ✅ 에러 시 | ❌ 없음 | 스택 트레이스 |
Fluent Bit 생성 필드
| 필드 | 타입 | 설명 |
|---|---|---|
@timestamp |
date | 로그 타임스탬프 |
time |
keyword | containerd CRI 타임스탬프 |
stream |
keyword | stdout / stderr |
logtag |
keyword | F (Full) / P (Partial) |
log |
text | 원본 로그 (JSON 문자열) |
cluster |
keyword | 클러스터 이름 (eco2-dev) |
environment |
keyword | 환경 (dev) |
K8s 메타데이터 (k8s_ prefix)
| 필드 | 설명 |
|---|---|
k8s_namespace_name |
네임스페이스 |
k8s_pod_name |
Pod 이름 |
k8s_pod_id |
Pod UID |
k8s_container_name |
컨테이너 이름 |
k8s_container_image |
컨테이너 이미지 |
k8s_host |
노드 이름 |
k8s_labels |
라벨 객체 |
앱 로그 vs 시스템 로그 비교
앱 로그 (chat-api 예시)
{
"@timestamp": "2025-12-18T09:50:26.958+00:00",
"message": "Chat message received",
"log.level": "info",
"log.logger": "domains.chat.services.chat",
"ecs.version": "8.11.0",
"service.name": "chat-api",
"service.version": "1.0.7",
"service.environment": "dev",
"trace.id": "632602a1d3946d5aba7ea9592034f576",
"span.id": "4b04fd1e7c05437f",
"kubernetes.namespace": "chat",
"kubernetes.pod.name": "chat-api-74456ccd68-7lgml",
"kubernetes.container.name": "chat-api",
"kubernetes.labels": {
"app": "chat-api",
"domain": "chat",
"version": "v1",
"tier": "business-logic"
},
"cluster": "eco2-dev",
"k8s_namespace_name": "chat",
"k8s_pod_name": "chat-api-74456ccd68-7lgml"
}
시스템 로그 (calico-node 예시)
{
"@timestamp": "2025-12-18T10:38:54.614Z",
"log": "2025-12-18 10:38:54.614 [INFO][55] felix/int_dataplane.go...",
"service.name": "calico-node",
"service.environment": "kube-system",
"kubernetes.namespace": "kube-system",
"kubernetes.pod.name": "calico-node-xv9c8",
"kubernetes.container.name": "calico-node",
"kubernetes.labels": {
"k8s-app": "calico-node"
},
"cluster": "eco2-dev",
"k8s_namespace_name": "kube-system",
"k8s_pod_name": "calico-node-xv9c8"
}
시스템 로그 (ArgoCD 예시)
{
"@timestamp": "2025-12-18T10:38:57.428Z",
"msg": "Alloc=220295 TotalAlloc=9444918855...",
"level": "info",
"service.name": "argocd-application-controller",
"service.environment": "argocd",
"kubernetes.namespace": "argocd",
"kubernetes.pod.name": "argocd-application-controller-0",
"kubernetes.container.name": "argocd-application-controller",
"kubernetes.labels": {
"app.kubernetes.io/name": "argocd-application-controller"
},
"cluster": "eco2-dev"
}
필드 소스 요약
| 필드 그룹 | 앱 로그 소스 | 시스템 로그 소스 |
|---|---|---|
service.* |
앱 코드 (ECSJsonFormatter) | Lua 필터 (K8s 라벨) |
trace.*, span.* |
OpenTelemetry SDK | ❌ 없음 |
log.* |
앱 코드 (ECSJsonFormatter) | ⚠️ 일부만 (level) |
kubernetes.* |
Lua 필터 | Lua 필터 |
k8s_* |
Fluent Bit K8s 필터 | Fluent Bit K8s 필터 |
cluster, environment |
Fluent Bit Modify 필터 | Fluent Bit Modify 필터 |
Kibana 검색 쿼리 예시
# 특정 서비스 로그
service.name:auth-api
# 트레이스 추적 (앱 로그만)
trace.id:632602a1d3946d5aba7ea9592034f576
# 에러 로그
log.level:error OR log.level:ERROR
# 시스템 로그 제외
service.name:* AND NOT kubernetes.namespace:(kube-system OR argocd OR logging)
# 특정 Pod 로그
kubernetes.pod.name:auth-api-*
# 라벨 기반 필터
kubernetes.labels.tier:business-logic
Jaeger ↔ Kibana 연동 워크플로우
Trace ID로 로그 찾기

실제 사용 예시
- Jaeger에서 문제 trace 식별
- URL:
https://jaeger.dev.growbin.app/trace/ef7445d2d5c540c585bcef3896fd960b - 느린 span 또는 에러 발견
- URL:
- Kibana에서 상세 로그 확인
- URL:
https://kibana.dev.growbin.app/app/discover - 쿼리:
trace_id:ef7445d2d5c540c585bcef3896fd960b - 해당 요청의 모든 로그 확인
- URL:
참고: Replace_Dots 설정 이유
문제: Elasticsearch 필드명 제약
Elasticsearch는 기본적으로 필드명에 .이 포함되면 object hierarchy로 해석한다.
// 의도: 단일 필드
{ "trace.id": "abc" }
// ES 해석: 중첩 객체
{ "trace": { "id": "abc" } }
ES 8.x 이후: subobjects 옵션
Elasticsearch 8.3+에서 subobjects: false 매핑 옵션이 추가되었다.
PUT logs-template
{
"mappings": {
"subobjects": false,
"properties": {
"trace.id": { "type": "keyword" },
"span.id": { "type": "keyword" },
"log.level": { "type": "keyword" }
}
}
}
이 설정으로 dot 필드명을 평탄하게 유지할 수 있다. 현재 클러스터 버전 8.11.0에서 사용 가능.
참고: Elasticsearch subobjects 공식 문서
현재 선택: Replace_Dots On
| 옵션 | 장점 | 단점 |
|---|---|---|
Replace_Dots On |
설정 간단, 호환성 보장 | ECS 필드명과 불일치 (trace_id) |
subobjects: false |
ECS 표준 유지 (trace.id) |
Index Template 설정 필요 |
현재 선택: Fluent Bit 파싱 + Replace_Dots Off + subobjects: false Index Template
핵심: Fluent Bit에서 JSON 파싱 (부하분산) + ES에서 dot 필드명 유지
- Fluent Bit:
Merge_Log On- JSON 파싱하여 필드 추출 (각 노드에서 분산 처리) - Fluent Bit OUTPUT:
Replace_Dots Off- dot notation 그대로 ES 전송 - Index Template:
subobjects: false- ES가 dot을 nested로 해석하지 않음
App (JSON log) → Fluent Bit (parse + distributed) → ES (subobjects:false) → Index
↓ ↓
각 노드에서 분산 파싱 trace.id, log.level 등 ECS 필드명 유지
구현 파일
| 파일 | 설정 |
|---|---|
workloads/logging/base/fluent-bit.yaml |
Merge_Log On, Replace_Dots Off |
workloads/logging/base/elasticsearch-index-template.yaml |
subobjects: false Index Template |
장점
- 부하분산: 각 노드의 DaemonSet에서 분산 파싱 (ES 부하 감소)
- ECS 표준: dot notation 필드명 유지 (
trace.id,log.level) - 단순성: ES Ingest Pipeline 불필요
Kibana에서 service.name 표시 확인
문제
Kibana에서 service.name 컬럼이 -로 표시됨.
원인
- Data View 필드 미갱신: 새 필드가 아직 인식되지 않음
- 일부 로그만 해당 필드 보유: 시스템 로그(argocd 등)에는
service.name이 없음
확인
ES에서 직접 검색하면 정상:
# service.name으로 검색
curl "ES/_search" -d '{"query":{"term":{"service.name":"auth-api"}}}'
# 결과
{
"message": "HTTP 401 UNAUTHORIZED: Missing refresh token",
"service.name": "auth-api" # ✅ 정상 저장됨
}
해결 방법
- Kibana Data View 새로고침:
- Stack Management → Data Views → logs-* → Refresh field list
- 검색 필터로 애플리케이션 로그만 표시:
service.name:* AND NOT k8s_namespace_name:(kube-system OR argocd OR logging)
커밋
feat(logging): lift log_processed fields to top level for trace correlation
- Add nest lift filter to promote log_processed fields to root level
- Enables direct trace_id/span_id search in Kibana without nested path
- Before: log_processed.trace_id:xxx
- After: trace_id:xxx
SHA: 39b662a7
feat(auth): add error logging with trace context
- Log HTTP errors (401, 403, etc.) with trace.id for correlation
- Log validation errors with field information
- Log unexpected exceptions with full traceback
- Enables trace.id search in Kibana for error debugging
SHA: eecc958b
시스템 로그 ECS 표준화
문제 상황
Kibana Discover에서 service.name 필드가 Available fields에 표시되지 않음.
통계
- service.name 있는 로그: 57건 (0.3%)
- 전체 로그: 19,015건 (100%)
→ 99.7%가 시스템 로그 (calico, argocd 등)로 service.name 없음
Kibana Discover 동작 원리
| 영역 | 표시 필드 |
|---|---|
| Available fields | 현재 검색 결과에 값이 있는 필드만 표시 |
| Empty fields | 현재 검색 결과에 값이 없는 필드 |
| Data View Management | 전체 매핑된 필드 표시 (521개) |
service.name이 0.3%에만 있으니 기본 검색에서 Empty fields로 분류됨.
해결: 시스템 로그에 ECS 필드 자동 매핑
K8s 메타데이터를 활용하여 모든 로그에 service.name 자동 추가.
라벨 분석
# 앱 로그 (우리 서비스)
app=auth-api, domain=auth, environment=dev, version=v1
# 시스템 로그 (ArgoCD, Istio)
app.kubernetes.io/name=argocd-server
# 시스템 로그 (Calico)
k8s-app=calico-node
ECS 매핑 전략
| ECS 필드 | 소스 (우선순위) |
|---|---|
service.name |
app > app.kubernetes.io/name > k8s-app > container_name |
service.environment |
environment 라벨 > namespace |
service.version |
version > app.kubernetes.io/version |
kubernetes.namespace |
namespace 정보 |
kubernetes.pod.name |
Pod 이름 |
kubernetes.labels.* |
모든 라벨 보존 |
구현: Fluent Bit Lua 필터
workloads/logging/base/fluent-bit.yaml:
# ECS 필드 자동 매핑 - K8s 메타데이터에서 ECS 표준 필드 생성
[FILTER]
Name lua
Match kube.*
script /fluent-bit/etc/ecs-enrichment.lua
call enrich_with_ecs_fields
Lua 스크립트 (ecs-enrichment.lua)
function enrich_with_ecs_fields(tag, timestamp, record)
local modified = false
-- 1. service.name 매핑 (앱 로그에서 이미 있으면 유지)
if not record["service.name"] then
local service_name = record["k8s_labels_app"]
or record["k8s_labels_app.kubernetes.io/name"]
or record["k8s_labels_k8s-app"]
or record["k8s_container_name"]
if service_name then
record["service.name"] = service_name
modified = true
end
end
-- 2. service.environment 매핑
if not record["service.environment"] then
local env = record["k8s_labels_environment"]
or record["k8s_namespace_name"]
if env then
record["service.environment"] = env
modified = true
end
end
-- 3. service.version 매핑
if not record["service.version"] then
local version = record["k8s_labels_version"]
or record["k8s_labels_app.kubernetes.io/version"]
if version then
record["service.version"] = version
modified = true
end
end
-- 4. kubernetes.* ECS 필드 매핑
if record["k8s_namespace_name"] then
record["kubernetes.namespace"] = record["k8s_namespace_name"]
modified = true
end
if record["k8s_pod_name"] then
record["kubernetes.pod.name"] = record["k8s_pod_name"]
modified = true
end
-- 5. kubernetes.labels 객체로 라벨 보존
local labels = {}
local label_keys = {"app", "domain", "environment", "version", "tier", ...}
for _, key in ipairs(label_keys) do
local label_field = "k8s_labels_" .. key
if record[label_field] then
labels[key] = record[label_field]
end
end
if next(labels) ~= nil then
record["kubernetes.labels"] = labels
modified = true
end
if modified then
return 1, timestamp, record
else
return 0, timestamp, record
end
end
적용 후 결과
시스템 로그 (Calico)
{
"service.name": "calico-node",
"service.environment": "kube-system",
"kubernetes.namespace": "kube-system",
"kubernetes.pod.name": "calico-node-4t5k9",
"kubernetes.labels": {
"k8s-app": "calico-node"
}
}
시스템 로그 (ArgoCD)
{
"service.name": "argocd-server",
"service.environment": "argocd",
"kubernetes.namespace": "argocd",
"kubernetes.labels": {
"app.kubernetes.io/name": "argocd-server"
}
}
앱 로그 (auth-api) - 기존 유지
{
"service.name": "auth-api",
"service.environment": "dev",
"service.version": "1.0.0",
"trace.id": "abc123...",
"kubernetes.labels": {
"app": "auth-api",
"domain": "auth",
"tier": "business-logic"
}
}
장점
| 항목 | 효과 |
|---|---|
| 검색 일관성 | 모든 로그에 service.name 보유 → Kibana 필터 항상 사용 가능 |
| 기존 로그 호환 | 앱 로그의 ECS 필드 유지 (Lua에서 조건부 처리) |
| 라벨 보존 | kubernetes.labels 객체로 원본 라벨 보존 |
| ECS 표준 준수 | kubernetes.* 필드셋은 ECS 공식 스펙 |
적용 방법
# 수동
# ConfigMap 업데이트
kubectl apply -f workloads/logging/base/fluent-bit.yaml
# DaemonSet 재시작
kubectl rollout restart daemonset fluent-bit -n logging
# 확인
kubectl get pods -n logging -w
# 자동 (GitOps)
# workloads/logging/base/fluent-bit.yaml Config Map 수정
# commit -> push -> PR(develop) -> MERGE(develop)
# ArgoCD App of apps, sync-wave로 클러스터 컴포넌트 선언적으로 관리
커밋
feat(logging): add ECS enrichment for system logs via Lua filter
- Add Lua filter to map K8s labels to ECS fields (service.name, etc.)
- Priority: app > app.kubernetes.io/name > k8s-app > container_name
- Preserve app logs' existing ECS fields (conditional mapping)
- Add kubernetes.labels object for label preservation
- All logs now have service.name for consistent Kibana filtering