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úmero | Texto | Descripción |
|---|---|---|
| 1-4 | TRACE | Detalles de debugging muy verbose |
| 5-8 | DEBUG | Información de debugging |
| 9-12 | INFO | Eventos informativos normales |
| 13-16 | WARN | Advertencias — algo inesperado pero no crítico |
| 17-20 | ERROR | Errores que requieren atención |
| 21-24 | FATAL | Error 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
| Aspecto | Logging tradicional | OTel Logs |
|---|---|---|
| Formato | Texto libre | Estructurado (JSON) |
| Correlación | Manual con IDs hardcodeados | Automática vía context |
| Exportación | File → Fluentd/Logstash | OTLP directo |
| Backends | Elk, Splunk | Loki, Elastic, cualquier OTLP |
| Sampling | No | Sí (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”.