Migración a 0.18.x
0.18.x es un reset completo del package: el refactor F1–F6 (rolldown subpath layout, ScopedLogger por delegación, sistema de transports, hooks awaited, log context MDC, OTel en TransportRecord) reemplaza el núcleo legacy. Las versiones 1.x–5.x publicadas en npm están deprecated y muestran Legacy — superseded by 0.18.x refactor al cargarse. La línea 0.18.x (último: 0.18.2-alpha.1) es el nuevo latest.
Esta página lista los breaking changes que necesitas migrar. Para el detalle exhaustivo por fase, ver CHANGELOG.md y los commits bajo el topic F1..F6.
Por qué el reset
El antiguo Logger monolítico (~970 líneas, single file) acumulaba deuda: handlers síncronos sin buffer, sin scopes ALS, sin OTel, MDC frágil con race conditions en scoped loggers. El refactor 0.18.x reorganiza el código en subpaths (./core, ./transports, ./context, ./hooks, ./serializers, ./styles, ./cli, ./playground, ./node) y reemplaza cada superficie legacy por una equivalente moderna.
Beneficios prácticos:
- Transports async con bounded buffer, retry con backoff, status check (sin OOM en outages largos).
- MDC correcto vía
AsyncLocalStorage(Node) ychild()inmutable (universal). - OTel-native: cada
TransportRecordllevaseverityNumber,severityText,traceId,spanId,attributes,resource. - Hooks awaited — el redact PII se aplica antes del emit, no después.
- ScopedLogger por delegación (~50-100x menos memoria por scope).
Breaking changes principales
1. withContext() requiere callback
logger.withContext({ bindings }) sin callback es no-op post-refactor. Era el setter global que mutaba el contexto del singleton; rompía scoped loggers con race conditions y se eliminó. Las dos formas correctas:
| Forma | Firma | Cuándo |
|---|---|---|
| Scoped (transitorio) | logger.withContext({ ... }, fn) o logger.withContextAsync({ ... }, async () => ...) |
Bindings vivos solo dentro del callback |
| Persistente (inmutable) | logger.child({ ... }) |
Bindings atados al child logger, sin tocar al padre |
// ❌ Ya no funciona (no-op, sin error, sin bindings aplicados)
logger.withContext({ requestId: 'r-42' });
logger.info('processing'); // attributes vacío
// ✅ Forma scoped (ALS — Node 14+)
logger.withContext({ requestId: 'r-42' }, () => {
logger.info('processing'); // attributes: { requestId: 'r-42' }
});
// ✅ Forma persistente (inmutable, browser-safe)
const reqLog = logger.child({ requestId: 'r-42' });
reqLog.info('processing'); // attributes: { requestId: 'r-42' }
logger.info('unrelated'); // sin requestId (padre intacto)
Si necesitas bindings por request (Express/Hono/Fastify handler), el patrón canónico es:
app.use((req, _res, next) => {
req.log = logger.child({ requestId: req.headers['x-request-id'] });
next();
});
Más detalle en docs/context.md (MDC, ALS, browser fallback) y la referencia API.
2. Handlers → Transports
El sistema legacy basado en ILogHandler se reemplaza por el sistema de transports (ITransport con write / flush / close async).
| Legacy (eliminado) | Reemplazo |
|---|---|
FileLogHandler |
FileTransport (./transports) |
RemoteLogHandler / HttpLogHandler |
HttpTransport (./transports) |
AnalyticsLogHandler |
custom ITransport |
ExportLogHandler |
custom ITransport |
addHandler(handler) |
addTransport({ target }) (vía registry) |
// ❌ 5.x
logger.addHandler(new FileLogHandler({ destination: '/var/log/app.log' }));
// ✅ 0.18.x
import { FileTransport } from '@mks2508/better-logger/transports';
logger.addTransport({
target: new FileTransport({
destination: 'logs/app.log', // relativa a process.cwd()
batchSize: 100,
flushInterval: 5000,
maxBufferSize: 10_000 // bounded buffer
}),
level: 'info'
});
addHandler(ILogHandler) se conserva como shim legacy para callbacks síncronos simples (métricas, fire-and-forget). Para cualquier destino con buffer, retry, backoff o shutdown ordenado, implementa ITransport. Detalle completo en docs/transports.md.
3. Nivel trace añadido
LogLevel ahora es 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'critical' (+ 'silent'). trace se alinea con la banda TRACE de OpenTelemetry (severity 1-4), pensado para verbosidad máxima / debug profundo:
logger.trace('payload crudo', { bytes: 4096 });
logger.setVerbosity('debug'); // muestra trace + debug + info + ...
severityNumber y severityText se adjuntan a cada TransportRecord automáticamente — el OtlpTransport los usa para producir el payload OTLP logRecords spec-compliant.
Más sobre la jerarquía en docs/core.md.
4. log() family retorna Promise<void>
Las llamadas info(), warn(), error(), etc. ahora retornan Promise<void> porque internamente awaitean los hooks beforeLog. Fire-and-forget sigue funcionando (la promesa queda flotando), pero se recomienda await cuando registras hooks que mutan el mensaje (redacción PII, enriquecimiento):
// Antes: mutation async se perdía en el emit
logger.on('beforeLog', async (entry) => {
entry.message = await redactPII(entry.message);
});
// Ahora: awaita la mutación antes de despachar a transports
await logger.info('login password=s3cr3t');
// → "login password=***" (redacción aplicada)
Si el caller no puede await (sync required), los hooks deben ser síncronos o aceptar que la mutación se pierda en ese caso.
5. Default export lazy (Proxy)
import logger from '@mks2508/better-logger' devuelve un Proxy que se inicializa al primer uso. Esto permite top-level imports sin side-effects y simplifica hot-reload. Si necesitas la clase directa:
import { Logger } from '@mks2508/better-logger';
const appLogger = new Logger({ prefix: 'APP', verbosity: 'info' });
6. ScopedLogger por delegación
scope(), component(), api() ya no devuelven subclases de Logger. Internamente son ScopedLogger que delega al Logger raíz vía árbol interno — sin duplicar estado, sin capturar this. Beneficio: ~50-100x menos memoria por scope en aplicaciones con cientos de módulos.
const auth = logger.scope('Auth'); // ScopedLogger
const db = auth.scope('DB'); // ScopedLogger anidado
db.error('pool exhausted');
// → prefix efectivo: "Auth:DB" en el record
API pública no cambia. Si tu código dependía de instanceof Logger sobre scopes, ahora es instanceof ScopedLogger.
7. Campos OTel en TransportRecord
TransportRecord ahora incluye los campos del spec OTLP LogRecord además de los legacy:
interface TransportRecord {
// legacy
level: LogLevel;
message: string;
prefix?: string;
timestamp: number;
attrs?: Record<string, unknown>;
// OTel (nuevo)
severityNumber?: number; // 1-24 (TRACE=1-4, DEBUG=5-8, ...)
severityText?: string; // 'TRACE' | 'DEBUG' | 'INFO' | ...
traceId?: string; // 128-bit hex
spanId?: string; // 64-bit hex
attributes?: Record<string, ...>; // merged con MDC bindings
resource?: ILogResourceRef; // service.name / version / env
}
Si tu transport custom lee record.attrs, migrar a record.attributes (los transports built-in conservan attrs como alias deprecado por una versión; el custom deberías migrarlo).
8. Dead surface eliminada
El refactor F6 borró exports sin callers o con替代n Superior:
| Eliminado | Reemplazo |
|---|---|
CSS2ANSIAdapter |
Interno — no usar |
LogStyleBuilder |
StyleBuilder (subpath ./styles) vía proxy $ |
SemanticStyles |
THEME_PRESETS (./styles) o presets del Logger (logger.preset('cyberpunk')) |
HistoryCommand (CLI) |
Sin reemplazo — usar logger.scope() o un subpath CLI explícito |
// ❌ createStyle() no existe
import { createStyle } from '@mks2508/better-logger';
// ✅ $
import { $ } from '@mks2508/better-logger/styles';
const style = $.bg('linear-gradient(45deg, #667eea, #764ba2)')
.color('white').padding('12px 24px').rounded('8px').build();
console.log('%c🚀 Hello', style);
// ✅ o instancia directa
import { StyleBuilder } from '@mks2508/better-logger/styles';
const style = new StyleBuilder().bg(...).color(...).build();
Tabla de migración rápida
| 5.x (legacy) | 0.18.x | Notas |
|---|---|---|
logger.withContext({ k: v }) |
logger.withContext({ k: v }, fn) o logger.child({ k: v }) |
Sin callback = no-op |
new FileLogHandler({ ... }) + addHandler |
new FileTransport({ ... }) + addTransport |
Async, bounded buffer |
new RemoteLogHandler({ ... }) |
new HttpTransport({ ... }) |
Retry + backoff |
new ExportLogHandler({ ... }) |
new OtlpTransport({ ... }) |
OTLP/HTTP spec-compliant |
logger.success('ok') |
logger.success('ok') |
Sin cambio (sigue emitiendo a INFO con tag: 'success') |
addHandler(h) con callback custom |
addTransport({ target }) con ITransport |
Para side-effects simples, addHandler legacy sigue |
Sin nivel trace |
logger.trace(...) (+ setVerbosity('debug') por defecto) |
OTel TRACE severity 1-4 |
log() síncrono |
log() retorna Promise<void> |
await cuando hay hooks beforeLog que mutan |
TransportRecord.attrs |
TransportRecord.attributes (con attrs alias deprecado) |
OTel mapping 1:1 |
setVerbosity('info') |
igual | Sin cambio |
clearContext() |
igual | Sin cambio |
setResource({ 'service.name': ... }) |
igual | Sin cambio |
getContext() |
igual (snapshot read-only) | Sin cambio, pero NO incluye overlay ALS transitorio |
Antes y después: ejemplo completo
// ─────── 5.x (legacy) ───────
import logger, { FileLogHandler, RemoteLogHandler } from '@mks2508/better-logger';
logger.withContext({ requestId: getReqId() }); // setter global
logger.addHandler(new FileLogHandler({ destination: '/var/log/app.log' }));
logger.addHandler(new RemoteLogHandler({ url: 'https://logs/x', level: 'warn' }));
logger.info('login ok'); // síncrono
// ─────── 0.18.x ───────
import logger from '@mks2508/better-logger';
import { FileTransport, HttpTransport, OtlpTransport } from '@mks2508/better-logger/transports';
const reqLog = logger.child({ requestId: getReqId() }); // inmutable
logger.addTransport({
target: new FileTransport({ destination: 'logs/app.log', maxBufferSize: 10_000 }),
level: 'info'
});
logger.addTransport({
target: new HttpTransport({
url: 'https://logs.example.com/ingest',
maxRetries: 3,
initialBackoffMs: 250,
maxBufferSize: 10_000
}),
level: 'warn'
});
logger.addTransport({
target: new OtlpTransport({
endpoint: 'https://otelcollector.example.com:4318',
serviceName: 'my-app',
ingestKeyEnvVar: 'SIGNOZ_KEY'
})
});
logger.setResource({ 'service.name': 'my-app', 'service.version': '1.2.3' });
await reqLog.info('login ok'); // awaited
Verificación post-migración
Checklist rápido:
- Type-check limpio:
bunx tsc --noEmit(obun run typecheck). - No quedan imports de
FileLogHandler,RemoteLogHandler,AnalyticsLogHandler,ExportLogHandler,CSS2ANSIAdapter,LogStyleBuilder,SemanticStyles,HistoryCommand,createStyle. - No quedan
withContext({ ... })sin segundo argumento callback. Usargrep -rn "withContext(" src/. await logger.info(...)(o equivalente) en call sites donde tienes hooksbeforeLogque mutan el mensaje.- Transports async:
await logger.closeTransports()(oawait logger.cleanup()) en handlers deSIGTERMpara drenar buffers pendientes antes de exit.
Recursos
- CHANGELOG.md — detalle por fase (F1–F6) con commits
- docs/context.md — MDC,
withContext(fn),child(),setResource(), ALS, browser fallback - docs/transports.md —
FileTransport/HttpTransport/OtlpTransport, customITransport, flush/shutdown - docs/hooks.md —
beforeLogawaited, middlewareuse() - docs/styles.md —
StyleBuilder,$, presets, temas - API Reference — TypeDoc autogenerado desde
src/