Grafana Loki + Tempo: Trace ID 하나로 로그-트레이스 양방향 드릴다운 구현하기
이 글의 대상: 백엔드 개발자 중 Prometheus/Grafana는 운영해봤으나 Loki·Tempo는 처음인 분들을 위한 안내입니다. Kubernetes 기본 운영 경험과 Docker Compose 사용 경험이 있다면 충분히 따라오실 수 있습니다.
운영 서비스에서 장애가 발생했을 때, 전형적인 디버깅 루틴은 이렇게 흘러갑니다. Grafana 대시보드에서 에러율 급등을 발견하고, Kibana 탭을 열어 로그를 뒤지고, 다시 Jaeger를 열어 분산 트레이스를 탐색합니다. 이 컨텍스트 스위칭이 반복될수록 중요한 단서를 잃어버리고 평균 장애 탐지 시간(MTTI)이 길어집니다. 마이크로서비스 환경에서는 단일 요청이 수십 개 서비스를 거치기 때문에, 파편화된 도구들로는 근본 원인을 찾는 것 자체가 어렵습니다.
이 글을 읽고 나면, 로그 한 줄에서 클릭 한 번으로 분산 트레이스 전체를 확인하고, 반대로 느린 스팬에서 연관 로그로 역추적하는 양방향 드릴다운 환경을 직접 구성할 수 있게 됩니다. 핵심은 의외로 단순합니다. 로그에 Trace ID 하나만 심으면, Grafana Loki와 Tempo가 나머지를 연결해줍니다.
이 글에서는 OpenTelemetry Collector 파이프라인 구성(Step 1), Grafana 데이터소스 양방향 연결 설정(Step 2), Node.js 애플리케이션 계측(Step 3)을 순서대로 다룹니다. 각 예시 코드는 실제로 동작하는 설정 기준으로 작성했습니다.
핵심 개념
LGTM 스택과 신호 흐름
Grafana Labs가 제안하는 LGTM 스택은 관찰성의 세 가지 핵심 신호를 각각 전담하는 컴포넌트로 구성됩니다.
| 컴포넌트 | 역할 | 쿼리 언어 |
|---|---|---|
| Loki | 레이블 기반 로그 집계. 로그 내용 미인덱싱으로 비용 효율적 | LogQL |
| Grafana | 메트릭·로그·트레이스를 한 화면에서 시각화하는 통합 UI | — |
| Tempo | 고볼륨·저비용 분산 트레이싱 백엔드. 오브젝트 스토리지(S3 등) 활용 | TraceQL |
| Mimir | 대규모 장기 메트릭 저장소 (Prometheus 완전 호환) | PromQL |
용어 정의 LGTM 은 Loki·Grafana·Tempo·Mimir의 앞 글자를 딴 약어입니다. 최근에는 Pyroscope(지속적 프로파일링)가 추가되어 LGTMP 스택으로 확장되는 추세입니다.
신호 흐름을 파악하는 것이 설정의 출발점입니다. 모든 텔레메트리는 애플리케이션 → Collector → 각 백엔드 → Grafana의 단방향 파이프라인을 따릅니다.
애플리케이션 (OTel SDK)
│
│ OTLP (gRPC 4317 / HTTP 4318)
▼
OpenTelemetry Collector
├──── 로그 ────▶ Loki :3100
├──── 트레이스 ──▶ Tempo :4317
└──── 메트릭 ────▶ Mimir :9009
│
▼
Grafana :3000
(단일 탐색 UI)Collector가 중간에서 신호를 라우팅하기 때문에, 나중에 백엔드를 Loki에서 다른 로그 솔루션으로 교체하더라도 애플리케이션 코드를 수정할 필요가 없습니다.
양방향 연결의 핵심: Trace ID
Full-Stack Observability의 연결 고리는 Trace ID / Span ID입니다. 애플리케이션이 로그를 남길 때 현재 요청의 Trace ID를 함께 삽입하면, 다음과 같은 양방향 드릴다운이 가능해집니다.
메트릭 (Mimir)
│ 에러율 급등 감지 → 해당 시간대 탐색
▼
로그 (Loki)
│ 로그 라인의 traceId 클릭 → Derived Fields 작동
▼
트레이스 (Tempo)
│ 느린 스팬 확인 → 근본 원인 식별
▼
프로파일링 (Pyroscope) — 코드 라인 수준 분석핵심 인사이트 로그에 Trace ID 하나만 포함되어 있으면, Loki → Tempo, Tempo → Loki 양방향 이동이 클릭 한 번으로 완성됩니다. 이것이 단순한 로그 수집과 Full-Stack Observability의 결정적 차이입니다.
OpenTelemetry: 벤더 중립 계측 표준
2024년 이후 OpenTelemetry는 사실상의 계측 표준으로 자리 잡았습니다. 애플리케이션은 OTel SDK로 계측하고, OpenTelemetry Collector 단일 엔드포인트로 메트릭·로그·트레이스를 전송합니다. Collector가 각 신호를 알맞은 백엔드로 라우팅합니다.
로그 수집 에이전트로는 기존 Promtail과 Grafana Agent를 통합한 Grafana Alloy가 차세대 표준으로 자리잡고 있습니다. Alloy는 OpenTelemetry 네이티브 파이프라인을 지원하며 단일 에이전트로 모든 신호를 수집할 수 있어, 신규 프로젝트라면 Promtail 대신 Alloy를 고려해보시면 좋습니다.
실전 적용
세 개의 파일이 함께 동작하는 구조입니다.
project/
├── otel-collector-config.yaml # Step 1: Collector 파이프라인
├── datasources.yaml # Step 2: Grafana 데이터소스 프로비저닝
└── src/
├── tracing.ts # Step 3: SDK 초기화 (진입점보다 먼저 로드)
└── logger.ts # Step 3: Trace ID 자동 주입 로거Step 1: OpenTelemetry Collector 파이프라인 구성
LGTM 스택의 입구 역할을 하는 Collector를 먼저 구성합니다. 아래 설정은 OTLP로 수신한 세 가지 신호를 각각의 백엔드로 라우팅하는 최소 파이프라인입니다.
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc: {}
http: {}
processors:
batch: {} # 백엔드 전송을 묶어 처리. 생략하면 고부하 시 성능 문제 발생
exporters:
loki:
endpoint: http://loki:3100/loki/api/v1/push
labels:
attributes:
service.name: "service_name"
severity: "level"
otlp/tempo:
endpoint: tempo:4317 # gRPC: scheme 없이 host:port 형식 사용
tls:
insecure: true
prometheusremotewrite:
endpoint: http://mimir:9009/api/v1/push
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [loki]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheusremotewrite]| 설정 항목 | 설명 |
|---|---|
processors.batch |
요청을 묶어 일괄 전송. 운영 환경에서 반드시 포함되어야 합니다 |
receivers.otlp |
gRPC(4317), HTTP(4318) 두 프로토콜 동시 수신 |
exporters.loki.labels.attributes |
OTel 속성을 Loki 레이블로 매핑. 레이블 수를 최소화하는 것이 중요합니다 |
exporters.otlp/tempo.endpoint |
gRPC exporter는 scheme 없이 host:port 형식으로 지정 |
service.pipelines |
신호 유형별 독립적인 파이프라인 구성 |
Step 2: Grafana 데이터소스 양방향 연결 설정
Collector 파이프라인 구성 후, Grafana에서 두 데이터소스를 서로 연결하는 설정이 필요합니다. YAML 프로비저닝 파일로 관리하면 팀 전체가 동일한 설정을 코드로 공유할 수 있습니다.
# datasources.yaml (Grafana 프로비저닝)
apiVersion: 1
datasources:
- name: Tempo
type: tempo
uid: tempo
url: http://tempo:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki # 아래 Loki 데이터소스의 uid와 일치해야 합니다
spanStartTimeShift: "-2s" # 타임스탬프 오차 보정 (시계 동기화 오차 흡수)
spanEndTimeShift: "+2s"
filterByTraceID: true
filterBySpanID: false
customQuery: false
- name: Loki
type: loki
uid: loki
url: http://loki:3100
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"traceId":\s*"(\w+)"' # JSON 로그에서 traceId 필드 추출
url: "$${__value.raw}"
datasourceUid: tempo
urlDisplayLabel: "Tempo에서 트레이스 보기"| 항목 | 설명 |
|---|---|
tracesToLogsV2 |
Tempo → Loki 방향 연결. 스팬 클릭 시 해당 시간대 로그를 자동 조회 |
spanStartTimeShift / spanEndTimeShift |
서버 간 NTP 오차로 로그와 스팬 시각이 수 초 차이날 수 있습니다. 이 값이 없으면 연관 로그가 조회되지 않는 상황이 발생할 수 있습니다 |
derivedFields.matcherRegex |
Loki → Tempo 방향 연결. 로그 라인에서 Trace ID를 추출하는 정규식입니다. 로그 포맷에 맞게 조정이 필요합니다 |
datasourceUid |
Tempo와 Loki의 uid 값이 서로의 설정에서 올바르게 참조되어야 연결이 동작합니다 |
Step 3: 애플리케이션 계측 — Node.js OpenTelemetry SDK
애플리케이션이 로그에 Trace ID를 자동으로 포함하려면 OTel SDK 계측이 필요합니다.
src/tracing.ts — SDK 초기화 파일입니다. 애플리케이션 진입점보다 먼저 로드되어야 합니다.
// src/tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-api-service',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
logRecordProcessor: new BatchLogRecordProcessor(
// 개발 환경에서는 SimpleLogRecordProcessor로 교체해 즉시 전송 확인 가능
new OTLPLogExporter({ url: 'http://otel-collector:4318/v1/logs' })
),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();주의 HTTP exporter(
exporter-trace-otlp-http)는 URL에http://와 경로(/v1/traces)를 포함해야 합니다. gRPC exporter(exporter-trace-otlp-grpc)를 사용할 경우 scheme 없이host:port형식을 사용해야 연결 실패를 피할 수 있습니다.BatchLogRecordProcessor는 운영 환경에서 권장되며,SimpleLogRecordProcessor는 개발·디버깅용입니다.
애플리케이션 시작 시 tracing.ts를 가장 먼저 로드하려면 다음과 같이 실행합니다.
# ts-node 사용 시
ts-node -r ./src/tracing.ts src/main.ts
# 컴파일 후 실행 시
node -r ./dist/tracing.js dist/main.jssrc/logger.ts — Winston에서 현재 활성 스팬의 Trace ID를 자동으로 로그에 주입합니다.
// src/logger.ts
import winston from 'winston';
import { trace } from '@opentelemetry/api';
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format((info) => {
const span = trace.getActiveSpan();
if (span) {
const { traceId, spanId } = span.spanContext();
info.traceId = traceId;
info.spanId = spanId;
}
return info;
})(),
winston.format.json()
),
transports: [new winston.transports.Console()],
});
export default logger;이렇게 설정하면 모든 로그에 "traceId": "abc123..." 필드가 자동으로 포함됩니다. Loki Derived Fields 정규식이 이 필드를 감지해 Grafana Explore 화면에 "Tempo에서 트레이스 보기" 링크를 생성합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 비용 효율 | Loki는 로그 내용 미인덱싱, Tempo는 S3 등 오브젝트 스토리지 활용으로 ELK/Jaeger 대비 운영 비용이 크게 절감됩니다 |
| 단일 UI | 메트릭·로그·트레이스·프로파일링 모두 Grafana 한 화면에서 탐색 가능합니다 |
| 크로스-시그널 상관관계 | 메트릭 → 로그 → 트레이스 → 프로파일링 전 방향 드릴다운이 지원됩니다 |
| 벤더 중립 | OpenTelemetry 표준 채택으로 특정 클라우드·벤더에 종속되지 않습니다 |
| Kubernetes 친화성 | 공식 Helm Chart와 Alloy 에이전트로 클라우드 네이티브 배포가 간소화됩니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 다중 쿼리 언어 | PromQL, LogQL, TraceQL을 각각 학습해야 합니다 | 자주 쓰는 쿼리를 Grafana 대시보드로 저장해 재사용하는 것을 권장합니다 |
| 고카디널리티 문제 | Loki 레이블에 user_id, URL 등 값이 많은 필드 사용 시 성능이 급격히 저하됩니다 |
레이블은 service, env, level 등 낮은 카디널리티 값만 사용하고, 나머지는 로그 내용에 포함시키는 것을 권장합니다 |
| 운영 복잡성 | 컴포넌트별 개별 관리·업그레이드가 필요합니다 | 소규모 팀(3인 이하 DevOps 인력)은 Grafana Cloud 매니지드 서비스가 자체 운영보다 총비용이 낮을 수 있습니다 |
| 초기 설정 난이도 | Trace-to-Logs 양방향 설정 시 타임스탬프 오차와 Derived Fields 정규식 조정이 필요합니다 | 이 글의 프로비저닝 YAML을 코드로 관리하면 설정 재현성을 확보할 수 있습니다 |
| UI 성숙도 | Grafana Explore UI의 로그 분석 UX가 Kibana 대비 다소 부족하다는 평가가 있습니다 | Grafana 12 이후 지속적으로 개선 중이며, 필요시 Loki와 Kibana 혼용도 가능합니다 |
용어 보충 카디널리티(Cardinality) 란 특정 레이블이 가질 수 있는 고유한 값의 수를 뜻합니다.
env레이블은 production/staging/dev 정도로 카디널리티가 낮지만,user_id는 수백만 개의 고유 값을 가질 수 있어 고카디널리티입니다. Loki는 레이블 조합마다 별도의 스트림을 생성하므로 고카디널리티 레이블은 메모리와 성능에 심각한 영향을 줍니다.
실무에서 가장 흔한 실수
-
Loki 레이블에 고카디널리티 값 사용:
request_id나user_email을 레이블로 설정하면 스트림 수가 폭발적으로 증가합니다. 이런 값들은 레이블이 아닌 로그 메시지 본문에 포함시키는 것이 올바른 방법입니다. -
로그에 Trace ID를 포함하지 않음: Trace ID가 로그에 없으면 Derived Fields 정규식이 매칭되지 않아 Logs-to-Trace 링크가 생성되지 않습니다.
logger.ts에서와 같이 OTel SDK를 활용해 모든 로그에 Trace ID가 자동으로 포함되도록 설정하는 것을 권장합니다. -
타임스탬프 오차 미보정: 서버 간 NTP 동기화 오차로 스팬 시간과 로그 시간이 수 초 차이 날 수 있습니다.
spanStartTimeShift: "-2s"설정 없이는 Tempo에서 클릭해도 연관 로그가 조회되지 않는 상황이 발생할 수 있습니다.
마치며
로그에 Trace ID 한 줄을 심는 것만으로, 메트릭-로그-트레이스를 잇는 Full-Stack Observability의 핵심 연결이 완성됩니다. 복잡해 보이는 LGTM 스택도 OpenTelemetry Collector를 중심으로 단계적으로 구성하면 충분히 도입 가능합니다.
지금 바로 시작해볼 수 있는 3단계:
-
로컬에서 LGTM 스택을 먼저 경험해보실 수 있습니다. Grafana Labs 공식 예제 저장소인
grafana/intro-to-mlt를 클론하면docker compose up한 번으로 Loki·Tempo·Mimir·Grafana가 모두 동작하는 환경을 바로 확인해보실 수 있습니다. 이 저장소가 가장 먼저 살펴볼 자료입니다. -
기존 서비스에 OpenTelemetry SDK를 추가하고 로그에
traceId필드가 포함되는지 확인해보실 수 있습니다. Node.js라면@opentelemetry/auto-instrumentations-node패키지 하나로 HTTP, DB, 외부 요청에 대한 자동 트레이싱을 시작할 수 있습니다. -
이 글의
datasources.yaml프로비저닝 파일을 기반으로 팀 저장소에 설정을 코드로 관리하는 방법을 시도해보실 수 있습니다. UI에서 직접 설정하는 것보다 재현성이 높고, 팀원과 설정을 공유하기도 훨씬 편리합니다.
다음 글: TraceQL 심화 — Grafana Tempo 2.x에서 에러 스팬 필터링, 서비스별 P99 레이턴시 분석, Mimir 메트릭과의 크로스-시그널 쿼리 작성법
참고 자료
먼저 읽어보시면 좋은 문서:
- Configure trace to logs correlation | Grafana Docs — 이 글의 Step 2 설정 공식 레퍼런스
- Grafana intro-to-mlt | GitHub — 로컬 실습용 공식 예제 저장소
추가 참고:
- Grafana Tempo 공식 문서
- Trace correlations | Grafana Docs
- How to Build a Complete LGTM Stack with OpenTelemetry (2026)
- How to Correlate Logs and Traces with Loki and Tempo (2026)
- How to Set Up Trace-to-Logs Linking Between Grafana Tempo and Loki (2026)
- Full Stack Observability with Grafana, Prometheus, Loki, Tempo, and OpenTelemetry | Medium
- Kubernetes Observability with Grafana Stack (Prometheus, Loki, Tempo, Alloy) | Medium
- Monitoring Applications with OpenTelemetry, Alloy, Loki, Tempo & Mimir
- End-to-End Observability with Prometheus, Grafana, Loki, OpenTelemetry and Tempo | Improving
- Grafana Loki Pros and Cons 2025 | PeerSpot