@mks2508/better-logger 0.18.4 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1459 @@
1
+ const require_core = require("./core-CqS_UBzJ.cjs");
2
+ //#region src/types/transports.ts
3
+ /**
4
+ * Mapea un `LogLevel` a la severidad numérica de OpenTelemetry (1-24) usada
5
+ * por SigNoz / cualquier backend OTLP/HTTP. Valores por banda conformes a la
6
+ * spec: TRACE=1-4, DEBUG=5-8, INFO=9-12, WARN=13-16, ERROR=17-20, FATAL=21-24.
7
+ * Se usa el valor canónico del medio de cada banda.
8
+ *
9
+ * @see https://opentelemetry.io/docs/specs/otel/logs/data-model/#severity-fields
10
+ */
11
+ const LOG_LEVEL_TO_SEVERITY_NUMBER = {
12
+ trace: 1,
13
+ debug: 5,
14
+ info: 9,
15
+ warn: 13,
16
+ error: 17,
17
+ critical: 21
18
+ };
19
+ /**
20
+ * Mapea un `LogLevel` a su nombre de severidad OpenTelemetry (mayúsculas, spec OTel).
21
+ */
22
+ const LOG_LEVEL_TO_SEVERITY_TEXT = {
23
+ trace: "TRACE",
24
+ debug: "DEBUG",
25
+ info: "INFO",
26
+ warn: "WARN",
27
+ error: "ERROR",
28
+ critical: "FATAL"
29
+ };
30
+ //#endregion
31
+ //#region src/transports/ConsoleTransport.ts
32
+ /**
33
+ * Transport por defecto que escribe cada {@link TransportRecord} al `console`
34
+ * global del runtime (navegador o Node.js).
35
+ *
36
+ * Es el transport que el Logger registra automáticamente cuando no se configura
37
+ * ninguno explícito, garantizando que los registros siempre lleguen a un
38
+ * destino visible sin configuración adicional.
39
+ *
40
+ * **Mapeo level → console method**: traduce cada nivel de log al método más
41
+ * cercano de la API `console`, de forma que el filtrado nativo del DevTools /
42
+ * `NODE_DEBUG` siga funcionando:
43
+ *
44
+ * | LogLevel | console method |
45
+ * |--------------|----------------|
46
+ * | `debug` | `console.log` |
47
+ * | `info` | `console.info` |
48
+ * | `warn` | `console.warn` |
49
+ * | `error` | `console.error`|
50
+ * | `critical` | `console.error`|
51
+ *
52
+ * **Formato de output**: cada línea se compone como
53
+ * `[LEVEL] [prefix] message (file:line)`, donde `prefix` y la localización
54
+ * se omiten si el registro no las trae.
55
+ *
56
+ * @example
57
+ * // Uso directo como ITransport
58
+ * import { ConsoleTransport } from '@mks2508/better-logger/transports';
59
+ * const transport = new ConsoleTransport();
60
+ * transport.write({
61
+ * level: 'info',
62
+ * msg: 'Arrancando worker',
63
+ * prefix: 'worker',
64
+ * // ... resto del TransportRecord
65
+ * });
66
+ * // → console.info("[INFO] [worker] Arrancando worker (worker.ts:12)")
67
+ *
68
+ * @example
69
+ * // Registro a través del Logger (típico — el logger lo añade por defecto)
70
+ * logger.addTransport({ target: new ConsoleTransport() });
71
+ *
72
+ * @see {@link ITransport}
73
+ * @see {@link TransportRecord}
74
+ */
75
+ var ConsoleTransport = class {
76
+ options;
77
+ /** Identificador del transport usado por el Logger para deduplicar y exponer metadatos. */
78
+ name = "console";
79
+ /**
80
+ * Crea una instancia de {@link ConsoleTransport}.
81
+ *
82
+ * El parámetro `options` se acepta para cumplir con la firma canónica de
83
+ * {@link TransportOptions} (filtros de nivel, formateadores, etc.), aunque
84
+ * la implementación actual escribe el registro tal cual llega sin
85
+ * transformaciones adicionales.
86
+ *
87
+ * @param {TransportOptions} [options] - Configuración opcional del transport
88
+ * (nivel mínimo, formatter, etc.).
89
+ *
90
+ * @example
91
+ * const transport = new ConsoleTransport({ level: 'warn' });
92
+ */
93
+ constructor(options) {
94
+ this.options = options;
95
+ }
96
+ /**
97
+ * Escribe un {@link TransportRecord} al `console` global.
98
+ *
99
+ * Selecciona el método de console según el nivel del registro, compone el
100
+ * prefijo `[LEVEL] [prefix]` y, si la localización está disponible, añade
101
+ * el sufijo `(file:line)`. No lanza ni retorna errores: si `console[method]`
102
+ * fallara (raro), la excepción propagaría al caller.
103
+ *
104
+ * @param {TransportRecord} record - Registro normalizado producido por el Logger.
105
+ * @returns {void}
106
+ *
107
+ * @example
108
+ * transport.write({
109
+ * level: 'error',
110
+ * msg: 'DB connection lost',
111
+ * prefix: 'db',
112
+ * location: { file: 'pool.ts', line: 87, function: 'acquire' },
113
+ * // ... resto del TransportRecord
114
+ * });
115
+ * // → console.error("[ERROR] [db] DB connection lost (pool.ts:87)")
116
+ */
117
+ write(record) {
118
+ const method = this.getConsoleMethod(record.level);
119
+ const prefix = record.prefix ? `[${record.prefix}] ` : "";
120
+ const location = record.location ? ` (${record.location.file}:${record.location.line})` : "";
121
+ console[method](`[${record.level.toUpperCase()}]${prefix} ${record.msg}${location}`);
122
+ }
123
+ /**
124
+ * Resuelve el método de `console` apropiado para un {@link LogLevel}.
125
+ *
126
+ * Tabla de mapeo:
127
+ * - `debug` → `log` (sin ruido en DevTools por defecto)
128
+ * - `info` → `info`
129
+ * - `warn` → `warn`
130
+ * - `error` → `error`
131
+ * - `critical` → `error` (no existe `console.critical`)
132
+ * - cualquier otro → `log` (fallback seguro)
133
+ *
134
+ * @internal Método privado; no forma parte de la API pública del transport.
135
+ *
136
+ * @param {LogLevel} level - Nivel del registro a traducir.
137
+ * @returns {'log' | 'info' | 'warn' | 'error'} Nombre del método de `console`.
138
+ */
139
+ getConsoleMethod(level) {
140
+ switch (level) {
141
+ case "debug": return "log";
142
+ case "info": return "info";
143
+ case "warn": return "warn";
144
+ case "error":
145
+ case "critical": return "error";
146
+ default: return "log";
147
+ }
148
+ }
149
+ };
150
+ //#endregion
151
+ //#region src/transports/FileTransport.ts
152
+ const MAX_BUFFER_DEFAULT = 1e4;
153
+ const BATCH_SIZE_DEFAULT = 100;
154
+ const FS_PROMISES_LOAD_TIMEOUT_MS = 50;
155
+ const LOCAL_STORAGE_KEY_PREFIX = "better-logger:";
156
+ /**
157
+ * Transport que escribe registros a fichero (Node) o `localStorage` (browser).
158
+ *
159
+ * En Node, hace `appendFile` asíncrono vía `fs.promises` cargado con dynamic
160
+ * import (no bloquea el event loop, no envía código Node-only al bundle del
161
+ * browser). En el browser, acumula en `localStorage` con prefijo `better-logger:`
162
+ * y degrada a no-op silencioso si el storage no está disponible (modo privado,
163
+ * sandbox de iframes, quota agotada).
164
+ *
165
+ * El buffer es bounded: al llegar a `maxBufferSize` suelta el registro más
166
+ * viejo (drop-oldest) e invoca `onError` con el payload descartado, de modo
167
+ * que un pico de tráfico sostenido no agota memoria.
168
+ *
169
+ * El `destination` se sanea antes de usarse:
170
+ * - Node: se rechazan rutas con segmento `..` o `~` (path traversal / tilde
171
+ * sin expandir). Una ruta absoluta explícita (POSIX o Windows) se acepta
172
+ * tal cual — es al dueño del proceso a quien le toca decidir dónde
173
+ * escribe. Si se rechaza, ese flush no escribe nada (no hay fallback
174
+ * silencioso a otra ubicación); se reporta vía `onError` si está cableado.
175
+ * - Browser: se colapsa a `[a-zA-Z0-9_-]` recortado a 64 caracteres.
176
+ *
177
+ * @implements {IBufferedTransport}
178
+ *
179
+ * @example
180
+ * // Node: append a fichero con flush cada segundo
181
+ * logger.addTransport({
182
+ * target: new FileTransport({
183
+ * destination: 'logs/app.log',
184
+ * batchSize: 100,
185
+ * flushInterval: 1000,
186
+ * onError: (entry) => captureFailure(entry)
187
+ * })
188
+ * });
189
+ *
190
+ * @example
191
+ * // Browser: persiste en localStorage bajo 'better-logger:audit'
192
+ * logger.addTransport({
193
+ * target: new FileTransport({ destination: 'audit' })
194
+ * });
195
+ *
196
+ * @see {@link FileTransportOptions}
197
+ * @see {@link IBufferedTransport}
198
+ */
199
+ var FileTransport = class {
200
+ /** Identificador del transport dentro del pipeline (`'file'`). */
201
+ name = "file";
202
+ buffer = [];
203
+ flushTimer;
204
+ options;
205
+ closed = false;
206
+ /**
207
+ * Construye el transport. Si se pasa `flushInterval`, arranca un timer
208
+ * periódico que vacía el buffer al vencimiento; si no, el flush se
209
+ * dispara solo cuando el buffer alcanza `batchSize`.
210
+ *
211
+ * @param {FileTransportOptions} [options] - Configuración. Defaults: `batchSize=100`, `maxBufferSize=10000`.
212
+ *
213
+ * @example
214
+ * const t = new FileTransport({ destination: 'app.log', flushInterval: 1000 });
215
+ */
216
+ constructor(options) {
217
+ this.options = {
218
+ batchSize: BATCH_SIZE_DEFAULT,
219
+ maxBufferSize: MAX_BUFFER_DEFAULT,
220
+ ...options ?? {}
221
+ };
222
+ if (this.options.flushInterval) this.flushTimer = setInterval(() => {
223
+ this.flush();
224
+ }, this.options.flushInterval);
225
+ }
226
+ /** Registros pendientes en el buffer (aún sin flush). */
227
+ get bufferSize() {
228
+ return this.buffer.length;
229
+ }
230
+ /** Capacidad máxima del buffer; al superarla se aplica drop-oldest. */
231
+ get maxBufferSize() {
232
+ return this.options.maxBufferSize ?? MAX_BUFFER_DEFAULT;
233
+ }
234
+ /**
235
+ * Indica si el transport acepta escrituras. Devuelve `false` después de
236
+ * {@link FileTransport.close} — cualquier `write` posterior se descarta.
237
+ *
238
+ * @returns {boolean} `true` mientras el transport no esté cerrado.
239
+ */
240
+ isReady() {
241
+ return !this.closed;
242
+ }
243
+ /**
244
+ * Encola un registro serializado (JSON + `\n`). Si el buffer está a tope,
245
+ * suelta el registro más viejo (drop-oldest) y emite un evento `onError`
246
+ * con el payload descartado para que la pérdida sea observable. Si al
247
+ * encolar se alcanza `batchSize`, dispara un flush asíncrono.
248
+ *
249
+ * No-op silencioso si el transport está cerrado.
250
+ *
251
+ * @param {TransportRecord} record - Registro a escribir.
252
+ */
253
+ write(record) {
254
+ if (this.closed) return;
255
+ if (this.buffer.length >= this.maxBufferSize) {
256
+ const dropped = this.buffer.shift();
257
+ if (dropped && this.options.onError) {
258
+ const entry = {
259
+ level: "warn",
260
+ message: "FileTransport buffer overflow: oldest record dropped",
261
+ args: [],
262
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
263
+ hookEvent: "onError",
264
+ error: /* @__PURE__ */ new Error("FileTransport buffer overflow"),
265
+ extra: { droppedRecord: dropped }
266
+ };
267
+ this.options.onError(entry);
268
+ }
269
+ }
270
+ this.buffer.push(JSON.stringify(record) + "\n");
271
+ const batchSize = this.options.batchSize ?? BATCH_SIZE_DEFAULT;
272
+ if (this.buffer.length >= batchSize) this.flush();
273
+ }
274
+ /**
275
+ * Vuelca el buffer al destino. En Node concatena el contenido y hace
276
+ * un único `appendFile`; en browser hace un único `setItem` sobre
277
+ * `localStorage`. El buffer se vacía antes del I/O para que los registros
278
+ * entrantes no esperen al disco. Los errores de escritura se reportan
279
+ * vía `onError` (nunca lanzan al caller).
280
+ *
281
+ * @returns {Promise<void>} Resuelve cuando el I/O terminó o falló.
282
+ */
283
+ async flush() {
284
+ if (this.closed || this.buffer.length === 0) return;
285
+ const payload = this.buffer.join("");
286
+ this.buffer = [];
287
+ if (isNodeLike()) await this.flushNode(payload);
288
+ else await this.flushBrowser(payload);
289
+ }
290
+ async flushNode(payload) {
291
+ const destination = this.resolveNodeDestination();
292
+ if (destination === null) return;
293
+ try {
294
+ await (await loadNodeFsPromises()).appendFile(destination, payload, "utf8");
295
+ } catch (error) {
296
+ this.emitError("FileTransport failed to write to disk", error);
297
+ }
298
+ }
299
+ async flushBrowser(payload) {
300
+ if (typeof localStorage === "undefined") {
301
+ this.emitError("FileTransport: localStorage is not available in this environment", null);
302
+ return;
303
+ }
304
+ try {
305
+ const key = LOCAL_STORAGE_KEY_PREFIX + this.resolveBrowserKey();
306
+ const existing = localStorage.getItem(key) ?? "";
307
+ localStorage.setItem(key, existing + payload);
308
+ } catch (error) {
309
+ this.emitError("FileTransport: localStorage write failed (quota? private mode?)", error);
310
+ }
311
+ }
312
+ /**
313
+ * Cierra el transport: detiene el timer de flush y dispara un flush
314
+ * final para no perder registros pendientes. Tras cerrar, `write` y
315
+ * `flush` se vuelven no-op.
316
+ *
317
+ * @returns {Promise<void>} Resuelve cuando el flush final termina.
318
+ */
319
+ async close() {
320
+ this.closed = true;
321
+ if (this.flushTimer) {
322
+ clearInterval(this.flushTimer);
323
+ this.flushTimer = void 0;
324
+ }
325
+ await this.flush();
326
+ }
327
+ resolveNodeDestination() {
328
+ const requested = this.options.destination ?? "app.log";
329
+ const sanitised = sanitiseNodePath(requested);
330
+ if (sanitised === null) {
331
+ this.emitError(`FileTransport: refusing destination with traversal segment: ${requested}`, null);
332
+ return null;
333
+ }
334
+ return sanitised;
335
+ }
336
+ resolveBrowserKey() {
337
+ return sanitiseBrowserKey(this.options.destination ?? "default");
338
+ }
339
+ emitError(message, cause) {
340
+ if (!this.options.onError) return;
341
+ const entry = {
342
+ level: "error",
343
+ message,
344
+ args: [],
345
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
346
+ hookEvent: "onError",
347
+ error: cause instanceof Error ? cause : new Error(String(cause))
348
+ };
349
+ this.options.onError(entry);
350
+ }
351
+ };
352
+ /**
353
+ * Detecta si el runtime es Node comprobando `process.versions.node`.
354
+ *
355
+ * @internal Dispatch Node/browser dentro del transport.
356
+ * @returns {boolean} `true` si corre sobre Node.
357
+ */
358
+ function isNodeLike() {
359
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
360
+ }
361
+ /**
362
+ * Sanea la ruta pedida por el caller: colapsa backslashes a `/`, descarta
363
+ * segmentos `.` y rechaza traversal (`..`) o tilde sin expandir (`~`). Una
364
+ * ruta absoluta explícita (POSIX `/...` o Windows `C:/...`) es una elección
365
+ * legítima del dueño del proceso y se preserva intacta — el guard existe
366
+ * para sanear input no confiable, no para impedir que el caller elija dónde
367
+ * escribe su propio log.
368
+ *
369
+ * @internal
370
+ * @param {string} input - Ruta cruda pedida por el caller.
371
+ * @returns {string | null} Ruta saneada (con el prefijo absoluto intacto si
372
+ * lo tenía), o `null` si contiene un segmento `..` o `~`.
373
+ */
374
+ function sanitiseNodePath(input) {
375
+ if (!input) return null;
376
+ const normalised = input.replace(/\\/g, "/").trim();
377
+ if (normalised.length === 0) return null;
378
+ const isPosixAbsolute = normalised.startsWith("/");
379
+ const segments = normalised.split("/").filter((s) => s.length > 0 && s !== ".");
380
+ if (segments.some((s) => s === ".." || s === "~")) return null;
381
+ const joined = segments.join("/");
382
+ return isPosixAbsolute ? "/" + joined : joined;
383
+ }
384
+ /**
385
+ * Convierte cualquier string en una clave válida para `localStorage`:
386
+ * colapsa todo carácter fuera de `[a-zA-Z0-9_-]` a `_` y recorta a 64
387
+ * caracteres. Devuelve `'default'` si el resultado es vacío.
388
+ *
389
+ * @internal
390
+ * @param {string} input - Clave cruda pedida por el caller.
391
+ * @returns {string} Clave saneada lista para `localStorage`.
392
+ */
393
+ function sanitiseBrowserKey(input) {
394
+ return input.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "default";
395
+ }
396
+ /**
397
+ * Dynamic import cacheado de `node:fs/promises`. La caché es intencionadamente
398
+ * module-scoped para que los flushes posteriores no paguen el coste del import.
399
+ * Además corre el import contra un timeout corto (`FS_PROMISES_LOAD_TIMEOUT_MS`)
400
+ * para que un entorno Node roto (bindings nativos corruptos) no bloquee el
401
+ * transport indefinidamente.
402
+ *
403
+ * @internal
404
+ * @returns {Promise<typeof import('node:fs/promises')>} Módulo `fs/promises` resuelto.
405
+ * @throws {Error} Si el import excede el timeout o el módulo no está disponible.
406
+ */
407
+ let _fsPromisesPromise = null;
408
+ async function loadNodeFsPromises() {
409
+ if (_fsPromisesPromise) return _fsPromisesPromise;
410
+ const importPromise = import("node:fs/promises");
411
+ const timeoutPromise = new Promise((_, reject) => {
412
+ setTimeout(() => reject(/* @__PURE__ */ new Error(`fs/promises import timed out after ${FS_PROMISES_LOAD_TIMEOUT_MS}ms`)), FS_PROMISES_LOAD_TIMEOUT_MS);
413
+ });
414
+ _fsPromisesPromise = Promise.race([importPromise, timeoutPromise]).catch((err) => {
415
+ _fsPromisesPromise = null;
416
+ throw err;
417
+ });
418
+ return _fsPromisesPromise;
419
+ }
420
+ //#endregion
421
+ //#region src/transports/HttpTransport.ts
422
+ const DEFAULT_MAX_BUFFER = 1e4;
423
+ const DEFAULT_BATCH_SIZE = 50;
424
+ const DEFAULT_MAX_RETRIES = 3;
425
+ const DEFAULT_INITIAL_BACKOFF = 250;
426
+ const DEFAULT_MAX_BACKOFF = 5e3;
427
+ const DEFAULT_FETCH_TIMEOUT = 1e4;
428
+ /**
429
+ * Transport basado en HTTP. Bufferea records, los batcha por tamaño o
430
+ * intervalo, POSTea el batch como JSON, y reporta fallos vía retry, buffer
431
+ * acotado y un hook `onError` — nunca un `.catch(() => {})` silencioso.
432
+ *
433
+ * Lifecycle de cada batch (driveado internamente por `sendWithRetry`):
434
+ * 1. `fetch(url, { method: 'POST', body, signal })` con un `AbortController`
435
+ * que aborta tras `fetchTimeoutMs`.
436
+ * 2. Si `response.ok` → batch considerado entregado.
437
+ * 3. Si `4xx` → dropeado sin reintento: el cliente nunca se recupera de un
438
+ * error de URL/auth/payload mal formado. Se dispara `onError`.
439
+ * 4. Si `5xx` o `fetch` lanza (red caída / abort por timeout) → reintento con
440
+ * backoff exponencial: arranca en `initialBackoffMs`, duplica por intento,
441
+ * techo `maxBackoffMs`, hasta `maxRetries` intentos. Tras el agotamiento
442
+ * el batch se re-bufferiza (o se trimea contra `maxBufferSize`) y se
443
+ * dispara `onError` con `droppedCount`.
444
+ *
445
+ * El body por defecto es el envelope JSON `{ logs: TransportRecord[] }`.
446
+ * Para cambiar el wire format, sobrescribe los hooks `protected`
447
+ * {@link HttpTransport.serializeBody} y {@link HttpTransport.buildHeaders}
448
+ * (referencia: {@link OtlpTransport}).
449
+ *
450
+ * Extender esta clase es la vía recomendada para shippear un transport
451
+ * nuevo orientado a HTTP.
452
+ *
453
+ * @example
454
+ * // Registro en un logger
455
+ * import logger from '@mks2508/better-logger';
456
+ * import { HttpTransport } from '@mks2508/better-logger/transports';
457
+ *
458
+ * logger.addTransport({
459
+ * target: new HttpTransport({
460
+ * url: 'https://logs.example.com/ingest',
461
+ * flushInterval: 5_000,
462
+ * batchSize: 100,
463
+ * onError: (entry) => console.error('[log-drop]', entry.message)
464
+ * })
465
+ * });
466
+ *
467
+ * @see {@link HttpTransportOptions}
468
+ * @see {@link OtlpTransport}
469
+ */
470
+ var HttpTransport = class {
471
+ /** Identificador del transport. Los loggers lo usan para lookup, dedup y logs de diagnóstico. */
472
+ name = "http";
473
+ buffer = [];
474
+ flushTimer;
475
+ closed = false;
476
+ /** Bag de options — `protected` para que subclasses (ej. {@link OtlpTransport}) puedan leerlo o extenderlo. */
477
+ options;
478
+ /**
479
+ * Crea una instancia de {@link HttpTransport}.
480
+ *
481
+ * Los campos omitidos en `options` se rellenan con defaults sensatos
482
+ * (`batchSize=50`, `maxBufferSize=10_000`, `maxRetries=3`,
483
+ * `initialBackoffMs=250`, `maxBackoffMs=5_000`, `fetchTimeoutMs=10_000`).
484
+ * Si se pasa `flushInterval`, arranca un `setInterval` que flushea cada
485
+ * N ms; si se omite, el flush solo dispara por llenado de `batchSize`.
486
+ *
487
+ * @param {HttpTransportOptions} [options] - Configuración opcional. Si se omite por completo, el transport queda inactivo hasta que se setee `options.url` por otra vía (subclasses).
488
+ *
489
+ * @example
490
+ * const t = new HttpTransport({
491
+ * url: 'https://logs.example.com/ingest',
492
+ * flushInterval: 5_000
493
+ * });
494
+ */
495
+ constructor(options) {
496
+ this.options = {
497
+ batchSize: DEFAULT_BATCH_SIZE,
498
+ maxBufferSize: DEFAULT_MAX_BUFFER,
499
+ maxRetries: DEFAULT_MAX_RETRIES,
500
+ initialBackoffMs: DEFAULT_INITIAL_BACKOFF,
501
+ maxBackoffMs: DEFAULT_MAX_BACKOFF,
502
+ fetchTimeoutMs: DEFAULT_FETCH_TIMEOUT,
503
+ ...options ?? {}
504
+ };
505
+ if (this.options.flushInterval) this.flushTimer = setInterval(() => {
506
+ this.flush();
507
+ }, this.options.flushInterval);
508
+ }
509
+ /** Records actualmente encolados esperando el próximo flush. */
510
+ get bufferSize() {
511
+ return this.buffer.length;
512
+ }
513
+ /** Capacidad máxima del buffer. Al superarla, el registro más viejo se dropea y se notifica vía `onError`. */
514
+ get maxBufferSize() {
515
+ return this.options.maxBufferSize ?? DEFAULT_MAX_BUFFER;
516
+ }
517
+ /**
518
+ * Indica si el transport está listo para aceptar y entregar records.
519
+ * Devuelve `false` tras {@link close} o si no se configuró `url`.
520
+ *
521
+ * @returns {boolean} `true` si el transport puede enviar.
522
+ */
523
+ isReady() {
524
+ return !this.closed && Boolean(this.options.url);
525
+ }
526
+ /**
527
+ * Encola un record en el buffer. Si el buffer está lleno, aplica la
528
+ * política de overflow (dropea el más viejo + dispara `onError`). Si tras
529
+ * el push se alcanza `batchSize`, dispara un flush asíncrono (sin await).
530
+ *
531
+ * No-op si el transport ya fue cerrado ({@link close}).
532
+ *
533
+ * @param {TransportRecord} record - Registro a encolar.
534
+ */
535
+ write(record) {
536
+ if (this.closed) return;
537
+ if (this.buffer.length >= this.maxBufferSize) this.applyOverflowPolicy();
538
+ this.buffer.push(record);
539
+ const batchSize = this.options.batchSize ?? DEFAULT_BATCH_SIZE;
540
+ if (this.buffer.length >= batchSize) this.flush();
541
+ }
542
+ /**
543
+ * Serializa un batch al body de la request. Las subclasses sobrescriben
544
+ * para cambiar la codificación (ej. {@link OtlpTransport} produce OTLP/HTTP
545
+ * JSON en vez del envelope default `{ logs: [...] }`).
546
+ *
547
+ */
548
+ serializeBody(records) {
549
+ return JSON.stringify({ logs: records });
550
+ }
551
+ /**
552
+ * Construye los headers de la request. Las subclasses pueden prependear
553
+ * headers transport-specific (ej. `signoz-ingestion-key`).
554
+ *
555
+ */
556
+ buildHeaders() {
557
+ return {
558
+ "Content-Type": "application/json",
559
+ ...this.options.headers ?? {}
560
+ };
561
+ }
562
+ applyOverflowPolicy() {
563
+ const dropped = this.buffer.shift();
564
+ if (dropped && this.options.onError) {
565
+ const entry = {
566
+ level: "warn",
567
+ message: "HttpTransport buffer overflow: oldest record dropped",
568
+ args: [dropped],
569
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
570
+ hookEvent: "onError",
571
+ error: /* @__PURE__ */ new Error("HttpTransport buffer overflow"),
572
+ extra: { droppedRecord: dropped }
573
+ };
574
+ this.options.onError(entry);
575
+ }
576
+ }
577
+ /**
578
+ * Flushea el buffer actual: toma un snapshot de los records pendientes,
579
+ * los envía con retry/backoff vía `sendWithRetry`, y ante fallo los
580
+ * re-bufferiza preservando el orden. Si la re-bufferización excede
581
+ * `maxBufferSize`, trimea los más viejos y dispara `onError` con
582
+ * `droppedCount`.
583
+ *
584
+ * No-op si el transport está cerrado, el buffer está vacío o no hay
585
+ * `url` configurada.
586
+ *
587
+ * @returns {Promise<void>} Resuelve cuando el intento de entrega del batch actual terminó (success, drop definitivo o no-op).
588
+ *
589
+ * @see {@link HttpTransportOptions.onError}
590
+ */
591
+ async flush() {
592
+ if (this.closed || this.buffer.length === 0 || !this.options.url) return;
593
+ const records = [...this.buffer];
594
+ this.buffer = [];
595
+ if (!await this.sendWithRetry(records)) {
596
+ const combined = records.concat(this.buffer);
597
+ if (combined.length > this.maxBufferSize) {
598
+ const trimmed = combined.slice(combined.length - this.maxBufferSize);
599
+ const droppedCount = combined.length - trimmed.length;
600
+ if (droppedCount > 0 && this.options.onError) {
601
+ const entry = {
602
+ level: "error",
603
+ message: `HttpTransport dropped ${droppedCount} records after retry exhaustion`,
604
+ args: [],
605
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
606
+ hookEvent: "onError",
607
+ error: /* @__PURE__ */ new Error("HttpTransport retry exhaustion"),
608
+ extra: { droppedCount }
609
+ };
610
+ this.options.onError(entry);
611
+ }
612
+ this.buffer = trimmed;
613
+ } else this.buffer = combined;
614
+ }
615
+ }
616
+ async sendWithRetry(records) {
617
+ const url = this.options.url;
618
+ if (!url) return false;
619
+ const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
620
+ const initialBackoff = this.options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF;
621
+ const maxBackoff = this.options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF;
622
+ const fetchTimeout = this.options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT;
623
+ const body = this.serializeBody(records);
624
+ const headers = this.buildHeaders();
625
+ let attempt = 0;
626
+ let backoff = initialBackoff;
627
+ while (attempt <= maxRetries) {
628
+ try {
629
+ const controller = new AbortController();
630
+ const timeoutId = setTimeout(() => controller.abort(), fetchTimeout);
631
+ const response = await fetch(url, {
632
+ method: "POST",
633
+ headers,
634
+ body,
635
+ signal: controller.signal
636
+ });
637
+ clearTimeout(timeoutId);
638
+ if (response.ok) return true;
639
+ if (response.status >= 400 && response.status < 500) {
640
+ if (this.options.onError) {
641
+ const entry = {
642
+ level: "error",
643
+ message: `HttpTransport ${response.status} ${response.statusText} (not retried)`,
644
+ args: [],
645
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
646
+ hookEvent: "onError",
647
+ error: /* @__PURE__ */ new Error(`HTTP ${response.status}`),
648
+ extra: { responseStatus: response.status }
649
+ };
650
+ this.options.onError(entry);
651
+ }
652
+ return false;
653
+ }
654
+ if (attempt === maxRetries) break;
655
+ await sleep(backoff);
656
+ backoff = Math.min(backoff * 2, maxBackoff);
657
+ } catch (error) {
658
+ if (attempt === maxRetries) {
659
+ if (this.options.onError) {
660
+ const entry = {
661
+ level: "error",
662
+ message: `HttpTransport fetch failed after ${maxRetries + 1} attempts`,
663
+ args: [],
664
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
665
+ hookEvent: "onError",
666
+ error: error instanceof Error ? error : new Error(String(error))
667
+ };
668
+ this.options.onError(entry);
669
+ }
670
+ return false;
671
+ }
672
+ await sleep(backoff);
673
+ backoff = Math.min(backoff * 2, maxBackoff);
674
+ }
675
+ attempt++;
676
+ }
677
+ return false;
678
+ }
679
+ /**
680
+ * Cierra el transport: marca el flag `closed`, detiene el timer de
681
+ * `flushInterval` si estaba corriendo, y ejecuta un flush final para
682
+ * entregar lo pendiente.
683
+ *
684
+ * Tras `close()`, todo {@link write} posterior es no-op y
685
+ * {@link isReady} devuelve `false`.
686
+ *
687
+ * @returns {Promise<void>} Resuelve cuando el flush final termina.
688
+ *
689
+ * @see {@link flush}
690
+ */
691
+ async close() {
692
+ this.closed = true;
693
+ if (this.flushTimer) {
694
+ clearInterval(this.flushTimer);
695
+ this.flushTimer = void 0;
696
+ }
697
+ await this.flush();
698
+ }
699
+ };
700
+ function sleep(ms) {
701
+ return new Promise((resolve) => setTimeout(resolve, ms));
702
+ }
703
+ //#endregion
704
+ //#region src/transports/OtlpTransport.ts
705
+ /**
706
+ * Transport OTLP/HTTP para SigNoz (o cualquier backend compatible con OTLP).
707
+ *
708
+ * Extiende {@link HttpTransport} — hereda retry, buffer acotado, status check,
709
+ * close asincrónico y el hook on-error. Overridea únicamente la shape del
710
+ * payload y los headers del request.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * logger.addTransport({
715
+ * target: new OtlpTransport({
716
+ * endpoint: 'https://otelcollector.example.com:4318',
717
+ * serviceName: 'my-app',
718
+ * serviceVersion: '1.2.3',
719
+ * environment: 'production',
720
+ * ingestKeyEnvVar: 'SIGNOZ_KEY'
721
+ * })
722
+ * });
723
+ * ```
724
+ */
725
+ var OtlpTransport = class extends HttpTransport {
726
+ name = "otlp";
727
+ resource;
728
+ /** Resuelto al construir. Nunca se loguea, nunca se escribe a source. */
729
+ ingestKeyValue;
730
+ constructor(options) {
731
+ if (!options.endpoint) throw new Error("OtlpTransport: `endpoint` is required");
732
+ if (!options.serviceName) throw new Error("OtlpTransport: `serviceName` is required");
733
+ const ingestKey = readIngestKey(options.ingestKeyEnvVar);
734
+ const httpOptions = {
735
+ url: `${stripTrailingSlash(options.endpoint)}/v1/logs`,
736
+ headers: {
737
+ ...ingestKey ? { "signoz-ingestion-key": ingestKey } : {},
738
+ ...options.headers ?? {}
739
+ },
740
+ batchSize: options.batchSize,
741
+ flushInterval: options.flushInterval,
742
+ maxBufferSize: options.maxBufferSize,
743
+ maxRetries: options.maxRetries,
744
+ initialBackoffMs: options.initialBackoffMs,
745
+ maxBackoffMs: options.maxBackoffMs,
746
+ fetchTimeoutMs: options.fetchTimeoutMs,
747
+ onError: options.onError
748
+ };
749
+ super(httpOptions);
750
+ this.resource = {
751
+ "service.name": options.serviceName,
752
+ ...options.serviceVersion ? { "service.version": options.serviceVersion } : {},
753
+ ...options.environment ? { "deployment.environment": options.environment } : {},
754
+ ...options.resourceAttributes ?? {}
755
+ };
756
+ this.ingestKeyValue = ingestKey;
757
+ }
758
+ /**
759
+ * Construye el payload JSON OTLP/HTTP a partir de los records bufferizados.
760
+ * Un bloque `resourceLogs` por batch (sigue la guía de batching del
761
+ * collector OTel). Expuesto para tests y para subclasses custom de transport.
762
+ *
763
+ * @param records - Records de log a serializar dentro del payload.
764
+ * @returns Objeto `LogsData` listo para `JSON.stringify`.
765
+ * @see {@link OtlpLogsPayload}
766
+ */
767
+ buildPayload(records) {
768
+ return { resourceLogs: [{
769
+ resource: { attributes: Object.entries(this.resource).filter((entry) => typeof entry[1] === "string").map(([key, value]) => ({
770
+ key,
771
+ value: { stringValue: value }
772
+ })) },
773
+ scopeLogs: [{
774
+ scope: {
775
+ name: "better-logger",
776
+ version: "5.1.0"
777
+ },
778
+ logRecords: records.map((r) => this.toLogRecord(r))
779
+ }]
780
+ }] };
781
+ }
782
+ /**
783
+ * Serializa un batch al body JSON del request OTLP/HTTP. Override de
784
+ * {@link HttpTransport.serializeBody}.
785
+ *
786
+ * @param records - Records del batch a serializar.
787
+ * @returns String JSON listo para usar como `body` del fetch POST.
788
+ */
789
+ serializeBody(records) {
790
+ return JSON.stringify(this.buildPayload(records));
791
+ }
792
+ /**
793
+ * Construye los headers del request. Agrega `signoz-ingestion-key` con el
794
+ * valor resuelto al construir (nunca se loguea, nunca se escribe a source).
795
+ *
796
+ * @returns Record de headers a merguear en el fetch.
797
+ */
798
+ buildHeaders() {
799
+ return {
800
+ "Content-Type": "application/json",
801
+ ...this.ingestKeyValue ? { "signoz-ingestion-key": this.ingestKeyValue } : {},
802
+ ...this.options.headers ?? {}
803
+ };
804
+ }
805
+ toLogRecord(record) {
806
+ const timeUnixNano = String(BigInt(record.time) * 1000000n);
807
+ const base = {
808
+ timeUnixNano,
809
+ observedTimeUnixNano: timeUnixNano,
810
+ severityNumber: LOG_LEVEL_TO_SEVERITY_NUMBER[record.level],
811
+ severityText: record.severityText,
812
+ body: { stringValue: record.msg }
813
+ };
814
+ const attributes = collectAttributes(record);
815
+ if (attributes.length > 0) base.attributes = attributes;
816
+ if (record.traceId) base.traceId = record.traceId;
817
+ if (record.spanId) base.spanId = record.spanId;
818
+ return base;
819
+ }
820
+ };
821
+ /**
822
+ * Quita la barra final de un endpoint si la lleva.
823
+ *
824
+ * @internal Compartido con {@link OtlpTraceTransport}; no es API pública.
825
+ */
826
+ function stripTrailingSlash(value) {
827
+ return value.endsWith("/") ? value.slice(0, -1) : value;
828
+ }
829
+ /**
830
+ * Lee la ingest API key desde `process.env[name]`. Devuelve `undefined`
831
+ * silenciosamente si `process` no está disponible (bundles estrictos de
832
+ * browser) o si la variable no está seteada. Nunca throwea, nunca loguea
833
+ * el valor.
834
+ *
835
+ * @internal Helper del constructor de {@link OtlpTransport} y
836
+ * {@link OtlpTraceTransport}; no es API pública.
837
+ * @param envVarName - Nombre de la env var a leer.
838
+ * @returns Valor de la key, o `undefined` si no está disponible.
839
+ */
840
+ function readIngestKey(envVarName) {
841
+ if (!envVarName) return void 0;
842
+ if (typeof process === "undefined") return void 0;
843
+ const value = process.env?.[envVarName];
844
+ if (!value) return void 0;
845
+ return value;
846
+ }
847
+ function collectAttributes(record) {
848
+ const out = [];
849
+ if (record.prefix) out.push({
850
+ key: "logger.prefix",
851
+ value: { stringValue: record.prefix }
852
+ });
853
+ if (record.tag) out.push({
854
+ key: "logger.tag",
855
+ value: { stringValue: record.tag }
856
+ });
857
+ if (record.location) {
858
+ out.push({
859
+ key: "code.filepath",
860
+ value: { stringValue: record.location.file }
861
+ });
862
+ out.push({
863
+ key: "code.lineno",
864
+ value: { intValue: record.location.line }
865
+ });
866
+ if (record.location.function) out.push({
867
+ key: "code.function",
868
+ value: { stringValue: record.location.function }
869
+ });
870
+ }
871
+ if (record.attributes) for (const [key, value] of Object.entries(record.attributes)) {
872
+ const mapped = toOtlpAttribute(value);
873
+ if (mapped) out.push({
874
+ key,
875
+ value: mapped
876
+ });
877
+ }
878
+ return out;
879
+ }
880
+ /**
881
+ * Convierte un valor JS a un `AnyValue` de OTel. Compartido con
882
+ * {@link OtlpTraceTransport} para serializar attributes de span con el mismo
883
+ * mapping.
884
+ *
885
+ * @internal Exportado para reuse entre transports OTLP; no es API pública.
886
+ */
887
+ function toOtlpAttribute(value) {
888
+ if (value === null || value === void 0) return null;
889
+ if (typeof value === "string") return { stringValue: value };
890
+ if (typeof value === "number") {
891
+ if (Number.isInteger(value)) return { intValue: value };
892
+ return { doubleValue: value };
893
+ }
894
+ if (typeof value === "boolean") return { boolValue: value };
895
+ if (Array.isArray(value)) return { arrayValue: { values: value.map((v) => toOtlpAttribute(v)).filter((v) => v !== null) } };
896
+ return { stringValue: JSON.stringify(value) };
897
+ }
898
+ //#endregion
899
+ //#region src/transports/OtlpTraceTransport.ts
900
+ /**
901
+ * Transport OTLP/HTTP para traces (spans) — POST a `<endpoint>/v1/traces`.
902
+ *
903
+ * Espeja {@link OtlpTransport} (que va a `/v1/logs`): hereda de
904
+ * {@link HttpTransport} el batch, retry con backoff, buffer acotado, close
905
+ * asincrónico y el hook on-error. Solo cambia la shape del payload y acepta
906
+ * exclusivamente records de kind `'span'` (`accepts = ['span']`): el
907
+ * {@link TransportManager} garantiza que jamás recibe un log record.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * logger.addTransport({
912
+ * target: new OtlpTraceTransport({
913
+ * endpoint: 'https://otelcollector.example.com:4318',
914
+ * serviceName: 'my-app',
915
+ * scopeVersion: '1.2.3'
916
+ * })
917
+ * });
918
+ *
919
+ * await logger.span('db.query', { table: 'users' }, async () => {
920
+ * await db.select();
921
+ * });
922
+ * ```
923
+ */
924
+ var OtlpTraceTransport = class extends HttpTransport {
925
+ name = "otlp-trace";
926
+ /** Este transport SOLO acepta spans — jamás recibe log records. */
927
+ accepts = ["span"];
928
+ resource;
929
+ scopeVersion;
930
+ /** Resuelto al construir. Nunca se loguea, nunca se escribe a source. */
931
+ ingestKeyValue;
932
+ constructor(options) {
933
+ if (!options.endpoint) throw new Error("OtlpTraceTransport: `endpoint` is required");
934
+ if (!options.serviceName) throw new Error("OtlpTraceTransport: `serviceName` is required");
935
+ const ingestKey = readIngestKey(options.ingestKeyEnvVar);
936
+ const httpOptions = {
937
+ url: `${stripTrailingSlash(options.endpoint)}/v1/traces`,
938
+ headers: {
939
+ ...ingestKey ? { "signoz-ingestion-key": ingestKey } : {},
940
+ ...options.headers ?? {}
941
+ },
942
+ batchSize: options.batchSize,
943
+ flushInterval: options.flushInterval,
944
+ maxBufferSize: options.maxBufferSize,
945
+ maxRetries: options.maxRetries,
946
+ initialBackoffMs: options.initialBackoffMs,
947
+ maxBackoffMs: options.maxBackoffMs,
948
+ fetchTimeoutMs: options.fetchTimeoutMs,
949
+ onError: options.onError
950
+ };
951
+ super(httpOptions);
952
+ this.resource = {
953
+ "service.name": options.serviceName,
954
+ ...options.serviceVersion ? { "service.version": options.serviceVersion } : {},
955
+ ...options.environment ? { "deployment.environment": options.environment } : {},
956
+ ...options.resourceAttributes ?? {}
957
+ };
958
+ this.scopeVersion = options.scopeVersion;
959
+ this.ingestKeyValue = ingestKey;
960
+ }
961
+ /**
962
+ * Construye el payload JSON OTLP/HTTP a partir de los spans bufferizados.
963
+ * Un bloque `resourceSpans` por batch. Expuesto para tests y subclasses.
964
+ *
965
+ * @param spans - Spans a serializar dentro del payload.
966
+ * @returns Objeto `TracesData` listo para `JSON.stringify`.
967
+ */
968
+ buildPayload(spans) {
969
+ return { resourceSpans: [{
970
+ resource: { attributes: Object.entries(this.resource).filter((entry) => typeof entry[1] === "string").map(([key, value]) => ({
971
+ key,
972
+ value: { stringValue: value }
973
+ })) },
974
+ scopeSpans: [{
975
+ scope: {
976
+ name: "better-logger",
977
+ ...this.scopeVersion ? { version: this.scopeVersion } : {}
978
+ },
979
+ spans: spans.map((s) => this.toOtlpSpan(s))
980
+ }]
981
+ }] };
982
+ }
983
+ /**
984
+ * Serializa un batch al body JSON del request OTLP/HTTP. Override de
985
+ * {@link HttpTransport.serializeBody}.
986
+ */
987
+ serializeBody(records) {
988
+ return JSON.stringify(this.buildPayload(records));
989
+ }
990
+ /**
991
+ * Construye los headers del request. Agrega `signoz-ingestion-key` con el
992
+ * valor resuelto al construir.
993
+ */
994
+ buildHeaders() {
995
+ return {
996
+ "Content-Type": "application/json",
997
+ ...this.ingestKeyValue ? { "signoz-ingestion-key": this.ingestKeyValue } : {},
998
+ ...this.options.headers ?? {}
999
+ };
1000
+ }
1001
+ toOtlpSpan(span) {
1002
+ const attributes = [{
1003
+ key: "logger.scope",
1004
+ value: { stringValue: span.scope }
1005
+ }];
1006
+ for (const [key, value] of Object.entries(span.attributes)) {
1007
+ const mapped = toOtlpAttribute(value);
1008
+ if (mapped) attributes.push({
1009
+ key,
1010
+ value: mapped
1011
+ });
1012
+ }
1013
+ if (span.incomplete) attributes.push({
1014
+ key: "span.incomplete",
1015
+ value: { boolValue: true }
1016
+ });
1017
+ const out = {
1018
+ traceId: span.traceId,
1019
+ spanId: span.spanId,
1020
+ ...span.parentSpanId ? { parentSpanId: span.parentSpanId } : {},
1021
+ name: span.name,
1022
+ kind: span.spanKind,
1023
+ startTimeUnixNano: span.startTimeUnixNano,
1024
+ endTimeUnixNano: span.endTimeUnixNano,
1025
+ attributes
1026
+ };
1027
+ if (span.status) out.status = span.status;
1028
+ return out;
1029
+ }
1030
+ };
1031
+ //#endregion
1032
+ //#region src/transports/TransportManager.ts
1033
+ /**
1034
+ * Genera un id opaco y estable en el tiempo para cada transport añadido al
1035
+ * manager. Combina timestamp (base36) con un sufijo aleatorio para evitar
1036
+ * colisiones entre adds casi simultáneos.
1037
+ *
1038
+ * @internal No es API pública — el formato del id es inestable y los callers
1039
+ * deben tratarlo como opaco (solo compararlo y pasárselo a `remove`).
1040
+ */
1041
+ function generateTransportId() {
1042
+ return `transport-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1043
+ }
1044
+ /**
1045
+ * Registry estático de los transports built-in, con el que se inicializa el
1046
+ * registry vivo de cada instancia de {@link TransportManager}. Los customs
1047
+ * se añaden al registry por-instancia vía {@link TransportManager.register}
1048
+ * (no a este Map módulo-nivel).
1049
+ *
1050
+ * Los cinco built-ins registrados automáticamente:
1051
+ * - `'console'` → {@link ConsoleTransport} (logs)
1052
+ * - `'file'` → {@link FileTransport} (logs)
1053
+ * - `'http'` → {@link HttpTransport} (logs)
1054
+ * - `'otlp'` → {@link OtlpTransport} (logs → `/v1/logs`)
1055
+ * - `'otlp-trace'` → {@link OtlpTraceTransport} (spans → `/v1/traces`)
1056
+ *
1057
+ * @internal No exportado — los callers externos usan
1058
+ * {@link TransportManager.register} / {@link TransportManager.listRegistered}.
1059
+ */
1060
+ const BUILTIN_REGISTRY = /* @__PURE__ */ new Map([
1061
+ ["console", ConsoleTransport],
1062
+ ["file", FileTransport],
1063
+ ["http", HttpTransport],
1064
+ ["otlp", OtlpTransport],
1065
+ ["otlp-trace", OtlpTraceTransport]
1066
+ ]);
1067
+ /** `accepts` efectivo cuando el transport no lo declara: log-only. */
1068
+ const DEFAULT_ACCEPTS = ["log"];
1069
+ /** Estrecha un record del pipeline a su kind: todo lo que no es span es log. */
1070
+ function recordKind(record) {
1071
+ return record.kind === "span" ? "span" : "log";
1072
+ }
1073
+ /**
1074
+ * Registry y dispatcher de transports. Mantiene el set activo de destinos
1075
+ * de log (console, file, http, otlp, o transports custom registrados vía
1076
+ * {@link TransportManager.register}) y les dispatcha cada
1077
+ * {@link TransportRecord} producido por el Logger.
1078
+ *
1079
+ * Cada transport added se identifica por un `id` opaco (generado por la
1080
+ * propia instancia) que devuelven {@link add} y {@link list}, y que acepta
1081
+ * {@link remove}. El registry arranca con los cuatro built-ins
1082
+ * (`console` / `file` / `http` / `otlp`) ya cargados; {@link register}
1083
+ * añade customs sin poder sobreescribir los built-ins (lanza).
1084
+ *
1085
+ * El dispatch ({@link write}) aplica el filtro de `level` por transport y
1086
+ * el `transform` opcional de {@link TransportOptions}, y captura toda
1087
+ * excepción / rechazo de cada transport para que un transport roto nunca
1088
+ * rompa el log call del caller. El propio Logger habla con el manager a
1089
+ * través del facade {@link TransportBridge} (ver
1090
+ * `Logger.addTransport` / `Logger.getTransportManager`).
1091
+ *
1092
+ * Implementa {@link ITransportManager}.
1093
+ *
1094
+ * @example
1095
+ * // Manager con nivel default y transports añadidos por nombre
1096
+ * const tm = new TransportManager('info');
1097
+ * tm.add({ target: 'console' });
1098
+ * tm.add({ target: 'file', options: { path: './app.log' } });
1099
+ *
1100
+ * @example
1101
+ * // Registrar un transport custom y usarlo por nombre
1102
+ * tm.register('datadog', DatadogTransport);
1103
+ * tm.add({ target: 'datadog', options: { apiKey: env('DD_KEY') } });
1104
+ *
1105
+ * @example
1106
+ * // Pasar una instancia de ITransport directamente (sin pasar por registry)
1107
+ * tm.add({ target: new MyTransport(), level: 'warn' });
1108
+ *
1109
+ * @example
1110
+ * // Dispatch + ciclo de vida
1111
+ * await tm.write(record);
1112
+ * await tm.flush(); // flusha todos los buffered
1113
+ * await tm.close(); // flush + close + clear
1114
+ *
1115
+ * @see {@link ITransportManager}
1116
+ * @see {@link TransportTarget}
1117
+ * @see {@link TransportBridge}
1118
+ */
1119
+ var TransportManager = class {
1120
+ transports = /* @__PURE__ */ new Map();
1121
+ defaultLevel = "info";
1122
+ registry = new Map(BUILTIN_REGISTRY);
1123
+ /**
1124
+ * Crea un nuevo manager.
1125
+ *
1126
+ * @param {LogLevel} [defaultLevel='info'] - Nivel mínimo por defecto
1127
+ * para transports añadidos sin `level` explícito en su
1128
+ * {@link TransportTarget}. Records con `levelValue` inferior al del
1129
+ * transport se descartan en {@link write}.
1130
+ *
1131
+ * @example
1132
+ * const tm = new TransportManager('debug'); // todo pasa salvo filter propio
1133
+ */
1134
+ constructor(defaultLevel) {
1135
+ if (defaultLevel) this.defaultLevel = defaultLevel;
1136
+ }
1137
+ /**
1138
+ * Registra un constructor de transport bajo un nombre string. Tras el
1139
+ * registro, `add({ target: name, options })` instancia ese transport con
1140
+ * las options pasadas.
1141
+ *
1142
+ * No se puede sobreescribir un built-in (`console` / `file` / `http` /
1143
+ * `otlp`): lanza para evitar silenciar un transport crítico por un
1144
+ * accidente de naming. El registro es por-instancia (no comparte entre
1145
+ * managers).
1146
+ *
1147
+ * @param {string} name - Nombre bajo el que registrar (usado luego como
1148
+ * `target` en {@link TransportTarget}).
1149
+ * @param {TransportConstructor} ctor - Constructor que acepta
1150
+ * {@link TransportOptions} y devuelve un {@link ITransport}.
1151
+ * @throws {Error} Si `name` colisiona con un built-in del registry.
1152
+ *
1153
+ * @example
1154
+ * tm.register('datadog', DatadogTransport);
1155
+ * tm.add({ target: 'datadog', options: { apiKey: env('DD_KEY') } });
1156
+ *
1157
+ * @see {@link listRegistered}
1158
+ */
1159
+ register(name, ctor) {
1160
+ if (BUILTIN_REGISTRY.has(name)) throw new Error(`TransportManager.register: '${name}' is a built-in and cannot be overridden`);
1161
+ this.registry.set(name, ctor);
1162
+ }
1163
+ /**
1164
+ * Lista los nombres de transports registrados en esta instancia
1165
+ * (built-ins + customs añadidos vía {@link register}).
1166
+ *
1167
+ * @returns {string[]} Nombres registrados. Incluye siempre los cuatro
1168
+ * built-ins.
1169
+ *
1170
+ * @example
1171
+ * tm.register('loki', LokiTransport);
1172
+ * tm.listRegistered(); // ['console', 'file', 'http', 'otlp', 'loki']
1173
+ */
1174
+ listRegistered() {
1175
+ return [...this.registry.keys()];
1176
+ }
1177
+ /**
1178
+ * Añade un transport al set activo y devuelve su id opaco.
1179
+ *
1180
+ * Acepta dos formas de `target`:
1181
+ * - **string**: se resuelve contra el registry (built-ins + customs
1182
+ * vía {@link register}). Lanza si el nombre no existe, listando los
1183
+ * registrados para facilitar el debug.
1184
+ * - **ITransport instancia**: se registra tal cual, sin pasar por el
1185
+ * registry. Útil para transports one-off o cuya configuración no
1186
+ * encaja en un constructor reutilizable.
1187
+ *
1188
+ * El `level` (explícito en el target o `defaultLevel` del manager) fija
1189
+ * el filtro por transport — los records con `levelValue` inferior se
1190
+ * descartan en {@link write}. El `transform` opcional de
1191
+ * {@link TransportOptions} se aplica por transport antes de delegar al
1192
+ * `write` concreto.
1193
+ *
1194
+ * @param {TransportTarget} target - Especificación del transport a añadir.
1195
+ * @returns {string} Id opaco del transport añadido. Úsalo con
1196
+ * {@link remove}; aparece en {@link list}.
1197
+ * @throws {Error} Si `target.target` es un string no presente en el
1198
+ * registry.
1199
+ *
1200
+ * @example
1201
+ * // Por nombre (built-in o custom registrado)
1202
+ * const id = tm.add({ target: 'console' });
1203
+ *
1204
+ * @example
1205
+ * // Instancia directa con nivel y transform propios
1206
+ * const id = tm.add({
1207
+ * target: new MyTransport(),
1208
+ * level: 'warn',
1209
+ * options: { transform: r => r.level === 'debug' ? null : r }
1210
+ * });
1211
+ *
1212
+ * @see {@link TransportTarget}
1213
+ */
1214
+ add(target) {
1215
+ const id = generateTransportId();
1216
+ const level = target.level || this.defaultLevel;
1217
+ let transport;
1218
+ if (typeof target.target === "string") {
1219
+ const ctor = this.registry.get(target.target);
1220
+ if (!ctor) throw new Error(`TransportManager.add: unknown transport name '${target.target}'. Registered: ${this.listRegistered().join(", ")}`);
1221
+ transport = new ctor(target.options ?? {});
1222
+ } else transport = target.target;
1223
+ const entry = {
1224
+ id,
1225
+ transport,
1226
+ options: target.options || {},
1227
+ level,
1228
+ levelValue: require_core.LOG_LEVELS[level]
1229
+ };
1230
+ this.transports.set(id, entry);
1231
+ return id;
1232
+ }
1233
+ /**
1234
+ * Elimina un transport del set por su id. Si el transport expone
1235
+ * `close()`, lo invoca para liberar recursos (timers, sockets, file
1236
+ * handles). Si `close()` devuelve una Promise, su eventual rechazo se
1237
+ * ignora de forma best-effort — los errores de close durante la
1238
+ * remoción no se propagan al caller.
1239
+ *
1240
+ * @param {string} id - Id devuelto por {@link add}.
1241
+ * @returns {boolean} `true` si había un transport con ese id (y se
1242
+ * eliminó), `false` si el id no existía.
1243
+ *
1244
+ * @example
1245
+ * const id = tm.add({ target: 'file', options: { path: './app.log' } });
1246
+ * tm.remove(id); // true — FileTransport.close() se invoca
1247
+ */
1248
+ remove(id) {
1249
+ const entry = this.transports.get(id);
1250
+ if (entry) {
1251
+ const closeResult = entry.transport.close?.();
1252
+ if (closeResult instanceof Promise) closeResult.catch(() => {});
1253
+ return this.transports.delete(id);
1254
+ }
1255
+ return false;
1256
+ }
1257
+ /**
1258
+ * Dispatcha un {@link TransportRecord} o {@link SpanRecord} a todos los
1259
+ * transports activos que acepten su kind.
1260
+ *
1261
+ * Por cada transport, en orden:
1262
+ * 1. **Routing por kind**: el kind del record (`'log'` o `'span'`)
1263
+ * debe estar en el `accepts` del transport (default `['log']` cuando
1264
+ * no lo declara). Un `SpanRecord` JAMÁS llega a un transport
1265
+ * log-only, y viceversa — esto evita que un `OtlpTransport` serialice
1266
+ * spans como `logRecords` hacia `/v1/logs`.
1267
+ * 2. **Filtro de nivel** (solo logs): si `record.levelValue <
1268
+ * entry.levelValue`, se skipa. Los spans no tienen nivel.
1269
+ * 3. **Transform** (solo logs): si `entry.options.transform` está
1270
+ * seteado, se aplica al record. Si devuelve `null`, el record se
1271
+ * droppea para este transport. Los spans NO pasan por transform —
1272
+ * su contrato asume shape de log (muta `record.message`).
1273
+ * 4. **Write**: se llama a `transport.write(record)`. Si
1274
+ * devuelve una Promise, se añade al batch await. Toda excepción
1275
+ * sincrónica o rechazo de Promise se captura y se loguea por
1276
+ * consola — el log call original del caller nunca rompe por un
1277
+ * transport roto.
1278
+ *
1279
+ * El método es async: retorna después de que todos los transports hayan
1280
+ * resuelto (o fallado) su write. Para fire-and-forget desde el Logger,
1281
+ * el caller puede ignorar la Promise.
1282
+ *
1283
+ * @param {TransportDispatchRecord} record - Record (log o span) a dispatchar.
1284
+ * @returns {Promise<void>} Resuelve cuando todos los writes terminaron
1285
+ * (exitosos o fallidos). Nunca rechaza.
1286
+ *
1287
+ * @example
1288
+ * await tm.write({
1289
+ * level: 'info', levelValue: 1, severityNumber: 9, severityText: 'INFO',
1290
+ * time: Date.now(), msg: 'boot ok'
1291
+ * });
1292
+ *
1293
+ * @see {@link TransportRecord}
1294
+ * @see {@link SpanRecord}
1295
+ */
1296
+ async write(record) {
1297
+ const promises = [];
1298
+ const kind = recordKind(record);
1299
+ for (const entry of this.transports.values()) {
1300
+ if (!(entry.transport.accepts ?? DEFAULT_ACCEPTS).includes(kind)) continue;
1301
+ let dispatchRecord = record;
1302
+ if (kind === "log") {
1303
+ const logRecord = record;
1304
+ if (logRecord.levelValue < entry.levelValue) continue;
1305
+ if (entry.options.transform) {
1306
+ const result = entry.options.transform(logRecord);
1307
+ if (result === null) continue;
1308
+ dispatchRecord = result;
1309
+ }
1310
+ }
1311
+ try {
1312
+ const result = entry.transport.write(dispatchRecord);
1313
+ if (result instanceof Promise) promises.push(result.catch((err) => {
1314
+ console.error("TransportManager.write: transport failed:", err);
1315
+ }));
1316
+ } catch (error) {
1317
+ console.error("TransportManager.write: transport threw synchronously:", error);
1318
+ }
1319
+ }
1320
+ await Promise.all(promises);
1321
+ }
1322
+ /**
1323
+ * Flushea todos los transports que expongan `flush()` (buffered transports
1324
+ * como {@link HttpTransport}, {@link FileTransport}, {@link OtlpTransport}).
1325
+ * Los transports sin `flush()` (p.ej. {@link ConsoleTransport}) se
1326
+ * skipan sin error.
1327
+ *
1328
+ * Errores de flush (sync throw o Promise rejection) se capturan y
1329
+ * loguean por consola — nunca propagan al caller. Útil para forzar el
1330
+ * envío del batch pendiente antes de un graceful shutdown.
1331
+ *
1332
+ * @returns {Promise<void>} Resuelve cuando todos los flushes terminaron.
1333
+ *
1334
+ * @example
1335
+ * await tm.flush();
1336
+ */
1337
+ async flush() {
1338
+ const promises = [];
1339
+ for (const entry of this.transports.values()) if (entry.transport.flush) try {
1340
+ const result = entry.transport.flush();
1341
+ if (result instanceof Promise) promises.push(result.catch((err) => {
1342
+ console.error("TransportManager.flush: transport failed:", err);
1343
+ }));
1344
+ } catch (error) {
1345
+ console.error("TransportManager.flush: transport threw synchronously:", error);
1346
+ }
1347
+ await Promise.all(promises);
1348
+ }
1349
+ /**
1350
+ * Shutdown ordenado: flushea todos los transports buffered, luego
1351
+ * invoca `close()` en cada uno, y finalmente limpia el set interno.
1352
+ * Tras esto la instancia queda sin transports — reusarla requiere
1353
+ * `add()` de nuevo.
1354
+ *
1355
+ * Los errores de close (sync throw o Promise rejection) se capturan y
1356
+ * loguean — nunca propagan al caller.
1357
+ *
1358
+ * @returns {Promise<void>} Resuelve cuando flush + close de todos los
1359
+ * transports terminaron.
1360
+ *
1361
+ * @example
1362
+ * // Shutdown limpio del proceso
1363
+ * process.on('SIGTERM', async () => {
1364
+ * await logger.getTransportManager()?.close();
1365
+ * process.exit(0);
1366
+ * });
1367
+ */
1368
+ async close() {
1369
+ await this.flush();
1370
+ const promises = [];
1371
+ for (const entry of this.transports.values()) if (entry.transport.close) try {
1372
+ const result = entry.transport.close();
1373
+ if (result instanceof Promise) promises.push(result.catch((err) => {
1374
+ console.error("TransportManager.close: transport failed:", err);
1375
+ }));
1376
+ } catch (error) {
1377
+ console.error("TransportManager.close: transport threw synchronously:", error);
1378
+ }
1379
+ await Promise.all(promises);
1380
+ this.transports.clear();
1381
+ }
1382
+ /**
1383
+ * Número de transports actualmente en el set activo.
1384
+ *
1385
+ * @returns {number}
1386
+ *
1387
+ * @example
1388
+ * if (tm.count === 0) console.warn('no transports configured');
1389
+ */
1390
+ get count() {
1391
+ return this.transports.size;
1392
+ }
1393
+ /**
1394
+ * Lista los ids opacos de los transports actualmente activos.
1395
+ *
1396
+ * @returns {string[]} Array de ids (mismo formato opaco que devolvió
1397
+ * {@link add}).
1398
+ *
1399
+ * @example
1400
+ * for (const id of tm.list()) {
1401
+ * console.log('removing transport', id);
1402
+ * tm.remove(id);
1403
+ * }
1404
+ */
1405
+ list() {
1406
+ return Array.from(this.transports.keys());
1407
+ }
1408
+ };
1409
+ //#endregion
1410
+ Object.defineProperty(exports, "ConsoleTransport", {
1411
+ enumerable: true,
1412
+ get: function() {
1413
+ return ConsoleTransport;
1414
+ }
1415
+ });
1416
+ Object.defineProperty(exports, "FileTransport", {
1417
+ enumerable: true,
1418
+ get: function() {
1419
+ return FileTransport;
1420
+ }
1421
+ });
1422
+ Object.defineProperty(exports, "HttpTransport", {
1423
+ enumerable: true,
1424
+ get: function() {
1425
+ return HttpTransport;
1426
+ }
1427
+ });
1428
+ Object.defineProperty(exports, "LOG_LEVEL_TO_SEVERITY_NUMBER", {
1429
+ enumerable: true,
1430
+ get: function() {
1431
+ return LOG_LEVEL_TO_SEVERITY_NUMBER;
1432
+ }
1433
+ });
1434
+ Object.defineProperty(exports, "LOG_LEVEL_TO_SEVERITY_TEXT", {
1435
+ enumerable: true,
1436
+ get: function() {
1437
+ return LOG_LEVEL_TO_SEVERITY_TEXT;
1438
+ }
1439
+ });
1440
+ Object.defineProperty(exports, "OtlpTraceTransport", {
1441
+ enumerable: true,
1442
+ get: function() {
1443
+ return OtlpTraceTransport;
1444
+ }
1445
+ });
1446
+ Object.defineProperty(exports, "OtlpTransport", {
1447
+ enumerable: true,
1448
+ get: function() {
1449
+ return OtlpTransport;
1450
+ }
1451
+ });
1452
+ Object.defineProperty(exports, "TransportManager", {
1453
+ enumerable: true,
1454
+ get: function() {
1455
+ return TransportManager;
1456
+ }
1457
+ });
1458
+
1459
+ //# sourceMappingURL=transports-eCvDrc2q.cjs.map