logixia 1.10.2 → 1.11.0

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.
Files changed (57) hide show
  1. package/README.md +121 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/{index-Co47qPnq.d.mts → index-CSFeEGLb.d.ts} +32 -2
  4. package/dist/index-CSFeEGLb.d.ts.map +1 -0
  5. package/dist/{index-F-A7hg1u.d.ts → index-Cw-sN_0_.d.mts} +32 -2
  6. package/dist/index-Cw-sN_0_.d.mts.map +1 -0
  7. package/dist/index.d.mts +256 -5
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.d.ts +256 -5
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +569 -33
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +557 -34
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{logitron-logger.module-BLT1y5Iq.d.ts → logitron-logger.module-DGwNfjBX.d.mts} +21 -2
  16. package/dist/logitron-logger.module-DGwNfjBX.d.mts.map +1 -0
  17. package/dist/{logitron-logger.module-bJ1hGhaL.js → logitron-logger.module-DHFampon.js} +186 -38
  18. package/dist/logitron-logger.module-DHFampon.js.map +1 -0
  19. package/dist/{logitron-logger.module-B8NklSC4.d.mts → logitron-logger.module-DfyBsT_K.d.ts} +21 -2
  20. package/dist/logitron-logger.module-DfyBsT_K.d.ts.map +1 -0
  21. package/dist/{logitron-logger.module-Bt_Jei1V.mjs → logitron-logger.module-QYBy_Kkq.mjs} +186 -38
  22. package/dist/logitron-logger.module-QYBy_Kkq.mjs.map +1 -0
  23. package/dist/middleware.d.mts +1 -1
  24. package/dist/middleware.d.mts.map +1 -1
  25. package/dist/middleware.d.ts +1 -1
  26. package/dist/middleware.d.ts.map +1 -1
  27. package/dist/middleware.js +4 -3
  28. package/dist/middleware.js.map +1 -1
  29. package/dist/middleware.mjs +4 -3
  30. package/dist/middleware.mjs.map +1 -1
  31. package/dist/nest.d.mts +2 -2
  32. package/dist/nest.d.mts.map +1 -1
  33. package/dist/nest.d.ts +2 -2
  34. package/dist/nest.d.ts.map +1 -1
  35. package/dist/nest.js +2 -2
  36. package/dist/nest.mjs +2 -2
  37. package/dist/testing.d.mts +1 -1
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/{transport.manager-zgEZCJhR.js → transport.manager-B9LF9uDd.js} +130 -56
  40. package/dist/transport.manager-B9LF9uDd.js.map +1 -0
  41. package/dist/{transport.manager-CaL4XuLD.mjs → transport.manager-Cij_sA-b.mjs} +128 -56
  42. package/dist/transport.manager-Cij_sA-b.mjs.map +1 -0
  43. package/dist/transports.d.mts +42 -3
  44. package/dist/transports.d.mts.map +1 -1
  45. package/dist/transports.d.ts +42 -3
  46. package/dist/transports.d.ts.map +1 -1
  47. package/dist/transports.js +1 -1
  48. package/dist/transports.mjs +1 -1
  49. package/package.json +1 -1
  50. package/dist/index-Co47qPnq.d.mts.map +0 -1
  51. package/dist/index-F-A7hg1u.d.ts.map +0 -1
  52. package/dist/logitron-logger.module-B8NklSC4.d.mts.map +0 -1
  53. package/dist/logitron-logger.module-BLT1y5Iq.d.ts.map +0 -1
  54. package/dist/logitron-logger.module-Bt_Jei1V.mjs.map +0 -1
  55. package/dist/logitron-logger.module-bJ1hGhaL.js.map +0 -1
  56. package/dist/transport.manager-CaL4XuLD.mjs.map +0 -1
  57. package/dist/transport.manager-zgEZCJhR.js.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { p as internalWarn, u as safeToString } from "./transport.manager-CaL4XuLD.mjs";
