loggily 0.7.0 → 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.
- package/README.md +145 -54
- package/dist/context.mjs +1 -1
- package/dist/{core-Du3sIje6.mjs → core-B3pox577.mjs} +521 -347
- package/dist/core-B3pox577.mjs.map +1 -0
- package/dist/core-Dm2PQUoS.d.mts +191 -0
- package/dist/core-Dm2PQUoS.d.mts.map +1 -0
- package/dist/{index-Co4jC3mx.d.mts → file-writer-DtaY8Njt.d.mts} +60 -49
- package/dist/file-writer-DtaY8Njt.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-Cl9-wCmt.d.mts +73 -0
- package/dist/pipeline-Cl9-wCmt.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 +22 -3
- package/dist/core-DAFH-huv.d.mts +0 -199
- package/dist/core-DAFH-huv.d.mts.map +0 -1
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMetricsCollector, withMetrics } from "./metrics.mjs";
|
|
1
2
|
import { closeSync, openSync, writeSync } from "node:fs";
|
|
2
3
|
//#region src/colors.ts
|
|
3
4
|
/**
|
|
@@ -90,6 +91,110 @@ function createFileWriter(filePath, options = {}) {
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
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
|
|
93
198
|
//#region src/pipeline.ts
|
|
94
199
|
const LOG_LEVEL_PRIORITY = {
|
|
95
200
|
trace: 0,
|
|
@@ -107,16 +212,36 @@ function writeStderr(text) {
|
|
|
107
212
|
if (_process?.stderr?.write) _process.stderr.write(text + "\n");
|
|
108
213
|
else console.error(text);
|
|
109
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
|
+
}
|
|
110
230
|
function safeStringify(value) {
|
|
111
231
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
112
232
|
return JSON.stringify(value, (_key, val) => {
|
|
113
233
|
if (typeof val === "bigint") return val.toString();
|
|
114
234
|
if (typeof val === "symbol") return val.toString();
|
|
115
|
-
if (val instanceof Error)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
245
|
if (typeof val === "object" && val !== null) {
|
|
121
246
|
if (seen.has(val)) return "[Circular]";
|
|
122
247
|
seen.add(val);
|
|
@@ -177,6 +302,10 @@ function formatJSONEvent(event) {
|
|
|
177
302
|
}
|
|
178
303
|
function matchesPattern(namespace, pattern) {
|
|
179
304
|
if (pattern === "*") return true;
|
|
305
|
+
if (pattern.endsWith(":*")) {
|
|
306
|
+
const prefix = pattern.slice(0, -2);
|
|
307
|
+
return namespace === prefix || namespace.startsWith(prefix + ":");
|
|
308
|
+
}
|
|
180
309
|
return namespace === pattern || namespace.startsWith(pattern + ":");
|
|
181
310
|
}
|
|
182
311
|
function parseNsFilter(ns) {
|
|
@@ -194,30 +323,39 @@ function parseNsFilter(ns) {
|
|
|
194
323
|
return true;
|
|
195
324
|
};
|
|
196
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
|
+
}
|
|
197
356
|
function createConsoleSink(format) {
|
|
198
357
|
const formatter = format === "json" ? formatJSONEvent : formatConsoleEvent;
|
|
199
|
-
return (event) =>
|
|
200
|
-
const text = formatter(event);
|
|
201
|
-
if (event.kind === "span") {
|
|
202
|
-
writeStderr(text);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
switch (event.level) {
|
|
206
|
-
case "trace":
|
|
207
|
-
case "debug":
|
|
208
|
-
console.debug(text);
|
|
209
|
-
break;
|
|
210
|
-
case "info":
|
|
211
|
-
console.info(text);
|
|
212
|
-
break;
|
|
213
|
-
case "warn":
|
|
214
|
-
console.warn(text);
|
|
215
|
-
break;
|
|
216
|
-
case "error":
|
|
217
|
-
console.error(text);
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
358
|
+
return (event) => writeToConsole(formatter(event), event);
|
|
221
359
|
}
|
|
222
360
|
function createFileSink(path, format) {
|
|
223
361
|
const writer = createFileWriter(path);
|
|
@@ -227,14 +365,24 @@ function createFileSink(path, format) {
|
|
|
227
365
|
dispose: () => writer.close()
|
|
228
366
|
};
|
|
229
367
|
}
|
|
368
|
+
function isNodeStream(obj) {
|
|
369
|
+
return typeof obj === "object" && obj !== null && ("_write" in obj || "writable" in obj || "fd" in obj);
|
|
370
|
+
}
|
|
230
371
|
function createWritableSink(writable, format) {
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
233
377
|
}
|
|
234
378
|
const VALID_CONFIG_KEYS = new Set([
|
|
235
379
|
"level",
|
|
236
380
|
"ns",
|
|
237
|
-
"format"
|
|
381
|
+
"format",
|
|
382
|
+
"spans",
|
|
383
|
+
"metrics",
|
|
384
|
+
"idFormat",
|
|
385
|
+
"sampleRate"
|
|
238
386
|
]);
|
|
239
387
|
const SINK_KEYS = new Set(["file", "otel"]);
|
|
240
388
|
function isPojo(obj) {
|
|
@@ -243,7 +391,7 @@ function isPojo(obj) {
|
|
|
243
391
|
return proto === Object.prototype || proto === null;
|
|
244
392
|
}
|
|
245
393
|
function isWritable(obj) {
|
|
246
|
-
return typeof obj === "object" && obj !== null && "write" in obj && typeof obj.write === "function"
|
|
394
|
+
return typeof obj === "object" && obj !== null && "write" in obj && typeof obj.write === "function";
|
|
247
395
|
}
|
|
248
396
|
function isValidLogLevel(val) {
|
|
249
397
|
return typeof val === "string" && val in LOG_LEVEL_PRIORITY;
|
|
@@ -254,6 +402,7 @@ function buildPipeline(elements, parentConfig) {
|
|
|
254
402
|
ns: parentConfig?.ns ?? readEnvNs(),
|
|
255
403
|
format: parentConfig?.format ?? readEnvFormat()
|
|
256
404
|
};
|
|
405
|
+
let spansEnabled = true;
|
|
257
406
|
const stages = [];
|
|
258
407
|
const outputs = [];
|
|
259
408
|
const branches = [];
|
|
@@ -265,11 +414,7 @@ function buildPipeline(elements, parentConfig) {
|
|
|
265
414
|
disposables.push(() => branch.dispose());
|
|
266
415
|
continue;
|
|
267
416
|
}
|
|
268
|
-
if (
|
|
269
|
-
stages.push(element);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (element === console) {
|
|
417
|
+
if (element === console || element === "console") {
|
|
273
418
|
outputs.push({
|
|
274
419
|
levelPriority: LOG_LEVEL_PRIORITY[config.level],
|
|
275
420
|
nsFilter: config.ns,
|
|
@@ -277,6 +422,10 @@ function buildPipeline(elements, parentConfig) {
|
|
|
277
422
|
});
|
|
278
423
|
continue;
|
|
279
424
|
}
|
|
425
|
+
if (typeof element === "function") {
|
|
426
|
+
stages.push(element);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
280
429
|
if (isWritable(element)) {
|
|
281
430
|
outputs.push({
|
|
282
431
|
levelPriority: LOG_LEVEL_PRIORITY[config.level],
|
|
@@ -307,16 +456,30 @@ function buildPipeline(elements, parentConfig) {
|
|
|
307
456
|
dispose: sink.dispose
|
|
308
457
|
});
|
|
309
458
|
}
|
|
459
|
+
if (obj.otel !== void 0) throw new Error("loggily: OTEL sink is not yet implemented. See loggily/otel for the planned bridge.");
|
|
310
460
|
continue;
|
|
311
461
|
}
|
|
312
462
|
if (isValidLogLevel(obj.level)) config.level = obj.level;
|
|
313
463
|
if (obj.ns !== void 0) config.ns = parseNsFilter(obj.ns);
|
|
314
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
|
+
});
|
|
315
477
|
continue;
|
|
316
478
|
}
|
|
317
|
-
throw new Error(`loggily: unsupported config element of type "${typeof element}". Config arrays accept: objects (config), arrays (branches), functions (stages), console, or writables ({ write }).`);
|
|
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 }).`);
|
|
318
480
|
}
|
|
319
481
|
const dispatch = (event) => {
|
|
482
|
+
if (event.kind === "span" && !spansEnabled) return;
|
|
320
483
|
let e = event;
|
|
321
484
|
for (const stage of stages) {
|
|
322
485
|
const result = stage(e);
|
|
@@ -338,76 +501,27 @@ function buildPipeline(elements, parentConfig) {
|
|
|
338
501
|
}
|
|
339
502
|
};
|
|
340
503
|
}
|
|
341
|
-
const runtimeState = {
|
|
342
|
-
suppressConsole: false,
|
|
343
|
-
writers: []
|
|
344
|
-
};
|
|
345
|
-
function defaultPipeline() {
|
|
346
|
-
const rt = runtimeState;
|
|
347
|
-
const disposables = [];
|
|
348
|
-
let fileSink = null;
|
|
349
|
-
const logFile = getEnv("LOG_FILE");
|
|
350
|
-
if (logFile) {
|
|
351
|
-
const sink = createFileSink(logFile, "json");
|
|
352
|
-
fileSink = sink.write;
|
|
353
|
-
disposables.push(sink.dispose);
|
|
354
|
-
}
|
|
355
|
-
const dispatch = (event) => {
|
|
356
|
-
const currentLevel = readEnvLevel();
|
|
357
|
-
if (event.kind === "log") {
|
|
358
|
-
if (LOG_LEVEL_PRIORITY[event.level] < LOG_LEVEL_PRIORITY[currentLevel]) return;
|
|
359
|
-
} else if (event.kind === "span") {
|
|
360
|
-
const trace = readEnvTrace();
|
|
361
|
-
if (!trace.enabled) return;
|
|
362
|
-
if (trace.filter && !trace.filter(event.namespace)) return;
|
|
363
|
-
}
|
|
364
|
-
const currentNs = readEnvNs();
|
|
365
|
-
if (currentNs && !currentNs(event.namespace)) return;
|
|
366
|
-
const formatter = readEnvFormat() === "json" || getEnv("NODE_ENV") === "production" || getEnv("TRACE_FORMAT") === "json" ? formatJSONEvent : formatConsoleEvent;
|
|
367
|
-
if (rt.writers.length > 0) {
|
|
368
|
-
const formatted = formatter(event);
|
|
369
|
-
const lvl = event.kind === "log" ? event.level : "span";
|
|
370
|
-
for (const w of rt.writers) w(formatted, lvl);
|
|
371
|
-
}
|
|
372
|
-
if (rt.suppressConsole) {
|
|
373
|
-
fileSink?.(event);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const text = formatter(event);
|
|
377
|
-
if (event.kind === "span") writeStderr(text);
|
|
378
|
-
else switch (event.level) {
|
|
379
|
-
case "trace":
|
|
380
|
-
case "debug":
|
|
381
|
-
console.debug(text);
|
|
382
|
-
break;
|
|
383
|
-
case "info":
|
|
384
|
-
console.info(text);
|
|
385
|
-
break;
|
|
386
|
-
case "warn":
|
|
387
|
-
console.warn(text);
|
|
388
|
-
break;
|
|
389
|
-
case "error":
|
|
390
|
-
console.error(text);
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
fileSink?.(event);
|
|
394
|
-
};
|
|
395
|
-
return {
|
|
396
|
-
dispatch,
|
|
397
|
-
get level() {
|
|
398
|
-
return readEnvLevel();
|
|
399
|
-
},
|
|
400
|
-
dispose: () => {
|
|
401
|
-
for (const d of disposables) d();
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
504
|
function readEnvLevel() {
|
|
406
505
|
const env = getEnv("LOG_LEVEL")?.toLowerCase();
|
|
407
506
|
let level = env === "trace" || env === "debug" || env === "info" || env === "warn" || env === "error" || env === "silent" ? env : "info";
|
|
408
507
|
if (getEnv("DEBUG") && LOG_LEVEL_PRIORITY[level] > LOG_LEVEL_PRIORITY.debug) level = "debug";
|
|
409
508
|
return level;
|
|
410
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
|
+
}
|
|
411
525
|
function readEnvNs() {
|
|
412
526
|
const debugEnv = getEnv("DEBUG");
|
|
413
527
|
if (!debugEnv) return null;
|
|
@@ -441,99 +555,6 @@ function readEnvTrace() {
|
|
|
441
555
|
};
|
|
442
556
|
}
|
|
443
557
|
//#endregion
|
|
444
|
-
//#region src/tracing.ts
|
|
445
|
-
let currentIdFormat = "simple";
|
|
446
|
-
/**
|
|
447
|
-
* Set the ID format for new spans and traces.
|
|
448
|
-
* - "simple": sp_1, sp_2, tr_1, tr_2 (default, lightweight)
|
|
449
|
-
* - "w3c": 32-char hex trace ID, 16-char hex span ID (W3C Trace Context compatible)
|
|
450
|
-
*/
|
|
451
|
-
function setIdFormat(format) {
|
|
452
|
-
currentIdFormat = format;
|
|
453
|
-
}
|
|
454
|
-
/** Get the current ID format */
|
|
455
|
-
function getIdFormat() {
|
|
456
|
-
return currentIdFormat;
|
|
457
|
-
}
|
|
458
|
-
let simpleSpanCounter = 0;
|
|
459
|
-
let simpleTraceCounter = 0;
|
|
460
|
-
/** Generate a hex string of the given byte length using crypto.randomUUID */
|
|
461
|
-
function randomHex(bytes) {
|
|
462
|
-
return crypto.randomUUID().replace(/-/g, "").slice(0, bytes * 2);
|
|
463
|
-
}
|
|
464
|
-
/** Generate a span ID according to the current format */
|
|
465
|
-
function generateSpanId() {
|
|
466
|
-
if (currentIdFormat === "w3c") return randomHex(8);
|
|
467
|
-
return `sp_${(++simpleSpanCounter).toString(36)}`;
|
|
468
|
-
}
|
|
469
|
-
/** Generate a trace ID according to the current format */
|
|
470
|
-
function generateTraceId() {
|
|
471
|
-
if (currentIdFormat === "w3c") return randomHex(16);
|
|
472
|
-
return `tr_${(++simpleTraceCounter).toString(36)}`;
|
|
473
|
-
}
|
|
474
|
-
/** Reset ID counters (for testing) */
|
|
475
|
-
function resetIdCounters() {
|
|
476
|
-
simpleSpanCounter = 0;
|
|
477
|
-
simpleTraceCounter = 0;
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Format a W3C traceparent header from span data.
|
|
481
|
-
*
|
|
482
|
-
* Format: `{version}-{trace-id}-{span-id}-{trace-flags}`
|
|
483
|
-
* - version: "00" (current W3C spec version)
|
|
484
|
-
* - trace-id: 32 hex chars (128 bits)
|
|
485
|
-
* - span-id: 16 hex chars (64 bits)
|
|
486
|
-
* - trace-flags: "01" (sampled) or "00" (not sampled)
|
|
487
|
-
*
|
|
488
|
-
* Works with both simple and W3C ID formats. Simple IDs are zero-padded to spec length.
|
|
489
|
-
*
|
|
490
|
-
* @param spanData - Span data with id and traceId
|
|
491
|
-
* @param options - Optional settings (sampled flag). Defaults to sampled=true.
|
|
492
|
-
* @returns W3C traceparent header string
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* ```typescript
|
|
496
|
-
* const span = log.span("http-request")
|
|
497
|
-
* const header = traceparent(span.spanData)
|
|
498
|
-
* // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
|
|
499
|
-
* fetch(url, { headers: { traceparent: header } })
|
|
500
|
-
* ```
|
|
501
|
-
*/
|
|
502
|
-
function traceparent(spanData, options) {
|
|
503
|
-
return `00-${padHex(spanData.traceId, 32)}-${padHex(spanData.id, 16)}-${options?.sampled ?? true ? "01" : "00"}`;
|
|
504
|
-
}
|
|
505
|
-
/** Pad or hash an ID to the specified hex length */
|
|
506
|
-
function padHex(id, length) {
|
|
507
|
-
if (id.length === length && /^[0-9a-f]+$/.test(id)) return id;
|
|
508
|
-
let hex = "";
|
|
509
|
-
for (let i = 0; i < id.length; i++) hex += id.charCodeAt(i).toString(16).padStart(2, "0");
|
|
510
|
-
return hex.padStart(length, "0").slice(-length);
|
|
511
|
-
}
|
|
512
|
-
let sampleRate = 1;
|
|
513
|
-
/**
|
|
514
|
-
* Set the head-based sampling rate for new traces.
|
|
515
|
-
* Applied at trace creation — all spans within a sampled trace are kept.
|
|
516
|
-
*
|
|
517
|
-
* @param rate - Sampling rate from 0.0 (sample nothing) to 1.0 (sample everything, default)
|
|
518
|
-
*/
|
|
519
|
-
function setSampleRate(rate) {
|
|
520
|
-
if (rate < 0 || rate > 1) throw new Error(`Sample rate must be between 0.0 and 1.0, got ${rate}`);
|
|
521
|
-
sampleRate = rate;
|
|
522
|
-
}
|
|
523
|
-
/** Get the current sampling rate */
|
|
524
|
-
function getSampleRate() {
|
|
525
|
-
return sampleRate;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Determine whether a new trace should be sampled.
|
|
529
|
-
* Called at trace creation time (head-based sampling).
|
|
530
|
-
*/
|
|
531
|
-
function shouldSample() {
|
|
532
|
-
if (sampleRate >= 1) return true;
|
|
533
|
-
if (sampleRate <= 0) return false;
|
|
534
|
-
return Math.random() < sampleRate;
|
|
535
|
-
}
|
|
536
|
-
//#endregion
|
|
537
558
|
//#region src/core.ts
|
|
538
559
|
/**
|
|
539
560
|
* loggily v2 — Structured logging with spans
|
|
@@ -552,11 +573,6 @@ function shouldSample() {
|
|
|
552
573
|
* ])
|
|
553
574
|
* log.info?.('server started', { port: 3000 })
|
|
554
575
|
*/
|
|
555
|
-
/** @internal */
|
|
556
|
-
let _ambientRecorder = null;
|
|
557
|
-
function _setAmbientRecorder(recorder) {
|
|
558
|
-
_ambientRecorder = recorder;
|
|
559
|
-
}
|
|
560
576
|
function resetIds() {
|
|
561
577
|
resetIdCounters();
|
|
562
578
|
}
|
|
@@ -618,30 +634,35 @@ function clearCollectedSpans() {
|
|
|
618
634
|
function resolveMessage(msg) {
|
|
619
635
|
return typeof msg === "function" ? msg() : msg;
|
|
620
636
|
}
|
|
621
|
-
function createLoggerImpl(name, props, pipeline
|
|
637
|
+
function createLoggerImpl(name, props, pipeline) {
|
|
622
638
|
const emitLog = (level, msgOrError, dataOrMsg, extraData) => {
|
|
623
639
|
let message;
|
|
624
640
|
let data;
|
|
625
641
|
if (msgOrError instanceof Error) {
|
|
626
642
|
const err = msgOrError;
|
|
643
|
+
const contextTags = _getContextTags?.() ?? {};
|
|
627
644
|
if (typeof dataOrMsg === "string") {
|
|
628
645
|
message = dataOrMsg;
|
|
629
646
|
data = {
|
|
647
|
+
...contextTags,
|
|
630
648
|
...props,
|
|
631
649
|
...extraData,
|
|
632
650
|
error_type: err.name,
|
|
633
651
|
error_message: err.message,
|
|
634
652
|
error_stack: err.stack,
|
|
635
|
-
error_code: err.code
|
|
653
|
+
error_code: err.code,
|
|
654
|
+
error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
|
|
636
655
|
};
|
|
637
656
|
} else {
|
|
638
657
|
message = err.message;
|
|
639
658
|
data = {
|
|
659
|
+
...contextTags,
|
|
640
660
|
...props,
|
|
641
661
|
...dataOrMsg,
|
|
642
662
|
error_type: err.name,
|
|
643
663
|
error_stack: err.stack,
|
|
644
|
-
error_code: err.code
|
|
664
|
+
error_code: err.code,
|
|
665
|
+
error_cause: err.cause !== void 0 ? serializeCause(err.cause) : void 0
|
|
645
666
|
};
|
|
646
667
|
}
|
|
647
668
|
} else {
|
|
@@ -669,16 +690,14 @@ function createLoggerImpl(name, props, pipeline, spanMeta, parentSpanId, traceId
|
|
|
669
690
|
return {
|
|
670
691
|
name,
|
|
671
692
|
props: Object.freeze({ ...props }),
|
|
672
|
-
get
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
duration: spanMeta.endTime !== null ? spanMeta.endTime - spanMeta.startTime : Date.now() - spanMeta.startTime
|
|
681
|
-
}), spanMeta.attrs);
|
|
693
|
+
get level() {
|
|
694
|
+
return pipeline.level;
|
|
695
|
+
},
|
|
696
|
+
dispatch(event) {
|
|
697
|
+
pipeline.dispatch(event);
|
|
698
|
+
},
|
|
699
|
+
[Symbol.dispose]() {
|
|
700
|
+
pipeline.dispose();
|
|
682
701
|
},
|
|
683
702
|
trace: (msg, data) => emitLog("trace", msg, data),
|
|
684
703
|
debug: (msg, data) => emitLog("debug", msg, data),
|
|
@@ -686,202 +705,358 @@ function createLoggerImpl(name, props, pipeline, spanMeta, parentSpanId, traceId
|
|
|
686
705
|
warn: (msg, data) => emitLog("warn", msg, data),
|
|
687
706
|
error: (msgOrError, dataOrMsg, extraData) => emitLog("error", msgOrError, dataOrMsg, extraData),
|
|
688
707
|
logger(namespace, childProps) {
|
|
689
|
-
return
|
|
690
|
-
...props,
|
|
691
|
-
...childProps
|
|
692
|
-
}, pipeline, null, parentSpanId, traceId, traceSampled), () => pipeline.level);
|
|
708
|
+
return this.child(namespace ?? "", childProps);
|
|
693
709
|
},
|
|
694
|
-
span(
|
|
695
|
-
|
|
696
|
-
const resolvedChildProps = typeof childProps === "function" ? childProps() : childProps;
|
|
697
|
-
const mergedProps = {
|
|
698
|
-
...props,
|
|
699
|
-
...resolvedChildProps
|
|
700
|
-
};
|
|
701
|
-
const newSpanId = generateSpanId();
|
|
702
|
-
let resolvedParentId = parentSpanId;
|
|
703
|
-
let resolvedTraceId = traceId;
|
|
704
|
-
if (!resolvedParentId && _getContextParent) {
|
|
705
|
-
const ctxParent = _getContextParent();
|
|
706
|
-
if (ctxParent) {
|
|
707
|
-
resolvedParentId = ctxParent.spanId;
|
|
708
|
-
resolvedTraceId = resolvedTraceId || ctxParent.traceId;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const isNewTrace = !resolvedTraceId;
|
|
712
|
-
const finalTraceId = resolvedTraceId || generateTraceId();
|
|
713
|
-
const sampled = isNewTrace ? shouldSample() : traceSampled;
|
|
714
|
-
const newSpanData = {
|
|
715
|
-
id: newSpanId,
|
|
716
|
-
traceId: finalTraceId,
|
|
717
|
-
parentId: resolvedParentId,
|
|
718
|
-
startTime: Date.now(),
|
|
719
|
-
endTime: null,
|
|
720
|
-
duration: null,
|
|
721
|
-
attrs: {}
|
|
722
|
-
};
|
|
723
|
-
const spanLogger = createLoggerImpl(childName, mergedProps, pipeline, newSpanData, newSpanId, finalTraceId, sampled);
|
|
724
|
-
_enterContext?.(newSpanId, finalTraceId, resolvedParentId);
|
|
725
|
-
spanLogger[Symbol.dispose] = () => {
|
|
726
|
-
if (newSpanData.endTime !== null) return;
|
|
727
|
-
newSpanData.endTime = Date.now();
|
|
728
|
-
newSpanData.duration = newSpanData.endTime - newSpanData.startTime;
|
|
729
|
-
if (collectSpans) collectedSpans.push(createSpanDataProxy(() => ({
|
|
730
|
-
id: newSpanData.id,
|
|
731
|
-
traceId: newSpanData.traceId,
|
|
732
|
-
parentId: newSpanData.parentId,
|
|
733
|
-
startTime: newSpanData.startTime,
|
|
734
|
-
endTime: newSpanData.endTime,
|
|
735
|
-
duration: newSpanData.duration
|
|
736
|
-
}), { ...newSpanData.attrs }));
|
|
737
|
-
_exitContext?.(newSpanId);
|
|
738
|
-
_ambientRecorder?.recordSpan({
|
|
739
|
-
name: childName,
|
|
740
|
-
durationMs: newSpanData.duration
|
|
741
|
-
});
|
|
742
|
-
if (sampled) {
|
|
743
|
-
const spanEvent = {
|
|
744
|
-
kind: "span",
|
|
745
|
-
time: newSpanData.endTime,
|
|
746
|
-
namespace: childName,
|
|
747
|
-
name: childName,
|
|
748
|
-
duration: newSpanData.duration,
|
|
749
|
-
props: {
|
|
750
|
-
...mergedProps,
|
|
751
|
-
...newSpanData.attrs
|
|
752
|
-
},
|
|
753
|
-
spanId: newSpanData.id,
|
|
754
|
-
traceId: newSpanData.traceId,
|
|
755
|
-
parentId: newSpanData.parentId
|
|
756
|
-
};
|
|
757
|
-
pipeline.dispatch(spanEvent);
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
return spanLogger;
|
|
710
|
+
span(_namespace, _childProps) {
|
|
711
|
+
throw new Error("loggily: span() requires the withSpans() plugin. Use pipe(baseCreateLogger, withSpans()) or the default createLogger.");
|
|
761
712
|
},
|
|
762
|
-
child(
|
|
763
|
-
if (typeof
|
|
713
|
+
child(namespaceOrContext, childProps) {
|
|
714
|
+
if (typeof namespaceOrContext === "string") return wrapConditional(createLoggerImpl(namespaceOrContext ? `${name}:${namespaceOrContext}` : name, {
|
|
715
|
+
...props,
|
|
716
|
+
...childProps
|
|
717
|
+
}, pipeline), () => pipeline.level);
|
|
764
718
|
return wrapConditional(createLoggerImpl(name, {
|
|
765
719
|
...props,
|
|
766
|
-
...
|
|
767
|
-
}, pipeline
|
|
720
|
+
...namespaceOrContext
|
|
721
|
+
}, pipeline), () => pipeline.level);
|
|
768
722
|
},
|
|
769
|
-
end() {
|
|
770
|
-
if (spanMeta?.endTime === null) this[Symbol.dispose]?.();
|
|
771
|
-
}
|
|
723
|
+
end() {}
|
|
772
724
|
};
|
|
773
725
|
}
|
|
774
726
|
function wrapConditional(logger, getLevel) {
|
|
775
727
|
return new Proxy(logger, { get(target, prop) {
|
|
776
|
-
if (prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
|
|
728
|
+
if (typeof prop === "string" && prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
|
|
777
729
|
if (LOG_LEVEL_PRIORITY[prop] < LOG_LEVEL_PRIORITY[getLevel()]) return;
|
|
778
730
|
}
|
|
731
|
+
if (prop === "span") {
|
|
732
|
+
const val = target[prop];
|
|
733
|
+
if (val === baseSpanStub) return void 0;
|
|
734
|
+
return val;
|
|
735
|
+
}
|
|
779
736
|
return target[prop];
|
|
780
737
|
} });
|
|
781
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
|
+
};
|
|
782
742
|
/**
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
* @param config - Optional config array. Objects configure, arrays branch, values write.
|
|
787
|
-
*
|
|
788
|
-
* @example
|
|
789
|
-
* // Zero config (reads LOG_LEVEL, DEBUG, LOG_FORMAT from env)
|
|
790
|
-
* const log = createLogger('myapp')
|
|
791
|
-
*
|
|
792
|
-
* @example
|
|
793
|
-
* // Configured pipeline
|
|
794
|
-
* const log = createLogger('myapp', [
|
|
795
|
-
* { level: 'debug', ns: '-sql' },
|
|
796
|
-
* console,
|
|
797
|
-
* { file: '/tmp/app.log', level: 'info', format: 'json' },
|
|
798
|
-
* ])
|
|
743
|
+
* Plugin: adds span creation capability to loggers.
|
|
744
|
+
* Without this plugin, `.span` is undefined on ConditionalLogger.
|
|
745
|
+
* Included by default in `createLogger`.
|
|
799
746
|
*/
|
|
800
|
-
function
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
};
|
|
803
863
|
}
|
|
804
864
|
/**
|
|
805
|
-
*
|
|
865
|
+
* Base createLogger — requires a config array.
|
|
866
|
+
* Use the default `createLogger` export (with `withEnvDefaults`) for zero-config.
|
|
806
867
|
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
* import withSentry from "@sentry/loggily"
|
|
810
|
-
*
|
|
811
|
-
* const createLogger = compose(base, withSentry({ dsn: "..." }))
|
|
812
|
-
* const log = createLogger("myapp")
|
|
868
|
+
* Note: loggers from baseCreateLogger do NOT have `.span()` capability.
|
|
869
|
+
* Use `pipe(baseCreateLogger, withSpans())` or the default `createLogger` for spans.
|
|
813
870
|
*/
|
|
814
|
-
function
|
|
815
|
-
|
|
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);
|
|
816
886
|
}
|
|
817
887
|
const _env = (typeof process !== "undefined" ? process : void 0)?.env ?? {};
|
|
818
|
-
|
|
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 */
|
|
819
1007
|
function setLogLevel(level) {
|
|
820
1008
|
_env.LOG_LEVEL = level;
|
|
821
1009
|
}
|
|
822
|
-
/** @deprecated Level is per-logger in v2 */
|
|
823
1010
|
function getLogLevel() {
|
|
824
|
-
return
|
|
1011
|
+
return currentLevel();
|
|
825
1012
|
}
|
|
826
|
-
/** @deprecated Use TRACE=1 env var */
|
|
827
1013
|
function enableSpans() {
|
|
828
1014
|
_env.TRACE = "1";
|
|
829
1015
|
}
|
|
830
|
-
/** @deprecated */
|
|
831
1016
|
function disableSpans() {
|
|
832
1017
|
delete _env.TRACE;
|
|
833
1018
|
}
|
|
834
|
-
/** @deprecated */
|
|
835
1019
|
function spansAreEnabled() {
|
|
836
1020
|
return !!_env.TRACE;
|
|
837
1021
|
}
|
|
838
|
-
/** @deprecated Use TRACE=namespace env var */
|
|
839
1022
|
function setTraceFilter(namespaces) {
|
|
840
1023
|
if (!namespaces || namespaces.length === 0) delete _env.TRACE;
|
|
841
1024
|
else _env.TRACE = namespaces.join(",");
|
|
842
1025
|
}
|
|
843
|
-
/** @deprecated */
|
|
844
1026
|
function getTraceFilter() {
|
|
845
1027
|
return _env.TRACE ? _env.TRACE.split(",") : null;
|
|
846
1028
|
}
|
|
847
|
-
/** @deprecated Use DEBUG=namespace env var or { ns } in config array */
|
|
848
1029
|
function setDebugFilter(namespaces) {
|
|
849
1030
|
if (!namespaces || namespaces.length === 0) delete _env.DEBUG;
|
|
850
1031
|
else _env.DEBUG = namespaces.join(",");
|
|
851
1032
|
}
|
|
852
|
-
/** @deprecated */
|
|
853
1033
|
function getDebugFilter() {
|
|
854
1034
|
return _env.DEBUG ? _env.DEBUG.split(",") : null;
|
|
855
1035
|
}
|
|
856
|
-
/** @deprecated Use { format } in config array */
|
|
857
1036
|
function setLogFormat(format) {
|
|
858
1037
|
_env.LOG_FORMAT = format;
|
|
859
1038
|
}
|
|
860
|
-
/** @deprecated */
|
|
861
1039
|
function getLogFormat() {
|
|
862
|
-
return
|
|
1040
|
+
return currentFormat();
|
|
863
1041
|
}
|
|
864
|
-
/** @deprecated Omit console from config array instead */
|
|
865
1042
|
function setSuppressConsole(value) {
|
|
866
|
-
|
|
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.");
|
|
867
1047
|
}
|
|
868
|
-
/** @deprecated Use config array */
|
|
869
|
-
function setOutputMode(_mode) {}
|
|
870
|
-
/** @deprecated */
|
|
871
1048
|
function getOutputMode() {
|
|
872
1049
|
return "console";
|
|
873
1050
|
}
|
|
874
|
-
/** @deprecated Pass writers in config array instead */
|
|
875
1051
|
function addWriter(writer) {
|
|
876
|
-
|
|
1052
|
+
_writers.push(writer);
|
|
877
1053
|
return () => {
|
|
878
|
-
const
|
|
879
|
-
if (
|
|
1054
|
+
const i = _writers.indexOf(writer);
|
|
1055
|
+
if (i !== -1) _writers.splice(i, 1);
|
|
880
1056
|
};
|
|
881
1057
|
}
|
|
882
|
-
/** @deprecated Spans dispatch through the pipeline automatically */
|
|
883
1058
|
function writeSpan(namespace, duration, attrs) {
|
|
884
|
-
|
|
1059
|
+
createEnvPipeline().dispatch({
|
|
885
1060
|
kind: "span",
|
|
886
1061
|
time: Date.now(),
|
|
887
1062
|
namespace,
|
|
@@ -891,10 +1066,9 @@ function writeSpan(namespace, duration, attrs) {
|
|
|
891
1066
|
spanId: attrs.span_id ?? "",
|
|
892
1067
|
traceId: attrs.trace_id ?? "",
|
|
893
1068
|
parentId: attrs.parent_id ?? null
|
|
894
|
-
};
|
|
895
|
-
defaultPipeline().dispatch(event);
|
|
1069
|
+
});
|
|
896
1070
|
}
|
|
897
1071
|
//#endregion
|
|
898
|
-
export {
|
|
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 };
|
|
899
1073
|
|
|
900
|
-
//# sourceMappingURL=core-
|
|
1074
|
+
//# sourceMappingURL=core-B3pox577.mjs.map
|