loggily 0.8.0 → 0.10.1

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.
@@ -49,15 +49,21 @@ const colors = {
49
49
  function createFileWriter(filePath, options = {}) {
50
50
  const bufferSize = options.bufferSize ?? 4096;
51
51
  const flushInterval = options.flushInterval ?? 100;
52
+ const fs = options.__fs ?? {
53
+ openSync,
54
+ writeSync,
55
+ closeSync
56
+ };
52
57
  let buffer = "";
53
58
  let fd = null;
54
59
  let timer = null;
55
60
  let closed = false;
56
- fd = openSync(filePath, "a");
61
+ fd = fs.openSync(filePath, "a");
57
62
  /** Flush buffer contents to disk synchronously */
58
63
  function flush() {
59
64
  if (buffer.length === 0 || fd === null) return;
60
- writeSync(fd, buffer);
65
+ const data = buffer;
66
+ fs.writeSync(fd, data);
61
67
  buffer = "";
62
68
  }
63
69
  timer = setInterval(flush, flushInterval);
@@ -82,7 +88,7 @@ function createFileWriter(filePath, options = {}) {
82
88
  flush();
83
89
  } catch {} finally {
84
90
  if (fd !== null) {
85
- closeSync(fd);
91
+ fs.closeSync(fd);
86
92
  fd = null;
87
93
  }
88
94
  process.removeListener("exit", exitHandler);
@@ -149,7 +155,8 @@ function resetIdCounters() {
149
155
  *
150
156
  * @example
151
157
  * ```typescript
152
- * const span = log.span("http-request")
158
+ * const span = log.span?.("http-request")
159
+ * if (!span) return
153
160
  * const header = traceparent(span.spanData)
154
161
  * // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
155
162
  * fetch(url, { headers: { traceparent: header } })
@@ -195,6 +202,211 @@ function shouldSample() {
195
202
  return Math.random() < sampleRate;
196
203
  }
197
204
  //#endregion
205
+ //#region src/console-sinks.ts
206
+ /**
207
+ * Console sinks — runtime-aware structured output to console.
208
+ *
209
+ * Two sinks, one contract: `(event: Event) => void`. They do NOT pre-format
210
+ * events into a single ANSI string. Instead they spread structured arguments
211
+ * to `console.*` so the platform can render rich output:
212
+ *
213
+ * Terminal (Node / Bun):
214
+ * console.info(ansiPrefix, message, ...userArgs)
215
+ * — util.format keeps objects inspectable in devtools
216
+ *
217
+ * Browser (Chrome / Firefox / Safari DevTools):
218
+ * console.info("%c<level> %c<namespace>", levelCss, nsCss, message, ...userArgs)
219
+ * — DevTools renders colored prefix and keeps every user arg as an
220
+ * expandable, clickable object.
221
+ *
222
+ * Source-location preservation:
223
+ * We call `console.<level>(...)` from arrow-function sinks. DevTools attribute
224
+ * the log line to the caller's frame, not this file, as long as no wrapper
225
+ * closes over a bound console reference. `Function.prototype.bind.call(console.info, ...)`
226
+ * works too but defeats `vi.spyOn(console, 'info')` because bind captures
227
+ * the function at bind time. Plain arrow functions re-read `console.info`
228
+ * on each call — mockable AND location-preserving.
229
+ *
230
+ * The sinks consume `event.userArgs` (raw user-supplied data/errors) when
231
+ * present, falling back to `event.props` so they still work with code paths
232
+ * that have not yet been migrated.
233
+ */
234
+ /**
235
+ * True when running inside a browser-like environment. We check for the
236
+ * presence of a `window` with `document` — the Node DOM test env (jsdom)
237
+ * also matches, but that is the correct behaviour for DevTools-style output.
238
+ */
239
+ function isBrowserRuntime() {
240
+ return typeof globalThis?.window !== "undefined" && typeof globalThis.document !== "undefined";
241
+ }
242
+ function timeStr(time) {
243
+ return new Date(time).toISOString().split("T")[1]?.split(".")[0] ?? "";
244
+ }
245
+ function levelLabel(level) {
246
+ switch (level) {
247
+ case "trace": return "TRACE";
248
+ case "debug": return "DEBUG";
249
+ case "info": return "INFO";
250
+ case "warn": return "WARN";
251
+ case "error": return "ERROR";
252
+ }
253
+ }
254
+ /**
255
+ * Return the raw user-supplied data attached to the event, in call order.
256
+ * Prefers the explicit `userArgs` field (new path). Falls back to `props`
257
+ * (coerced to a single trailing object) for events emitted by older code or
258
+ * by `writeSpan`. Filters out `undefined` entries.
259
+ */
260
+ function userArgsOf(event) {
261
+ if ("userArgs" in event && Array.isArray(event.userArgs)) return event.userArgs.filter((v) => v !== void 0);
262
+ if (event.props && Object.keys(event.props).length > 0) return [event.props];
263
+ return [];
264
+ }
265
+ /**
266
+ * Terminal (ANSI) sink. Emits `[ansiPrefix, message, ...userArgs]` so that:
267
+ * - stdout gets a colored prefix
268
+ * - util.format leaves objects inspectable
269
+ * - vi.spyOn(console, 'info') intercepts calls (arrows re-read console)
270
+ *
271
+ * Spans and JSON format still go through the pre-formatted single-arg path —
272
+ * their consumers are log aggregators, not humans.
273
+ */
274
+ function createTerminalConsoleSink(format = "console") {
275
+ if (format === "json") return (event) => routeSingle(event, formatJSONEvent(event));
276
+ return (event) => {
277
+ if (event.kind === "span") {
278
+ writeStderrLine(formatConsoleEvent(event));
279
+ return;
280
+ }
281
+ const prefix = `${colors.dim(timeStr(event.time))} ${levelAnsi(event.level)} ${colors.cyan(event.namespace)}`;
282
+ const args = userArgsOf(event);
283
+ invokeForLevel(event.level, prefix, event.message, ...args);
284
+ };
285
+ }
286
+ /** Emit one line to process.stderr when available (no-op in browser). */
287
+ function writeStderrLine(text) {
288
+ const p = typeof process !== "undefined" ? process : void 0;
289
+ if (p?.stderr && typeof p.stderr.write === "function") {
290
+ p.stderr.write(text + "\n");
291
+ return;
292
+ }
293
+ console.info(text);
294
+ }
295
+ function levelAnsi(level) {
296
+ switch (level) {
297
+ case "trace": return colors.dim("TRACE");
298
+ case "debug": return colors.dim("DEBUG");
299
+ case "info": return colors.blue("INFO");
300
+ case "warn": return colors.yellow("WARN");
301
+ case "error": return colors.red("ERROR");
302
+ }
303
+ }
304
+ /**
305
+ * Browser (DevTools) sink. Uses `%c` CSS format specifiers so DevTools
306
+ * renders a colored level + namespace prefix, then spreads user args raw so
307
+ * DevTools keeps them as expandable object references (clickable source
308
+ * locations for Error, drill-in for plain objects).
309
+ *
310
+ * For JSON format we still emit a single string — consumers that explicitly
311
+ * asked for JSON want machine-readable output, not DevTools theatrics.
312
+ */
313
+ function createBrowserConsoleSink(format = "console") {
314
+ if (format === "json") return (event) => routeSingle(event, formatJSONEvent(event));
315
+ return (event) => {
316
+ if (event.kind === "span") {
317
+ const spanTemplate = `%c%s %cSPAN %c%s %c(%sms)`;
318
+ const args = userArgsOf(event);
319
+ const spanPropsString = args.length > 0 ? ` ${safeStringify(Object.assign({}, ...args.filter(isPlainRecord)))}` : "";
320
+ const { durationLabel } = { durationLabel: String(event.duration) };
321
+ invokeForLevel("info", spanTemplate + (spanPropsString ? "%s" : ""), cssDim(), timeStr(event.time), cssSpan(), cssNamespace(), event.namespace, cssDim(), durationLabel, ...spanPropsString ? [cssDim(), spanPropsString] : []);
322
+ return;
323
+ }
324
+ const template = `%c%s %c%s %c%s`;
325
+ const args = userArgsOf(event);
326
+ invokeForLevel(event.level, template, cssDim(), timeStr(event.time), cssLevel(event.level), levelLabel(event.level), cssNamespace(), event.namespace, event.message, ...args);
327
+ };
328
+ }
329
+ function isPlainRecord(v) {
330
+ return typeof v === "object" && v !== null && !Array.isArray(v);
331
+ }
332
+ function cssDim() {
333
+ return "color: #888";
334
+ }
335
+ function cssNamespace() {
336
+ return "color: #0aa; font-weight: bold";
337
+ }
338
+ function cssSpan() {
339
+ return "color: #a0a; font-weight: bold";
340
+ }
341
+ function cssLevel(level) {
342
+ switch (level) {
343
+ case "trace":
344
+ case "debug": return "color: #888; font-weight: bold";
345
+ case "info": return "color: #36f; font-weight: bold";
346
+ case "warn": return "color: #b80; font-weight: bold";
347
+ case "error": return "color: #c33; font-weight: bold";
348
+ }
349
+ }
350
+ /**
351
+ * Dispatch to the right console method for the event's level.
352
+ * We intentionally use arrow-style invocation rather than
353
+ * `Function.prototype.bind.call(console.info, console, ...)` so that:
354
+ * 1. Tests can `vi.spyOn(console, 'info')` AFTER the sink is created.
355
+ * 2. DevTools attribute the log line to the caller's source location
356
+ * (arrows don't appear in the stack between the call and `console.*`).
357
+ */
358
+ function invokeForLevel(level, ...args) {
359
+ switch (level) {
360
+ case "trace":
361
+ case "debug":
362
+ console.debug(...args);
363
+ return;
364
+ case "info":
365
+ console.info(...args);
366
+ return;
367
+ case "warn":
368
+ console.warn(...args);
369
+ return;
370
+ case "error":
371
+ console.error(...args);
372
+ return;
373
+ }
374
+ }
375
+ /**
376
+ * Route an already-formatted single string to the correct console level.
377
+ * Used for JSON format and for span events where we preserve the pre-formatted
378
+ * shape.
379
+ */
380
+ function routeSingle(event, text) {
381
+ if (event.kind === "span") {
382
+ console.info(text);
383
+ return;
384
+ }
385
+ switch (event.level) {
386
+ case "trace":
387
+ case "debug":
388
+ console.debug(text);
389
+ return;
390
+ case "info":
391
+ console.info(text);
392
+ return;
393
+ case "warn":
394
+ console.warn(text);
395
+ return;
396
+ case "error":
397
+ console.error(text);
398
+ return;
399
+ }
400
+ }
401
+ /**
402
+ * Pick the right sink for the current runtime. Browsers get `%c` CSS, Node
403
+ * gets ANSI. Both emit multi-arg spreads so the platform can render objects
404
+ * as expandable references.
405
+ */
406
+ function createConsoleSink$1(format = "console") {
407
+ return isBrowserRuntime() ? createBrowserConsoleSink(format) : createTerminalConsoleSink(format);
408
+ }
409
+ //#endregion
198
410
  //#region src/pipeline.ts
199
411
  const LOG_LEVEL_PRIORITY = {
200
412
  trace: 0,
@@ -324,38 +536,12 @@ function parseNsFilter(ns) {
324
536
  };
325
537
  }
326
538
  /**
327
- * Write formatted text to console using the appropriate log level.
328
- *
329
- * IMPORTANT: We use Function.bind() to preserve caller source locations in
330
- * browser DevTools. When you click a log line in DevTools, it shows where
331
- * YOUR code called log.info?.(), not where pipeline.ts called console.info().
332
- * DO NOT replace bind() with direct console.info(text) calls — it breaks
333
- * source location tracking in browsers.
539
+ * Console sink used inside the pipeline builder. Delegates to the structured
540
+ * console sink (browser %c or terminal multi-arg) so the pipeline preserves
541
+ * expandable user args end-to-end.
334
542
  */
335
- function writeToConsole(text, event) {
336
- if (event.kind === "span") {
337
- writeStderr(text);
338
- return;
339
- }
340
- switch (event.level) {
341
- case "trace":
342
- case "debug":
343
- Function.prototype.bind.call(console.debug, console, text)();
344
- break;
345
- case "info":
346
- Function.prototype.bind.call(console.info, console, text)();
347
- break;
348
- case "warn":
349
- Function.prototype.bind.call(console.warn, console, text)();
350
- break;
351
- case "error":
352
- Function.prototype.bind.call(console.error, console, text)();
353
- break;
354
- }
355
- }
356
543
  function createConsoleSink(format) {
357
- const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
358
- return (event) => writeToConsole(formatter(event), event);
544
+ return createConsoleSink$1(format);
359
545
  }
360
546
  function createFileSink(path, format) {
361
547
  const writer = createFileWriter(path);
@@ -375,6 +561,19 @@ function createWritableSink(writable, format) {
375
561
  }
376
562
  return (event) => writable.write(event);
377
563
  }
564
+ /** Consecutive failures before a throwing output sink is disabled. */
565
+ const OUTPUT_MAX_STRIKES = 3;
566
+ /**
567
+ * Report a dispatch-guard failure WITHOUT going through any pipeline —
568
+ * the error channel for the logger itself must not recurse into the logger.
569
+ * Routes through writeStderr (the `_process` guard) for browser safety.
570
+ */
571
+ function reportGuard(what, err) {
572
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
573
+ try {
574
+ writeStderr(`loggily: ${what}: ${detail}`);
575
+ } catch {}
576
+ }
378
577
  const VALID_CONFIG_KEYS = new Set([
379
578
  "level",
380
579
  "ns",
@@ -478,23 +677,59 @@ function buildPipeline(elements, parentConfig) {
478
677
  }
479
678
  throw new Error(`loggily: unsupported config element of type "${typeof element}". Config arrays accept: objects (config), arrays (branches), functions (stages), console, "console", or writables ({ write }).`);
480
679
  }
680
+ const stageReported = [];
681
+ const branchReported = [];
481
682
  const dispatch = (event) => {
482
683
  if (event.kind === "span" && !spansEnabled) return;
483
684
  let e = event;
484
- for (const stage of stages) {
485
- const result = stage(e);
685
+ for (let i = 0; i < stages.length; i++) {
686
+ let result;
687
+ try {
688
+ result = stages[i](e);
689
+ } catch (err) {
690
+ if (!stageReported[i]) {
691
+ stageReported[i] = true;
692
+ reportGuard(`stage #${i} threw — dropping event, fail-closed (reported once)`, err);
693
+ }
694
+ return;
695
+ }
486
696
  if (result === null) return;
487
697
  if (result !== void 0) e = result;
488
698
  }
489
699
  for (const output of outputs) {
700
+ if (output.disabled) continue;
490
701
  if (e.kind === "log" && LOG_LEVEL_PRIORITY[e.level] < output.levelPriority) continue;
491
702
  if (output.nsFilter && !output.nsFilter(e.namespace)) continue;
492
- output.write(e);
703
+ try {
704
+ output.write(e);
705
+ if (output.failures) output.failures = 0;
706
+ } catch (err) {
707
+ output.failures = (output.failures ?? 0) + 1;
708
+ if (output.failures === 1) reportGuard("output write threw — event lost for this sink", err);
709
+ if (output.failures >= OUTPUT_MAX_STRIKES) {
710
+ output.disabled = true;
711
+ reportGuard(`output disabled after ${output.failures} consecutive failures`, err);
712
+ }
713
+ }
493
714
  }
494
- for (const branch of branches) branch.dispatch(e);
715
+ for (let i = 0; i < branches.length; i++) try {
716
+ branches[i].dispatch(e);
717
+ } catch (err) {
718
+ if (!branchReported[i]) {
719
+ branchReported[i] = true;
720
+ reportGuard(`branch #${i} dispatch threw (reported once)`, err);
721
+ }
722
+ }
723
+ };
724
+ const spanEnabledForNamespace = (namespace) => {
725
+ if (!spansEnabled) return false;
726
+ if (outputs.some((output) => !output.nsFilter || output.nsFilter(namespace))) return true;
727
+ if (branches.some((branch) => branch.spanEnabled(namespace))) return true;
728
+ return stages.length > 0;
495
729
  };
