OpenTelemetry 트레이스와 로그 상호 연관 (Correlating OpenTelemetry Traces and Logs)
OpenTelemetry 트레이스와 로그를 Datadog에서 상호 연관하는 방법을 안내해요.
출처: 문서
본문
개요
트레이스와 로그를 상호 연관하면 트레이스의 특정 스팬에서 해당 작업 중 생성된 로그로 직접 이동해 문제를 조사할 수 있어요. 이렇게 하면 오류나 성능 문제를 이해하는 데 필요한 정확한 컨텍스트를 제공해 디버깅을 더 빠르고 직관적으로 만들어요.
요구 사항
시작하기 전에 통합 서비스 태깅을 구성했는지 확인해요. 이는 Datadog에서 모든 데이터 상호 연관에 필요해요.
설정
Datadog에서 OpenTelemetry 트레이스와 로그를 상호 연관하려면 다음을 수행해야 해요:
-
트레이스 컨텍스트 주입: 애플리케이션의 로거가 활성 트레이스의
trace_id와span_id로 로그를 강화하도록 구성해야 해요. 권장 접근 방식은 OpenTelemetry 인식 로깅 라이브러리나 appender를 사용하는 것이에요. 이러한 도구는 활성 트레이스 컨텍스트를 자동으로 캡처하고trace_id와span_id를 로그 레코드의 최상위 필드로 포함시키는데, 이것이 상호 연관의 표준 방법이에요. -
Datadog으로 로그 전송: 트레이스 컨텍스트로 강화된 로그를 수집해 Datadog으로 보내야 해요.
1. 로그에 트레이스 컨텍스트 주입
다음 예시는 로깅 브리지 또는 자동 계측을 사용해요. 이러한 도구는 일반 로깅 라이브러리(예: zap, Logback, Winston)의 로그를 가로채 OpenTelemetry 로그 데이터 모델로 변환하고 OpenTelemetry SDK로 전달해요. 이 과정은 활성 트레이스 컨텍스트로 로그를 자동으로 강화해요.
완전하고 작동하는 애플리케이션은 Datadog OpenTelemetry Examples 저장소를 참조하세요.
Go
먼저 초기화된 OpenTelemetry LoggerProvider가 있는지 확인해요. 그런 다음 이를 사용해 zap 로거 인스턴스를 만들어요:
import "go.opentelemetry.io/contrib/bridges/otelzap"
// 표준 zap 로거를 otelzap 기반 로거로 교체
logger := zap.New(otelzap.NewCore(
"my-service-name",
otelzap.WithLoggerProvider(loggerProvider),
))
// 이제 이 로거로 작성된 로그는 자동으로 상호 연관됨
logger.Info("Processing user request")
완전한 애플리케이션에서 LoggerProvider가 어떻게 구성되는지 보려면 예시 저장소의 전체 Go 예시를 참조하세요.
Java
Java에서 트레이스 컨텍스트를 주입하려면 OpenTelemetry Logback Appender를 사용할 수 있어요. 프로젝트에 io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0 종속성을 추가하고 logback.xml에서 구성해요:
<configuration>
<appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
<immediateFlush>true</immediateFlush>
</appender>
<root level="INFO">
<appender-ref ref="OpenTelemetry"/>
</root>
</configuration>
완전하고 작동하는 구성 예시는 예시 저장소의 전체 Java 예시를 참조하세요.
Node.js
Node.js의 경우 Winston과 함께 @opentelemetry/instrumentation-winston 패키지를 사용해 로그에 트레이스 컨텍스트를 자동으로 주입해요. 필요한 패키지를 설치해요:
npm install winston @opentelemetry/instrumentation-winston
그런 다음 OpenTelemetry SDK 설정의 일부로 Winston 계측을 등록해요:
const { WinstonInstrumentation } = require('@opentelemetry/instrumentation-winston');
const { NodeSDK } = require('@opentelemetry/sdk-node');
const sdk = new NodeSDK({
instrumentations: [
new WinstonInstrumentation(),
// ... 다른 계측
],
});
sdk.start();
계측을 등록한 후 활성 트레이스 내에서 로그가 생성되면 모든 Winston 로거가 trace_id, span_id, trace_flags 필드를 자동으로 포함해요:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
// 추적된 컨텍스트 내에서 생성된 로그는 자동으로 trace_id와 span_id를 포함
logger.info('Processing user request');
동일한 접근 방식이 @opentelemetry/instrumentation-pino 패키지를 사용해 Pino에서도 작동해요.
2. 로그 파이프라인 선택
로그에 트레이스 컨텍스트가 계측되면 Datadog으로 보내야 해요. 가장 간단한 방법은 OTLP를 사용해 애플리케이션에서 OpenTelemetry Collector로 직접 보내는 것이에요. 그러나 파일에서 로그를 스크랩하거나 Datadog Agent로 로그를 수집할 수도 있어요.
OTLP로 로그 보내기
이것은 가장 간단하고 직접적인 방법이에요. 애플리케이션이 OTLP 엔드포인트로 로그를 직접 보내 로컬 파일에 쓰고 파싱하는 복잡성을 피해요.
OpenTelemetry Collector와 Datadog Agent 모두 OTLP 로그를 수신할 수 있어요.
- OTLP로 로그를 내보내도록 애플리케이션 구성: OpenTelemetry SDK 설정에서
LogRecordProcessor가OTLPLogExporter를 사용하도록 구성해요. 다음 예시는 Python에서 이를 수행하는 방법을 보여줘요:# Python용 OTel SDK 설정에서 import logging from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler from opentelemetry.sdk._logs.export import BatchLogRecordProcessor from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter # Collector로 보내도록 OTLP Log Exporter 구성 # 참고: 엔드포인트는 OpenTelemetry Collector를 가리켜야 함. # 기본 포트는 gRPC의 경우 4317, HTTP의 경우 4318. exporter = OTLPLogExporter(endpoint="localhost:4317", insecure=True) log_provider = LoggerProvider() log_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) # 루트 로거에 연결 handler = LoggingHandler(logger_provider=log_provider) logging.getLogger().addHandler(handler) - OTLP 로그를 수신하고 내보내도록 Collector 구성: Collector의
config.yaml에서otlp수신기를 활성화하고 이를logs파이프라인에 추가해요. OpenTelemetry Collector 설정의 OTLP HTTP 내보내기 구성을 사용해요:receivers: otlp: protocols: grpc: http: processors: resource_detection: detectors: [env, system] exporters: otlp_http: endpoint: https://otlp.${env:DD_SITE} headers: dd-api-key: *** service: pipelines: logs: receivers: [otlp] processors: [resource_detection] exporters: [otlp_http]
파일에서 로그 스크랩
이 접근 방식은 규정 준수나 다른 도구를 위해 로컬 로그 파일을 유지해야 하는 요구 사항이 있을 때 유용해요.
Datadog이 로그와 트레이스를 상호 연관하려면 로그 파일에 올바르게 형식화된 특정 필드가 포함되어야 해요:
trace_id: 트레이스의 ID. 32자 소문자 16진수 문자열이어야 해요.span_id: 스팬의 ID. 16자 소문자 16진수 문자열이어야 해요.
OpenTelemetry SDK는 일반적으로 이를 원시 형식(예: 정수 또는 바이트 배열)으로 제공하는데, 0x 접두사 없는 16진수 문자열로 형식화해야 해요.
JSON 로그
-
JSON 로그를 출력하도록 애플리케이션 구성: 표준 로깅 라이브러리를 사용해 로그를 JSON으로 파일이나
stdout에 작성해요. 다음 Python 예시는 표준logging라이브러리를 사용해요. -
트레이스 컨텍스트 수동 주입: 애플리케이션 코드에서 현재 스팬 컨텍스트를 검색하고
trace_id와span_id를 로그 레코드에 추가해요. 다음 Python 예시는 커스텀logging.Filter를 만들어 이를 자동으로 수행하는 방법을 보여줘요:import logging import sys from opentelemetry import trace from pythonjsonlogger import jsonlogger # 1. 트레이스 컨텍스트를 주입할 필터 생성 class TraceContextFilter(logging.Filter): def filter(self, record): span = trace.get_current_span() if span.is_recording(): span_context = span.get_span_context() record.trace_id = f'{span_context.trace_id:032x}' record.span_id = f'{span_context.span_id:016x}' return True # 2. JSON 로거 구성 logger = logging.getLogger("my-json-logger") logger.setLevel(logging.DEBUG) # 3. 로거에 필터 추가 logger.addFilter(TraceContextFilter()) handler = logging.StreamHandler(sys.stdout) formatter = jsonlogger.JsonFormatter( '%(asctime)s %(name)s %(levelname)s %(message)s %(trace_id)s %(span_id)s' ) handler.setFormatter(formatter) logger.addHandler(handler) # 로그에는 이제 trace_id와 span_id가 포함됨 logger.info("Processing user request with trace context.") -
로그 파일을 스크랩하도록 Collector 구성: Collector의
config.yaml에서filelog수신기를 활성화해요. 로그 파일을 찾아 JSON으로 파싱하도록 구성해요.receivers: filelog: include: [ /var/log/my-app/*.log ] # 로그 파일 경로 operators: - type: json_parser # 타임스탬프와 심각도 필드는 JSON 출력과 일치해야 함 timestamp: parse_from: attributes.asctime layout: '%Y-%m-%d %H:%M:%S,%f' severity: parse_from: attributes.levelname # ... 로그 파이프라인 ...
비-JSON 로그
애플리케이션에서 로그를 JSON 대신 일반 텍스트 또는 키-값 형식으로 출력한다면 여전히 트레이스와 상호 연관할 수 있어요. 로그 문자열에 트레이스 컨텍스트를 수동으로 주입하고 regex_parser로 이를 추출하도록 Collector를 구성해야 해요.
다음 Node.js 예시는 커스텀 포맷터로 Winston을 사용해 trace_id와 span_id를 일반 텍스트 로그 줄에 주입해요:
const { trace, context } = require('@opentelemetry/api');
const winston = require('winston');
// 일반 텍스트 로그에 트레이스 컨텍스트를 주입하는 커스텀 형식
const traceFormat = winston.format((info) => {
const span = trace.getSpan(context.active());
if (span) {
const spanContext = span.spanContext();
info.trace_id = spanContext.traceId;
info.span_id = spanContext.spanId;
}
return info;
});
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
traceFormat(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message, trace_id, span_id }) => {
return `${timestamp} ${level} trace_id=${trace_id || '0'} span_id=${span_id || '0'} ${message}`;
})
),
transports: [
new winston.transports.File({ filename: '/var/log/my-app/app.log' })
],
});
// 출력: 2025-01-15T12:00:00.000Z info trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7 Processing request
logger.info('Processing request');
그런 다음 Collector의 filelog 수신기를 regex_parser로 구성해 일반 텍스트 로그 줄에서 트레이스 컨텍스트를 추출해요:
receivers:
filelog:
include: [ /var/log/my-app/*.log ]
operators:
- type: regex_parser
regex: '^(?P<timestamp>\S+) (?P<severity>\S+) trace_id=(?P<trace_id>[a-f0-9]+) span_id=(?P<span_id>[a-f0-9]+) (?P<body>.*)$'
timestamp:
parse_from: attributes.timestamp
layout: '%Y-%m-%dT%H:%M:%S.%fZ'
severity:
parse_from: attributes.severity
trace:
trace_id:
parse_from: attributes.trace_id
span_id:
parse_from: attributes.span_id
# ... 로그 파이프라인 ...
이 접근 방식은 모든 로깅 라이브러리나 언어에서 작동해요. 핵심 요구 사항은:
- 로그 출력에
trace_id와span_id를 파싱 가능한 필드로 포함. - Collector의
regex_parser가 로그 형식과 일치하고 트레이스 컨텍스트 필드를 추출하도록 구성.
Datadog Agent로 로그 수집
OpenTelemetry Collector를 거치지 않고 Datadog Agent로 직접 로그를 수집한다면 로그에 트레이스 ID가 있는지 확인해야 해요.
- 트레이스 ID 형식: Datadog은 Datadog SDK가 사용하는
dd.trace_id및dd.span_id규약과 OpenTelemetry 표준trace_id및span_id를 자동으로 감지해요. 표준에 대한 자세한 내용은 OpenTelemetry 호환성 문서를 참조하세요.
참고: 로깅 계측이 트레이스/스팬 ID에 다른 속성 이름을 사용한다면 해당 속성이 유효한 트레이스 ID로 인식되도록 JSON 로그 전처리 구성에 추가되어야 해요.
- 속성 매핑: Datadog Agent는 OTel 리소스 속성(예:
service.name)을 Datadog의 표준 태그로 자동 변환하지 않아요. 통합 서비스 태깅을 유지하려면 로그 처리 파이프라인에서 이러한 속성을 수동으로 리매핑해야 할 수 있어요.
Datadog에서 연관 데이터 보기
애플리케이션이 트레이스를 보내기 시작하면 Datadog에서 이들 사이를 탐색할 수 있어요.
트레이스에서 로그로
- APM > Traces로 이동해요.
- 계측된 서비스의 트레이스를 찾아 클릭해요.
- 플레임 그래프에서 아무 스팬이나 선택해 세부 정보를 봐요.
- Logs 탭을 클릭해요.
여기에서 해당 특정 스팬의 실행 중 생성된 모든 로그를 볼 수 있어요.
로그에서 트레이스로
- Logs > Explorer로 이동해요.
- 계측된 서비스의 로그 항목을 찾아 클릭해요.
- Trace 탭을 클릭해요.
여기에서 로그를 생성한 스팬이 포함된 연관 트레이스의 플레임 그래프를 볼 수 있어요.
View Trace in APM을 클릭하면 해당 로그 이벤트와 연관된 전체 APM 트레이스로 직접 이동해 전체 요청의 컨텍스트를 볼 수 있어요.