Logging estructurado y redacción de datos en Express.js | Nicolás Garzón
Un log útil es un evento estructurado que permite reconstruir qué ocurrió sin exponer secretos. Imprimir strings arbitrarios no es una estrategia de observabilidad.
Logging responde preguntas concretas:
¿Qué request falló?
¿En qué etapa?
¿Qué dependencia participó?
¿Cuánto tardó?
¿Qué decisión tomó la aplicación?
La estructura facilita búsqueda y agregación:
JSON
Copiar {
"level" : "info" ,
"event" : "http_request_completed" ,
"requestId" : "req_123" ,
"method" : "POST" ,
"route" : "/orders" ,
"statusCode" : 201 ,
"durationMs" : 84 ,
"tenantId" : "ten_42"
} Texto
Copiar Something failedno permite correlacionar, filtrar ni distinguir contextos. Strings concatenados también dificultan ocultar datos sensibles.
Pino, Winston u otra librería pueden producir JSON. La elección importa menos que definir esquema, niveles y redacción.
TypeScript
Copiar const requestLogger = logger. child ( {
requestId,
tenantId,
actorId,
} ) ; Los child loggers evitan repetir contexto, pero no guardes objetos de request completos.
Acepta un ID externo únicamente si cumple formato y límites; de lo contrario genera uno.
TypeScript
Copiar const incoming = request. get ( 'x-request-id' ) ;
const requestId = isValidRequestId ( incoming)
? incoming
: crypto. randomUUID ( ) ; Propágalo a dependencias y devuélvelo en response. Un trace ID puede cumplir una función relacionada, pero request ID y trace no son necesariamente idénticos.
Registra al finalizar, no solo al entrar:
TypeScript
Copiar function accessLog ( request, response, next) {
const started = performance. now ( ) ;
const log = logger. child ( { requestId: response. locals. requestId } ) ;
response. once ( 'finish' , ( ) => {
log. info ( {
event: 'http_request_completed' ,
method: request. method,
route: request. route?. path ?? 'unmatched' ,
statusCode: response. statusCode,
durationMs: performance. now ( ) - started,
} ) ;
} ) ;
response. once ( 'close' , ( ) => {
if ( ! response. writableFinished) {
log. warn ( { event: 'http_request_aborted' } ) ;
}
} ) ;
next ( ) ;
} Usa la plantilla de ruta, no URLs completas con IDs, para evitar alta cardinalidad.
debug: diagnóstico detallado habilitado temporalmente.
info: eventos normales importantes.
warn: degradación recuperable o comportamiento anómalo.
error: operación fallida que requiere investigación.
fatal: proceso no puede continuar.
No marques como error cada 404 esperado. El nivel debe reflejar impacto operativo.
Serializa Error de forma que conserve:
Tipo.
Mensaje interno.
Stack.
Cause.
Código.
Dependencia.
El cliente recibe una versión pública; el log interno puede contener más detalle, sujeto a redacción.
Passwords.
Tokens y cookies.
Authorization header.
API keys.
Secretos de webhook.
Números completos de tarjeta.
Bodies enteros por defecto.
Datos personales sin necesidad.
URLs firmadas.
La redacción debe ocurrir en el logger, no depender solo de que cada desarrollador recuerde.
TypeScript
Copiar const logger = pino ( {
redact: {
paths: [
'req.headers.authorization' ,
'req.headers.cookie' ,
'*.password' ,
'*.token' ,
] ,
censor: '[REDACTED]' ,
} ,
} ) ; Los paths exactos dependen de la estructura real.
Endpoints de alto volumen pueden generar demasiados logs. Sampling reduce coste, pero conserva:
Todos los errores importantes.
Requests lentas.
Eventos de seguridad.
Una muestra de tráfico normal.
No samples de forma que desaparezcan incidentes raros.
Un log técnico no es un audit trail confiable. Auditoría requiere:
Actor.
Acción.
Recurso.
Antes/después relevante.
Timestamp.
Integridad y retención.
Acceso restringido.
Separa ambos propósitos. Los logs operativos pueden rotarse o samplearse.
Una orden falla por stock:
JSON
Copiar {
"level" : "warn" ,
"event" : "order_creation_rejected" ,
"requestId" : "req_123" ,
"tenantId" : "ten_42" ,
"actorId" : "usr_8" ,
"reason" : "INSUFFICIENT_STOCK" ,
"productCount" : 3 ,
"durationMs" : 31
} No necesitas registrar todos los items ni el body completo para diagnosticar el tipo de rechazo.
Registra nombre lógico, operación, duración y resultado:
JSON
Copiar {
"event" : "dependency_call_completed" ,
"dependency" : "payment-provider" ,
"operation" : "create_intent" ,
"durationMs" : 420 ,
"result" : "timeout" ,
"attempt" : 2
} Evita URL con query sensible y response body completo.
Logging síncrono o serialización de objetos grandes puede afectar latencia. En producción suele escribirse a stdout y un agente recoge. No hagas rotación compleja dentro de cada contenedor salvo necesidad.
Tiempo de retención.
Índices buscables.
Coste.
Permisos.
Región y cumplimiento.
Procedimiento de eliminación.
Más retención no siempre es mejor si contiene datos sensibles.
Redacción de secretos.
Request ID inválido.
Finish y close.
Errores con cause.
Template route en lugar de ID.
Logger no rompe la request si el sink falla.
Logs de seguridad obligatorios.
console.log(request.body).
Mensajes sin campos.
Un log por cada línea.
Error level para eventos normales.
IDs arbitrarios como labels de métricas.
Confiar en logs como auditoría.
No probar redacción.
Los logs son eventos estructurados.
El contexto debe propagarse por request.
Registra finalización, duración y resultado.
Redacta por diseño.
Separa logging, auditoría y métricas.
Optimiza volumen sin perder errores críticos.
¿Por qué usar template route?
¿Qué diferencia existe entre log y audit trail?
¿Dónde debe aplicarse redacción?
¿Qué eventos no conviene samplear?
Ver respuestas
Reduce cardinalidad y evita IDs sensibles.
Auditoría necesita garantías, retención e integridad específicas.
Centralmente en el logger y contratos, no solo manualmente.
Errores importantes, seguridad y requests lentas.
Observabilidad: logs, métricas y trazas combina señales para responder no solo qué ocurrió, sino por qué y con qué impacto.