Transports
Los transports reciben cada TransportRecord que produce el Logger y lo envían a un destino externo: console, fichero, endpoint HTTP, collector OTLP, o cualquier destino custom. Se registran con logger.addTransport() y devuelven un id opaco que puedes usar con removeTransport().
Cada transport filtra por level de forma independiente, así que puedes tener un FileTransport a info y un HttpTransport a warn sobre el mismo Logger.
🔑 Distinción clave —
addHandler()vsaddTransport()No son la misma cosa. No los mezcles.
addHandler(handler)addTransport(target)Contrato ILogHandlerITransportFirma handle(level, msg, args, metadata): voidwrite(record),flush?(),close?()Estilo Callback síncrono legacy Pipeline async, buffered Buffer / retry / batch ❌ ✅ Retorna voidid: stringPara qué Side-effects simples ( métrica, fire-and-forget) File / HTTP / OTLP / custom con lifecycle
addHandlerqueda como compatibilidad legacy para un callback síncrono; para cualquier destino con buffer, flush o shutdown, implementaITransporty úsalo conaddTransport.Referencia:
ILogHandler·ITransport
📥 Imports
Las clases de transport viven en el subpath @mks2508/better-logger/transports:
import {
ConsoleTransport,
FileTransport,
HttpTransport,
OtlpTransport,
TransportManager
} from '@mks2508/better-logger/transports';
// tipos y registro desde el main
import type { ITransport, TransportRecord, TransportTarget } from '@mks2508/better-logger';
import logger from '@mks2508/better-logger';
🖥️ ConsoleTransport
Transport por defecto. El Logger registra uno automáticamente si no configuras ninguno. Mapea cada nivel al método de console más cercano para que el filtrado nativo de DevTools / NODE_DEBUG siga funcionando.
Mapeo level → método: debug → console.log, info → console.info, warn → console.warn, error/critical → console.error.
Formato: [LEVEL] [prefix] msg (file:line) (prefix y localización se omiten si el registro no las trae).
import { ConsoleTransport } from '@mks2508/better-logger/transports';
logger.addTransport({ target: new ConsoleTransport() });
logger.addTransport({ target: new ConsoleTransport({ level: 'warn' }) });
// → console.warn("[WARN] [db] pool exhausted (pool.ts:87)")
Raramente necesitas instanciarlo a mano — el Logger ya tiene uno. Úsalo solo si quieres reemplazar el default o forzar un level distinto del global.
Referencia: ConsoleTransport
📁 FileTransport
Escritura a fichero en Node, fallback a localStorage en browser.
- Node:
fs.promises.appendFilevía dynamic import (no bloquea el event loop, no envía código Node-only al bundle del browser). - Browser: acumula en
localStoragecon prefijobetter-logger:; no-op silencioso si el storage no está disponible (modo privado, iframes sandbox, quota agotada). - Bounded buffer: al llegar a
maxBufferSizesuelta el registro más viejo (drop-oldest) y emiteonErrorcon el payload descartado. - Sanitización de
destination: en Node rechaza segmentos..,~y rutas absolutas (path traversal); en browser colapsa a[a-zA-Z0-9_-].
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, // hard cap → drop oldest on overflow
onError: (entry) => metrics.increment('log_drop', { reason: entry.message })
}),
level: 'info'
});
// Browser: persiste en localStorage bajo 'better-logger:audit'
logger.addTransport({ target: new FileTransport({ destination: 'audit' }) });
| Opción | Default | Descripción |
|---|---|---|
destination |
'app.log' / 'default' |
Ruta Node o clave localStorage |
batchSize |
100 |
Flush al alcanzar este nº de records |
flushInterval |
— | Flush periódico (ms) |
maxBufferSize |
10_000 |
Techo del buffer (drop-oldest) |
onError |
— | Hook de overflow / fallo de escritura |
Referencia: FileTransport
🌐 HttpTransport
POSTea batches a un endpoint HTTP. Pensado para ingestion services propios.
- Batching por tamaño (
batchSize) o intervalo (flushInterval). - Retry con backoff exponencial (bounded): arranca en
initialBackoffMs, duplica por intento, techomaxBackoffMs, hastamaxRetries. - Status check:
2xx→ entregado;4xx→ dropeado sin retry (cliente no se recupera);5xxo fallo de red → retry. - Timeout por intento con
AbortController(fetchTimeoutMs). - Bounded buffer: en outage largo, drop-oldest con
onErrorobservable. - Body default:
{ logs: TransportRecord[] }. Cambia el wire format sobrescribiendo los hooksprotected serializeBody()/buildHeaders()(verOtlpTransportcomo referencia).
import { HttpTransport } from '@mks2508/better-logger/transports';
logger.addTransport({
target: new HttpTransport({
url: 'https://logs.example.com/ingest',
headers: { Authorization: `Bearer ${process.env.LOG_TOKEN}` },
batchSize: 50,
flushInterval: 5000,
maxBufferSize: 10_000,
maxRetries: 3,
initialBackoffMs: 250,
maxBackoffMs: 5_000,
fetchTimeoutMs: 10_000,
onError: (entry) => console.error('[log-drop]', entry.message)
}),
level: 'warn'
});
| Opción | Default | Descripción |
|---|---|---|
url |
— | Endpoint POST |
headers |
— | Headers extra (auth, custom) |
batchSize |
50 |
Records por POST |
flushInterval |
— | Flush periódico (ms) |
maxBufferSize |
10_000 |
Techo del buffer |
maxRetries |
3 |
Reintentos antes de drop |
initialBackoffMs |
250 |
Backoff inicial |
maxBackoffMs |
5_000 |
Techo de backoff |
fetchTimeoutMs |
10_000 |
Timeout por intento |
onError |
— | Hook de overflow / 4xx / retry exhausto |
Referencia: HttpTransport
🔭 OtlpTransport → SigNoz
Extiende HttpTransport y produce un payload OTLP/HTTP JSON spec-compliant (LogsData → resourceLogs → scopeLogs → logRecords). Compatible con SigNoz o cualquier collector OTLP/HTTP.
POSTea a <endpoint>/v1/logs e inyecta el header signoz-ingestion-key con la key resuelta al construir.
import { OtlpTransport } from '@mks2508/better-logger/transports';
logger.addTransport({
target: new OtlpTransport({
endpoint: 'https://otelcollector.example.com:4318', // requerido
serviceName: 'my-app', // requerido (service.name)
serviceVersion: '1.2.3',
environment: 'production',
ingestKeyEnvVar: 'SIGNOZ_KEY', // ← lee process.env.SIGNOZ_KEY
batchSize: 50,
flushInterval: 5000
})
});
logger.info('Hola desde better-logger → SigNoz');
// → POST https://otelcollector.example.com:4318/v1/logs
// → LogsData con resourceLogs/scopeLogs/logRecords OTLP
🔐 Seguridad — ingestion key
La key vive en tu gestor de secrets (Bitwarden / Coolify env) y se inyecta vía
process.env. El transport leeprocess.env[ingestKeyEnvVar]al construirse y nunca la loguea, serializa ni escribe a source. Si la env var no está seteada, el header se omite silenciosamente (el collector OTLP usa el path default sin auth).Nunca hardcodees la key en código, ni la pases como string literal en
headers.
Referencia: OtlpTransport
🏷️ Registry de strings
Para los 4 built-ins puedes usar el nombre en vez de la instancia — el TransportManager los resuelve contra su registry interno:
logger.addTransport({ target: 'file', options: { destination: 'logs/app.log' } });
logger.addTransport({ target: 'console', level: 'warn' });
logger.addTransport({ target: 'http', options: { url: 'https://logs.example.com/ingest' } });
Built-ins registrados: 'console', 'file', 'http', 'otlp'.
Para OtlpTransport conviene la instancia directa — las options (endpoint, serviceName, ingestKeyEnvVar) están tipadas y el constructor valida los campos requeridos.
Registrar un transport custom por nombre
Si tienes un transport reutilizable, lo registras una vez y luego lo referencias por string:
import { TransportManager } from '@mks2508/better-logger/transports';
const tm = logger.getTransportManager();
if (tm) {
tm.register('datadog', DatadogTransport); // ahora usable por nombre
logger.addTransport({ target: 'datadog', options: { apiKey: process.env.DD_KEY } });
tm.listRegistered(); // ['console','file','http','otlp','datadog']
}
Los built-ins no se pueden sobrescribir: register('file', ...) lanza para evitar silenciar un transport crítico por accidente de naming. El registro es por-instancia (no comparte entre Logger distintos).
Referencia: TransportManager
🧩 Custom transport
Implementa la interfaz ITransport:
interface ITransport {
readonly name: string;
write(record: TransportRecord): void | Promise<void>;
flush?(): void | Promise<void>;
close?(): void | Promise<void>;
isReady?(): boolean;
}
Ejemplo mínimo — un transport que POSTea cada record a Elasticsearch:
import type { ITransport, TransportRecord } from '@mks2508/better-logger';
const elastic: ITransport = {
name: 'elasticsearch',
async write(record: TransportRecord) {
await fetch('https://es.example.com/logs/_doc', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(record)
});
},
async flush() { /* drain si tu transport bufferiza */ },
async close() { /* cerrar sockets / timers */ }
};
logger.addTransport({ target: elastic, level: 'info' });
Para transports HTTP con buffer, retry y backoff ya resueltos, extiende HttpTransport y sobrescribe solo serializeBody() / buildHeaders() — es lo que hace OtlpTransport.
Transform por transport
TransportOptions.transform muta o descarta el record antes de que llegue al write concreto. Devolver null dropea el record solo para ese transport:
logger.addTransport({
target: new FileTransport({ destination: 'logs/errorsonly.log' }),
level: 'debug',
options: { transform: (r) => (r.level === 'error' || r.level === 'critical' ? r : null) }
});
🔌 Flush y shutdown
Los transports buffered (File, Http, Otlp) acumulan records y los envían por batch. Para forzar el envío del buffer o cerrar todo de forma ordenada:
await logger.flushTransports(); // fuerza el flush del buffer de todos
await logger.closeTransports(); // flush + close de todos (drain)
// Shutdown limpio del proceso:
process.on('SIGTERM', async () => {
await logger.cleanup(); // drain de transports + reset de estado del Logger
process.exit(0);
});
flushTransports()— vacía buffers pendientes sin cerrar transports.closeTransports()— flush final +close()en cada transport (libera timers, sockets, file handles).cleanup()— drain de transports + reset completo del Logger (útil para tests o reinicio en caliente).
Los errores de flush / close (sync throw o Promise rejection) se capturan y se loguean por consola — nunca propagan al caller, para que un transport roto no rompa el shutdown.