2
- import { A as redactObject, B as LogLevel, C as setTraceId, D as registerForShutdown, E as flushOnExit, F as isError, G as createExpressContextMiddleware, H as globalPluginRegistry, I as normalizeError, K as createFastifyContextHook, L as serializeError, M as getActiveOtelContext, N as getOtelMetaFields, O as resetShutdownHandlers, P as initOtelBridge, R as DEFAULT_LOG_COLORS, S as runWithTraceId, T as deregisterFromShutdown, U as usePlugin, V as PluginRegistry, W as LogixiaContext, _ as _setActiveContextKey, b as getCurrentTraceId, c as KafkaTraceInterceptor, d as __decorateMetadata, f as LogixiaLogger, g as TraceContext, h as TRACE_CONTEXT_KEY, i as WebSocketTraceInterceptor, j as disableOtelBridge, k as applyRedaction, l as LogixiaLoggerService, m as DEFAULT_TRACE_HEADERS, n as LOGIXIA_LOGGER_PREFIX, o as resolveResponseHeader, p as createLogger$1, r as LogixiaLoggerModule, s as __decorateParam, t as LOGIXIA_LOGGER_CONFIG, u as __decorate, v as createTraceMiddleware, w as traceStorage, x as getTraceContextKey, y as extractTraceId, z as DEFAULT_LOG_LEVELS } from "./logitron-logger.module-Bt_Jei1V.mjs";
1
+ import { d as internalError, f as internalLog, p as internalWarn, u as safeToString } from "./transport.manager-Cij_sA-b.mjs";
2
+ import { A as redactObject, B as LogLevel, C as setTraceId, D as registerForShutdown, E as flushOnExit, F as isError, G as createExpressContextMiddleware, H as globalPluginRegistry, I as normalizeError, K as createFastifyContextHook, L as serializeError, M as getActiveOtelContext, N as getOtelMetaFields, O as resetShutdownHandlers, P as initOtelBridge, R as DEFAULT_LOG_COLORS, S as runWithTraceId, T as deregisterFromShutdown, U as usePlugin, V as PluginRegistry, W as LogixiaContext, _ as _setActiveContextKey, b as getCurrentTraceId, c as KafkaTraceInterceptor, d as __decorateMetadata, f as LogixiaLogger, g as TraceContext, h as TRACE_CONTEXT_KEY, i as WebSocketTraceInterceptor, j as disableOtelBridge, k as applyRedaction, l as LogixiaLoggerService, m as DEFAULT_TRACE_HEADERS, n as LOGIXIA_LOGGER_PREFIX, o as resolveResponseHeader, p as createLogger$1, r as LogixiaLoggerModule, s as __decorateParam, t as LOGIXIA_LOGGER_CONFIG, u as __decorate, v as createTraceMiddleware, w as traceStorage, x as getTraceContextKey, y as extractTraceId, z as DEFAULT_LOG_LEVELS } from "./logitron-logger.module-QYBy_Kkq.mjs";
3
3
  import "./search-DOvSI-mb.mjs";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import { Catch, Inject, Optional } from "@nestjs/common";
5
6
 
6
7
  //#region src/exceptions/exception.ts
