Tus logs no sirven justo cuando los necesitás

La escena es siempre la misma. Alguien reporta que “no le funcionó” hace un rato. Vas a los registros y encontrás millones de líneas como esta:
2026-09-03 14:22:07 ERROR Error al procesar el pago
No sabés de qué usuario, ni de qué pedido, ni qué pasó antes. Buscás por hora y aparecen cuarenta errores parecidos. Ninguno te dice cuál era el de esa persona.
El problema no es la cantidad de registros. Es que están escritos para que los lea un humano línea por línea, que es exactamente lo que nadie hace cuando hay un problema real.
1. Escribí objetos, no oraciones
Un registro estructurado es un objeto con campos, no una frase:
// Antes
logger.error(`Error al procesar el pago del pedido ${pedido.id}`);
// Después
logger.error('pago_fallido', {
pedido_id: pedido.id,
usuario_id: usuario.id,
monto: pedido.total,
moneda: pedido.moneda,
pasarela: 'stripe',
codigo_error: err.code,
reintento: intento,
});
Ahora podés preguntar cosas que antes eran imposibles: cuántos pagos fallaron por cada código de error, si se concentran en una pasarela, cuál es el monto promedio de los que fallan. Eso son consultas, no lectura.
Dos reglas que hacen la diferencia:
El mensaje es un identificador estable, no una descripción. pago_fallido
se puede agrupar y contar. “Error al procesar el pago del pedido 4821” es único
por definición, así que no agrupa con nada.
Nunca metas datos personales ni secretos en los campos. Los registros suelen tener retención larga, viajar a servicios de terceros y ser accesibles para más gente que la base de datos. El identificador del usuario sí; su correo, su documento o el número de tarjeta, no.
2. Un identificador que atraviese todo
Con registros estructurados ya podés filtrar por usuario_id. Pero una sola
petición pasa por varios servicios, y en cada uno genera decenas de líneas. Lo
que falta es un hilo que las una.
Ese hilo es el contexto de traza, y está estandarizado: la cabecera HTTP
traceparent, del W3C.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
‾‾ ‾‾‾‾‾‾‾‾‾‾ id de la traza ‾‾‾‾‾‾ ‾ id del tramo ‾ ‾‾
El identificador de traza es el mismo en toda la operación, aunque atraviese seis servicios. El de tramo identifica el paso concreto.
Lo importante en la práctica: si ese identificador aparece en cada línea de registro, el reporte de un usuario se convierte en una sola consulta.
logger.error('pago_fallido', {
trace_id: contextoActual.traceId,
span_id: contextoActual.spanId,
pedido_id: pedido.id,
codigo_error: err.code,
});
Y no hace falta pasarlo a mano por todas las funciones. Los kits de
OpenTelemetry lo inyectan solos en los marcos de registro más comunes —Log4j,
SLF4J, el módulo logging de Python— con lo cual obtenés la correlación sin
reescribir una sola línea de tus registros existentes.
Conviene saber en qué estado está el soporte de registros de OpenTelemetry según el lenguaje, porque es desparejo: estable en Java, .NET, C++ y PHP; beta en Go y Rust; en desarrollo en Python, JavaScript, Ruby, Swift, Elixir y Kotlin. La adopción general ronda el 48 % de las organizaciones, con otro 25 % planificándola.
Si tu lenguaje está en desarrollo, la correlación se hace igual: es agregar dos campos al objeto que ya estás escribiendo.
3. Registrá decisiones, no pasos
El error más común no es registrar poco. Es registrar mucho de lo que no sirve:
logger.info('Entrando a procesarPago'); // ruido
logger.info('Validando tarjeta'); // ruido
logger.info('Llamando a la pasarela'); // ruido
logger.info('Respuesta recibida'); // ruido
Nada de eso te va a ayudar a las tres de la mañana. Lo que sí:
// Una decisión: por qué el sistema eligió este camino y no otro
logger.info('pago_enrutado', {
pasarela: 'stripe',
motivo: 'moneda_no_soportada_en_primaria',
moneda: 'ARS',
});
// Un límite que se cruzó
logger.warn('reintentos_agotados', { pedido_id, intentos: 3, ultimo_codigo: '502' });
// Un dato externo que llegó distinto a lo esperado
logger.warn('respuesta_inesperada', {
servicio: 'pasarela',
esperado: 'approved|declined',
recibido: 'pending_review',
});
La pregunta para decidir si una línea merece existir: ¿me va a permitir explicar por qué el sistema hizo lo que hizo? Si la respuesta es no, es ruido que además cuesta plata almacenar.
El tercer ejemplo es el más valioso y el que menos se escribe. Cuando un servicio externo empieza a devolver un valor que tu código no contempla, ese registro es la diferencia entre entenderlo en cinco minutos o en dos días.
Un detalle sobre el muestreo
Cuando el volumen crece, la reacción es muestrear: guardar una de cada cien trazas. Tiene sentido para las que salieron bien.
Muestreá lo exitoso, nunca lo que falla. Una traza con error es justamente la que vas a necesitar, y es rara por definición: guardarlas todas cuesta poco. La configuración correcta descarta la mayoría de las peticiones exitosas y conserva el 100 % de las que terminaron mal o tardaron de más.
Por dónde empezar
Si tenés que elegir un solo cambio: poné el identificador de traza en cada línea de registro. Es lo que convierte “algo falló hace dos horas” en una consulta que devuelve exactamente las líneas de esa persona, en todos los servicios, en orden.
Lo demás mejora la vida. Eso cambia cuánto tardás en encontrar un problema.
Comentarios
Iniciá sesión para comentar y dar "me gusta".
Todavía no hay comentarios. Sé la primera persona en escribir uno.