loggily 0.6.2 → 0.8.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.
@@ -0,0 +1,1074 @@
1
+ import { createMetricsCollector, withMetrics } from "./metrics.mjs";
2
+ import { closeSync, openSync, writeSync } from "node:fs";
3
+ //#region src/colors.ts
4
+ /**
5
+ * Vendored ANSI color functions — replaces picocolors dependency.
6
+ * Supports NO_COLOR, FORCE_COLOR, and TTY detection.
7
+ */
8
+ const _process$1 = typeof process !== "undefined" ? process : void 0;
9
+ const enabled = _process$1?.env?.["FORCE_COLOR"] !== void 0 && _process$1?.env?.["FORCE_COLOR"] !== "0" ? true : _process$1?.env?.["NO_COLOR"] !== void 0 ? false : _process$1?.stdout?.isTTY ?? false;
10
+ function wrap(open, close) {
11
+ if (!enabled) return (str) => str;
12
+ return (str) => open + str + close;
13
+ }
14
+ const colors = {
15
+ dim: wrap("\x1B[2m", "\x1B[22m"),
16
+ blue: wrap("\x1B[34m", "\x1B[39m"),
17
+ yellow: wrap("\x1B[33m", "\x1B[39m"),
18
+ red: wrap("\x1B[31m", "\x1B[39m"),
19
+ magenta: wrap("\x1B[35m", "\x1B[39m"),
20
+ cyan: wrap("\x1B[36m", "\x1B[39m")
21
+ };
22
+ //#endregion
23
+ //#region src/file-writer.ts
24
+ /**
25
+ * File writer for loggily — Node.js/Bun only.
26
+ *
27
+ * Separated from core logger to allow tree-shaking in browser bundles.
28
+ * Uses dynamic import("node:fs") to avoid static dependency on Node APIs.
29
+ */
30
+ /**
31
+ * Create an async buffered file writer for log output.
32
+ * Buffers writes and flushes on size threshold or interval.
33
+ * Registers a process.on('exit') handler to flush remaining buffer.
34
+ *
35
+ * **Node.js/Bun only** — not available in browser environments.
36
+ *
37
+ * @param filePath - Path to the log file (opened in append mode)
38
+ * @param options - Buffer size and flush interval configuration
39
+ * @returns FileWriter with write, flush, and close methods
40
+ *
41
+ * @example
42
+ * const writer = createFileWriter('/tmp/app.log')
43
+ * const unsubscribe = addWriter((formatted) => writer.write(formatted))
44
+ *
45
+ * // On shutdown:
46
+ * unsubscribe()
47
+ * writer.close()
48
+ */
49
+ function createFileWriter(filePath, options = {}) {
50
+ const bufferSize = options.bufferSize ?? 4096;
51
+ const flushInterval = options.flushInterval ?? 100;
52
+ let buffer = "";
53
+ let fd = null;
54
+ let timer = null;
55
+ let closed = false;
56
+ fd = openSync(filePath, "a");
57
+ /** Flush buffer contents to disk synchronously */
58
+ function flush() {
59
+ if (buffer.length === 0 || fd === null) return;
60
+ writeSync(fd, buffer);
61
+ buffer = "";
62
+ }
63
+ timer = setInterval(flush, flushInterval);
64
+ if (timer && typeof timer === "object" && "unref" in timer) timer.unref();
65
+ const exitHandler = () => flush();
66
+ process.on("exit", exitHandler);
67
+ return {
68
+ write(line) {
69
+ if (closed) return;
70
+ buffer += line + "\n";
71
+ if (buffer.length >= bufferSize) flush();
72
+ },
73
+ flush,
74
+ close() {
75
+ if (closed) return;
76
+ closed = true;
77
+ if (timer !== null) {
78
+ clearInterval(timer);
79
+ timer = null;
80
+ }
81
+ try {
82
+ flush();
83
+ } catch {} finally {
84
+ if (fd !== null) {
85
+ closeSync(fd);
86
+ fd = null;
87
+ }
88
+ process.removeListener("exit", exitHandler);
89
+ }
90
+ }
91
+ };
92
+ }
93
+ //#endregion
94
+ //#region src/tracing.ts
95
+ let currentIdFormat = "simple";
96
+ /**
97
+ * Set the ID format for new spans and traces.
98
+ * - "simple": sp_1, sp_2, tr_1, tr_2 (default, lightweight)
99
+ * - "w3c": 32-char hex trace ID, 16-char hex span ID (W3C Trace Context compatible)
100
+ *
101
+ * @deprecated Use the `TRACE_ID_FORMAT` env var or `{ idFormat: "w3c" }` in the config array instead.
102
+ */
103
+ function setIdFormat(format) {
104
+ currentIdFormat = format;
105
+ }
106
+ /**
107
+ * Get the current ID format.
108
+ *
109
+ * @deprecated Use the `TRACE_ID_FORMAT` env var or `{ idFormat: "w3c" }` in the config array instead.
110
+ */
111
+ function getIdFormat() {
112
+ return currentIdFormat;
113
+ }
114
+ let simpleSpanCounter = 0;
115
+ let simpleTraceCounter = 0;
116
+ /** Generate a hex string of the given byte length using crypto.randomUUID */
117
+ function randomHex(bytes) {
118
+ return crypto.randomUUID().replace(/-/g, "").slice(0, bytes * 2);
119
+ }
120
+ /** Generate a span ID according to the current format */
121
+ function generateSpanId() {
122
+ if (currentIdFormat === "w3c") return randomHex(8);
123
+ return `sp_${(++simpleSpanCounter).toString(36)}`;
124
+ }
125
+ /** Generate a trace ID according to the current format */
126
+ function generateTraceId() {
127
+ if (currentIdFormat === "w3c") return randomHex(16);
128
+ return `tr_${(++simpleTraceCounter).toString(36)}`;
129
+ }
130
+ /** Reset ID counters (for testing) */
131
+ function resetIdCounters() {
132
+ simpleSpanCounter = 0;
133
+ simpleTraceCounter = 0;
134
+ }
135
+ /**
136
+ * Format a W3C traceparent header from span data.
137
+ *
138
+ * Format: `{version}-{trace-id}-{span-id}-{trace-flags}`
139
+ * - version: "00" (current W3C spec version)
140
+ * - trace-id: 32 hex chars (128 bits)
141
+ * - span-id: 16 hex chars (64 bits)
142
+ * - trace-flags: "01" (sampled) or "00" (not sampled)
143
+ *
144
+ * Works with both simple and W3C ID formats. Simple IDs are zero-padded to spec length.
145
+ *
146
+ * @param spanData - Span data with id and traceId
147
+ * @param options - Optional settings (sampled flag). Defaults to sampled=true.
148
+ * @returns W3C traceparent header string
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const span = log.span("http-request")
153
+ * const header = traceparent(span.spanData)
154
+ * // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
155
+ * fetch(url, { headers: { traceparent: header } })
156
+ * ```
157
+ */
158
+ function traceparent(spanData, options) {
159
+ return `00-${padHex(spanData.traceId, 32)}-${padHex(spanData.id, 16)}-${options?.sampled ?? true ? "01" : "00"}`;
160
+ }
161
+ /** Pad or hash an ID to the specified hex length */
162
+ function padHex(id, length) {
163
+ if (id.length === length && /^[0-9a-f]+$/.test(id)) return id;
164
+ let hex = "";
165
+ for (let i = 0; i < id.length; i++) hex += id.charCodeAt(i).toString(16).padStart(2, "0");
166
+ return hex.padStart(length, "0").slice(-length);
167
+ }
168
+ let sampleRate = 1;
169
+ /**
170
+ * Set the head-based sampling rate for new traces.
171
+ * Applied at trace creation — all spans within a sampled trace are kept.
172
+ *
173
+ * @deprecated Use the `TRACE_SAMPLE_RATE` env var or `{ sampleRate: 0.1 }` in the config array instead.
174
+ * @param rate - Sampling rate from 0.0 (sample nothing) to 1.0 (sample everything, default)
175
+ */
176
+ function setSampleRate(rate) {
177
+ if (rate < 0 || rate > 1) throw new Error(`Sample rate must be between 0.0 and 1.0, got ${rate}`);
178
+ sampleRate = rate;
179
+ }
180
+ /**
181
+ * Get the current sampling rate.
182
+ *
183
+ * @deprecated Use the `TRACE_SAMPLE_RATE` env var or `{ sampleRate: 0.1 }` in the config array instead.
184
+ */
185
+ function getSampleRate() {
186
+ return sampleRate;
187
+ }
188
+ /**
189
+ * Determine whether a new trace should be sampled.
190
+ * Called at trace creation time (head-based sampling).
191
+ */
192
+ function shouldSample() {
193
+ if (sampleRate >= 1) return true;
194
+ if (sampleRate <= 0) return false;
195
+ return Math.random() < sampleRate;
196
+ }
197
+ //#endregion
198
+ //#region src/pipeline.ts
199
+ const LOG_LEVEL_PRIORITY = {
200
+ trace: 0,
201
+ debug: 1,
202
+ info: 2,
203
+ warn: 3,
204
+ error: 4,
205
+ silent: 5
206
+ };
207
+ const _process = typeof process !== "undefined" ? process : void 0;
208
+ function getEnv(key) {
209
+ return _process?.env?.[key];
210
+ }
211
+ function writeStderr(text) {
212
+ if (_process?.stderr?.write) _process.stderr.write(text + "\n");
213
+ else console.error(text);
214
+ }
215
+ /** Serialize Error.cause chains up to a max depth */
216
+ function serializeCause(cause, maxDepth = 3) {
217
+ if (maxDepth <= 0 || cause === void 0 || cause === null) return void 0;
218
+ if (cause instanceof Error) {
219
+ const result = {
220
+ name: cause.name,
221
+ message: cause.message,
222
+ stack: cause.stack
223
+ };
224
+ if (cause.code) result.code = cause.code;
225
+ if (cause.cause !== void 0) result.cause = serializeCause(cause.cause, maxDepth - 1);
226
+ return result;
227
+ }
228
+ return cause;
229
+ }
230
+ function safeStringify(value) {
231
+ const seen = /* @__PURE__ */ new WeakSet();
232
+ return JSON.stringify(value, (_key, val) => {
233
+ if (typeof val === "bigint") return val.toString();
234
+ if (typeof val === "symbol") return val.toString();
235
+ if (val instanceof Error) {
236
+ const result = {
237
+ message: val.message,
238
+ stack: val.stack,
239
+ name: val.name
240
+ };
241
+ if (val.code) result.code = val.code;
242
+ if (val.cause !== void 0) result.cause = serializeCause(val.cause);
243
+ return result;
244
+ }
245
+ if (typeof val === "object" && val !== null) {
246
+ if (seen.has(val)) return "[Circular]";
247
+ seen.add(val);
248
+ }
249
+ return val;
250
+ });
251
+ }
252
+ function formatConsoleEvent(event) {
253
+ const time = colors.dim(new Date(event.time).toISOString().split("T")[1]?.split(".")[0] || "");
254
+ const ns = colors.cyan(event.namespace);
255
+ if (event.kind === "span") {
256
+ const message = `(${event.duration}ms)`;
257
+ let output = `${time} ${colors.magenta("SPAN")} ${ns} ${message}`;
258
+ if (event.props && Object.keys(event.props).length > 0) output += ` ${colors.dim(safeStringify(event.props))}`;
259
+ return output;
260
+ }
261
+ let levelStr;
262
+ switch (event.level) {
263
+ case "trace":
264
+ levelStr = colors.dim("TRACE");
265
+ break;
266
+ case "debug":
267
+ levelStr = colors.dim("DEBUG");
268
+ break;
269
+ case "info":
270
+ levelStr = colors.blue("INFO");
271
+ break;
272
+ case "warn":
273
+ levelStr = colors.yellow("WARN");
274
+ break;
275
+ case "error":
276
+ levelStr = colors.red("ERROR");
277
+ break;
278
+ }
279
+ let output = `${time} ${levelStr} ${ns} ${event.message}`;
280
+ if (event.props && Object.keys(event.props).length > 0) output += ` ${colors.dim(safeStringify(event.props))}`;
281
+ return output;
282
+ }
283
+ function formatJSONEvent(event) {
284
+ if (event.kind === "span") return safeStringify({
285
+ time: new Date(event.time).toISOString(),
286
+ level: "span",
287
+ name: event.namespace,
288
+ msg: `(${event.duration}ms)`,
289
+ duration: event.duration,
290
+ span_id: event.spanId,
291
+ trace_id: event.traceId,
292
+ parent_id: event.parentId,
293
+ ...event.props
294
+ });
295
+ return safeStringify({
296
+ time: new Date(event.time).toISOString(),
297
+ level: event.level,
298
+ name: event.namespace,
299
+ msg: event.message,
300
+ ...event.props
301
+ });
302
+ }
303
+ function matchesPattern(namespace, pattern) {
304
+ if (pattern === "*") return true;
305
+ if (pattern.endsWith(":*")) {
306
+ const prefix = pattern.slice(0, -2);
307
+ return namespace === prefix || namespace.startsWith(prefix + ":");
308
+ }
309
+ return namespace === pattern || namespace.startsWith(pattern + ":");
310
+ }
311
+ function parseNsFilter(ns) {
312
+ const patterns = typeof ns === "string" ? ns.split(",").map((s) => s.trim()) : ns;
313
+ const includes = [];
314
+ const excludes = [];
315
+ for (const p of patterns) if (p.startsWith("-")) excludes.push(p.slice(1));
316
+ else includes.push(p);
317
+ return (namespace) => {
318
+ for (const exc of excludes) if (matchesPattern(namespace, exc)) return false;
319
+ if (includes.length > 0) {
320
+ for (const inc of includes) if (matchesPattern(namespace, inc)) return true;
321
+ return false;
322
+ }
323
+ return true;
324
+ };
325
+ }
326
+ /**
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.
334
+ */
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
+ function createConsoleSink(format) {
357
+ const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
358
+ return (event) => writeToConsole(formatter(event), event);
359
+ }
360
+ function createFileSink(path, format) {
361
+ const writer = createFileWriter(path);
362
+ const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
363
+ return {
364
+ write: (event) => writer.write(formatter(event)),
365
+ dispose: () => writer.close()
366
+ };
367
+ }
368
+ function isNodeStream(obj) {
369
+ return typeof obj === "object" && obj !== null && ("_write" in obj || "writable" in obj || "fd" in obj);
370
+ }
371
+ function createWritableSink(writable, format) {
372
+ if (!(writable.objectMode ?? !isNodeStream(writable))) {
373
+ const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
374
+ return (event) => writable.write(formatter(event) + "\n");
375
+ }
376
+ return (event) => writable.write(event);
377
+ }
378
+ const VALID_CONFIG_KEYS = new Set([
379
+ "level",
380
+ "ns",
381
+ "format",
382
+ "spans",
383
+ "metrics",
384
+ "idFormat",
385
+ "sampleRate"
386
+ ]);
387
+ const SINK_KEYS = new Set(["file", "otel"]);
388
+ function isPojo(obj) {
389
+ if (typeof obj !== "object" || obj === null) return false;
390
+ const proto = Object.getPrototypeOf(obj);
391
+ return proto === Object.prototype || proto === null;
392
+ }
393
+ function isWritable(obj) {
394
+ return typeof obj === "object" && obj !== null && "write" in obj && typeof obj.write === "function";
395
+ }
396
+ function isValidLogLevel(val) {
397
+ return typeof val === "string" && val in LOG_LEVEL_PRIORITY;
398
+ }
399
+ function buildPipeline(elements, parentConfig) {
400
+ const config = {
401
+ level: parentConfig?.level ?? readEnvLevel(),
402
+ ns: parentConfig?.ns ?? readEnvNs(),
403
+ format: parentConfig?.format ?? readEnvFormat()
404
+ };
405
+ let spansEnabled = true;
406
+ const stages = [];
407
+ const outputs = [];
408
+ const branches = [];
409
+ const disposables = [];
410
+ for (const element of elements) {
411
+ if (Array.isArray(element)) {
412
+ const branch = buildPipeline(element, { ...config });
413
+ branches.push(branch);
414
+ disposables.push(() => branch.dispose());
415
+ continue;
416
+ }
417
+ if (element === console || element === "console") {
418
+ outputs.push({
419
+ levelPriority: LOG_LEVEL_PRIORITY[config.level],
420
+ nsFilter: config.ns,
421
+ write: createConsoleSink(config.format)
422
+ });
423
+ continue;
424
+ }
425
+ if (typeof element === "function") {
426
+ stages.push(element);
427
+ continue;
428
+ }
429
+ if (isWritable(element)) {
430
+ outputs.push({
431
+ levelPriority: LOG_LEVEL_PRIORITY[config.level],
432
+ nsFilter: config.ns,
433
+ write: createWritableSink(element, config.format)
434
+ });
435
+ continue;
436
+ }
437
+ if (isPojo(element)) {
438
+ const obj = element;
439
+ const keys = Object.keys(obj);
440
+ const hasSinkKey = keys.some((k) => SINK_KEYS.has(k));
441
+ if (keys.some((k) => !VALID_CONFIG_KEYS.has(k) && !SINK_KEYS.has(k))) {
442
+ const unknown = keys.find((k) => !VALID_CONFIG_KEYS.has(k) && !SINK_KEYS.has(k));
443
+ throw new Error(`loggily: unknown config key "${unknown}" in config object. Valid keys: ${[...VALID_CONFIG_KEYS, ...SINK_KEYS].join(", ")}`);
444
+ }
445
+ if (hasSinkKey) {
446
+ if (typeof obj.file === "string") {
447
+ const outputLevel = isValidLogLevel(obj.level) ? obj.level : config.level;
448
+ const outputNs = obj.ns ? parseNsFilter(obj.ns) : config.ns;
449
+ const outputFormat = obj.format ?? config.format;
450
+ const sink = createFileSink(obj.file, outputFormat);
451
+ disposables.push(sink.dispose);
452
+ outputs.push({
453
+ levelPriority: LOG_LEVEL_PRIORITY[outputLevel],
454
+ nsFilter: outputNs,
455
+ write: sink.write,
456
+ dispose: sink.dispose
457
+ });
458
+ }
459
+ if (obj.otel !== void 0) throw new Error("loggily: OTEL sink is not yet implemented. See loggily/otel for the planned bridge.");
460
+ continue;
461
+ }
462
+ if (isValidLogLevel(obj.level)) config.level = obj.level;
463
+ if (obj.ns !== void 0) config.ns = parseNsFilter(obj.ns);
464
+ if (obj.format === "console" || obj.format === "json") config.format = obj.format;
465
+ if (obj.spans === true) spansEnabled = true;
466
+ if (obj.spans === false) spansEnabled = false;
467
+ if (obj.idFormat === "simple" || obj.idFormat === "w3c") setIdFormat(obj.idFormat);
468
+ if (typeof obj.sampleRate === "number") setSampleRate(obj.sampleRate);
469
+ continue;
470
+ }
471
+ if (element === "stderr" && typeof process !== "undefined") {
472
+ outputs.push({
473
+ levelPriority: LOG_LEVEL_PRIORITY[config.level],
474
+ nsFilter: config.ns,
475
+ write: createWritableSink(process.stderr, config.format)
476
+ });
477
+ continue;
478
+ }
479
+ 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
+ }
481
+ const dispatch = (event) => {
482
+ if (event.kind === "span" && !spansEnabled) return;
483
+ let e = event;
484
+ for (const stage of stages) {
485
+ const result = stage(e);
486
+ if (result === null) return;
487
+ if (result !== void 0) e = result;
488
+ }
489
+ for (const output of outputs) {
490
+ if (e.kind === "log" && LOG_LEVEL_PRIORITY[e.level] < output.levelPriority) continue;
491
+ if (output.nsFilter && !output.nsFilter(e.namespace)) continue;
492
+ output.write(e);
493
+ }
494
+ for (const branch of branches) branch.dispatch(e);
495
+ };
496
+ return {
497
+ dispatch,
498
+ level: config.level,
499
+ dispose: () => {
500
+ for (const d of disposables) d();
501
+ }
502
+ };
503
+ }
504
+ function readEnvLevel() {
505
+ const env = getEnv("LOG_LEVEL")?.toLowerCase();
506
+ let level = env === "trace" || env === "debug" || env === "info" || env === "warn" || env === "error" || env === "silent" ? env : "info";
507
+ if (getEnv("DEBUG") && LOG_LEVEL_PRIORITY[level] > LOG_LEVEL_PRIORITY.debug) level = "debug";
508
+ return level;
509
+ }
510
+ /**
511
+ * Namespace-aware level: only bumps to debug if the namespace matches the DEBUG filter.
512
+ * This enables zero-overhead conditional gating — `log.debug?.()` returns undefined
513
+ * for namespaces outside the DEBUG filter, skipping argument evaluation entirely.
514
+ */
515
+ function readEnvLevelForNamespace(namespace) {
516
+ const env = getEnv("LOG_LEVEL")?.toLowerCase();
517
+ const baseLevel = env === "trace" || env === "debug" || env === "info" || env === "warn" || env === "error" || env === "silent" ? env : "info";
518
+ if (getEnv("DEBUG") && LOG_LEVEL_PRIORITY[baseLevel] > LOG_LEVEL_PRIORITY.debug) {
519
+ const nsFilter = readEnvNs();
520
+ if (nsFilter && nsFilter(namespace)) return "debug";
521
+ return baseLevel;
522
+ }
523
+ return baseLevel;
524
+ }
525
+ function readEnvNs() {
526
+ const debugEnv = getEnv("DEBUG");
527
+ if (!debugEnv) return null;
528
+ return parseNsFilter(debugEnv.split(",").map((s) => s.trim()));
529
+ }
530
+ function readEnvFormat() {
531
+ const envFormat = getEnv("LOG_FORMAT")?.toLowerCase();
532
+ if (envFormat === "json") return "json";
533
+ if (envFormat === "console") return "console";
534
+ if (getEnv("TRACE_FORMAT") === "json") return "json";
535
+ if (getEnv("NODE_ENV") === "production") return "json";
536
+ return "console";
537
+ }
538
+ function readEnvTrace() {
539
+ const traceEnv = getEnv("TRACE");
540
+ if (!traceEnv) return {
541
+ enabled: false,
542
+ filter: null
543
+ };
544
+ if (traceEnv === "1" || traceEnv === "true") return {
545
+ enabled: true,
546
+ filter: null
547
+ };
548
+ const prefixes = traceEnv.split(",").map((s) => s.trim());
549
+ return {
550
+ enabled: true,
551
+ filter: (namespace) => {
552
+ for (const prefix of prefixes) if (matchesPattern(namespace, prefix)) return true;
553
+ return false;
554
+ }
555
+ };
556
+ }
557
+ //#endregion
558
+ //#region src/core.ts
559
+ /**
560
+ * loggily v2 — Structured logging with spans
561
+ *
562
+ * One import. Objects configure. Arrays branch. Values write.
563
+ *
564
+ * @example
565
+ * const log = createLogger('myapp')
566
+ * log.info?.('starting')
567
+ *
568
+ * @example
569
+ * const log = createLogger('myapp', [
570
+ * { level: 'debug', ns: '-sql' },
571
+ * console,
572
+ * { file: '/tmp/app.log', level: 'info', format: 'json' },
573
+ * ])
574
+ * log.info?.('server started', { port: 3000 })
575
+ */
576
+ function resetIds() {
577
+ resetIdCounters();
578
+ }
579
+ let _getContextTags = null;
580
+ let _getContextParent = null;
581
+ let _enterContext = null;
582
+ let _exitContext = null;
583
+ /** @internal */
584
+ function _setContextHooks(hooks) {
585
+ _getContextTags = hooks.getContextTags;
586
+ _getContextParent = hooks.getContextParent;
587
+ _enterContext = hooks.enterContext;
588
+ _exitContext = hooks.exitContext;
589
+ }
590
+ /** @internal */
591
+ function _clearContextHooks() {
592
+ _getContextTags = null;
593
+ _getContextParent = null;
594
+ _enterContext = null;
595
+ _exitContext = null;
596
+ }
597
+ function createSpanDataProxy(getFields, attrs) {
598
+ const READONLY_KEYS = new Set([
599
+ "id",
600
+ "traceId",
601
+ "parentId",
602
+ "startTime",
603
+ "endTime",
604
+ "duration"
605
+ ]);
606
+ return new Proxy(attrs, {
607
+ get(_target, prop) {
608
+ if (READONLY_KEYS.has(prop)) return getFields()[prop];
609
+ return attrs[prop];
610
+ },
611
+ set(_target, prop, value) {
612
+ if (READONLY_KEYS.has(prop)) return false;
613
+ attrs[prop] = value;
614
+ return true;
615
+ }
616
+ });
617
+ }
618
+ const collectedSpans = [];
619
+ let collectSpans = false;
620
+ function startCollecting() {
621
+ collectSpans = true;
622
+ collectedSpans.length = 0;
623
+ }
624
+ function stopCollecting() {
625
+ collectSpans = false;
626
+ return [...collectedSpans];
627
+ }
628
+ function getCollectedSpans() {
629
+ return [...collectedSpans];
630
+ }
631
+ function clearCollectedSpans() {
632
+ collectedSpans.length = 0;
633
+ }
634
+ function resolveMessage(msg) {
635
+ return typeof msg === "function" ? msg() : msg;
636
+ }
637
+ function createLoggerImpl(name, props, pipeline) {
638
+ const emitLog = (level, msgOrError, dataOrMsg, extraData) => {
639
+ let message;
640
+ let data;
641
+ if (msgOrError instanceof Error) {
642
+ const err = msgOrError;
643
+ const contextTags = _getContextTags?.() ?? {};
644
+ if (typeof dataOrMsg === "string") {
645
+ message = dataOrMsg;
646
+ data = {
647
+ ...contextTags,
648
+ ...props,
649
+ ...extraData,
650
+ error_type: err.name,
651
+ error_message: err.message,
652
+ error_stack: err.stack,
653
+ error_code: err.code,
654
+ error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
655
+ };
656
+ } else {
657
+ message = err.message;
658
+ data = {
659
+ ...contextTags,
660
+ ...props,
661
+ ...dataOrMsg,
662
+ error_type: err.name,
663
+ error_stack: err.stack,
664
+ error_code: err.code,
665
+ error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
666
+ };
667
+ }
668
+ } else {
669
+ message = resolveMessage(msgOrError);
670
+ const contextTags = _getContextTags?.();
671
+ data = contextTags && Object.keys(contextTags).length > 0 ? {
672
+ ...contextTags,
673
+ ...props,
674
+ ...dataOrMsg
675
+ } : Object.keys(props).length > 0 || dataOrMsg ? {
676
+ ...props,
677
+ ...dataOrMsg
678
+ } : void 0;
679
+ }
680
+ const event = {
681
+ kind: "log",
682
+ time: Date.now(),
683
+ namespace: name,
684
+ level,
685
+ message,
686
+ props: data
687
+ };
688
+ pipeline.dispatch(event);
689
+ };
690
+ return {
691
+ name,
692
+ props: Object.freeze({ ...props }),
693
+ get level() {
694
+ return pipeline.level;
695
+ },
696
+ dispatch(event) {
697
+ pipeline.dispatch(event);
698
+ },
699
+ [Symbol.dispose]() {
700
+ pipeline.dispose();
701
+ },
702
+ trace: (msg, data) => emitLog("trace", msg, data),
703
+ debug: (msg, data) => emitLog("debug", msg, data),
704
+ info: (msg, data) => emitLog("info", msg, data),
705
+ warn: (msg, data) => emitLog("warn", msg, data),
706
+ error: (msgOrError, dataOrMsg, extraData) => emitLog("error", msgOrError, dataOrMsg, extraData),
707
+ logger(namespace, childProps) {
708
+ return this.child(namespace ?? "", childProps);
709
+ },
710
+ span(_namespace, _childProps) {
711
+ throw new Error("loggily: span() requires the withSpans() plugin. Use pipe(baseCreateLogger, withSpans()) or the default createLogger.");
712
+ },
713
+ child(namespaceOrContext, childProps) {
714
+ if (typeof namespaceOrContext === "string") return wrapConditional(createLoggerImpl(namespaceOrContext ? `${name}:${namespaceOrContext}` : name, {
715
+ ...props,
716
+ ...childProps
717
+ }, pipeline), () => pipeline.level);
718
+ return wrapConditional(createLoggerImpl(name, {
719
+ ...props,
720
+ ...namespaceOrContext
721
+ }, pipeline), () => pipeline.level);
722
+ },
723
+ end() {}
724
+ };
725
+ }
726
+ function wrapConditional(logger, getLevel) {
727
+ return new Proxy(logger, { get(target, prop) {
728
+ if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
729
+ if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[getLevel()]) return;
730
+ }
731
+ if (prop === "span") {
732
+ const val = target[prop];
733
+ if (val === baseSpanStub) return void 0;
734
+ return val;
735
+ }
736
+ return target[prop];
737
+ } });
738
+ }
739
+ const baseSpanStub = function baseSpanStub(_namespace, _childProps) {
740
+ throw new Error("loggily: span() requires the withSpans() plugin. Use pipe(baseCreateLogger, withSpans()) or the default createLogger.");
741
+ };
742
+ /**
743
+ * Plugin: adds span creation capability to loggers.
744
+ * Without this plugin, `.span` is undefined on ConditionalLogger.
745
+ * Included by default in `createLogger`.
746
+ */
747
+ function withSpans() {
748
+ return (factory, _ctx) => {
749
+ return (name, configOrProps) => {
750
+ return augmentWithSpans(factory(name, configOrProps), null, null, true);
751
+ };
752
+ };
753
+ }
754
+ function augmentWithSpans(logger, parentSpanId, traceId, traceSampled) {
755
+ const spanState = {
756
+ parentSpanId,
757
+ traceId,
758
+ traceSampled
759
+ };
760
+ return new Proxy(logger, { get(target, prop) {
761
+ if (prop === "span") return createSpanMethod(target, spanState);
762
+ if (prop === "child") return function child(namespaceOrContext, childProps) {
763
+ return augmentWithSpans(target.child(namespaceOrContext, childProps), spanState.parentSpanId, spanState.traceId, spanState.traceSampled);
764
+ };
765
+ if (prop === "logger") return function logger(namespace, childProps) {
766
+ return augmentWithSpans(target.logger(namespace, childProps), spanState.parentSpanId, spanState.traceId, spanState.traceSampled);
767
+ };
768
+ return target[prop];
769
+ } });
770
+ }
771
+ function createSpanMethod(logger, spanState) {
772
+ return (namespace, childProps) => {
773
+ const childName = namespace ? `${logger.name}:${namespace}` : logger.name;
774
+ const resolvedChildProps = typeof childProps === "function" ? childProps() : childProps;
775
+ const mergedProps = {
776
+ ...logger.props,
777
+ ...resolvedChildProps
778
+ };
779
+ const newSpanId = generateSpanId();
780
+ let resolvedParentId = spanState.parentSpanId;
781
+ let resolvedTraceId = spanState.traceId;
782
+ if (!resolvedParentId && _getContextParent) {
783
+ const ctxParent = _getContextParent();
784
+ if (ctxParent) {
785
+ resolvedParentId = ctxParent.spanId;
786
+ resolvedTraceId = resolvedTraceId || ctxParent.traceId;
787
+ }
788
+ }
789
+ const isNewTrace = !resolvedTraceId;
790
+ const finalTraceId = resolvedTraceId || generateTraceId();
791
+ const sampled = isNewTrace ? shouldSample() : spanState.traceSampled;
792
+ const newSpanData = {
793
+ id: newSpanId,
794
+ traceId: finalTraceId,
795
+ parentId: resolvedParentId,
796
+ startTime: Date.now(),
797
+ endTime: null,
798
+ duration: null,
799
+ attrs: {}
800
+ };
801
+ const spanAugmented = augmentWithSpans(logger.child(namespace ?? "", resolvedChildProps), newSpanId, finalTraceId, sampled);
802
+ _enterContext?.(newSpanId, finalTraceId, resolvedParentId);
803
+ const disposeSpan = () => {
804
+ if (newSpanData.endTime !== null) return;
805
+ newSpanData.endTime = Date.now();
806
+ newSpanData.duration = newSpanData.endTime - newSpanData.startTime;
807
+ if (collectSpans) collectedSpans.push(createSpanDataProxy(() => ({
808
+ id: newSpanData.id,
809
+ traceId: newSpanData.traceId,
810
+ parentId: newSpanData.parentId,
811
+ startTime: newSpanData.startTime,
812
+ endTime: newSpanData.endTime,
813
+ duration: newSpanData.duration
814
+ }), { ...newSpanData.attrs }));
815
+ _exitContext?.(newSpanId);
816
+ if (sampled) {
817
+ const spanEvent = {
818
+ kind: "span",
819
+ time: newSpanData.endTime,
820
+ namespace: childName,
821
+ name: childName,
822
+ duration: newSpanData.duration,
823
+ props: {
824
+ ...mergedProps,
825
+ ...newSpanData.attrs
826
+ },
827
+ spanId: newSpanData.id,
828
+ traceId: newSpanData.traceId,
829
+ parentId: newSpanData.parentId
830
+ };
831
+ logger.dispatch(spanEvent);
832
+ }
833
+ };
834
+ const spanDataProxy = createSpanDataProxy(() => ({
835
+ id: newSpanData.id,
836
+ traceId: newSpanData.traceId,
837
+ parentId: newSpanData.parentId,
838
+ startTime: newSpanData.startTime,
839
+ endTime: newSpanData.endTime,
840
+ duration: newSpanData.endTime !== null ? newSpanData.endTime - newSpanData.startTime : Date.now() - newSpanData.startTime
841
+ }), newSpanData.attrs);
842
+ let currentDispose = disposeSpan;
843
+ return new Proxy(spanAugmented, {
844
+ get(target, prop) {
845
+ if (prop === "spanData") return spanDataProxy;
846
+ if (prop === Symbol.dispose) return currentDispose;
847
+ if (prop === "end") return () => {
848
+ if (newSpanData.endTime === null) currentDispose();
849
+ };
850
+ if (prop === "name") return childName;
851
+ if (prop === "props") return Object.freeze({ ...mergedProps });
852
+ return target[prop];
853
+ },
854
+ set(_target, prop, value) {
855
+ if (prop === Symbol.dispose) {
856
+ currentDispose = value;
857
+ return true;
858
+ }
859
+ return false;
860
+ }
861
+ });
862
+ };
863
+ }
864
+ /**
865
+ * Base createLogger — requires a config array.
866
+ * Use the default `createLogger` export (with `withEnvDefaults`) for zero-config.
867
+ *
868
+ * Note: loggers from baseCreateLogger do NOT have `.span()` capability.
869
+ * Use `pipe(baseCreateLogger, withSpans())` or the default `createLogger` for spans.
870
+ */
871
+ function baseCreateLogger(name, configOrProps) {
872
+ let pipeline;
873
+ let props = {};
874
+ if (Array.isArray(configOrProps)) pipeline = buildPipeline(configOrProps);
875
+ else if (configOrProps && typeof configOrProps === "object") {
876
+ props = configOrProps;
877
+ pipeline = buildPipeline(["console"]);
878
+ } else pipeline = buildPipeline(["console"]);
879
+ const logger = createLoggerImpl(name, props, pipeline);
880
+ logger.span = baseSpanStub;
881
+ return wrapConditional(logger, () => pipeline.level);
882
+ }
883
+ function pipe(base, ...plugins) {
884
+ const ctx = {};
885
+ return plugins.reduce((factory, plugin) => plugin(factory, ctx), base);
886
+ }
887
+ const _env = (typeof process !== "undefined" ? process : void 0)?.env ?? {};
888
+ function currentLevel() {
889
+ return readEnvLevel();
890
+ }
891
+ function currentNs() {
892
+ return readEnvNs();
893
+ }
894
+ function currentFormat() {
895
+ return readEnvFormat();
896
+ }
897
+ function currentTrace() {
898
+ return readEnvTrace();
899
+ }
900
+ const _writers = [];
901
+ let _suppressConsole = false;
902
+ let _logFileWriterFactory = null;
903
+ /** @internal */
904
+ function _setLogFileWriterFactory(factory) {
905
+ _logFileWriterFactory = factory;
906
+ }
907
+ /**
908
+ * Plugin: read defaults from environment variables (LOG_LEVEL, DEBUG, LOG_FORMAT, TRACE, LOG_FILE).
909
+ * Included by default. Omit to disable env-var behavior entirely.
910
+ *
911
+ * When no config array is given, provides console output + env-var-based config.
912
+ * When a config array IS given, env vars are already used as defaults by buildPipeline.
913
+ * Legacy setters (setLogLevel, addWriter, etc.) affect loggers created without explicit config.
914
+ */
915
+ function withEnvDefaults() {
916
+ return (factory, _ctx) => (name, configOrProps) => {
917
+ const envIdFormat = _env.TRACE_ID_FORMAT?.toLowerCase();
918
+ if (envIdFormat === "simple" || envIdFormat === "w3c") setIdFormat(envIdFormat);
919
+ const envSampleRate = _env.TRACE_SAMPLE_RATE;
920
+ if (envSampleRate !== void 0) {
921
+ const rate = Number.parseFloat(envSampleRate);
922
+ if (!Number.isNaN(rate) && rate >= 0 && rate <= 1) setSampleRate(rate);
923
+ }
924
+ if (Array.isArray(configOrProps)) return factory(name, configOrProps);
925
+ const envPipeline = createEnvPipeline();
926
+ const envStage = (event) => {
927
+ envPipeline.dispatch(event);
928
+ return null;
929
+ };
930
+ if (configOrProps && typeof configOrProps === "object") return applyNamespaceGating(factory(name, [{ level: "trace" }, envStage]).child(configOrProps));
931
+ return applyNamespaceGating(factory(name, [{ level: "trace" }, envStage]));
932
+ };
933
+ }
934
+ /**
935
+ * Wrap a logger so that conditional method gating is namespace-aware.
936
+ * When DEBUG=myapp:db, only loggers whose namespace matches get debug enabled.
937
+ * Without this, all loggers get debug because readEnvLevel() bumps globally.
938
+ */
939
+ function applyNamespaceGating(logger) {
940
+ return new Proxy(logger, { get(target, prop) {
941
+ if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
942
+ const nsLevel = readEnvLevelForNamespace(target.name);
943
+ if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[nsLevel]) return;
944
+ }
945
+ return target[prop];
946
+ } });
947
+ }
948
+ function createEnvPipeline() {
949
+ const disposables = [];
950
+ const logFile = _env.LOG_FILE;
951
+ let fileSink = null;
952
+ if (logFile && _logFileWriterFactory) {
953
+ const writer = _logFileWriterFactory(logFile);
954
+ fileSink = (event) => {
955
+ const fmt = currentFormat() === "json" ? formatJSONEvent : formatConsoleEvent;
956
+ writer.write(fmt(event));
957
+ };
958
+ disposables.push(() => writer.close());
959
+ }
960
+ const dispatch = (event) => {
961
+ if (event.kind === "log" && LOG_LEVEL_PRIORITY[event.level] < LOG_LEVEL_PRIORITY[currentLevel()]) return;
962
+ if (event.kind === "span") {
963
+ const trace = currentTrace();
964
+ if (!trace.enabled) return;
965
+ if (trace.filter && !trace.filter(event.namespace)) return;
966
+ }
967
+ const ns = currentNs();
968
+ if (ns && !ns(event.namespace)) return;
969
+ const text = (currentFormat() === "json" ? formatJSONEvent : formatConsoleEvent)(event);
970
+ const lvl = event.kind === "log" ? event.level : "span";
971
+ for (const w of _writers) w(text, lvl);
972
+ if (!_suppressConsole) writeToConsole(text, event);
973
+ fileSink?.(event);
974
+ };
975
+ return {
976
+ dispatch,
977
+ get level() {
978
+ return currentLevel();
979
+ },
980
+ dispose: () => {
981
+ for (const d of disposables) d();
982
+ }
983
+ };
984
+ }
985
+ /**
986
+ * Plugin: when `{ metrics: true }` appears in the config array, automatically
987
+ * creates a MetricsCollector and applies withMetrics to the logger.
988
+ * The collector is accessible via `logger.metrics`.
989
+ */
990
+ function withConfigMetrics() {
991
+ return (factory, _ctx) => {
992
+ return (name, configOrProps) => {
993
+ const logger = factory(name, configOrProps);
994
+ if (!Array.isArray(configOrProps)) return logger;
995
+ if (!configOrProps.some((el) => typeof el === "object" && el !== null && !Array.isArray(el) && "metrics" in el && el.metrics === true)) return logger;
996
+ return withMetrics(createMetricsCollector())(logger);
997
+ };
998
+ };
999
+ }
1000
+ /** Default createLogger — includes withEnvDefaults + withSpans + withConfigMetrics. */
1001
+ const createLogger = pipe(baseCreateLogger, withEnvDefaults(), withSpans(), withConfigMetrics());
1002
+ /** Test helper — all levels, console output. */
1003
+ function createTestLogger(name) {
1004
+ return pipe(baseCreateLogger, withSpans())(name, [{ level: "trace" }, "console"]);
1005
+ }
1006
+ /** @deprecated Use config array */
1007
+ function setLogLevel(level) {
1008
+ _env.LOG_LEVEL = level;
1009
+ }
1010
+ function getLogLevel() {
1011
+ return currentLevel();
1012
+ }
1013
+ function enableSpans() {
1014
+ _env.TRACE = "1";
1015
+ }
1016
+ function disableSpans() {
1017
+ delete _env.TRACE;
1018
+ }
1019
+ function spansAreEnabled() {
1020
+ return !!_env.TRACE;
1021
+ }
1022
+ function setTraceFilter(namespaces) {
1023
+ if (!namespaces || namespaces.length === 0) delete _env.TRACE;
1024
+ else _env.TRACE = namespaces.join(",");
1025
+ }
1026
+ function getTraceFilter() {
1027
+ return _env.TRACE ? _env.TRACE.split(",") : null;
1028
+ }
1029
+ function setDebugFilter(namespaces) {
1030
+ if (!namespaces || namespaces.length === 0) delete _env.DEBUG;
1031
+ else _env.DEBUG = namespaces.join(",");
1032
+ }
1033
+ function getDebugFilter() {
1034
+ return _env.DEBUG ? _env.DEBUG.split(",") : null;
1035
+ }
1036
+ function setLogFormat(format) {
1037
+ _env.LOG_FORMAT = format;
1038
+ }
1039
+ function getLogFormat() {
1040
+ return currentFormat();
1041
+ }
1042
+ function setSuppressConsole(value) {
1043
+ _suppressConsole = value;
1044
+ }
1045
+ function setOutputMode(_mode) {
1046
+ throw new Error("loggily: setOutputMode() is removed in v2. Use config arrays: omit console from array for writers-only, use \"stderr\" for stderr mode.");
1047
+ }
1048
+ function getOutputMode() {
1049
+ return "console";
1050
+ }
1051
+ function addWriter(writer) {
1052
+ _writers.push(writer);
1053
+ return () => {
1054
+ const i = _writers.indexOf(writer);
1055
+ if (i !== -1) _writers.splice(i, 1);
1056
+ };
1057
+ }
1058
+ function writeSpan(namespace, duration, attrs) {
1059
+ createEnvPipeline().dispatch({
1060
+ kind: "span",
1061
+ time: Date.now(),
1062
+ namespace,
1063
+ name: namespace,
1064
+ duration,
1065
+ props: attrs,
1066
+ spanId: attrs.span_id ?? "",
1067
+ traceId: attrs.trace_id ?? "",
1068
+ parentId: attrs.parent_id ?? null
1069
+ });
1070
+ }
1071
+ //#endregion
1072
+ 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
+
1074
+ //# sourceMappingURL=core-B3pox577.mjs.map