@@ -227,8 +228,11 @@ const InjectLogger = () => Inject(`${LOGIXIA_LOGGER_PREFIX}SERVICE`);
227
228
  /**
228
229
  * Method decorator that auto-logs entry, exit, duration, and errors.
229
230
  *
230
- * Works on both async and sync methods. Attaches to the logger found on the
231
- * class instance via a `logger` property (the conventional NestJS name).
231
+ * Preserves the original method's sync/async contract: a synchronous method
232
+ * stays synchronous (returns its value directly, with logs emitted
233
+ * fire-and-forget), and an async method is awaited so exit/error logs reflect
234
+ * the resolved result. Attaches to the logger found on the class instance via a
235
+ * `logger` property (the conventional NestJS name).
232
236
  *
233
237
  * @example
234
238
  * ```ts
@@ -246,7 +250,23 @@ function LogMethod(options = {}) {
246
250
  const className = ((_constructor = target.constructor) === null || _constructor === void 0 ? void 0 : _constructor.name) ?? "Unknown";
247
251
  const label = options.label ?? `${className}.${methodName}`;
248
252
  let _warnedNoLogger = false;
249
- descriptor.value = async function(...args) {
253
+ const reportLogFailure = (phase, err) => {
254
+ process.stderr.write(`[logixia] @LogMethod(${label}) ${phase} log failed: ${String(err)}\n`);
255
+ };
256
+ const emit$1 = (logger$1, phase, message, data) => {
257
+ const logFnRaw = logger$1[level];
258
+ const p = (typeof logFnRaw === "function" ? logFnRaw : logger$1.debug).bind(logger$1)(message, data);
259
+ if (p && typeof p.catch === "function") p.catch((e) => reportLogFailure(phase, e));
260
+ };
261
+ const emitError = (logger$1, error, start) => {
262
+ const err = error instanceof Error ? error : new Error(String(error));
263
+ const errLog = logger$1.error(err, {
264
+ method: label,
265
+ durationMs: Date.now() - start
266
+ });
267
+ if (errLog && typeof errLog.catch === "function") errLog.catch((e) => reportLogFailure("error", e));
268
+ };
269
+ descriptor.value = function(...args) {
250
270
  const logger$1 = this.logger ?? LogixiaLoggerModule._globalLogger ?? void 0;
251
271
  if (!logger$1 && !_warnedNoLogger) {
252
272
  _warnedNoLogger = true;
@@ -255,36 +275,31 @@ function LogMethod(options = {}) {
255
275
  const start = Date.now();
256
276
  const entry = { method: label };
257
277
  if (logArgs && args.length > 0) entry["args"] = args;
258
- const reportLogFailure = (phase, err) => {
259
- process.stderr.write(`[logixia] @LogMethod(${label}) ${phase} log failed: ${String(err)}\n`);
260
- };
261
- if (logger$1) {
262
- const logFnRaw = logger$1[level];
263
- await (typeof logFnRaw === "function" ? logFnRaw : logger$1.debug).bind(logger$1)(`→ ${label}`, entry).catch((e) => reportLogFailure("entry", e));
264
- }
265
- try {
266
- const result = await originalMethod.apply(this, args);
278
+ if (logger$1) emit$1(logger$1, "entry", `→ ${label}`, entry);
279
+ const buildExit = (result$1) => {
267
280
  const exit = {
268
281
  method: label,
269
282
  durationMs: Date.now() - start
270
283
  };
271
- if (logResult) exit["result"] = result;
272
- if (logger$1) {
273
- const logFnRaw = logger$1[level];
274
- await (typeof logFnRaw === "function" ? logFnRaw : logger$1.debug).bind(logger$1)(`← ${label}`, exit).catch((e) => reportLogFailure("exit", e));
275
- }
276
- return result;
284
+ if (logResult) exit["result"] = result$1;
285
+ return exit;
286
+ };
287
+ let result;
288
+ try {
289
+ result = originalMethod.apply(this, args);
277
290
  } catch (error) {
278
- if (logger$1 && logErrors) {
279
- const err = error instanceof Error ? error : new Error(String(error));
280
- const errLog = logger$1.error(err, {
281
- method: label,
282
- durationMs: Date.now() - start
283
- });
284
- if (errLog !== void 0 && errLog !== null && typeof errLog.catch === "function") errLog.catch((e) => reportLogFailure("error", e));
285
- }
291
+ if (logger$1 && logErrors) emitError(logger$1, error, start);
286
292
  throw error;
287
293
  }
294
+ if (result && typeof result.then === "function") return result.then((resolved) => {
295
+ if (logger$1) emit$1(logger$1, "exit", `← ${label}`, buildExit(resolved));
296
+ return resolved;
297
+ }, (error) => {
298
+ if (logger$1 && logErrors) emitError(logger$1, error, start);
299
+ throw error;
300
+ });
301
+ if (logger$1) emit$1(logger$1, "exit", `← ${label}`, buildExit(result));
302
+ return result;
288
303
  };
289
304
  return descriptor;
290
305
  };
@@ -344,6 +359,21 @@ LogixiaExceptionFilter = __decorate([
344
359
 
345
360
  //#endregion
346
361
  //#region src/formatters/json.formatter.ts
362
+ /**
363
+ * Build a JSON.stringify replacer that replaces circular references with the
364
+ * string '[Circular]' instead of throwing. A fresh replacer must be created per
365
+ * stringify call because it holds per-serialization state (the seen set).
366
+ */
367
+ function createCircularReplacer() {
368
+ const seen = /* @__PURE__ */ new WeakSet();
369
+ return function(_key, value) {
370
+ if (typeof value === "object" && value !== null) {
371
+ if (seen.has(value)) return "[Circular]";
372
+ seen.add(value);
373
+ }
374
+ return value;
375
+ };
376
+ }
347
377
  var JsonFormatter = class {
348
378
  constructor(options = {}) {
349
379
  this.includeTimestamp = options.includeTimestamp ?? true;
@@ -371,7 +401,8 @@ var JsonFormatter = class {
371
401
  hostname: process.env.HOSTNAME || "unknown",
372
402
  version: process.version
373
403
  };
374
- return this.prettyPrint ? JSON.stringify(formatted, null, 2) : JSON.stringify(formatted);
404
+ const replacer = createCircularReplacer();
405
+ return this.prettyPrint ? JSON.stringify(formatted, replacer, 2) : JSON.stringify(formatted, replacer);
375
406
  }
376
407
  serializePayload(payload) {
377
408
  const serialized = {};
@@ -474,11 +505,11 @@ var TextFormatter = class TextFormatter {
474
505
  if (typeof value === "string") return `${key}="${value}"`;
475
506
  if (typeof value === "number" || typeof value === "boolean") return `${key}=${value}`;
476
507
  if (value instanceof Date) return `${key}=${value.toISOString()}`;
477
- if (typeof value === "object") return `${key}=${JSON.stringify(value)}`;
508
+ if (typeof value === "object") return `${key}=${stripControls(safeToString(value))}`;
478
509
  return `${key}=${String(value)}`;
479
510
  }).join(" ");
480
511
  } catch {
481
- return JSON.stringify(payload);
512
+ return stripControls(safeToString(payload));
482
513
  }
483
514
  }