496
730
  return {
497
731
  dispatch,
732
+ spanEnabled: spanEnabledForNamespace,
498
733
  level: config.level,
499
734
  dispose: () => {
500
735
  for (const d of disposables) d();
@@ -573,6 +808,12 @@ function readEnvTrace() {
573
808
  * ])
574
809
  * log.info?.('server started', { port: 3000 })
575
810
  */
811
+ const SPAN_ENABLED = Symbol("loggily.spanEnabled");
812
+ function spanIsEnabled(logger, namespace) {
813
+ if (collectSpans) return true;
814
+ const checker = logger[SPAN_ENABLED];
815
+ return checker ? checker(namespace) : true;
816
+ }
576
817
  function resetIds() {
577
818
  resetIdCounters();
578
819
  }
@@ -638,6 +879,7 @@ function createLoggerImpl(name, props, pipeline) {
638
879
  const emitLog = (level, msgOrError, dataOrMsg, extraData) => {
639
880
  let message;
640
881
  let data;
882
+ const userArgs = [];
641
883
  if (msgOrError instanceof Error) {
642
884
  const err = msgOrError;
643
885
  const contextTags = _getContextTags?.() ?? {};
@@ -665,6 +907,8 @@ function createLoggerImpl(name, props, pipeline) {
665
907
  error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
666
908
  };
667
909
  }
910
+ userArgs.push(err);
911
+ if (data && Object.keys(data).length > 0) userArgs.push(data);
668
912
  } else {
669
913
  message = resolveMessage(msgOrError);
670
914
  const contextTags = _getContextTags?.();
@@ -676,6 +920,7 @@ function createLoggerImpl(name, props, pipeline) {
676
920
  ...props,
677
921
  ...dataOrMsg
678
922
  } : void 0;
923
+ if (data && Object.keys(data).length > 0) userArgs.push(data);
679
924
  }
680
925
  const event = {
681
926
  kind: "log",
@@ -683,7 +928,8 @@ function createLoggerImpl(name, props, pipeline) {
683
928
  namespace: name,
684
929
  level,
685
930
  message,
686
- props: data
931
+ props: data,
932
+ userArgs
687
933
  };
688
934
  pipeline.dispatch(event);
689
935
  };
@@ -693,6 +939,9 @@ function createLoggerImpl(name, props, pipeline) {
693
939
  get level() {
694
940
  return pipeline.level;
695
941
  },
942
+ [SPAN_ENABLED](namespace) {
943
+ return pipeline.spanEnabled(namespace);
944
+ },
696
945
  dispatch(event) {
697
946
  pipeline.dispatch(event);
698
947
  },
@@ -704,6 +953,7 @@ function createLoggerImpl(name, props, pipeline) {
704
953
  info: (msg, data) => emitLog("info", msg, data),
705
954
  warn: (msg, data) => emitLog("warn", msg, data),
706
955
  error: (msgOrError, dataOrMsg, extraData) => emitLog("error", msgOrError, dataOrMsg, extraData),
956
+ /** @deprecated Use .child() instead */
707
957
  logger(namespace, childProps) {
708
958
  return this.child(namespace ?? "", childProps);
709
959
  },
@@ -757,8 +1007,9 @@ function augmentWithSpans(logger, parentSpanId, traceId, traceSampled) {
757
1007
  traceId,
758
1008
  traceSampled
759
1009
  };
1010
+ const spanMethod = spanIsEnabled(logger, logger.name) ? createSpanMethod(logger, spanState) : void 0;
760
1011
  return new Proxy(logger, { get(target, prop) {
761
- if (prop === "span") return createSpanMethod(target, spanState);
1012
+ if (prop === "span") return spanMethod;
762
1013
  if (prop === "child") return function child(namespaceOrContext, childProps) {
763
1014
  return augmentWithSpans(target.child(namespaceOrContext, childProps), spanState.parentSpanId, spanState.traceId, spanState.traceSampled);
764
1015
  };
@@ -796,7 +1047,8 @@ function createSpanMethod(logger, spanState) {
796
1047
  startTime: Date.now(),
797
1048
  endTime: null,
798
1049
  duration: null,
799
- attrs: {}
1050
+ attrs: {},
1051
+ laps: []
800
1052
  };
801
1053
  const spanAugmented = augmentWithSpans(logger.child(namespace ?? "", resolvedChildProps), newSpanId, finalTraceId, sampled);
802
1054
  _enterContext?.(newSpanId, finalTraceId, resolvedParentId);
@@ -814,6 +1066,7 @@ function createSpanMethod(logger, spanState) {
814
1066
  }), { ...newSpanData.attrs }));
815
1067
  _exitContext?.(newSpanId);
816
1068
  if (sampled) {
1069
+ const lapsProp = newSpanData.laps.length > 0 ? Object.fromEntries(newSpanData.laps.map((l) => [l.name, l.deltaMs])) : void 0;
817
1070
  const spanEvent = {
818
1071
  kind: "span",
819
1072
  time: newSpanData.endTime,
@@ -822,7 +1075,8 @@ function createSpanMethod(logger, spanState) {
822
1075
  duration: newSpanData.duration,
823
1076
  props: {
824
1077
  ...mergedProps,
825
- ...newSpanData.attrs
1078
+ ...newSpanData.attrs,
1079
+ ...lapsProp ? { laps: lapsProp } : {}
826
1080
  },
827
1081
  spanId: newSpanData.id,
828
1082
  traceId: newSpanData.traceId,
@@ -831,6 +1085,16 @@ function createSpanMethod(logger, spanState) {
831
1085
  logger.dispatch(spanEvent);
832
1086
  }
833
1087
  };
1088
+ const lapImpl = (name) => {
1089
+ const elapsedMs = Date.now() - newSpanData.startTime;
1090
+ const lastLap = newSpanData.laps[newSpanData.laps.length - 1];
1091
+ const deltaMs = lastLap ? elapsedMs - lastLap.elapsedMs : elapsedMs;
1092
+ newSpanData.laps.push({
1093
+ name,
1094
+ elapsedMs,
1095
+ deltaMs
1096
+ });
1097
+ };
834
1098
  const spanDataProxy = createSpanDataProxy(() => ({
835
1099
  id: newSpanData.id,
836
1100
  traceId: newSpanData.traceId,
@@ -847,6 +1111,7 @@ function createSpanMethod(logger, spanState) {
847
1111
  if (prop === "end") return () => {
848
1112
  if (newSpanData.endTime === null) currentDispose();
849
1113
  };
1114
+ if (prop === "lap") return lapImpl;
850
1115
  if (prop === "name") return childName;
851
1116
  if (prop === "props") return Object.freeze({ ...mergedProps });
852
1117
  return target[prop];
@@ -938,6 +1203,15 @@ function withEnvDefaults() {
938
1203
  */
939
1204
  function applyNamespaceGating(logger) {
940
1205
  return new Proxy(logger, { get(target, prop) {
1206
+ if (prop === SPAN_ENABLED) return (namespace) => {
1207
+ if (collectSpans) return true;
1208
+ const trace = currentTrace();
1209
+ if (!trace.enabled) return false;
1210
+ if (trace.filter && !trace.filter(namespace)) return false;
1211
+ const ns = currentNs();
1212
+ if (ns && !ns(namespace)) return false;
1213
+ return true;
1214
+ };
941
1215
  if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
942
1216
  const nsLevel = readEnvLevelForNamespace(target.name);
943
1217
  if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[nsLevel]) return;
@@ -966,14 +1240,23 @@ function createEnvPipeline() {
966
1240
  }
967
1241
  const ns = currentNs();
968
1242
  if (ns && !ns(event.namespace)) return;
969
- const text = (currentFormat() === "json" ? formatJSONEvent : formatConsoleEvent)(event);
1243
+ const format = currentFormat();
1244
+ const text = (format === "json" ? formatJSONEvent : formatConsoleEvent)(event);
970
1245
  const lvl = event.kind === "log" ? event.level : "span";
971
- for (const w of _writers) w(text, lvl);
972
- if (!_suppressConsole) writeToConsole(text, event);
1246
+ for (const w of _writers) w(text, lvl, event.namespace, event);
1247
+ if (!_suppressConsole) createConsoleSink$1(format)(event);
973
1248
  fileSink?.(event);
974
1249
  };
975
1250
  return {
976
1251
  dispatch,
1252
+ spanEnabled(namespace) {
1253
+ const trace = currentTrace();
1254
+ if (!trace.enabled) return false;
1255
+ if (trace.filter && !trace.filter(namespace)) return false;
1256
+ const ns = currentNs();
1257
+ if (ns && !ns(namespace)) return false;
1258
+ return true;
1259
+ },
977
1260
  get level() {
978
1261
  return currentLevel();
979
1262
  },
@@ -1048,7 +1331,21 @@ function setOutputMode(_mode) {
1048
1331
  function getOutputMode() {
1049
1332
  return "console";
1050
1333
  }
1051
- function addWriter(writer) {
1334
+ function addWriter(arg1, arg2) {
1335
+ if (typeof arg1 === "function") return _addRawWriter(arg1);
1336
+ if (!arg2) throw new Error("addWriter: writer fn required when first argument is a config object");
1337
+ const config = arg1;
1338
+ const matches = config.ns ? parseNsFilter(config.ns) : null;
1339
+ const minPriority = config.level ? LOG_LEVEL_PRIORITY[config.level] : null;
1340
+ return _addRawWriter((formatted, level, namespace, event) => {
1341
+ if (matches && !matches(namespace)) return;
1342
+ if (minPriority !== null) {
1343
+ if ((LOG_LEVEL_PRIORITY[level] ?? LOG_LEVEL_PRIORITY.info) < minPriority) return;
1344
+ }
1345
+ arg2(formatted, level, namespace, event);
1346
+ });
1347
+ }
1348
+ function _addRawWriter(writer) {
1052
1349
  _writers.push(writer);
1053
1350
  return () => {
1054
1351
  const i = _writers.indexOf(writer);
@@ -1071,4 +1368,4 @@ function writeSpan(namespace, duration, attrs) {
1071
1368
  //#endregion
1072
1369
  export { withEnvDefaults as A, setSampleRate as B, setOutputMode as C, startCollecting as D, spansAreEnabled as E, safeStringify as F, createFileWriter as H, serializeCause as I, getIdFormat as L, writeSpan as M, LOG_LEVEL_PRIORITY as N, stopCollecting as O, buildPipeline as P, getSampleRate as R, setLogLevel as S, setTraceFilter as T, traceparent as V, getTraceFilter as _, baseCreateLogger as a, setDebugFilter as b, createSpanDataProxy as c, enableSpans as d, getCollectedSpans as f, getOutputMode as g, getLogLevel as h, addWriter as i, withSpans as j, withConfigMetrics as k, createTestLogger as l, getLogFormat as m, _setContextHooks as n, clearCollectedSpans as o, getDebugFilter as p, _setLogFileWriterFactory as r, createLogger as s, _clearContextHooks as t, disableSpans as u, pipe as v, setSuppressConsole as w, setLogFormat as x, resetIds as y, setIdFormat as z };
1073
1370
 
1074
- //# sourceMappingURL=core-B3pox577.mjs.map
1371
+ //# sourceMappingURL=core-DKwMMOZw.mjs.map