OpenTelemetry 로그 데이터 모델, 로그-트레이스 상관관계, 기존 로거 브릿지(Python logging, Go slog), 구조화 로그와 로그 레벨 전략을 학습합니다.
OpenTelemetry는 트레이스와 메트릭과 달리 로그에 대해 독자적인 로깅 프레임워크를 제공하지 않습니다. 대신 기존에 널리 사용되는 로깅 라이브러리(Python logging, Go slog, Java Log4j 등)와 통합하는 브릿지(Bridge) 접근 방식을 채택했습니다.
이 설계 결정의 이유는 명확합니다. 이미 모든 언어와 프레임워크에는 성숙한 로깅 라이브러리가 존재하며, 개발자들은 이미 익숙한 로깅 패턴을 가지고 있습니다. OTel이 새로운 로깅 API를 강요하는 대신, 기존 로거에서 생성된 로그를 OTel 파이프라인으로 연결하는 것이 현실적입니다.
OpenTelemetry의 로그 레코드는 다음 필드로 구성됩니다.
| 필드 | 타입 | 설명 |
|---|---|---|
timestamp | 나노초 | 로그 발생 시각 |
observed_timestamp | 나노초 | 로그 수집 시각 |
severity_number | 정수 | 심각도 수치 (1-24) |
severity_text | 문자열 | 심각도 텍스트 (DEBUG, INFO, WARN, ERROR 등) |
body | Any | 로그 메시지 본문 |
attributes | Map | 로그 속성 키-값 쌍 |
resource | Resource | 로그를 생성한 리소스 정보 |
instrumentation_scope | Scope | 계측 라이브러리 정보 |
trace_id | 바이트 | 연관된 트레이스 ID |
span_id | 바이트 | 연관된 스팬 ID |
trace_flags | 바이트 | 트레이스 플래그 (샘플링 여부) |
trace_id와 span_id 필드가 로그-트레이스 상관관계의 핵심입니다. 로그가 트레이스 컨텍스트 내에서 생성되면, 해당 트레이스와 스팬의 ID가 자동으로 주입됩니다.
로그-트레이스 상관관계는 "이 에러 로그가 어떤 요청에서 발생했는가"를 즉시 파악할 수 있게 해주는 핵심 기능입니다.
trace_id가 자동 첨부되어 있습니다trace_id로 트레이스를 조회하여 전체 요청 경로를 파악합니다Python의 표준 logging 모듈과 OTel을 연결하는 방법입니다.
import logging
from opentelemetry import trace
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk.resources import Resource
# 리소스 정의
resource = Resource.create({
"service.name": "user-service",
"service.version": "1.2.0",
})
# LoggerProvider 초기화
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint="http://collector:4317")
)
)
# Python logging에 OTel 핸들러 추가
handler = LoggingHandler(
level=logging.INFO,
logger_provider=logger_provider,
)
# 기존 로거에 핸들러 연결
logger = logging.getLogger("user-service")
logger.addHandler(handler)
logger.setLevel(logging.INFO)이제 기존 방식대로 로그를 작성하면 OTel 파이프라인을 통해 전송됩니다.
from opentelemetry import trace
tracer = trace.get_tracer("user-service")
logger = logging.getLogger("user-service")
def get_user(user_id):
with tracer.start_as_current_span("get-user") as span:
span.set_attribute("user.id", user_id)
logger.info("사용자 조회 시작", extra={"user_id": user_id})
try:
user = db.find_user(user_id)
logger.info("사용자 조회 성공", extra={"user_id": user_id})
return user
except UserNotFoundError:
logger.warning("사용자를 찾을 수 없음", extra={"user_id": user_id})
raise
except Exception as e:
logger.error("사용자 조회 실패", extra={"user_id": user_id, "error": str(e)})
raiseOTel Log Bridge가 활성화되면, 트레이스 컨텍스트 내에서 생성된 로그에 trace_id와 span_id가 자동으로 주입됩니다. 별도의 코드 변경 없이 기존 logging.info(), logging.error() 호출이 그대로 동작합니다.
Go 1.21부터 표준 라이브러리에 포함된 slog 패키지와의 통합입니다.
package main
import (
"context"
"log/slog"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel"
)
func main() {
// OTel LoggerProvider 설정 (생략)
// slog에 OTel 핸들러 연결
handler := otelslog.NewHandler("my-service")
logger := slog.New(handler)
slog.SetDefault(logger)
// 기존 방식으로 로그 작성 -- trace_id 자동 주입
tracer := otel.Tracer("my-service")
ctx, span := tracer.Start(context.Background(), "process-request")
defer span.End()
slog.InfoContext(ctx, "요청 처리 시작",
slog.String("request.id", "req-001"),
slog.Int("retry.count", 0),
)
}2026-03-23 14:30:22 ERROR Failed to process order ORD-123 for user usr-456: insufficient funds{
"timestamp": "2026-03-23T14:30:22.123Z",
"severity": "ERROR",
"body": "Failed to process order",
"attributes": {
"order.id": "ORD-123",
"user.id": "usr-456",
"error.type": "InsufficientFundsError",
"payment.amount": 50000,
"payment.balance": 30000
},
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7"
}구조화 로그의 장점은 명확합니다.
order.id = "ORD-123"으로 정확한 필터링error.type별 에러 횟수 통계trace_id로 트레이스 연결로그 메시지 본문(body)은 사람이 읽기 위한 고정 텍스트로 작성하고, 가변 데이터는 모두 속성(attributes)으로 분리하세요. 이렇게 하면 로그 메시지 패턴으로 그룹화가 가능하고, 속성으로 필터링과 집계가 가능합니다.
# 나쁜 예 -- 가변 데이터가 메시지에 포함
logger.error(f"주문 {order_id} 결제 실패: {error_message}, 금액: {amount}")
# 좋은 예 -- 고정 메시지 + 구조화 속성
logger.error(
"주문 결제 실패",
extra={
"order.id": order_id,
"error.message": error_message,
"payment.amount": amount,
"payment.currency": "KRW",
}
)OpenTelemetry는 24단계의 심각도 수치를 정의하지만, 실무에서는 다음 5단계를 주로 사용합니다.
| 레벨 | OTel 수치 | 용도 |
|---|---|---|
| DEBUG | 5 | 개발 환경 디버깅 정보 |
| INFO | 9 | 정상 동작 확인 (요청 시작/완료, 상태 변경) |
| WARN | 13 | 잠재적 문제 (재시도 발생, 임계값 근접) |
| ERROR | 17 | 처리 실패 (요청 에러, 외부 서비스 장애) |
| FATAL | 21 | 프로세스 종료가 필요한 심각한 오류 |
# 개발 환경 -- 상세 디버깅 가능
development:
default_level: DEBUG
noisy_libraries: INFO # ORM, HTTP 클라이언트 등
# 스테이징 환경 -- 프로덕션과 유사하되 INFO 허용
staging:
default_level: INFO
noisy_libraries: WARN
# 프로덕션 환경 -- 비용 최적화
production:
default_level: INFO
noisy_libraries: ERROR
high_traffic_paths: WARN # 초당 수천 건 발생하는 경로프로덕션 환경에서 불필요한 로그를 Collector 수준에서 필터링할 수 있습니다.
processors:
filter/logs:
logs:
log_record:
# DEBUG 로그 제거
- severity_number < 9
attributes/logs:
actions:
# 민감 정보 마스킹
- key: user.email
action: hash
- key: request.body
action: delete
service:
pipelines:
logs:
receivers: [otlp]
processors: [filter/logs, attributes/logs, batch]
exporters: [otlp/loki]로그는 3대 신호 중 저장 비용이 가장 높은 신호입니다. 효과적인 비용 관리 전략이 필수입니다.
processors:
filter/healthcheck:
logs:
log_record:
# 헬스 체크 로그 제거
- IsMatch(body, ".*healthcheck.*")
- IsMatch(body, ".*readiness.*")
transform/truncate:
log_statements:
- context: log
statements:
# 로그 본문이 4KB를 초과하면 잘라내기
- truncate_all(attributes, 4096)모든 로그를 동일한 저장소에 보관할 필요는 없습니다.
| 티어 | 저장소 | 보존 기간 | 대상 |
|---|---|---|---|
| Hot | Loki (SSD) | 7일 | ERROR, WARN, 최근 로그 |
| Warm | Loki (HDD) | 30일 | INFO |
| Cold | S3 / GCS | 1년 | 감사 로그, 규정 준수 |
로그에 개인정보(PII)가 포함되지 않도록 주의하세요. 이메일, 전화번호, 주민번호 등은 로그에 기록하기 전에 마스킹하거나 해싱해야 합니다. Collector의 attributes 프로세서에서 hash 액션을 활용하면 중앙에서 일괄 처리할 수 있습니다.
이번 장에서는 OpenTelemetry의 로그 통합 접근 방식을 학습했습니다. OTel은 독자적인 로깅 프레임워크 대신 기존 로거와의 브릿지를 제공하며, trace_id와 span_id를 자동 주입하여 로그-트레이스 상관관계를 구현합니다. 구조화 로그의 설계 원칙과 환경별 로그 레벨 전략, 비용 관리 방법도 살펴보았습니다.
다음 장에서는 OTel SDK를 활용한 실전 계측을 다룹니다. 자동 계측과 수동 계측의 차이, Python/Node.js/Go 각 언어별 SDK 활용법, 커스텀 스팬과 메트릭 생성 실습을 진행합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
자동 계측과 수동 계측의 차이를 이해하고, Python/Node.js/Go 각 언어별 SDK 활용법과 커스텀 스팬/메트릭 생성을 실습합니다.
OpenTelemetry 메트릭의 종류(Counter, Gauge, Histogram), 카디널리티 관리, Exemplars를 통한 메트릭-트레이스 연결, Prometheus 호환을 학습합니다.
OTel Collector의 Receiver/Processor/Exporter 파이프라인, 핵심 프로세서 활용법, Kubernetes 환경에서의 DaemonSet/Deployment 배포를 학습합니다.