El Modelo de Registro de Node.js
Un registro es un registro estructurado y con marca de tiempo de un solo hecho observado por tu proceso: una solicitud completada, un pago fallido, un trabajo en segundo plano iniciado.
Busca en todas las páginas de la documentación
Un registro es un registro estructurado y con marca de tiempo de un solo hecho observado por tu proceso: una solicitud completada, un pago fallido, un trabajo en segundo plano iniciado.
Suena como console.log con pasos adicionales, pero la diferencia importa: una línea de registro escrita para una persona que observa una terminal y una línea de registro escrita para que una máquina la indexe, filtre y alerte, están resolviendo problemas diferentes, aunque parezcan similares en pantalla.
Esta página es el modelo mental detrás del resto de la sección de registro: Conceptos básicos de registro muestra código funcional para el registro estructurado con Pino, y las páginas sobre IDs de correlación, redacción y AsyncLocalStorage profundizan en mecánicas específicas.
Aquí, el objetivo es comprender qué es realmente un registro, qué tubería recorre y por qué esa tubería da forma a casi todas las decisiones de registro que tomarás.
Antes del registro estructurado, la mayoría del código de Node simplemente escribía texto en stdout: console.log("usuario inició sesión: " + userId).
Eso está bien para un humano que mira una terminal durante el desarrollo local, pero se desmorona en el momento en que hay más de una persona, más de un servicio o más de unas pocas solicitudes por minuto involucradas; un humano no puede extraer significado de miles de oraciones de formato libre lo suficientemente rápido como para importar durante un incidente.
El registro estructurado resuelve esto tratando cada línea de registro como datos primero, mensaje segundo: un objeto JSON con campos nombrados (userId, action, statusCode) y un mensaje corto legible por humanos adjunto, en lugar de una oración con valores incorporados.
Una analogía útil es el diario de un barco versus un diario personal.
El diario de un barco registra hechos discretos y estructurados en un formato fijo (hora, posición, evento) específicamente para que un lector diferente, días o años después, pueda reconstruir exactamente lo que sucedió y cuándo, sin necesidad del contexto del escritor original.
Un diario personal es prosa, escrita para el propio recuerdo del escritor; es expresivo, pero nadie puede escribir una consulta contra él.
El registro de producción apunta al diario de un barco, no al diario personal, incluso cuando sigue siendo legible a primera vista.
El nivel de registro es la otra idea fundamental: fatal, error, warn, info, debug (en la convención de Pino) no son una escala de estado de ánimo sobre lo molesto que estaba el desarrollador cuando escribió la línea; cada nivel es una promesa sobre lo que un operador debe hacer cuando lo ve. error significa que algo necesita atención; warn significa que algo se degradó pero se recuperó; info significa actividad normal y esperada que vale la pena registrar; debug significa detalles útiles en el desarrollo o la resolución de problemas específicos, no ruido de producción rutinario.
Cada línea de registro, desde el momento en que tu código llama a logger.info(...) hasta el momento en que alguien la busca en un panel, recorre una tubería:
sitio de llamada -> filtro de nivel -> serializar -> transporte -> agregador -> índice -> consulta
(logger.info) (por debajo (objeto a (stdout, (Datadog, (almacenamiento (paneles,
del umbral? cadena JSON) archivo, Loki, CW) buscable) alertas)
descartar
temprano)
El filtro de nivel se ejecuta primero y es más importante de lo que parece: un registrador bien construido verifica el nivel configurado antes de realizar el trabajo costoso de serializar el objeto de registro, por lo que una llamada debug en un proceso de producción que se ejecuta en info no cuesta casi nada: el objeto nunca se convierte en una cadena, nunca se escribe en ningún lugar.
Esta es la razón por la que el consejo general de "simplemente registrar todo" es ingenuo; no es realmente "todo" lo que es costoso, es serializar y transportar todo lo que es costoso, y el paso de filtro existe precisamente para evitar pagar ese costo por líneas que nadie leerá.
El transporte es donde el registro se cruza directamente con el bucle de eventos: escribir en stdout de forma sincrónica es barato para volúmenes pequeños, pero puede convertirse en una fuente de contrapresión bajo una carga pesada, ya que un consumidor lento (un proceso canalizado, un búfer de disco lleno) puede hacer que las escrituras se bloqueen.
La mayoría de las configuraciones de producción mantienen la ruta de escritura de la aplicación simple (escribir JSON estructurado en stdout) y permiten que un agente externo (un sidecar, un remitente de registros) maneje el trabajo más lento de transporte y agregación, de modo que el propio proceso de la aplicación nunca se bloquee en E/S de red solo para emitir una línea de registro.
// por qué el orden de serialización importa: el filtro se ejecuta antes del trabajo (costoso)
function log(level: "info" | "debug", threshold: "info" | "debug", fields: object): void {
const levels = { debug: 0, info: 1 };
if (levels[level] < levels[threshold]) return; // verificación barata, aún no hay serialización
process.stdout.write(JSON.stringify({ level, ...fields }) + "\n");
}La correlación es el mecanismo que une las líneas de registro dispersas de una solicitud: un requestId (o trace_id cuando se correlaciona con el rastreo) se adjunta una vez, al principio del ciclo de vida de la solicitud, y se lleva a través de cada línea de registro subsiguiente para esa solicitud, ya sea pasando un registrador hijo por la pila de llamadas o leyéndolo de AsyncLocalStorage en cada sitio de llamada.
Sin correlación, los registros de un servicio ocupado son un flujo intercalado de solicitudes no relacionadas, y reconstruir "todo lo que sucedió para esta solicitud fallida" se convierte en una búsqueda manual y propensa a errores.
La cardinalidad suele ser el verdadero motor de costos en el registro a escala, más que el recuento de líneas sin procesar.
Un campo como statusCode tiene un puñado de valores posibles y se comprime maravillosamente en la mayoría de los índices de registro; un campo como userId o un mensaje de error sin procesar con un ID incrustado tiene valores distintos efectivamente ilimitados, y la indexación en campos de alta cardinalidad es lo que realmente infla el costo de almacenamiento y consulta en los modelos de precios de la mayoría de las plataformas de agregación de registros.
Esta es la razón por la que los equipos a menudo reservan campos como userId para valores que buscarás selectivamente, en lugar de indexar todos los campos posibles por defecto.
El muestreo de registros, la eliminación deliberada de una fracción de eventos rutinarios y de alto volumen (como pings de verificación de estado exitosos) mientras se mantienen todos los errores, es la respuesta habitual una vez que "registrar todo" se vuelve financiera u operativamente insostenible; intercambia una pequeña cantidad de completitud en la ruta rutinaria por una gran reducción de ruido y costo, mientras que intencionalmente nunca muestrea los eventos que realmente importan para la depuración.
El registro también está en tensión con la seguridad y el cumplimiento: los mismos campos estructurados que hacen que los registros sean útiles para la depuración (direcciones de correo electrónico, IP, cuerpos de solicitud) son datos frecuentemente regulados bajo GDPR, PCI-DSS o marcos similares, razón por la cual la redacción en el momento de la serialización, no "recuerda no registrar contraseñas" como un hábito de desarrollador, es el control confiable. Redacción y Cumplimiento de PII cubre esto en profundidad.
La dirección de la industria es la convergencia, no la divergencia: la señal de Registros de OpenTelemetry está estandarizando los registros estructurados junto con los rastreos y las métricas bajo un modelo de correlación, por lo que una línea de registro, un span y un punto de datos métricos sobre la misma solicitud comparten cada vez más el mismo trace_id y nombres de campos semánticos en lugar de vivir en tres herramientas no relacionadas.
| Enfoque | Fortaleza | Debilidad | Mejor ajuste |
|---|---|---|---|
| Registros de texto no estructurados | Configuración cero; legible por humanos en una terminal sin procesar | No consultable a escala; sin extracción de campos confiable; fomenta mensajes inconsistentes | Solo desarrollo local |
| Registros JSON estructurados (Pino) | Analizable por máquina; rápido de filtrar/alertar; bajo sobrecarga por llamada | Requiere disciplina en la denominación de campos entre servicios; menos agradable de ver sin procesar | Servicios de producción, elección predeterminada |
| Señal de registros de OpenTelemetry | Correlación nativa con rastreos/métricas a través de trace_id compartido; tubería independiente del proveedor | Más reciente, menos universalmente compatible con todos los backends; agrega superficie de SDK | Servicios ya instrumentados con rastreo OTel |
console.log está bien mientras funcione." Omite por completo el filtrado de nivel, la serialización estructurada y la redacción, y suele ser lo primero que rompe una tubería de registros solo JSON en el flujo descendente.error para eventos rutinarios y de auto-recuperación (como un solo reintento) entrena a los operadores para ignorar errores reales.Un registro estructurado es un objeto de datos (típicamente serializado a JSON) con campos nombrados más un mensaje corto, en lugar de una única cadena de formato libre con valores interpolados en ella. La distinción importa porque las máquinas pueden filtrar, agregar y alertar sobre campos nombrados; no pueden analizar de forma confiable oraciones arbitrarias.
Porque el filtro de nivel se ejecuta primero, antes de que ocurra cualquier serialización o E/S; una llamada debug en un servicio que se ejecuta en nivel info no cuesta casi nada, ya que el objeto nunca se convierte en una cadena ni se escribe en ningún lugar. Comprender este orden explica por qué el uso liberal de llamadas debug es barato en producción siempre que el nivel esté configurado correctamente.
El nivel de registro es una señal técnica sobre la respuesta del operador (investigar ahora, observar una tendencia, informativo), mientras que el impacto comercial es un eje separado: un reintento de nivel warn podría tener un impacto comercial cero si tiene éxito, mientras que una línea de nivel info "reembolso procesado" podría ser muy importante para un equipo específico aunque nada esté roto.
Se genera o recibe un identificador único (un requestId, o un trace_id compartido con el rastreo) una vez, al principio del ciclo de vida de la solicitud, y luego se adjunta a cada línea de registro subsiguiente para esa solicitud, ya sea a través de un registrador "hijo" vinculado pasado por la pila de llamadas, o leído del almacenamiento de contexto (AsyncLocalStorage) en cada sitio de llamada.
La mayoría de las plataformas de agregación de registros indexan campos para hacerlos buscables, y el costo de indexación escala con la cantidad de valores distintos que tiene un campo, no solo con la cantidad de líneas existentes. Un campo como statusCode (un puñado de valores) se indexa de forma económica; un campo con valores únicos efectivamente ilimitados (como un mensaje de error sin procesar con un ID incrustado) puede ser mucho más costoso de indexar de lo que sugiere el recuento de líneas sin procesar.
Cuando la ruta rutinaria y de alto volumen (verificaciones de estado exitosas, sondeos repetitivos) contribuye a los costos y al ruido sin agregar valor de depuración, muestrea esos, mientras que nunca muestrees los eventos que realmente indican un problema (errores, eventos de seguridad, transacciones críticas para el negocio).
No, omite por completo el filtrado de nivel, la serialización de campos estructurados y cualquier regla de redacción, y escribe de forma sincrónica de una manera que no se compone con las tuberías de registro estructuradas en el flujo descendente. Un registrador real es un sistema pequeño con comportamiento configurable; console.log es un único comportamiento fijo.
Responden a preguntas diferentes: un rastreo muestra el tiempo y la estructura causal entre servicios para una solicitud, mientras que un registro lleva detalles específicos, a menudo arbitrarios (un mensaje de error exacto, un campo de carga útil) que los spans estructurados de un rastreo típicamente no capturan. Correlacionarlos a través de un ID compartido te permite saltar de "esta solicitud fue lenta" (rastreo) a "aquí está exactamente por qué" (registro) en un solo paso.
Los registros se retienen con mayor frecuencia, se replican en más sistemas (agregadores, copias de seguridad, SaaS de terceros) y son accedidos por más personas que la base de datos principal, lo que significa que la PII en los registros puede violar GDPR, PCI-DSS o las políticas internas de manejo de datos, incluso si el almacén de datos principal cumple totalmente. La redacción en el momento de la serialización es la solución confiable, no solo la disciplina del desarrollador.
No en la práctica: herramientas como pino-pretty reformatean el JSON estructurado en una línea legible y coloreada para el desarrollo local, mientras que la producción aún recibe y almacena el objeto estructurado sin procesar. Obtienes ambos: almacenamiento consultable por máquina y salida local legible por humanos, a partir de los mismos datos subyacentes.
Pino sigue siendo la biblioteca de emisión; la señal de registros de OpenTelemetry es un esfuerzo de estandarización sobre cómo se representan los registros estructurados y se correlacionan con los rastreos y las métricas, utilizando identificadores compartidos como trace_id. Los dos no son competidores: muchas configuraciones usan Pino para emitir y una tubería compatible con OTel para correlacionar y exportar.
Tratarlo como gratuito (registrar todo en info sin pensar en el nivel, la cardinalidad o la redacción) y solo descubrir el costo (financiero, en ruido o en riesgo de cumplimiento) una vez que el volumen supera lo que un solo desarrollador que mira la salida puede absorber.
Versiones de la pila: Esta página es conceptual y no está ligada a una versión específica de la pila, aunque los fragmentos ilustrativos asumen Node.js 24 LTS y TypeScript 5.6+.
Revisado por Chris St. John·Última actualización: 19 jul 2026