loggily 0.7.0 → 0.10.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.
- package/README.md +46 -133
- package/dist/context.d.mts +2 -2
- package/dist/context.d.mts.map +1 -1
- package/dist/context.mjs +3 -3
- package/dist/context.mjs.map +1 -1
- package/dist/core-Byd3DY71.mjs +1324 -0
- package/dist/core-Byd3DY71.mjs.map +1 -0
- package/dist/core-DHB4KaG1.d.mts +260 -0
- package/dist/core-DHB4KaG1.d.mts.map +1 -0
- package/dist/{index-Co4jC3mx.d.mts → file-writer-BhJzFNKE.d.mts} +68 -50
- package/dist/file-writer-BhJzFNKE.d.mts.map +1 -0
- package/dist/index.browser.d.mts +10 -0
- package/dist/index.browser.d.mts.map +1 -0
- package/dist/index.browser.mjs +10 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +7 -2
- package/dist/index.mjs.map +1 -0
- package/dist/metrics.d.mts +2 -48
- package/dist/metrics.mjs +13 -43
- package/dist/metrics.mjs.map +1 -1
- package/dist/otel.d.mts +63 -0
- package/dist/otel.d.mts.map +1 -0
- package/dist/otel.mjs +82 -0
- package/dist/otel.mjs.map +1 -0
- package/dist/pipeline-D0omS7eF.d.mts +85 -0
- package/dist/pipeline-D0omS7eF.d.mts.map +1 -0
- package/dist/worker.d.mts +64 -94
- package/dist/worker.d.mts.map +1 -1
- package/dist/worker.mjs +107 -281
- package/dist/worker.mjs.map +1 -1
- package/package.json +26 -4
- package/dist/core-DAFH-huv.d.mts +0 -199
- package/dist/core-DAFH-huv.d.mts.map +0 -1
- package/dist/core-Du3sIje6.mjs +0 -900
- package/dist/core-Du3sIje6.mjs.map +0 -1
- package/dist/index-Co4jC3mx.d.mts.map +0 -1
- package/dist/metrics.d.mts.map +0 -1
|
@@ -0,0 +1,1324 @@
|
|
|
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
|
+
const fs = options.__fs ?? {
|
|
53
|
+
openSync,
|
|
54
|
+
writeSync,
|
|
55
|
+
closeSync
|
|
56
|
+
};
|
|
57
|
+
let buffer = "";
|
|
58
|
+
let fd = null;
|
|
59
|
+
let timer = null;
|
|
60
|
+
let closed = false;
|
|
61
|
+
fd = fs.openSync(filePath, "a");
|
|
62
|
+
/** Flush buffer contents to disk synchronously */
|
|
63
|
+
function flush() {
|
|
64
|
+
if (buffer.length === 0 || fd === null) return;
|
|
65
|
+
const data = buffer;
|
|
66
|
+
fs.writeSync(fd, data);
|
|
67
|
+
buffer = "";
|
|
68
|
+
}
|
|
69
|
+
timer = setInterval(flush, flushInterval);
|
|
70
|
+
if (timer && typeof timer === "object" && "unref" in timer) timer.unref();
|
|
71
|
+
const exitHandler = () => flush();
|
|
72
|
+
process.on("exit", exitHandler);
|
|
73
|
+
return {
|
|
74
|
+
write(line) {
|
|
75
|
+
if (closed) return;
|
|
76
|
+
buffer += line + "\n";
|
|
77
|
+
if (buffer.length >= bufferSize) flush();
|
|
78
|
+
},
|
|
79
|
+
flush,
|
|
80
|
+
close() {
|
|
81
|
+
if (closed) return;
|
|
82
|
+
closed = true;
|
|
83
|
+
if (timer !== null) {
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
timer = null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
flush();
|
|
89
|
+
} catch {} finally {
|
|
90
|
+
if (fd !== null) {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
fd = null;
|
|
93
|
+
}
|
|
94
|
+
process.removeListener("exit", exitHandler);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/tracing.ts
|
|
101
|
+
let currentIdFormat = "simple";
|
|
102
|
+
/**
|
|
103
|
+
* Set the ID format for new spans and traces.
|
|
104
|
+
* - "simple": sp_1, sp_2, tr_1, tr_2 (default, lightweight)
|
|
105
|
+
* - "w3c": 32-char hex trace ID, 16-char hex span ID (W3C Trace Context compatible)
|
|
106
|
+
*
|
|
107
|
+
* @deprecated Use the `TRACE_ID_FORMAT` env var or `{ idFormat: "w3c" }` in the config array instead.
|
|
108
|
+
*/
|
|
109
|
+
function setIdFormat(format) {
|
|
110
|
+
currentIdFormat = format;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the current ID format.
|
|
114
|
+
*
|
|
115
|
+
* @deprecated Use the `TRACE_ID_FORMAT` env var or `{ idFormat: "w3c" }` in the config array instead.
|
|
116
|
+
*/
|
|
117
|
+
function getIdFormat() {
|
|
118
|
+
return currentIdFormat;
|
|
119
|
+
}
|
|
120
|
+
let simpleSpanCounter = 0;
|
|
121
|
+
let simpleTraceCounter = 0;
|
|
122
|
+
/** Generate a hex string of the given byte length using crypto.randomUUID */
|
|
123
|
+
function randomHex(bytes) {
|
|
124
|
+
return crypto.randomUUID().replace(/-/g, "").slice(0, bytes * 2);
|
|
125
|
+
}
|
|
126
|
+
/** Generate a span ID according to the current format */
|
|
127
|
+
function generateSpanId() {
|
|
128
|
+
if (currentIdFormat === "w3c") return randomHex(8);
|
|
129
|
+
return `sp_${(++simpleSpanCounter).toString(36)}`;
|
|
130
|
+
}
|
|
131
|
+
/** Generate a trace ID according to the current format */
|
|
132
|
+
function generateTraceId() {
|
|
133
|
+
if (currentIdFormat === "w3c") return randomHex(16);
|
|
134
|
+
return `tr_${(++simpleTraceCounter).toString(36)}`;
|
|
135
|
+
}
|
|
136
|
+
/** Reset ID counters (for testing) */
|
|
137
|
+
function resetIdCounters() {
|
|
138
|
+
simpleSpanCounter = 0;
|
|
139
|
+
simpleTraceCounter = 0;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format a W3C traceparent header from span data.
|
|
143
|
+
*
|
|
144
|
+
* Format: `{version}-{trace-id}-{span-id}-{trace-flags}`
|
|
145
|
+
* - version: "00" (current W3C spec version)
|
|
146
|
+
* - trace-id: 32 hex chars (128 bits)
|
|
147
|
+
* - span-id: 16 hex chars (64 bits)
|
|
148
|
+
* - trace-flags: "01" (sampled) or "00" (not sampled)
|
|
149
|
+
*
|
|
150
|
+
* Works with both simple and W3C ID formats. Simple IDs are zero-padded to spec length.
|
|
151
|
+
*
|
|
152
|
+
* @param spanData - Span data with id and traceId
|
|
153
|
+
* @param options - Optional settings (sampled flag). Defaults to sampled=true.
|
|
154
|
+
* @returns W3C traceparent header string
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* const span = log.span?.("http-request")
|
|
159
|
+
* if (!span) return
|
|
160
|
+
* const header = traceparent(span.spanData)
|
|
161
|
+
* // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
|
|
162
|
+
* fetch(url, { headers: { traceparent: header } })
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
function traceparent(spanData, options) {
|
|
166
|
+
return `00-${padHex(spanData.traceId, 32)}-${padHex(spanData.id, 16)}-${options?.sampled ?? true ? "01" : "00"}`;
|
|
167
|
+
}
|
|
168
|
+
/** Pad or hash an ID to the specified hex length */
|
|
169
|
+
function padHex(id, length) {
|
|
170
|
+
if (id.length === length && /^[0-9a-f]+$/.test(id)) return id;
|
|
171
|
+
let hex = "";
|
|
172
|
+
for (let i = 0; i < id.length; i++) hex += id.charCodeAt(i).toString(16).padStart(2, "0");
|
|
173
|
+
return hex.padStart(length, "0").slice(-length);
|
|
174
|
+
}
|
|
175
|
+
let sampleRate = 1;
|
|
176
|
+
/**
|
|
177
|
+
* Set the head-based sampling rate for new traces.
|
|
178
|
+
* Applied at trace creation — all spans within a sampled trace are kept.
|
|
179
|
+
*
|
|
180
|
+
* @deprecated Use the `TRACE_SAMPLE_RATE` env var or `{ sampleRate: 0.1 }` in the config array instead.
|
|
181
|
+
* @param rate - Sampling rate from 0.0 (sample nothing) to 1.0 (sample everything, default)
|
|
182
|
+
*/
|
|
183
|
+
function setSampleRate(rate) {
|
|
184
|
+
if (rate < 0 || rate > 1) throw new Error(`Sample rate must be between 0.0 and 1.0, got ${rate}`);
|
|
185
|
+
sampleRate = rate;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the current sampling rate.
|
|
189
|
+
*
|
|
190
|
+
* @deprecated Use the `TRACE_SAMPLE_RATE` env var or `{ sampleRate: 0.1 }` in the config array instead.
|
|
191
|
+
*/
|
|
192
|
+
function getSampleRate() {
|
|
193
|
+
return sampleRate;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Determine whether a new trace should be sampled.
|
|
197
|
+
* Called at trace creation time (head-based sampling).
|
|
198
|
+
*/
|
|
199
|
+
function shouldSample() {
|
|
200
|
+
if (sampleRate >= 1) return true;
|
|
201
|
+
if (sampleRate <= 0) return false;
|
|
202
|
+
return Math.random() < sampleRate;
|
|
203
|
+
}
|
|
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
|
|
410
|
+
//#region src/pipeline.ts
|
|
411
|
+
const LOG_LEVEL_PRIORITY = {
|
|
412
|
+
trace: 0,
|
|
413
|
+
debug: 1,
|
|
414
|
+
info: 2,
|
|
415
|
+
warn: 3,
|
|
416
|
+
error: 4,
|
|
417
|
+
silent: 5
|
|
418
|
+
};
|
|
419
|
+
const _process = typeof process !== "undefined" ? process : void 0;
|
|
420
|
+
function getEnv(key) {
|
|
421
|
+
return _process?.env?.[key];
|
|
422
|
+
}
|
|
423
|
+
/** Serialize Error.cause chains up to a max depth */
|
|
424
|
+
function serializeCause(cause, maxDepth = 3) {
|
|
425
|
+
if (maxDepth <= 0 || cause === void 0 || cause === null) return void 0;
|
|
426
|
+
if (cause instanceof Error) {
|
|
427
|
+
const result = {
|
|
428
|
+
name: cause.name,
|
|
429
|
+
message: cause.message,
|
|
430
|
+
stack: cause.stack
|
|
431
|
+
};
|
|
432
|
+
if (cause.code) result.code = cause.code;
|
|
433
|
+
if (cause.cause !== void 0) result.cause = serializeCause(cause.cause, maxDepth - 1);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
return cause;
|
|
437
|
+
}
|
|
438
|
+
function safeStringify(value) {
|
|
439
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
440
|
+
return JSON.stringify(value, (_key, val) => {
|
|
441
|
+
if (typeof val === "bigint") return val.toString();
|
|
442
|
+
if (typeof val === "symbol") return val.toString();
|
|
443
|
+
if (val instanceof Error) {
|
|
444
|
+
const result = {
|
|
445
|
+
message: val.message,
|
|
446
|
+
stack: val.stack,
|
|
447
|
+
name: val.name
|
|
448
|
+
};
|
|
449
|
+
if (val.code) result.code = val.code;
|
|
450
|
+
if (val.cause !== void 0) result.cause = serializeCause(val.cause);
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
if (typeof val === "object" && val !== null) {
|
|
454
|
+
if (seen.has(val)) return "[Circular]";
|
|
455
|
+
seen.add(val);
|
|
456
|
+
}
|
|
457
|
+
return val;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function formatConsoleEvent(event) {
|
|
461
|
+
const time = colors.dim(new Date(event.time).toISOString().split("T")[1]?.split(".")[0] || "");
|
|
462
|
+
const ns = colors.cyan(event.namespace);
|
|
463
|
+
if (event.kind === "span") {
|
|
464
|
+
const message = `(${event.duration}ms)`;
|
|
465
|
+
let output = `${time} ${colors.magenta("SPAN")} ${ns} ${message}`;
|
|
466
|
+
if (event.props && Object.keys(event.props).length > 0) output += ` ${colors.dim(safeStringify(event.props))}`;
|
|
467
|
+
return output;
|
|
468
|
+
}
|
|
469
|
+
let levelStr;
|
|
470
|
+
switch (event.level) {
|
|
471
|
+
case "trace":
|
|
472
|
+
levelStr = colors.dim("TRACE");
|
|
473
|
+
break;
|
|
474
|
+
case "debug":
|
|
475
|
+
levelStr = colors.dim("DEBUG");
|
|
476
|
+
break;
|
|
477
|
+
case "info":
|
|
478
|
+
levelStr = colors.blue("INFO");
|
|
479
|
+
break;
|
|
480
|
+
case "warn":
|
|
481
|
+
levelStr = colors.yellow("WARN");
|
|
482
|
+
break;
|
|
483
|
+
case "error":
|
|
484
|
+
levelStr = colors.red("ERROR");
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
let output = `${time} ${levelStr} ${ns} ${event.message}`;
|
|
488
|
+
if (event.props && Object.keys(event.props).length > 0) output += ` ${colors.dim(safeStringify(event.props))}`;
|
|
489
|
+
return output;
|
|
490
|
+
}
|
|
491
|
+
function formatJSONEvent(event) {
|
|
492
|
+
if (event.kind === "span") return safeStringify({
|
|
493
|
+
time: new Date(event.time).toISOString(),
|
|
494
|
+
level: "span",
|
|
495
|
+
name: event.namespace,
|
|
496
|
+
msg: `(${event.duration}ms)`,
|
|
497
|
+
duration: event.duration,
|
|
498
|
+
span_id: event.spanId,
|
|
499
|
+
trace_id: event.traceId,
|
|
500
|
+
parent_id: event.parentId,
|
|
501
|
+
...event.props
|
|
502
|
+
});
|
|
503
|
+
return safeStringify({
|
|
504
|
+
time: new Date(event.time).toISOString(),
|
|
505
|
+
level: event.level,
|
|
506
|
+
name: event.namespace,
|
|
507
|
+
msg: event.message,
|
|
508
|
+
...event.props
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function matchesPattern(namespace, pattern) {
|
|
512
|
+
if (pattern === "*") return true;
|
|
513
|
+
if (pattern.endsWith(":*")) {
|
|
514
|
+
const prefix = pattern.slice(0, -2);
|
|
515
|
+
return namespace === prefix || namespace.startsWith(prefix + ":");
|
|
516
|
+
}
|
|
517
|
+
return namespace === pattern || namespace.startsWith(pattern + ":");
|
|
518
|
+
}
|
|
519
|
+
function parseNsFilter(ns) {
|
|
520
|
+
const patterns = typeof ns === "string" ? ns.split(",").map((s) => s.trim()) : ns;
|
|
521
|
+
const includes = [];
|
|
522
|
+
const excludes = [];
|
|
523
|
+
for (const p of patterns) if (p.startsWith("-")) excludes.push(p.slice(1));
|
|
524
|
+
else includes.push(p);
|
|
525
|
+
return (namespace) => {
|
|
526
|
+
for (const exc of excludes) if (matchesPattern(namespace, exc)) return false;
|
|
527
|
+
if (includes.length > 0) {
|
|
528
|
+
for (const inc of includes) if (matchesPattern(namespace, inc)) return true;
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Console sink used inside the pipeline builder. Delegates to the structured
|
|
536
|
+
* console sink (browser %c or terminal multi-arg) so the pipeline preserves
|
|
537
|
+
* expandable user args end-to-end.
|
|
538
|
+
*/
|
|
539
|
+
function createConsoleSink(format) {
|
|
540
|
+
return createConsoleSink$1(format);
|
|
541
|
+
}
|
|
542
|
+
function createFileSink(path, format) {
|
|
543
|
+
const writer = createFileWriter(path);
|
|
544
|
+
const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
|
|
545
|
+
return {
|
|
546
|
+
write: (event) => writer.write(formatter(event)),
|
|
547
|
+
dispose: () => writer.close()
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function isNodeStream(obj) {
|
|
551
|
+
return typeof obj === "object" && obj !== null && ("_write" in obj || "writable" in obj || "fd" in obj);
|
|
552
|
+
}
|
|
553
|
+
function createWritableSink(writable, format) {
|
|
554
|
+
if (!(writable.objectMode ?? !isNodeStream(writable))) {
|
|
555
|
+
const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
|
|
556
|
+
return (event) => writable.write(formatter(event) + "\n");
|
|
557
|
+
}
|
|
558
|
+
return (event) => writable.write(event);
|
|
559
|
+
}
|
|
560
|
+
const VALID_CONFIG_KEYS = new Set([
|
|
561
|
+
"level",
|
|
562
|
+
"ns",
|
|
563
|
+
"format",
|
|
564
|
+
"spans",
|
|
565
|
+
"metrics",
|
|
566
|
+
"idFormat",
|
|
567
|
+
"sampleRate"
|
|
568
|
+
]);
|
|
569
|
+
const SINK_KEYS = new Set(["file", "otel"]);
|
|
570
|
+
function isPojo(obj) {
|
|
571
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
572
|
+
const proto = Object.getPrototypeOf(obj);
|
|
573
|
+
return proto === Object.prototype || proto === null;
|
|
574
|
+
}
|
|
575
|
+
function isWritable(obj) {
|
|
576
|
+
return typeof obj === "object" && obj !== null && "write" in obj && typeof obj.write === "function";
|
|
577
|
+
}
|
|
578
|
+
function isValidLogLevel(val) {
|
|
579
|
+
return typeof val === "string" && val in LOG_LEVEL_PRIORITY;
|
|
580
|
+
}
|
|
581
|
+
function buildPipeline(elements, parentConfig) {
|
|
582
|
+
const config = {
|
|
583
|
+
level: parentConfig?.level ?? readEnvLevel(),
|
|
584
|
+
ns: parentConfig?.ns ?? readEnvNs(),
|
|
585
|
+
format: parentConfig?.format ?? readEnvFormat()
|
|
586
|
+
};
|
|
587
|
+
let spansEnabled = true;
|
|
588
|
+
const stages = [];
|
|
589
|
+
const outputs = [];
|
|
590
|
+
const branches = [];
|
|
591
|
+
const disposables = [];
|
|
592
|
+
for (const element of elements) {
|
|
593
|
+
if (Array.isArray(element)) {
|
|
594
|
+
const branch = buildPipeline(element, { ...config });
|
|
595
|
+
branches.push(branch);
|
|
596
|
+
disposables.push(() => branch.dispose());
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (element === console || element === "console") {
|
|
600
|
+
outputs.push({
|
|
601
|
+
levelPriority: LOG_LEVEL_PRIORITY[config.level],
|
|
602
|
+
nsFilter: config.ns,
|
|
603
|
+
write: createConsoleSink(config.format)
|
|
604
|
+
});
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (typeof element === "function") {
|
|
608
|
+
stages.push(element);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (isWritable(element)) {
|
|
612
|
+
outputs.push({
|
|
613
|
+
levelPriority: LOG_LEVEL_PRIORITY[config.level],
|
|
614
|
+
nsFilter: config.ns,
|
|
615
|
+
write: createWritableSink(element, config.format)
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (isPojo(element)) {
|
|
620
|
+
const obj = element;
|
|
621
|
+
const keys = Object.keys(obj);
|
|
622
|
+
const hasSinkKey = keys.some((k) => SINK_KEYS.has(k));
|
|
623
|
+
if (keys.some((k) => !VALID_CONFIG_KEYS.has(k) && !SINK_KEYS.has(k))) {
|
|
624
|
+
const unknown = keys.find((k) => !VALID_CONFIG_KEYS.has(k) && !SINK_KEYS.has(k));
|
|
625
|
+
throw new Error(`loggily: unknown config key "${unknown}" in config object. Valid keys: ${[...VALID_CONFIG_KEYS, ...SINK_KEYS].join(", ")}`);
|
|
626
|
+
}
|
|
627
|
+
if (hasSinkKey) {
|
|
628
|
+
if (typeof obj.file === "string") {
|
|
629
|
+
const outputLevel = isValidLogLevel(obj.level) ? obj.level : config.level;
|
|
630
|
+
const outputNs = obj.ns ? parseNsFilter(obj.ns) : config.ns;
|
|
631
|
+
const outputFormat = obj.format ?? config.format;
|
|
632
|
+
const sink = createFileSink(obj.file, outputFormat);
|
|
633
|
+
disposables.push(sink.dispose);
|
|
634
|
+
outputs.push({
|
|
635
|
+
levelPriority: LOG_LEVEL_PRIORITY[outputLevel],
|
|
636
|
+
nsFilter: outputNs,
|
|
637
|
+
write: sink.write,
|
|
638
|
+
dispose: sink.dispose
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (obj.otel !== void 0) throw new Error("loggily: OTEL sink is not yet implemented. See loggily/otel for the planned bridge.");
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (isValidLogLevel(obj.level)) config.level = obj.level;
|
|
645
|
+
if (obj.ns !== void 0) config.ns = parseNsFilter(obj.ns);
|
|
646
|
+
if (obj.format === "console" || obj.format === "json") config.format = obj.format;
|
|
647
|
+
if (obj.spans === true) spansEnabled = true;
|
|
648
|
+
if (obj.spans === false) spansEnabled = false;
|
|
649
|
+
if (obj.idFormat === "simple" || obj.idFormat === "w3c") setIdFormat(obj.idFormat);
|
|
650
|
+
if (typeof obj.sampleRate === "number") setSampleRate(obj.sampleRate);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (element === "stderr" && typeof process !== "undefined") {
|
|
654
|
+
outputs.push({
|
|
655
|
+
levelPriority: LOG_LEVEL_PRIORITY[config.level],
|
|
656
|
+
nsFilter: config.ns,
|
|
657
|
+
write: createWritableSink(process.stderr, config.format)
|
|
658
|
+
});
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
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 }).`);
|
|
662
|
+
}
|
|
663
|
+
const dispatch = (event) => {
|
|
664
|
+
if (event.kind === "span" && !spansEnabled) return;
|
|
665
|
+
let e = event;
|
|
666
|
+
for (const stage of stages) {
|
|
667
|
+
const result = stage(e);
|
|
668
|
+
if (result === null) return;
|
|
669
|
+
if (result !== void 0) e = result;
|
|
670
|
+
}
|
|
671
|
+
for (const output of outputs) {
|
|
672
|
+
if (e.kind === "log" && LOG_LEVEL_PRIORITY[e.level] < output.levelPriority) continue;
|
|
673
|
+
if (output.nsFilter && !output.nsFilter(e.namespace)) continue;
|
|
674
|
+
output.write(e);
|
|
675
|
+
}
|
|
676
|
+
for (const branch of branches) branch.dispatch(e);
|
|
677
|
+
};
|
|
678
|
+
const spanEnabledForNamespace = (namespace) => {
|
|
679
|
+
if (!spansEnabled) return false;
|
|
680
|
+
if (outputs.some((output) => !output.nsFilter || output.nsFilter(namespace))) return true;
|
|
681
|
+
if (branches.some((branch) => branch.spanEnabled(namespace))) return true;
|
|
682
|
+
return stages.length > 0;
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
dispatch,
|
|
686
|
+
spanEnabled: spanEnabledForNamespace,
|
|
687
|
+
level: config.level,
|
|
688
|
+
dispose: () => {
|
|
689
|
+
for (const d of disposables) d();
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function readEnvLevel() {
|
|
694
|
+
const env = getEnv("LOG_LEVEL")?.toLowerCase();
|
|
695
|
+
let level = env === "trace" || env === "debug" || env === "info" || env === "warn" || env === "error" || env === "silent" ? env : "info";
|
|
696
|
+
if (getEnv("DEBUG") && LOG_LEVEL_PRIORITY[level] > LOG_LEVEL_PRIORITY.debug) level = "debug";
|
|
697
|
+
return level;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Namespace-aware level: only bumps to debug if the namespace matches the DEBUG filter.
|
|
701
|
+
* This enables zero-overhead conditional gating — `log.debug?.()` returns undefined
|
|
702
|
+
* for namespaces outside the DEBUG filter, skipping argument evaluation entirely.
|
|
703
|
+
*/
|
|
704
|
+
function readEnvLevelForNamespace(namespace) {
|
|
705
|
+
const env = getEnv("LOG_LEVEL")?.toLowerCase();
|
|
706
|
+
const baseLevel = env === "trace" || env === "debug" || env === "info" || env === "warn" || env === "error" || env === "silent" ? env : "info";
|
|
707
|
+
if (getEnv("DEBUG") && LOG_LEVEL_PRIORITY[baseLevel] > LOG_LEVEL_PRIORITY.debug) {
|
|
708
|
+
const nsFilter = readEnvNs();
|
|
709
|
+
if (nsFilter && nsFilter(namespace)) return "debug";
|
|
710
|
+
return baseLevel;
|
|
711
|
+
}
|
|
712
|
+
return baseLevel;
|
|
713
|
+
}
|
|
714
|
+
function readEnvNs() {
|
|
715
|
+
const debugEnv = getEnv("DEBUG");
|
|
716
|
+
if (!debugEnv) return null;
|
|
717
|
+
return parseNsFilter(debugEnv.split(",").map((s) => s.trim()));
|
|
718
|
+
}
|
|
719
|
+
function readEnvFormat() {
|
|
720
|
+
const envFormat = getEnv("LOG_FORMAT")?.toLowerCase();
|
|
721
|
+
if (envFormat === "json") return "json";
|
|
722
|
+
if (envFormat === "console") return "console";
|
|
723
|
+
if (getEnv("TRACE_FORMAT") === "json") return "json";
|
|
724
|
+
if (getEnv("NODE_ENV") === "production") return "json";
|
|
725
|
+
return "console";
|
|
726
|
+
}
|
|
727
|
+
function readEnvTrace() {
|
|
728
|
+
const traceEnv = getEnv("TRACE");
|
|
729
|
+
if (!traceEnv) return {
|
|
730
|
+
enabled: false,
|
|
731
|
+
filter: null
|
|
732
|
+
};
|
|
733
|
+
if (traceEnv === "1" || traceEnv === "true") return {
|
|
734
|
+
enabled: true,
|
|
735
|
+
filter: null
|
|
736
|
+
};
|
|
737
|
+
const prefixes = traceEnv.split(",").map((s) => s.trim());
|
|
738
|
+
return {
|
|
739
|
+
enabled: true,
|
|
740
|
+
filter: (namespace) => {
|
|
741
|
+
for (const prefix of prefixes) if (matchesPattern(namespace, prefix)) return true;
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/core.ts
|
|
748
|
+
/**
|
|
749
|
+
* loggily v2 — Structured logging with spans
|
|
750
|
+
*
|
|
751
|
+
* One import. Objects configure. Arrays branch. Values write.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* const log = createLogger('myapp')
|
|
755
|
+
* log.info?.('starting')
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* const log = createLogger('myapp', [
|
|
759
|
+
* { level: 'debug', ns: '-sql' },
|
|
760
|
+
* console,
|
|
761
|
+
* { file: '/tmp/app.log', level: 'info', format: 'json' },
|
|
762
|
+
* ])
|
|
763
|
+
* log.info?.('server started', { port: 3000 })
|
|
764
|
+
*/
|
|
765
|
+
const SPAN_ENABLED = Symbol("loggily.spanEnabled");
|
|
766
|
+
function spanIsEnabled(logger, namespace) {
|
|
767
|
+
if (collectSpans) return true;
|
|
768
|
+
const checker = logger[SPAN_ENABLED];
|
|
769
|
+
return checker ? checker(namespace) : true;
|
|
770
|
+
}
|
|
771
|
+
function resetIds() {
|
|
772
|
+
resetIdCounters();
|
|
773
|
+
}
|
|
774
|
+
let _getContextTags = null;
|
|
775
|
+
let _getContextParent = null;
|
|
776
|
+
let _enterContext = null;
|
|
777
|
+
let _exitContext = null;
|
|
778
|
+
/** @internal */
|
|
779
|
+
function _setContextHooks(hooks) {
|
|
780
|
+
_getContextTags = hooks.getContextTags;
|
|
781
|
+
_getContextParent = hooks.getContextParent;
|
|
782
|
+
_enterContext = hooks.enterContext;
|
|
783
|
+
_exitContext = hooks.exitContext;
|
|
784
|
+
}
|
|
785
|
+
/** @internal */
|
|
786
|
+
function _clearContextHooks() {
|
|
787
|
+
_getContextTags = null;
|
|
788
|
+
_getContextParent = null;
|
|
789
|
+
_enterContext = null;
|
|
790
|
+
_exitContext = null;
|
|
791
|
+
}
|
|
792
|
+
function createSpanDataProxy(getFields, attrs) {
|
|
793
|
+
const READONLY_KEYS = new Set([
|
|
794
|
+
"id",
|
|
795
|
+
"traceId",
|
|
796
|
+
"parentId",
|
|
797
|
+
"startTime",
|
|
798
|
+
"endTime",
|
|
799
|
+
"duration"
|
|
800
|
+
]);
|
|
801
|
+
return new Proxy(attrs, {
|
|
802
|
+
get(_target, prop) {
|
|
803
|
+
if (READONLY_KEYS.has(prop)) return getFields()[prop];
|
|
804
|
+
return attrs[prop];
|
|
805
|
+
},
|
|
806
|
+
set(_target, prop, value) {
|
|
807
|
+
if (READONLY_KEYS.has(prop)) return false;
|
|
808
|
+
attrs[prop] = value;
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
const collectedSpans = [];
|
|
814
|
+
let collectSpans = false;
|
|
815
|
+
function startCollecting() {
|
|
816
|
+
collectSpans = true;
|
|
817
|
+
collectedSpans.length = 0;
|
|
818
|
+
}
|
|
819
|
+
function stopCollecting() {
|
|
820
|
+
collectSpans = false;
|
|
821
|
+
return [...collectedSpans];
|
|
822
|
+
}
|
|
823
|
+
function getCollectedSpans() {
|
|
824
|
+
return [...collectedSpans];
|
|
825
|
+
}
|
|
826
|
+
function clearCollectedSpans() {
|
|
827
|
+
collectedSpans.length = 0;
|
|
828
|
+
}
|
|
829
|
+
function resolveMessage(msg) {
|
|
830
|
+
return typeof msg === "function" ? msg() : msg;
|
|
831
|
+
}
|
|
832
|
+
function createLoggerImpl(name, props, pipeline) {
|
|
833
|
+
const emitLog = (level, msgOrError, dataOrMsg, extraData) => {
|
|
834
|
+
let message;
|
|
835
|
+
let data;
|
|
836
|
+
const userArgs = [];
|
|
837
|
+
if (msgOrError instanceof Error) {
|
|
838
|
+
const err = msgOrError;
|
|
839
|
+
const contextTags = _getContextTags?.() ?? {};
|
|
840
|
+
if (typeof dataOrMsg === "string") {
|
|
841
|
+
message = dataOrMsg;
|
|
842
|
+
data = {
|
|
843
|
+
...contextTags,
|
|
844
|
+
...props,
|
|
845
|
+
...extraData,
|
|
846
|
+
error_type: err.name,
|
|
847
|
+
error_message: err.message,
|
|
848
|
+
error_stack: err.stack,
|
|
849
|
+
error_code: err.code,
|
|
850
|
+
error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
|
|
851
|
+
};
|
|
852
|
+
} else {
|
|
853
|
+
message = err.message;
|
|
854
|
+
data = {
|
|
855
|
+
...contextTags,
|
|
856
|
+
...props,
|
|
857
|
+
...dataOrMsg,
|
|
858
|
+
error_type: err.name,
|
|
859
|
+
error_stack: err.stack,
|
|
860
|
+
error_code: err.code,
|
|
861
|
+
error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
userArgs.push(err);
|
|
865
|
+
if (data && Object.keys(data).length > 0) userArgs.push(data);
|
|
866
|
+
} else {
|
|
867
|
+
message = resolveMessage(msgOrError);
|
|
868
|
+
const contextTags = _getContextTags?.();
|
|
869
|
+
data = contextTags && Object.keys(contextTags).length > 0 ? {
|
|
870
|
+
...contextTags,
|
|
871
|
+
...props,
|
|
872
|
+
...dataOrMsg
|
|
873
|
+
} : Object.keys(props).length > 0 || dataOrMsg ? {
|
|
874
|
+
...props,
|
|
875
|
+
...dataOrMsg
|
|
876
|
+
} : void 0;
|
|
877
|
+
if (data && Object.keys(data).length > 0) userArgs.push(data);
|
|
878
|
+
}
|
|
879
|
+
const event = {
|
|
880
|
+
kind: "log",
|
|
881
|
+
time: Date.now(),
|
|
882
|
+
namespace: name,
|
|
883
|
+
level,
|
|
884
|
+
message,
|
|
885
|
+
props: data,
|
|
886
|
+
userArgs
|
|
887
|
+
};
|
|
888
|
+
pipeline.dispatch(event);
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
name,
|
|
892
|
+
props: Object.freeze({ ...props }),
|
|
893
|
+
get level() {
|
|
894
|
+
return pipeline.level;
|
|
895
|
+
},
|
|
896
|
+
[SPAN_ENABLED](namespace) {
|
|
897
|
+
return pipeline.spanEnabled(namespace);
|
|
898
|
+
},
|
|
899
|
+
dispatch(event) {
|
|
900
|
+
pipeline.dispatch(event);
|
|
901
|
+
},
|
|
902
|
+
[Symbol.dispose]() {
|
|
903
|
+
pipeline.dispose();
|
|
904
|
+
},
|
|
905
|
+
trace: (msg, data) => emitLog("trace", msg, data),
|
|
906
|
+
debug: (msg, data) => emitLog("debug", msg, data),
|
|
907
|
+
info: (msg, data) => emitLog("info", msg, data),
|
|
908
|
+
warn: (msg, data) => emitLog("warn", msg, data),
|
|
909
|
+
error: (msgOrError, dataOrMsg, extraData) => emitLog("error", msgOrError, dataOrMsg, extraData),
|
|
910
|
+
logger(namespace, childProps) {
|
|
911
|
+
return this.child(namespace ?? "", childProps);
|
|
912
|
+
},
|
|
913
|
+
span(_namespace, _childProps) {
|
|
914
|
+
throw new Error("loggily: span() requires the withSpans() plugin. Use pipe(baseCreateLogger, withSpans()) or the default createLogger.");
|
|
915
|
+
},
|
|
916
|
+
child(namespaceOrContext, childProps) {
|
|
917
|
+
if (typeof namespaceOrContext === "string") return wrapConditional(createLoggerImpl(namespaceOrContext ? `${name}:${namespaceOrContext}` : name, {
|
|
918
|
+
...props,
|
|
919
|
+
...childProps
|
|
920
|
+
}, pipeline), () => pipeline.level);
|
|
921
|
+
return wrapConditional(createLoggerImpl(name, {
|
|
922
|
+
...props,
|
|
923
|
+
...namespaceOrContext
|
|
924
|
+
}, pipeline), () => pipeline.level);
|
|
925
|
+
},
|
|
926
|
+
end() {}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function wrapConditional(logger, getLevel) {
|
|
930
|
+
return new Proxy(logger, { get(target, prop) {
|
|
931
|
+
if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
|
|
932
|
+
if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[getLevel()]) return;
|
|
933
|
+
}
|
|
934
|
+
if (prop === "span") {
|
|
935
|
+
const val = target[prop];
|
|
936
|
+
if (val === baseSpanStub) return void 0;
|
|
937
|
+
return val;
|
|
938
|
+
}
|
|
939
|
+
return target[prop];
|
|
940
|
+
} });
|
|
941
|
+
}
|
|
942
|
+
const baseSpanStub = function baseSpanStub(_namespace, _childProps) {
|
|
943
|
+
throw new Error("loggily: span() requires the withSpans() plugin. Use pipe(baseCreateLogger, withSpans()) or the default createLogger.");
|
|
944
|
+
};
|
|
945
|
+
/**
|
|
946
|
+
* Plugin: adds span creation capability to loggers.
|
|
947
|
+
* Without this plugin, `.span` is undefined on ConditionalLogger.
|
|
948
|
+
* Included by default in `createLogger`.
|
|
949
|
+
*/
|
|
950
|
+
function withSpans() {
|
|
951
|
+
return (factory, _ctx) => {
|
|
952
|
+
return (name, configOrProps) => {
|
|
953
|
+
return augmentWithSpans(factory(name, configOrProps), null, null, true);
|
|
954
|
+
};
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function augmentWithSpans(logger, parentSpanId, traceId, traceSampled) {
|
|
958
|
+
const spanState = {
|
|
959
|
+
parentSpanId,
|
|
960
|
+
traceId,
|
|
961
|
+
traceSampled
|
|
962
|
+
};
|
|
963
|
+
const spanMethod = spanIsEnabled(logger, logger.name) ? createSpanMethod(logger, spanState) : void 0;
|
|
964
|
+
return new Proxy(logger, { get(target, prop) {
|
|
965
|
+
if (prop === "span") return spanMethod;
|
|
966
|
+
if (prop === "child") return function child(namespaceOrContext, childProps) {
|
|
967
|
+
return augmentWithSpans(target.child(namespaceOrContext, childProps), spanState.parentSpanId, spanState.traceId, spanState.traceSampled);
|
|
968
|
+
};
|
|
969
|
+
if (prop === "logger") return function logger(namespace, childProps) {
|
|
970
|
+
return augmentWithSpans(target.logger(namespace, childProps), spanState.parentSpanId, spanState.traceId, spanState.traceSampled);
|
|
971
|
+
};
|
|
972
|
+
return target[prop];
|
|
973
|
+
} });
|
|
974
|
+
}
|
|
975
|
+
function createSpanMethod(logger, spanState) {
|
|
976
|
+
return (namespace, childProps) => {
|
|
977
|
+
const childName = namespace ? `${logger.name}:${namespace}` : logger.name;
|
|
978
|
+
const resolvedChildProps = typeof childProps === "function" ? childProps() : childProps;
|
|
979
|
+
const mergedProps = {
|
|
980
|
+
...logger.props,
|
|
981
|
+
...resolvedChildProps
|
|
982
|
+
};
|
|
983
|
+
const newSpanId = generateSpanId();
|
|
984
|
+
let resolvedParentId = spanState.parentSpanId;
|
|
985
|
+
let resolvedTraceId = spanState.traceId;
|
|
986
|
+
if (!resolvedParentId && _getContextParent) {
|
|
987
|
+
const ctxParent = _getContextParent();
|
|
988
|
+
if (ctxParent) {
|
|
989
|
+
resolvedParentId = ctxParent.spanId;
|
|
990
|
+
resolvedTraceId = resolvedTraceId || ctxParent.traceId;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const isNewTrace = !resolvedTraceId;
|
|
994
|
+
const finalTraceId = resolvedTraceId || generateTraceId();
|
|
995
|
+
const sampled = isNewTrace ? shouldSample() : spanState.traceSampled;
|
|
996
|
+
const newSpanData = {
|
|
997
|
+
id: newSpanId,
|
|
998
|
+
traceId: finalTraceId,
|
|
999
|
+
parentId: resolvedParentId,
|
|
1000
|
+
startTime: Date.now(),
|
|
1001
|
+
endTime: null,
|
|
1002
|
+
duration: null,
|
|
1003
|
+
attrs: {},
|
|
1004
|
+
laps: []
|
|
1005
|
+
};
|
|
1006
|
+
const spanAugmented = augmentWithSpans(logger.child(namespace ?? "", resolvedChildProps), newSpanId, finalTraceId, sampled);
|
|
1007
|
+
_enterContext?.(newSpanId, finalTraceId, resolvedParentId);
|
|
1008
|
+
const disposeSpan = () => {
|
|
1009
|
+
if (newSpanData.endTime !== null) return;
|
|
1010
|
+
newSpanData.endTime = Date.now();
|
|
1011
|
+
newSpanData.duration = newSpanData.endTime - newSpanData.startTime;
|
|
1012
|
+
if (collectSpans) collectedSpans.push(createSpanDataProxy(() => ({
|
|
1013
|
+
id: newSpanData.id,
|
|
1014
|
+
traceId: newSpanData.traceId,
|
|
1015
|
+
parentId: newSpanData.parentId,
|
|
1016
|
+
startTime: newSpanData.startTime,
|
|
1017
|
+
endTime: newSpanData.endTime,
|
|
1018
|
+
duration: newSpanData.duration
|
|
1019
|
+
}), { ...newSpanData.attrs }));
|
|
1020
|
+
_exitContext?.(newSpanId);
|
|
1021
|
+
if (sampled) {
|
|
1022
|
+
const lapsProp = newSpanData.laps.length > 0 ? Object.fromEntries(newSpanData.laps.map((l) => [l.name, l.deltaMs])) : void 0;
|
|
1023
|
+
const spanEvent = {
|
|
1024
|
+
kind: "span",
|
|
1025
|
+
time: newSpanData.endTime,
|
|
1026
|
+
namespace: childName,
|
|
1027
|
+
name: childName,
|
|
1028
|
+
duration: newSpanData.duration,
|
|
1029
|
+
props: {
|
|
1030
|
+
...mergedProps,
|
|
1031
|
+
...newSpanData.attrs,
|
|
1032
|
+
...lapsProp ? { laps: lapsProp } : {}
|
|
1033
|
+
},
|
|
1034
|
+
spanId: newSpanData.id,
|
|
1035
|
+
traceId: newSpanData.traceId,
|
|
1036
|
+
parentId: newSpanData.parentId
|
|
1037
|
+
};
|
|
1038
|
+
logger.dispatch(spanEvent);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
const lapImpl = (name) => {
|
|
1042
|
+
const elapsedMs = Date.now() - newSpanData.startTime;
|
|
1043
|
+
const lastLap = newSpanData.laps[newSpanData.laps.length - 1];
|
|
1044
|
+
const deltaMs = lastLap ? elapsedMs - lastLap.elapsedMs : elapsedMs;
|
|
1045
|
+
newSpanData.laps.push({
|
|
1046
|
+
name,
|
|
1047
|
+
elapsedMs,
|
|
1048
|
+
deltaMs
|
|
1049
|
+
});
|
|
1050
|
+
};
|
|
1051
|
+
const spanDataProxy = createSpanDataProxy(() => ({
|
|
1052
|
+
id: newSpanData.id,
|
|
1053
|
+
traceId: newSpanData.traceId,
|
|
1054
|
+
parentId: newSpanData.parentId,
|
|
1055
|
+
startTime: newSpanData.startTime,
|
|
1056
|
+
endTime: newSpanData.endTime,
|
|
1057
|
+
duration: newSpanData.endTime !== null ? newSpanData.endTime - newSpanData.startTime : Date.now() - newSpanData.startTime
|
|
1058
|
+
}), newSpanData.attrs);
|
|
1059
|
+
let currentDispose = disposeSpan;
|
|
1060
|
+
return new Proxy(spanAugmented, {
|
|
1061
|
+
get(target, prop) {
|
|
1062
|
+
if (prop === "spanData") return spanDataProxy;
|
|
1063
|
+
if (prop === Symbol.dispose) return currentDispose;
|
|
1064
|
+
if (prop === "end") return () => {
|
|
1065
|
+
if (newSpanData.endTime === null) currentDispose();
|
|
1066
|
+
};
|
|
1067
|
+
if (prop === "lap") return lapImpl;
|
|
1068
|
+
if (prop === "name") return childName;
|
|
1069
|
+
if (prop === "props") return Object.freeze({ ...mergedProps });
|
|
1070
|
+
return target[prop];
|
|
1071
|
+
},
|
|
1072
|
+
set(_target, prop, value) {
|
|
1073
|
+
if (prop === Symbol.dispose) {
|
|
1074
|
+
currentDispose = value;
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Base createLogger — requires a config array.
|
|
1084
|
+
* Use the default `createLogger` export (with `withEnvDefaults`) for zero-config.
|
|
1085
|
+
*
|
|
1086
|
+
* Note: loggers from baseCreateLogger do NOT have `.span()` capability.
|
|
1087
|
+
* Use `pipe(baseCreateLogger, withSpans())` or the default `createLogger` for spans.
|
|
1088
|
+
*/
|
|
1089
|
+
function baseCreateLogger(name, configOrProps) {
|
|
1090
|
+
let pipeline;
|
|
1091
|
+
let props = {};
|
|
1092
|
+
if (Array.isArray(configOrProps)) pipeline = buildPipeline(configOrProps);
|
|
1093
|
+
else if (configOrProps && typeof configOrProps === "object") {
|
|
1094
|
+
props = configOrProps;
|
|
1095
|
+
pipeline = buildPipeline(["console"]);
|
|
1096
|
+
} else pipeline = buildPipeline(["console"]);
|
|
1097
|
+
const logger = createLoggerImpl(name, props, pipeline);
|
|
1098
|
+
logger.span = baseSpanStub;
|
|
1099
|
+
return wrapConditional(logger, () => pipeline.level);
|
|
1100
|
+
}
|
|
1101
|
+
function pipe(base, ...plugins) {
|
|
1102
|
+
const ctx = {};
|
|
1103
|
+
return plugins.reduce((factory, plugin) => plugin(factory, ctx), base);
|
|
1104
|
+
}
|
|
1105
|
+
const _env = (typeof process !== "undefined" ? process : void 0)?.env ?? {};
|
|
1106
|
+
function currentLevel() {
|
|
1107
|
+
return readEnvLevel();
|
|
1108
|
+
}
|
|
1109
|
+
function currentNs() {
|
|
1110
|
+
return readEnvNs();
|
|
1111
|
+
}
|
|
1112
|
+
function currentFormat() {
|
|
1113
|
+
return readEnvFormat();
|
|
1114
|
+
}
|
|
1115
|
+
function currentTrace() {
|
|
1116
|
+
return readEnvTrace();
|
|
1117
|
+
}
|
|
1118
|
+
const _writers = [];
|
|
1119
|
+
let _suppressConsole = false;
|
|
1120
|
+
let _logFileWriterFactory = null;
|
|
1121
|
+
/** @internal */
|
|
1122
|
+
function _setLogFileWriterFactory(factory) {
|
|
1123
|
+
_logFileWriterFactory = factory;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Plugin: read defaults from environment variables (LOG_LEVEL, DEBUG, LOG_FORMAT, TRACE, LOG_FILE).
|
|
1127
|
+
* Included by default. Omit to disable env-var behavior entirely.
|
|
1128
|
+
*
|
|
1129
|
+
* When no config array is given, provides console output + env-var-based config.
|
|
1130
|
+
* When a config array IS given, env vars are already used as defaults by buildPipeline.
|
|
1131
|
+
* Legacy setters (setLogLevel, addWriter, etc.) affect loggers created without explicit config.
|
|
1132
|
+
*/
|
|
1133
|
+
function withEnvDefaults() {
|
|
1134
|
+
return (factory, _ctx) => (name, configOrProps) => {
|
|
1135
|
+
const envIdFormat = _env.TRACE_ID_FORMAT?.toLowerCase();
|
|
1136
|
+
if (envIdFormat === "simple" || envIdFormat === "w3c") setIdFormat(envIdFormat);
|
|
1137
|
+
const envSampleRate = _env.TRACE_SAMPLE_RATE;
|
|
1138
|
+
if (envSampleRate !== void 0) {
|
|
1139
|
+
const rate = Number.parseFloat(envSampleRate);
|
|
1140
|
+
if (!Number.isNaN(rate) && rate >= 0 && rate <= 1) setSampleRate(rate);
|
|
1141
|
+
}
|
|
1142
|
+
if (Array.isArray(configOrProps)) return factory(name, configOrProps);
|
|
1143
|
+
const envPipeline = createEnvPipeline();
|
|
1144
|
+
const envStage = (event) => {
|
|
1145
|
+
envPipeline.dispatch(event);
|
|
1146
|
+
return null;
|
|
1147
|
+
};
|
|
1148
|
+
if (configOrProps && typeof configOrProps === "object") return applyNamespaceGating(factory(name, [{ level: "trace" }, envStage]).child(configOrProps));
|
|
1149
|
+
return applyNamespaceGating(factory(name, [{ level: "trace" }, envStage]));
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Wrap a logger so that conditional method gating is namespace-aware.
|
|
1154
|
+
* When DEBUG=myapp:db, only loggers whose namespace matches get debug enabled.
|
|
1155
|
+
* Without this, all loggers get debug because readEnvLevel() bumps globally.
|
|
1156
|
+
*/
|
|
1157
|
+
function applyNamespaceGating(logger) {
|
|
1158
|
+
return new Proxy(logger, { get(target, prop) {
|
|
1159
|
+
if (prop === SPAN_ENABLED) return (namespace) => {
|
|
1160
|
+
if (collectSpans) return true;
|
|
1161
|
+
const trace = currentTrace();
|
|
1162
|
+
if (!trace.enabled) return false;
|
|
1163
|
+
if (trace.filter && !trace.filter(namespace)) return false;
|
|
1164
|
+
const ns = currentNs();
|
|
1165
|
+
if (ns && !ns(namespace)) return false;
|
|
1166
|
+
return true;
|
|
1167
|
+
};
|
|
1168
|
+
if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
|
|
1169
|
+
const nsLevel = readEnvLevelForNamespace(target.name);
|
|
1170
|
+
if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[nsLevel]) return;
|
|
1171
|
+
}
|
|
1172
|
+
return target[prop];
|
|
1173
|
+
} });
|
|
1174
|
+
}
|
|
1175
|
+
function createEnvPipeline() {
|
|
1176
|
+
const disposables = [];
|
|
1177
|
+
const logFile = _env.LOG_FILE;
|
|
1178
|
+
let fileSink = null;
|
|
1179
|
+
if (logFile && _logFileWriterFactory) {
|
|
1180
|
+
const writer = _logFileWriterFactory(logFile);
|
|
1181
|
+
fileSink = (event) => {
|
|
1182
|
+
const fmt = currentFormat() === "json" ? formatJSONEvent : formatConsoleEvent;
|
|
1183
|
+
writer.write(fmt(event));
|
|
1184
|
+
};
|
|
1185
|
+
disposables.push(() => writer.close());
|
|
1186
|
+
}
|
|
1187
|
+
const dispatch = (event) => {
|
|
1188
|
+
if (event.kind === "log" && LOG_LEVEL_PRIORITY[event.level] < LOG_LEVEL_PRIORITY[currentLevel()]) return;
|
|
1189
|
+
if (event.kind === "span") {
|
|
1190
|
+
const trace = currentTrace();
|
|
1191
|
+
if (!trace.enabled) return;
|
|
1192
|
+
if (trace.filter && !trace.filter(event.namespace)) return;
|
|
1193
|
+
}
|
|
1194
|
+
const ns = currentNs();
|
|
1195
|
+
if (ns && !ns(event.namespace)) return;
|
|
1196
|
+
const format = currentFormat();
|
|
1197
|
+
const text = (format === "json" ? formatJSONEvent : formatConsoleEvent)(event);
|
|
1198
|
+
const lvl = event.kind === "log" ? event.level : "span";
|
|
1199
|
+
for (const w of _writers) w(text, lvl, event.namespace, event);
|
|
1200
|
+
if (!_suppressConsole) createConsoleSink$1(format)(event);
|
|
1201
|
+
fileSink?.(event);
|
|
1202
|
+
};
|
|
1203
|
+
return {
|
|
1204
|
+
dispatch,
|
|
1205
|
+
spanEnabled(namespace) {
|
|
1206
|
+
const trace = currentTrace();
|
|
1207
|
+
if (!trace.enabled) return false;
|
|
1208
|
+
if (trace.filter && !trace.filter(namespace)) return false;
|
|
1209
|
+
const ns = currentNs();
|
|
1210
|
+
if (ns && !ns(namespace)) return false;
|
|
1211
|
+
return true;
|
|
1212
|
+
},
|
|
1213
|
+
get level() {
|
|
1214
|
+
return currentLevel();
|
|
1215
|
+
},
|
|
1216
|
+
dispose: () => {
|
|
1217
|
+
for (const d of disposables) d();
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Plugin: when `{ metrics: true }` appears in the config array, automatically
|
|
1223
|
+
* creates a MetricsCollector and applies withMetrics to the logger.
|
|
1224
|
+
* The collector is accessible via `logger.metrics`.
|
|
1225
|
+
*/
|
|
1226
|
+
function withConfigMetrics() {
|
|
1227
|
+
return (factory, _ctx) => {
|
|
1228
|
+
return (name, configOrProps) => {
|
|
1229
|
+
const logger = factory(name, configOrProps);
|
|
1230
|
+
if (!Array.isArray(configOrProps)) return logger;
|
|
1231
|
+
if (!configOrProps.some((el) => typeof el === "object" && el !== null && !Array.isArray(el) && "metrics" in el && el.metrics === true)) return logger;
|
|
1232
|
+
return withMetrics(createMetricsCollector())(logger);
|
|
1233
|
+
};
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
/** Default createLogger — includes withEnvDefaults + withSpans + withConfigMetrics. */
|
|
1237
|
+
const createLogger = pipe(baseCreateLogger, withEnvDefaults(), withSpans(), withConfigMetrics());
|
|
1238
|
+
/** Test helper — all levels, console output. */
|
|
1239
|
+
function createTestLogger(name) {
|
|
1240
|
+
return pipe(baseCreateLogger, withSpans())(name, [{ level: "trace" }, "console"]);
|
|
1241
|
+
}
|
|
1242
|
+
/** @deprecated Use config array */
|
|
1243
|
+
function setLogLevel(level) {
|
|
1244
|
+
_env.LOG_LEVEL = level;
|
|
1245
|
+
}
|
|
1246
|
+
function getLogLevel() {
|
|
1247
|
+
return currentLevel();
|
|
1248
|
+
}
|
|
1249
|
+
function enableSpans() {
|
|
1250
|
+
_env.TRACE = "1";
|
|
1251
|
+
}
|
|
1252
|
+
function disableSpans() {
|
|
1253
|
+
delete _env.TRACE;
|
|
1254
|
+
}
|
|
1255
|
+
function spansAreEnabled() {
|
|
1256
|
+
return !!_env.TRACE;
|
|
1257
|
+
}
|
|
1258
|
+
function setTraceFilter(namespaces) {
|
|
1259
|
+
if (!namespaces || namespaces.length === 0) delete _env.TRACE;
|
|
1260
|
+
else _env.TRACE = namespaces.join(",");
|
|
1261
|
+
}
|
|
1262
|
+
function getTraceFilter() {
|
|
1263
|
+
return _env.TRACE ? _env.TRACE.split(",") : null;
|
|
1264
|
+
}
|
|
1265
|
+
function setDebugFilter(namespaces) {
|
|
1266
|
+
if (!namespaces || namespaces.length === 0) delete _env.DEBUG;
|
|
1267
|
+
else _env.DEBUG = namespaces.join(",");
|
|
1268
|
+
}
|
|
1269
|
+
function getDebugFilter() {
|
|
1270
|
+
return _env.DEBUG ? _env.DEBUG.split(",") : null;
|
|
1271
|
+
}
|
|
1272
|
+
function setLogFormat(format) {
|
|
1273
|
+
_env.LOG_FORMAT = format;
|
|
1274
|
+
}
|
|
1275
|
+
function getLogFormat() {
|
|
1276
|
+
return currentFormat();
|
|
1277
|
+
}
|
|
1278
|
+
function setSuppressConsole(value) {
|
|
1279
|
+
_suppressConsole = value;
|
|
1280
|
+
}
|
|
1281
|
+
function setOutputMode(_mode) {
|
|
1282
|
+
throw new Error("loggily: setOutputMode() is removed in v2. Use config arrays: omit console from array for writers-only, use \"stderr\" for stderr mode.");
|
|
1283
|
+
}
|
|
1284
|
+
function getOutputMode() {
|
|
1285
|
+
return "console";
|
|
1286
|
+
}
|
|
1287
|
+
function addWriter(arg1, arg2) {
|
|
1288
|
+
if (typeof arg1 === "function") return _addRawWriter(arg1);
|
|
1289
|
+
if (!arg2) throw new Error("addWriter: writer fn required when first argument is a config object");
|
|
1290
|
+
const config = arg1;
|
|
1291
|
+
const matches = config.ns ? parseNsFilter(config.ns) : null;
|
|
1292
|
+
const minPriority = config.level ? LOG_LEVEL_PRIORITY[config.level] : null;
|
|
1293
|
+
return _addRawWriter((formatted, level, namespace, event) => {
|
|
1294
|
+
if (matches && !matches(namespace)) return;
|
|
1295
|
+
if (minPriority !== null) {
|
|
1296
|
+
if ((LOG_LEVEL_PRIORITY[level] ?? LOG_LEVEL_PRIORITY.info) < minPriority) return;
|
|
1297
|
+
}
|
|
1298
|
+
arg2(formatted, level, namespace, event);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
function _addRawWriter(writer) {
|
|
1302
|
+
_writers.push(writer);
|
|
1303
|
+
return () => {
|
|
1304
|
+
const i = _writers.indexOf(writer);
|
|
1305
|
+
if (i !== -1) _writers.splice(i, 1);
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function writeSpan(namespace, duration, attrs) {
|
|
1309
|
+
createEnvPipeline().dispatch({
|
|
1310
|
+
kind: "span",
|
|
1311
|
+
time: Date.now(),
|
|
1312
|
+
namespace,
|
|
1313
|
+
name: namespace,
|
|
1314
|
+
duration,
|
|
1315
|
+
props: attrs,
|
|
1316
|
+
spanId: attrs.span_id ?? "",
|
|
1317
|
+
traceId: attrs.trace_id ?? "",
|
|
1318
|
+
parentId: attrs.parent_id ?? null
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
//#endregion
|
|
1322
|
+
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 };
|
|
1323
|
+
|
|
1324
|
+
//# sourceMappingURL=core-Byd3DY71.mjs.map
|