LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 13분

로그와 구조화된 로깅

로그가 어려운 이유

메트릭은 숫자고 트레이스는 구조화된 스팬이지만, 로그는 원래 자유 텍스트다. print("error: something failed")부터 JSON 형식 구조화 로그, 파이썬 logging, Java SLF4J까지 도구와 포맷이 제각각이다. OTel이 메트릭과 트레이스보다 로그 표준화에 더 늦게 도달한 이유다.

OTel은 기존 로깅 프레임워크를 버리라고 하지 않는다. 대신 브리지(Bridge) 방식을 선택했다. 이미 쓰고 있는 logging, log4j, zap 같은 라이브러리의 핸들러를 교체해서 로그 레코드를 OTel 파이프라인으로 흘려보낸다.

LogRecord 데이터 모델

OTel 로그의 기본 단위는 LogRecord다. 기존 로그 문자열과 달리, LogRecord는 정해진 필드로 구성된 구조체다.

필드타입설명
Timestamp나노초원래 이벤트가 발생한 시각
ObservedTimestamp나노초OTel이 수집한 시각 (없으면 수집 시점으로 채움)
SeverityNumberint중증도 숫자 코드 (아래 표 참고)
SeverityTextstring원본 레벨 문자열 ("WARNING", "warn" 등)
BodyAnyValue로그 메시지 본문. 문자열, 맵, 배열 모두 가능
Attributesmap추가 키-값 메타데이터
TraceIdbytes연결된 트레이스 ID (16바이트)
SpanIdbytes연결된 스팬 ID (8바이트)
TraceFlagsbyteW3C Trace Context 플래그
Resourcemap서비스 이름, 버전, 호스트 등 인프라 메타데이터
InstrumentationScopeobject로그를 생성한 라이브러리 이름과 버전

SeverityNumber 표

OTel은 각 레벨을 4단계 세분화해서 다양한 로깅 프레임워크의 레벨을 포용한다.

범위레벨SeverityNumber
1–4TRACETRACE(1), TRACE2(2), TRACE3(3), TRACE4(4)
5–8DEBUGDEBUG(5), DEBUG2(6), DEBUG3(7), DEBUG4(8)
9–12INFOINFO(9), INFO2(10), INFO3(11), INFO4(12)
13–16WARNWARN(13), WARN2(14), WARN3(15), WARN4(16)
17–20ERRORERROR(17), ERROR2(18), ERROR3(19), ERROR4(20)
21–24FATALFATAL(21), FATAL2(22), FATAL3(23), FATAL4(24)

숫자가 클수록 더 심각하다. Python loggingWARNING=30, ERROR=40을 받아서 OTel WARN(13), ERROR(17)로 변환하는 것이 브리지의 역할이다.

브리지 패턴: 기존 로거를 OTel로 연결하기

애플리케이션 logger.info("주문 생성") logging / log4j / zap LogRecord Log Bridge (Appender / Handler) severity 매핑 trace_id/span_id 주입 Resource 첨부 OTel LogRecord OTel Logs SDK LoggerProvider 설정 루트 (Processor 등록) LogRecordProcessor SimpleLogRecordProcessor LogExporter OTLP / Console / File OTel Collector → Loki / ELK 직접 OTLP Grafana Cloud 등 트레이스 컨텍스트 자동 주입 활성 스팬 내에서 로그를 기록하면 trace_id, span_id가 자동으로 LogRecord에 삽입됨
OTel 로그 파이프라인 아키텍처

OTel Python에서 브리지를 설정하는 방법은 간단하다. LoggingHandler를 표준 logging 모듈에 붙이면 끝이다.

import logging
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
    BatchLogRecordProcessor, ConsoleLogExporter,
)
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs._internal.export import SimpleLogRecordProcessor

# 1. LoggerProvider 설정
otlp_exporter = OTLPLogExporter(endpoint="http://localhost:4317")
logger_provider = LoggerProvider()
logger_provider.add_log_record_processor(BatchLogRecordProcessor(otlp_exporter))
set_logger_provider(logger_provider)

# 2. 기존 logging 모듈에 OTel 핸들러 연결
from opentelemetry.instrumentation.logging import LoggingInstrumentor
LoggingInstrumentor().instrument(set_logging_format=True)

