Cap 5: Logs

Por: Artiko
opentelemetrylogsstructured-loggingcorrelacionloki

Logs en OTel: el tercer pilar

Los logs en OTel no son solo texto — son eventos estructurados que llevan contexto del trace activo. Esto habilita la correlación: desde una métrica anómala → al trace → al log exacto del error.

flowchart LR
    subgraph Observabilidad correlacionada
        M[Métrica\nlatencia p99 > 1s] -->|filtrar por tiempo| T[Trace\nspan lento en checkout]
        T -->|trace_id + span_id| L[Log\ndetalle del error]
    end

Estructura de un LogRecord

Un LogRecord tiene campos estandarizados:

{
  "timestamp": "2026-04-02T10:15:30.123Z",
  "observed_timestamp": "2026-04-02T10:15:30.125Z",
  "severity_number": 17,
  "severity_text": "ERROR",
  "body": "Payment declined for order ord-789",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "trace_flags": 1,
  "attributes": {
    "order.id": "ord-789",
    "payment.provider": "stripe",
    "payment.error_code": "card_declined",
    "enduser.id": "usr-123"
  },
  "resource": {
    "service.name": "order-service",
    "service.version": "2.1.0"
  }
}

Los campos trace_id y span_id se inyectan automáticamente cuando hay un span activo en el contexto.

Severity Levels

OTel define un rango numérico de severidades con texto canónico:

NúmeroTextoDescripción
1-4TRACEDetalles de debugging muy verbose
5-8DEBUGInformación de debugging
9-12INFOEventos informativos normales
13-16WARNAdvertencias — algo inesperado pero no crítico
17-20ERRORErrores que requieren atención
21-24FATALError crítico — proceso puede no continuar

La granularidad numérica permite INFO2 o ERROR3 si necesitas niveles intermedios.

Usar OTel Logs con el logger de Python

La forma más práctica es puente (LoggingHandler) que conecta el módulo logging estándar de Python con el SDK de OTel:

import logging
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggingHandler

# Configurar el provider
logger_provider = LoggerProvider(resource=resource)
set_logger_provider(logger_provider)

# Exportar vía OTLP
exporter = OTLPLogExporter(endpoint="http://localhost:4317")
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))

# Conectar con el módulo logging estándar
handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)

# Ahora el logger estándar exporta vía OTel
logger = logging.getLogger("order-service")
logger.setLevel(logging.INFO)

Usar en el código:

logger.info("Order created", extra={"order.id": "ord-789", "total_usd": 99.99})
logger.warning("Payment retry", extra={"attempt": 2, "order.id": "ord-789"})
logger.error("Payment failed", extra={"order.id": "ord-789", "error_code": "card_declined"})

El trace_id y span_id se inyectan automáticamente si hay un span activo cuando se llama al logger.

Usar el Logger nativo de OTel

Para más control, usar la API directamente:

from opentelemetry._logs import get_logger, SeverityNumber

logger = get_logger("order-service", "1.0.0")

logger.emit(
    logger.create_log_record(
        timestamp=time.time_ns(),
        severity_number=SeverityNumber.INFO,
        severity_text="INFO",
        body="Payment processed successfully",
        attributes={
            "order.id": "ord-789",
            "payment.amount_usd": 99.99,
            "payment.provider": "stripe",
        },
    )
)

Correlación trace → log

La correlación funciona así:

sequenceDiagram
    participant APP as Aplicación
    participant CTX as Context OTel
    participant LOG as Logger
    participant BACKEND as Backend

    APP->>CTX: start_span("process-payment")
    Note over CTX: trace_id=abc, span_id=111
    APP->>LOG: logger.error("Payment failed")
    LOG->>CTX: obtener trace_id, span_id del contexto activo
    LOG->>BACKEND: LogRecord{trace_id=abc, span_id=111, body="Payment failed"}
    APP->>CTX: end span

En Grafana, puedes ir de un log a su trace directamente clickeando el trace_id.

OTel vs logging tradicional

AspectoLogging tradicionalOTel Logs
FormatoTexto libreEstructurado (JSON)
CorrelaciónManual con IDs hardcodeadosAutomática vía context
ExportaciónFile → Fluentd/LogstashOTLP directo
BackendsElk, SplunkLoki, Elastic, cualquier OTLP
SamplingNoSí (aligned con traces)

Migración de logging existente

Si tienes una aplicación con logging estándar de Python, la migración es no-disruptiva: solo agrega el LoggingHandler y el código existente empieza a exportar vía OTel sin cambios.

# Código existente — sin modificar
logger = logging.getLogger("myapp")
logger.info("User logged in")   # ← ahora se exporta via OTel con trace_id
logger.error("DB timeout")      # ← con span_id del contexto activo

Log body vs atributos

Regla: el body es para humanos, los attributes son para máquinas.

# ✅ Correcto: body legible, atributos filtrables
logger.info(
    "Payment processed",
    extra={
        "order.id": "ord-789",           # atributo para filtrar
        "payment.amount_usd": 99.99,     # atributo para agregar
        "payment.provider": "stripe",    # atributo para agrupar
    }
)

# ❌ Incorrecto: todo en el body (no se puede filtrar)
logger.info(f"Payment processed for order ord-789, amount $99.99 via stripe")

Los atributos permiten hacer queries en el backend: “todos los errores de Stripe donde payment.amount_usd > 100”.