Log Context (MDC)
El contexto (MDC — Mapped Diagnostic Context) es el bag de pares key-value que el logger adjunta a cada record emitido y se mergea en TransportRecord.attributes. Permite correlacionar logs por requestId, userId, traceId, etc., sin tener que pasar esos valores a mano en cada llamada.
Hay dos formas de attachar contexto, y no son intercambiables: una es persistente (sin AsyncLocalStorage) y la otra es transitoria (scoped vía ALS). Esta página documenta ambas y corrige la forma legacy del setter (logger.withContext({ bindings }) sin callback), que post-refactor es no-op.
¿Qué es MDC? 🧩
El MDC son bindings estructurados que viajan con cada log:
import logger from '@mks2508/better-logger';
// Cualquier log emitido desde este logger lleva estos attributes:
// TransportRecord.attributes = { requestId: 'r-42', userId: 'u-7' }
logger.info('processing'); // el record lleva ambos bindings automáticamente
Esos attributes se propagan a todos los transports registrados (FileTransport, HttpTransport, OtlpTransport) y quedan disponibles para queries, traces y alertas.
El contexto se construye a partir de tres fuentes, mergueadas en orden de prioridad:
- Base inmutable — snapshot del logger padre capturado al crear un
child(). - Scope transitorio — overlay de
AsyncLocalStorageactivo durantewithContext(fn). - Resource OTel — metadatos estables del proceso (
service.name,service.version,deployment.environment), mergeados enTransportRecord.resource(no enattributes).
Las dos formas de attachar contexto
child(extra) |
withContext(bindings, fn) |
|
|---|---|---|
| Mutabilidad | Inmutable — devuelve un logger nuevo | Transitorio — bindings solo existen dentro de fn |
| AsyncLocalStorage | No requiere | Requiere ALS (Node 14+) |
| Browser | Funciona (no usa ALS) | fn se ejecuta sin scoping (no-op) |
| Persistencia | Persistente mientras viva el child | Solo durante la ejecución de fn |
| Patrón | Request-scoped logger, bindings por componente | Scope temporal async, chain de llamadas |
| Análogo | pino.child(), consola.withTag() |
cls-hooked, Java MDC |
Forma 1: child() — bindings persistentes (canónica)
Devuelve una copia inmutable del logger con extra bound. El logger padre no se muta. Es el patrón canónico para request-scoped logging.
import logger from '@mks2508/better-logger';
const requestLogger = logger.child({ requestId: 'r-42', userId: 'u-7' });
requestLogger.info('auth ok'); // attributes: { requestId: 'r-42', userId: 'u-7' }
requestLogger.warn('rate limit'); // mismo attributes
logger.info('unrelated'); // NO lleva requestId — el padre quedó intacto
Los children se pueden encadenar (cada uno captura el snapshot del padre):
const authLog = logger.child({ component: 'auth' });
const reqLog = authLog.child({ requestId: 'r-42' });
reqLog.info('login'); // attributes: { component: 'auth', requestId: 'r-42' }
Forma 2: withContext(bindings, fn) — scope transitorio (ALS)
Ejecuta fn dentro de un scope de AsyncLocalStorage donde bindings se mergean al contexto. Fuera de fn los bindings desaparecen.
import logger from '@mks2508/better-logger';
logger.withContext({ traceId: 't-9' }, () => {
logger.info('start'); // attributes incluye traceId
doWork();
logger.info('end'); // attributes incluye traceId
});
logger.info('after'); // NO lleva traceId — el scope terminó
Para callbacks async, usa withContextAsync() — el scope ALS sobrevive a los await:
await logger.withContextAsync({ traceId: 't-9' }, async () => {
const user = await fetchUser(); // logs internos con await ven traceId
logger.info('user loaded', { id: user.id });
});
⚠️ Corrección importante: el README previo al refactor mostraba
logger.withContext({ requestId })sin callback. Esa forma es no-op por backwards-compatibility (shim del viejo shape de setter). Los bindings no se aplican. UsawithContext({ ... }, fn)con callback, ochild({ ... })para bindings persistentes.
Snapshot y limpieza
getContext() — snapshot read-only
Devuelve una shallow copy del contexto actual (parent chain + bindings propios). No incluye el overlay ALS transitorio, que es lo que se aplica al dispatch pero no es parte del contexto base.
import logger from '@mks2508/better-logger';
const reqLog = logger.child({ requestId: 'r-42' });
const ctx = reqLog.getContext();
// ctx: { requestId: 'r-42' }
// Mutar el snapshot no afecta al logger:
(ctx as { requestId?: string }).requestId = 'tampered';
reqLog.info('still r-42'); // requestId sigue siendo 'r-42' en el record
clearContext() — drop bindings
Vacía el contexto bound. Los records emitidos después dejan de llevar attributes hasta que un nuevo withContext o child los restablezca.
logger.clearContext();
logger.info('clean'); // sin attributes
Resource OTel 🏷️
setResource() define metadatos estables del proceso (service identity) que viajan en TransportRecord.resource, no en attributes. Es lo que OTel/SigNoz usan para agrupar logs por servicio.
import logger from '@mks2508/better-logger';
logger.setResource({
'service.name': 'orders-api',
'service.version': '1.2.3',
'deployment.environment': 'production',
});
El resource se setea una vez por proceso y se mergea en cada record. El shape canónico es ILogResourceRef:
interface ILogResourceRef {
'service.name': string;
'service.version'?: string;
'deployment.environment'?: string;
[key: string]: string | undefined;
}
Cualquier setResource() posterior mergea (no replace) con el resource existente — es seguro llamarlo varias veces.
Browser fallback
El soporte browser depende de qué forma uses:
- ✅
child()— funciona en browser sin configuración extra. No depende deAsyncLocalStorage. - ⚠️
withContext(bindings, fn)/withContextAsync()— en browser sin ALS (Polyfill deshabilitado),fnse ejecuta directamente sin scoping: los bindings no se mergean al contexto. Si no se pasafn, retornaundefined.
El feature-detect es interno (typeof AsyncLocalStorage !== 'undefined'), sin configuración del caller.
// Browser-safe — usar child() para bindings por request/componente
const reqLog = logger.child({ requestId: getRequestId() });
reqLog.info('rendered'); // siempre lleva requestId, browser o Node
Para SPAs y apps browser, prefiere
child().withContextsolo aporta valor en Node, donde ALS propaga el contexto a través de la chain de llamadas async sin tener que pasar el logger a mano.
¿Cuándo usar cuál?
| Escenario | Usa |
|---|---|
| Bindings por HTTP request (Express, Fastify, Hono) | logger.child({ requestId }) una vez al entrar el request |
| Bindings por componente / módulo | logger.child({ component: 'auth' }) al instanciar |
| Scope temporal async (un handler con varios awaits) | logger.withContextAsync({ traceId }, async () => …) |
| Browser / SPA | logger.child({ … }) (sin ALS, withContext es no-op) |
| Reset de contexto en tests o warm shutdown | logger.clearContext() |
| Service identity para OTel/SigNoz | logger.setResource({ 'service.name': … }) al boot |
Regla práctica: si quieres bindings que vivan “mientras este logger exista” → child(). Si quieres bindings que vivan “mientras se ejecuta esta función” → withContext(fn).
Subpath /context
Los exports del subpath ./context dan acceso directo a la factory de MDC para construir contextos sin un Logger concreto (útil en tests, mocks, o logger custom):
import {
createLogContext,
type LogContext,
type ContextSnapshot,
} from '@mks2508/better-logger/context';
const ctx = createLogContext({
childLoggerFactory: (cfg) => myLoggerFactory(cfg),
initialContext: { service: 'orders-api' },
initialResource: { 'service.name': 'orders-api', 'deployment.environment': 'prod' },
});
ctx.child({ requestId: 'r-1' }); // crea un child vía factory inyectada
ctx.withContext({ traceId: 't-9' }, () => { /* … */ });
ctx.setResource({ 'service.version': '2.0' });
La factory inyectada evita el import circular entre Logger y LogContext.
Referencia API
Logger.child(extra)— child logger inmutableLogger.withContext(bindings, fn?)— scope ALS sincrónicoLogger.withContextAsync(bindings, fn)— scope ALS asyncLogger.getContext()— snapshot read-onlyLogger.clearContext()— drop bindingsLogger.setResource(resource)— OTel resourceLogContextinterface — contrato de la fachada MDCcreateLogContext(options)— factory del subpath/contextILogResourceRef— shape del resource OTel (definido inline enLogger.setResource)