# 3. 평소처럼 로그 사용
logger = logging.getLogger("order-service")
logger.setLevel(logging.INFO)
logger.info("주문 생성 완료", extra={"order.id": "ord-42", "user.id": "u-7"})

LoggingInstrumentor를 사용하면 활성 스팬 컨텍스트에서 발생하는 모든 로그에 trace_idspan_id가 자동으로 추가된다.

트레이스와 로그의 연관(Correlation)

LogRecord의 TraceIdSpanId가 가장 강력한 필드다. 메트릭의 Exemplar처럼, 특정 에러 로그에서 해당 요청의 전체 트레이스로 바로 이동할 수 있다.

# 활성 스팬 안에서 발생한 로그 (JSON 포맷으로 출력 시)
{
  "timestamp": "2026-06-20T09:14:22.381Z",
  "severity": "ERROR",
  "body": "DB 연결 타임아웃",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "attributes": {
    "db.system": "mysql",
    "db.statement": "SELECT * FROM orders WHERE id = ?",
    "order.id": "ord-42"
  },
  "resource": {
    "service.name": "order-service",
    "service.version": "2.1.0"
  }
}

Grafana에서는 이 trace_id를 클릭하면 Tempo에 저장된 해당 트레이스로 곧장 이동한다. 로그 → 트레이스 연결이 OTel 로그 통합의 핵심 가치다.

구조화된 로깅 vs 비구조화 로깅

방식예시문제
비구조화"주문 ord-42 처리 실패: 타임아웃"파싱 어려움, 검색 불일치
구조화(JSON){"order.id": "ord-42", "error": "timeout"}파싱 용이, 필드 검색 가능
OTel LogRecordBody + Attributes 분리표준화, 백엔드 교체 가능

OTel LogRecord에서 Body는 사람이 읽는 메시지, Attributes는 머신이 쿼리하는 구조화 필드로 역할을 나눈다. Body를 JSON 문자열로 만드는 대신, Attributes에 필드를 직접 넣는 것이 OTel 스타일이다.

BatchLogRecordProcessor vs SimpleLogRecordProcessor

프로세서동작권장 환경
SimpleLogRecordProcessor로그 발생 즉시 동기적으로 내보냄개발·디버그
BatchLogRecordProcessor버퍼에 모아 주기적으로 비동기 내보냄프로덕션

BatchLogRecordProcessormax_export_batch_size, schedule_delay_millis 등으로 튜닝할 수 있다. 프로덕션에서는 반드시 Batch를 써야 레이턴시 영향을 최소화할 수 있다.

언어별 안정성 현황

OTel Logs는 Tracing보다 늦게 안정화됐다. 2026년 기준:

언어상태
Java, .NET, C++, PHPStable
Go, RustBeta
Python, JavaScript, RubyDevelopment

Python과 JavaScript에서는 아직 API가 변경될 수 있으므로, 브리지 방식(LoggingInstrumentor)을 사용하면 OTel Logs API에 직접 의존하지 않아서 안정적이다.

한 줄 정리

OTel 로그는 기존 로깅 프레임워크를 브리지로 연결해 LogRecord로 변환하고, trace_id·span_id를 자동 주입해 트레이스와 연관시킨다. 구조화된 Attributes가 검색 품질을 결정하고, 프로덕션에서는 BatchLogRecordProcessor가 필수다.

References

  • https://opentelemetry.io/docs/concepts/signals/logs/
  • https://opentelemetry.io/docs/specs/otel/logs/data-model/
  • https://opentelemetry.io/docs/specs/otel/logs/
  • https://opentelemetry-python.readthedocs.io/en/latest/api/_logs.severity.html
  • https://opentelemetry-python.readthedocs.io/en/stable/examples/logs/README.html
  • https://medium.com/@alokrahuldevops/day-158-opentelemetry-logs-api-and-logs-sdk-building-structured-correlated-and-vendor-neutral-765ed1c19d86
  • https://oneuptime.com/blog/post/2026-02-20-opentelemetry-logs-guide/view
  • https://www.dash0.com/knowledge/opentelemetry-logging-explained