484
515
  /**
@@ -515,6 +546,234 @@ var TextFormatter = class TextFormatter {
515
546
  }
516
547
  };
517
548
 
549
+ //#endregion
550
+ //#region src/utils/runtime-control.ts
551
+ const DEFAULT_CYCLE = [
552
+ "error",
553
+ "warn",
554
+ "info",
555
+ "debug",
556
+ "trace",
557
+ "verbose"
558
+ ];
559
+ /**
560
+ * Register an OS-signal handler that cycles the logger's global level on each
561
+ * signal. Returns a dispose function that removes the listener.
562
+ *
563
+ * Cycling (rather than jumping straight to a fixed level) means a single,
564
+ * memorizable command (`kill -USR2 <pid>`) is enough to ratchet verbosity up
565
+ * while chasing a bug and back down again — no value to remember.
566
+ */
567
+ function registerLevelSignal(logger$1, options = {}) {
568
+ const signal = options.signal ?? "SIGUSR2";
569
+ const cycle = options.cycle && options.cycle.length > 0 ? options.cycle : [...DEFAULT_CYCLE];
570
+ const handler = () => {
571
+ const current = logger$1.getLevel();
572
+ const next = cycle[(cycle.indexOf(current) + 1) % cycle.length];
573
+ logger$1.setLevel(next);
574
+ internalLog(`runtime level changed via ${signal}: ${current} → ${next}`);
575
+ };
576
+ process.on(signal, handler);
577
+ return () => {
578
+ process.removeListener(signal, handler);
579
+ };
580
+ }
581
+ const VALID_LEVELS = new Set([
582
+ "error",
583
+ "warn",
584
+ "info",
585
+ "debug",
586
+ "trace",
587
+ "verbose",
588
+ "fatal"
589
+ ]);
590
+ /**
591
+ * Create an HTTP handler (Node `http`/Express-compatible) that reads and sets
592
+ * the logger's level at runtime.
593
+ *
594
+ * - `GET` → `{ level, namespaceLevels }`
595
+ * - `POST` → body `{ level?, namespaceLevels? }` applies them, returns the new state
596
+ *
597
+ * Custom levels are accepted too: any level the logger already knows passes
598
+ * through. Unknown levels are rejected with 400 so a typo can't silently mute
599
+ * logging. Mount behind your own auth — this intentionally has none.
600
+ */
601
+ function createLevelControlHandler(logger$1, options = {}) {
602
+ const allowed = options.allowedLevels && options.allowedLevels.length > 0 ? new Set(options.allowedLevels.map((l) => l.toLowerCase())) : VALID_LEVELS;
603
+ const snapshot = () => {
604
+ var _logger$getNamespaceL;
605
+ return {
606
+ level: logger$1.getLevel(),
607
+ namespaceLevels: ((_logger$getNamespaceL = logger$1.getNamespaceLevels) === null || _logger$getNamespaceL === void 0 ? void 0 : _logger$getNamespaceL.call(logger$1)) ?? {}
608
+ };
609
+ };
610
+ const send = (res, status, body) => {
611
+ var _res$setHeader;
612
+ res.statusCode = status;
613
+ (_res$setHeader = res.setHeader) === null || _res$setHeader === void 0 || _res$setHeader.call(res, "Content-Type", "application/json");
614
+ res.end(JSON.stringify(body));
615
+ };
616
+ const applyBody = (res, raw) => {
617
+ let parsed;
618
+ try {
619
+ parsed = raw ? JSON.parse(raw) : {};
620
+ } catch {
621
+ send(res, 400, { error: "invalid JSON body" });
622
+ return;
623
+ }
624
+ if (parsed.level !== void 0) {
625
+ const lvl = String(parsed.level).toLowerCase();
626
+ if (!allowed.has(lvl)) {
627
+ send(res, 400, {
628
+ error: `unknown level "${parsed.level}"`,
629
+ allowed: [...allowed]
630
+ });
631
+ return;
632
+ }
633
+ logger$1.setLevel(lvl);
634
+ }
635
+ if (parsed.namespaceLevels !== void 0) {
636
+ if (typeof parsed.namespaceLevels !== "object" || parsed.namespaceLevels === null || Array.isArray(parsed.namespaceLevels)) {
637
+ send(res, 400, { error: "namespaceLevels must be an object" });
638
+ return;
639
+ }
640
+ const nl = parsed.namespaceLevels;
641
+ for (const [pat, lvl] of Object.entries(nl)) if (!allowed.has(String(lvl).toLowerCase())) {
642
+ send(res, 400, { error: `unknown level "${String(lvl)}" for namespace "${pat}"` });
643
+ return;
644
+ }
645
+ if (logger$1.setNamespaceLevels) {
646
+ const coerced = {};
647
+ for (const [pat, lvl] of Object.entries(nl)) coerced[pat] = String(lvl).toLowerCase();
648
+ logger$1.setNamespaceLevels(coerced);
649
+ } else internalWarn("level control: logger does not support setNamespaceLevels — ignored");
650
+ }
651
+ send(res, 200, snapshot());
652
+ };
653
+ return function levelControlHandler(req, res) {
654
+ const method = (req.method ?? "GET").toUpperCase();
655
+ if (method === "GET") {
656
+ send(res, 200, snapshot());
657
+ return;
658
+ }
659
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
660
+ const maybeBody = req.body;
661
+ if (maybeBody !== void 0 && typeof req.on !== "function") {
662
+ applyBody(res, typeof maybeBody === "string" ? maybeBody : JSON.stringify(maybeBody));
663
+ return;
664
+ }
665
+ if (typeof req.on === "function") {
666
+ let raw = "";
667
+ req.on("data", (chunk) => {
668
+ raw += String(chunk ?? "");
669
+ });
670
+ req.on("end", () => applyBody(res, raw));
671
+ return;
672
+ }
673
+ applyBody(res, "");
674
+ return;
675
+ }
676
+ send(res, 405, { error: `method ${method} not allowed` });
677
+ };
678
+ }
679
+
680
+ //#endregion
681
+ //#region src/utils/safe-stringify.ts
682
+ /** Build a JSONPath like `$["a"][0]["b"]` for a decycle pointer. */
683
+ function jsonPath(parts) {
684
+ let path = "$";
685
+ for (const p of parts) path += typeof p === "number" ? `[${p}]` : `[${JSON.stringify(p)}]`;
686
+ return path;
687
+ }
688
+ /**
689
+ * Serialize any value to JSON without throwing on circular references or BigInt.
690
+ * Circular refs become `"[Circular]"` (or `{ $ref }` pointers when `decycle`).
691
+ */
692
+ function safeStringify(value, options = {}) {
693
+ const { indent, deterministic = false, decycle = false, bigint = "string" } = options;
694
+ if (decycle) return JSON.stringify(decycleValue(value, bigint), void 0, indent);
695
+ const seen = /* @__PURE__ */ new WeakSet();
696
+ const transform = (val) => {
697
+ if (typeof val === "bigint") return bigint === "number" ? Number(val) : val.toString();
698
+ if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
699
+ if (typeof val === "symbol") return val.toString();
700
+ if (val === null || typeof val !== "object") return val;
701
+ if (seen.has(val)) return "[Circular]";
702
+ seen.add(val);
703
+ let out;
704
+ if (Array.isArray(val)) out = val.map((item) => transform(item));
705
+ else if (val instanceof Date) out = val.toISOString();
706
+ else {
707
+ const rec = val;
708
+ const keys = deterministic ? Object.keys(rec).sort() : Object.keys(rec);
709
+ const obj = {};
710
+ for (const k of keys) {
711
+ if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
712
+ obj[k] = transform(rec[k]);
713
+ }
714
+ out = obj;
715
+ }
716
+ seen.delete(val);
717
+ return out;
718
+ };
719
+ return JSON.stringify(transform(value), void 0, indent);
720
+ }
721
+ /**
722
+ * Replace repeated object references with round-trippable `{ "$ref": "$..." }`
723
+ * JSONPath pointers (the classic Crockford decycle). Pair with {@link retrocycle}
724
+ * to reconstruct the original shared/circular graph.
725
+ */
726
+ function decycleValue(value, bigint = "string") {
727
+ const paths = /* @__PURE__ */ new WeakMap();
728
+ const walk = (val, path) => {
729
+ if (typeof val === "bigint") return bigint === "number" ? Number(val) : val.toString();
730
+ if (val === null || typeof val !== "object") return val;
731
+ if (val instanceof Date) return val.toISOString();
732
+ const existing = paths.get(val);
733
+ if (existing !== void 0) return { $ref: existing };
734
+ paths.set(val, jsonPath(path));
735
+ if (Array.isArray(val)) return val.map((item, i) => walk(item, [...path, i]));
736
+ const rec = val;
737
+ const obj = {};
738
+ for (const k of Object.keys(rec)) {
739
+ if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
740
+ obj[k] = walk(rec[k], [...path, k]);
741
+ }
742
+ return obj;
743
+ };
744
+ return walk(value, []);
745
+ }
746
+ /**
747
+ * Inverse of {@link decycleValue}: resolve `{ "$ref": "$..." }` pointers back
748
+ * into the live object graph (mutates and returns the parsed input).
749
+ */
750
+ function retrocycle(root) {
751
+ const refRe = /^\$(?:\[(?:\d+|"(?:[^"\\]|\\.)*")\])*$/;
752
+ const resolve = (path) => {
753
+ const segs = [];
754
+ const partRe = /\[(\d+|"(?:[^"\\]|\\.)*")\]/g;
755
+ let m;
756
+ while ((m = partRe.exec(path)) !== null) {
757
+ const raw = m[1];
758
+ segs.push(raw.startsWith("\"") ? JSON.parse(raw) : Number(raw));
759
+ }
760
+ let node = root;
761
+ for (const s of segs) node = node[s];
762
+ return node;
763
+ };
764
+ const walk = (val) => {
765
+ if (val === null || typeof val !== "object") return;
766
+ const rec = val;
767
+ for (const k of Object.keys(rec)) {
768
+ const child = rec[k];
769
+ if (child !== null && typeof child === "object" && typeof child.$ref === "string" && refRe.test(child.$ref)) rec[k] = resolve(child.$ref);
770
+ else walk(child);
771
+ }
772
+ };
773
+ walk(root);
774
+ return root;
775
+ }
776
+
518
777
  //#endregion
519
778
  //#region src/utils/typed-logger.ts
520
779
  /**
@@ -575,6 +834,258 @@ function createTypedLogger(logger$1, schema) {
575
834
  };
576
835
  }
577
836
 
837
+ //#endregion
838
+ //#region src/wide-events.ts
839
+ const _storage = new AsyncLocalStorage();
840
+ /**
841
+ * Merge fields into the wide event for the current async scope. No-op (with no
842
+ * throw) when called outside a `withWideEvent` / middleware scope, so business
843
+ * code can call it unconditionally.
844
+ */
845
+ function addEventFields(fields) {
846
+ const state = _storage.getStore();
847
+ if (!state || state.emitted) return;
848
+ Object.assign(state.fields, fields);
849
+ }
850
+ /** Set a single field on the current wide event. */
851
+ function setEventField(key, value) {
852
+ addEventFields({ [key]: value });
853
+ }
854
+ /** Read a shallow copy of the wide event accumulated so far, or undefined. */
855
+ function getEventFields() {
856
+ const state = _storage.getStore();
857
+ return state ? { ...state.fields } : void 0;
858
+ }
859
+ function emit(logger$1, state, options, extra) {
860
+ if (state.emitted) return;
861
+ state.emitted = true;
862
+ const level = options.level ?? "info";
863
+ const message = options.message ?? "request";
864
+ const durationField = options.durationField ?? "durationMs";
865
+ const includeTrace = options.includeTrace ?? true;
866
+ const payload = { ...state.fields };
867
+ if (extra) Object.assign(payload, extra);
868
+ payload[durationField] = Date.now() - state.startMs;
869
+ if (includeTrace) {
870
+ const traceId = getCurrentTraceId();
871
+ if (traceId !== void 0 && payload["traceId"] === void 0) payload["traceId"] = traceId;
872
+ }
873
+ const p = logger$1.logLevel(level, message, payload);
874
+ if (p && typeof p.catch === "function") p.catch(() => {});
875
+ }
876
+ /**
877
+ * Run `callback` inside a wide-event scope. `addEventFields` calls anywhere in
878
+ * the (async) call tree accumulate onto one event, which is emitted exactly once
879
+ * when the callback settles — on success OR error (the canonical "emit in
880
+ * finally" guarantee). On error, `error` + `errorMessage` fields are added.
881
+ */
882
+ async function withWideEvent(logger$1, initialFields, callback, options = {}) {
883
+ const state = {
884
+ fields: { ...initialFields },
885
+ startMs: Date.now(),
886
+ emitted: false
887
+ };
888
+ return _storage.run(state, async () => {
889
+ try {
890
+ const result = await callback();
891
+ emit(logger$1, state, options);
892
+ return result;
893
+ } catch (error) {
894
+ emit(logger$1, state, options, {
895
+ error: true,
896
+ errorMessage: error instanceof Error ? error.message : String(error)
897
+ });
898
+ throw error;
899
+ }
900
+ });
901
+ }
902
+ /**
903
+ * Express/Connect middleware that opens a wide-event scope per request and
904
+ * emits ONE canonical event on response `finish`/`close` — even if the handler
905
+ * throws or the client disconnects. Pre-populates method/url/ip; handlers add
906
+ * more via `addEventFields`. The completion event includes `statusCode` and the
907
+ * request duration.
908
+ */
909
+ function wideEventMiddleware(logger$1, options = {}) {
910
+ return function logixiaWideEventMiddleware(req, res, next) {
911
+ var _options$skip, _req$socket, _res$once, _res$once2;
912
+ if ((_options$skip = options.skip) === null || _options$skip === void 0 ? void 0 : _options$skip.call(options, req)) {
913
+ next();
914
+ return;
915
+ }
916
+ const state = {
917
+ fields: {
918
+ method: req.method,
919
+ url: req.originalUrl ?? req.url,
920
+ ip: req.ip ?? ((_req$socket = req.socket) === null || _req$socket === void 0 ? void 0 : _req$socket.remoteAddress),
921
+ ...options.enrich ? options.enrich(req) : {}
922
+ },
923
+ startMs: Date.now(),
924
+ emitted: false
925
+ };
926
+ const finalize = () => {
927
+ emit(logger$1, state, options, { statusCode: res.statusCode ?? 0 });
928
+ };
929
+ (_res$once = res.once) === null || _res$once === void 0 || _res$once.call(res, "finish", finalize);
930
+ (_res$once2 = res.once) === null || _res$once2 === void 0 || _res$once2.call(res, "close", finalize);
931
+ _storage.run(state, () => next());
932
+ };
933
+ }
934
+
935
+ //#endregion
936
+ //#region src/transports/otlp.transport.ts
937
+ /**
938
+ * Map a logixia level name to an OTel SeverityNumber (1–24) and text.
939
+ * Per the OTel logs SDK spec: TRACE=1, DEBUG=5, INFO=9, WARN=13, ERROR=17,
940
+ * FATAL=21. Custom/unknown levels fall back to INFO (9).
941
+ */
942
+ function toOtelSeverity(level) {
943
+ switch (level.toLowerCase()) {
944
+ case "trace": return {
945
+ number: 1,
946
+ text: "TRACE"
947
+ };
948
+ case "verbose": return {
949
+ number: 5,
950
+ text: "DEBUG"
951
+ };
952
+ case "debug": return {
953
+ number: 5,
954
+ text: "DEBUG"
955
+ };
956
+ case "info": return {
957
+ number: 9,
958
+ text: "INFO"
959
+ };
960
+ case "warn":
961
+ case "warning": return {
962
+ number: 13,
963
+ text: "WARN"
964
+ };
965
+ case "error": return {
966
+ number: 17,
967
+ text: "ERROR"
968
+ };
969
+ case "fatal": return {
970
+ number: 21,
971
+ text: "FATAL"
972
+ };
973
+ default: return {
974
+ number: 9,
975
+ text: "INFO"
976
+ };
977
+ }
978
+ }
979
+ /** Coerce a JS value into an OTLP AnyValue. */
980
+ function toAnyValue(value) {
981
+ if (typeof value === "string") return { stringValue: value };
982
+ if (typeof value === "boolean") return { boolValue: value };
983
+ if (typeof value === "number") return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
984
+ if (typeof value === "bigint") return { stringValue: value.toString() };
985
+ if (value === null || value === void 0) return { stringValue: "" };
986
+ try {
987
+ return { stringValue: JSON.stringify(value) };
988
+ } catch {
989
+ return { stringValue: String(value) };
990
+ }
991
+ }
992
+ /** Build the OTLP KeyValue attribute list from a flat record. */
993
+ function toAttributes(rec) {
994
+ return Object.entries(rec).map(([key, value]) => ({
995
+ key,
996
+ value: toAnyValue(value)
997
+ }));
998
+ }
999
+ var OtlpLogTransport = class {
1000
+ constructor(config) {
1001
+ this.name = "otlp";
1002
+ this.batch = [];
1003
+ this.flushTimer = null;
1004
+ this.url = config.url;
1005
+ this.headers = config.headers ?? {};
1006
+ this.batchSize = config.batchSize ?? 100;
1007
+ this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
1008
+ this.level = config.level;
1009
+ this.resourceAttrs = toAttributes({
1010
+ "service.name": config.serviceName ?? "logixia",
1011
+ ...config.serviceVersion ? { "service.version": config.serviceVersion } : {},
1012
+ ...config.environment ? { "deployment.environment": config.environment } : {},
1013
+ ...config.resourceAttributes ?? {}
1014
+ });
1015
+ this.flushTimer = setInterval(() => {
1016
+ this.flush().catch(() => {});
1017
+ }, this.flushIntervalMs);
1018
+ if (this.flushTimer.unref) this.flushTimer.unref();
1019
+ }
1020
+ write(entry) {
1021
+ this.batch.push(entry);
1022
+ if (this.batch.length >= this.batchSize) this.flush().catch(() => {});
1023
+ }
1024
+ /** Convert one entry into an OTLP LogRecord. */
1025
+ toLogRecord(entry) {
1026
+ const sev = toOtelSeverity(entry.level);
1027
+ const tsNanos = String(entry.timestamp.getTime() * 1e6);
1028
+ const attrs = { ...entry.data ?? {} };
1029
+ if (entry.context !== void 0) attrs["context"] = entry.context;
1030
+ if (entry.appName !== void 0) attrs["app.name"] = entry.appName;
1031
+ if (entry.environment !== void 0) attrs["deployment.environment"] = entry.environment;
1032
+ const record = {
1033
+ timeUnixNano: tsNanos,
1034
+ observedTimeUnixNano: tsNanos,
1035
+ severityNumber: sev.number,
1036
+ severityText: sev.text,
1037
+ body: { stringValue: entry.message },
1038
+ attributes: toAttributes(attrs)
1039
+ };
1040
+ if (entry.traceId) record["traceId"] = entry.traceId;
1041
+ return record;
1042
+ }
1043
+ buildPayload(entries) {
1044
+ return JSON.stringify({ resourceLogs: [{
1045
+ resource: { attributes: this.resourceAttrs },
1046
+ scopeLogs: [{
1047
+ scope: { name: "logixia" },
1048
+ logRecords: entries.map((e) => this.toLogRecord(e))
1049
+ }]
1050
+ }] });
1051
+ }
1052
+ async flush() {
1053
+ while (this.batch.length > 0) {
1054
+ const entries = this.batch.splice(0, this.batchSize);
1055
+ try {
1056
+ await this.send(entries);
1057
+ } catch (err) {
1058
+ internalError("OtlpLogTransport flush error", err);
1059
+ this.batch.unshift(...entries);
1060
+ return;
1061
+ }
1062
+ }
1063
+ }
1064
+ async send(entries) {
1065
+ if (typeof fetch !== "function") {
1066
+ internalWarn("OtlpLogTransport: global fetch unavailable — cannot export logs");
1067
+ return;
1068
+ }
1069
+ const res = await fetch(this.url, {
1070
+ method: "POST",
1071
+ headers: {
1072
+ "Content-Type": "application/json",
1073
+ ...this.headers
1074
+ },
1075
+ body: this.buildPayload(entries)
1076
+ });
1077
+ if (!res.ok) throw new Error(`OTLP export failed: HTTP ${res.status}`);
1078
+ }
1079
+ async close() {
1080
+ if (this.flushTimer) {
1081
+ clearInterval(this.flushTimer);
1082
+ this.flushTimer = null;
1083
+ }
1084
+ for (let attempt = 0; attempt < 3 && this.batch.length > 0; attempt += 1) await this.flush();
1085
+ if (this.batch.length > 0) internalError(`OtlpLogTransport closing with ${this.batch.length} undelivered record(s)`);
1086
+ }
1087
+ };
1088
+
578
1089
  //#endregion
579
1090
  //#region src/metrics.ts
580
1091
  const DEFAULT_BUCKETS = [
@@ -598,7 +1109,7 @@ function buildLabelKey(config, entry) {
598
1109
  const payload = entry.payload ?? {};
599
1110
  for (const name of labelNames) {
600
1111
  const raw = name === "level" ? entry.level : payload[name];
601
- pairs[name] = raw !== void 0 && raw !== null ? String(raw) : "";
1112
+ pairs[sanitizePromName(name)] = raw !== void 0 && raw !== null ? String(raw) : "";
602
1113
  }
603
1114
  return JSON.stringify(pairs);
604
1115
  }
@@ -611,6 +1122,18 @@ function escapeLabel(value) {
611
1122
  return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
612
1123
  }
613
1124
  /**
1125
+ * Coerce an arbitrary string into a valid Prometheus metric or label name
1126
+ * (`[a-zA-Z_][a-zA-Z0-9_]*`). Invalid characters become underscores and a
1127
+ * leading digit is prefixed with `_`. Without this, a user-supplied name like
1128
+ * `my-metric` or a label like `status code` would emit output Prometheus cannot
1129
+ * parse, breaking the ENTIRE scrape endpoint, not just that metric.
1130
+ */
1131
+ function sanitizePromName(name) {
1132
+ let safe = name.replace(/\W/g, "_");
1133
+ if (safe.length > 0 && /\d/.test(safe[0])) safe = `_${safe}`;
1134
+ return safe.length > 0 ? safe : "_";
1135
+ }
1136
+ /**
614
1137
  * A logixia plugin that extracts Prometheus-compatible metrics from log entries.
615
1138
  *
616
1139
  * Implements `LogixiaPlugin` — pass directly to `logger.use()`:
@@ -684,7 +1207,7 @@ var MetricsPlugin = class {
684
1207
  render() {
685
1208
  const lines = [];
686
1209
  for (const [rawName, config] of Object.entries(this.map)) {
687
- const metricName = `logixia_${rawName}`;
1210
+ const metricName = `logixia_${sanitizePromName(rawName)}`;
688
1211
  const state = this.metricState.get(rawName);
689
1212
  if (!state) continue;
690
1213
  const helpText = config.help ?? rawName.replace(/_/g, " ");
@@ -889,5 +1412,5 @@ const logger = new LogixiaLogger(DEFAULT_CONFIG);
889
1412
  */
890
1413
 
891
1414
  //#endregion
892
- export { DEFAULT_CONFIG, DEFAULT_LOG_COLORS, DEFAULT_LOG_LEVELS, DEFAULT_TRACE_HEADERS, ErrorResponseBuilder, InjectLogger, JsonFormatter, KafkaTraceInterceptor, LOGIXIA_LOGGER_CONFIG, LOGIXIA_LOGGER_PREFIX, LogLevel, LogMethod, LogixiaContext, LogixiaException, LogixiaExceptionFilter, LogixiaLogger, LogixiaLoggerModule, LogixiaLoggerService, MetricsPlugin, PluginRegistry, TRACE_CONTEXT_KEY, TextFormatter, TraceContext, WebSocketTraceInterceptor, _setActiveContextKey, applyRedaction, createExpressContextMiddleware, createFastifyContextHook, createLogger, createLoggerService, createMetricsPlugin, createTraceMiddleware, createTypedLogger, defineLogSchema, deregisterFromShutdown, disableOtelBridge, extractTraceId, flushOnExit, generateRequestId, generateTraceId, getActiveOtelContext, getCurrentTraceId, getOtelMetaFields, getTraceContextKey, globalPluginRegistry, initOtelBridge, isError, isLogixiaException, logger, normalizeError, redactObject, registerForShutdown, resetShutdownHandlers, runWithTraceId, serializeError, setTraceId, traceStorage, usePlugin };
1415
+ export { DEFAULT_CONFIG, DEFAULT_LOG_COLORS, DEFAULT_LOG_LEVELS, DEFAULT_TRACE_HEADERS, ErrorResponseBuilder, InjectLogger, JsonFormatter, KafkaTraceInterceptor, LOGIXIA_LOGGER_CONFIG, LOGIXIA_LOGGER_PREFIX, LogLevel, LogMethod, LogixiaContext, LogixiaException, LogixiaExceptionFilter, LogixiaLogger, LogixiaLoggerModule, LogixiaLoggerService, MetricsPlugin, OtlpLogTransport, PluginRegistry, TRACE_CONTEXT_KEY, TextFormatter, TraceContext, WebSocketTraceInterceptor, _setActiveContextKey, addEventFields, applyRedaction, createExpressContextMiddleware, createFastifyContextHook, createLevelControlHandler, createLogger, createLoggerService, createMetricsPlugin, createTraceMiddleware, createTypedLogger, decycleValue, defineLogSchema, deregisterFromShutdown, disableOtelBridge, extractTraceId, flushOnExit, generateRequestId, generateTraceId, getActiveOtelContext, getCurrentTraceId, getEventFields, getOtelMetaFields, getTraceContextKey, globalPluginRegistry, initOtelBridge, isError, isLogixiaException, logger, normalizeError, redactObject, registerForShutdown, registerLevelSignal, resetShutdownHandlers, retrocycle, runWithTraceId, safeStringify, serializeError, setEventField, setTraceId, toOtelSeverity, traceStorage, usePlugin, wideEventMiddleware, withWideEvent };
893
1416
  //# sourceMappingURL=index.mjs.map