loggily 0.3.0 → 0.4.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 +67 -22
- package/package.json +24 -11
- package/src/context.ts +26 -11
- package/src/core.ts +118 -72
- package/src/file-writer.ts +12 -6
- package/src/index.browser.ts +9 -1
- package/src/index.ts +9 -1
- package/src/tracing.ts +11 -3
- package/src/worker.ts +119 -132
- package/.github/workflows/docs.yml +0 -58
- package/.github/workflows/release.yml +0 -31
- package/.github/workflows/test.yml +0 -20
- package/CHANGELOG.md +0 -45
- package/CLAUDE.md +0 -299
- package/CONTRIBUTING.md +0 -58
- package/benchmarks/overhead.ts +0 -267
- package/bun.lock +0 -479
- package/docs/api-reference.md +0 -400
- package/docs/benchmarks.md +0 -106
- package/docs/comparison.md +0 -315
- package/docs/conditional-logging-research.md +0 -159
- package/docs/guide.md +0 -205
- package/docs/migration-from-debug.md +0 -310
- package/docs/migration-from-pino.md +0 -178
- package/docs/migration-from-winston.md +0 -179
- package/docs/site/.vitepress/config.ts +0 -67
- package/docs/site/api/configuration.md +0 -94
- package/docs/site/api/index.md +0 -61
- package/docs/site/api/logger.md +0 -99
- package/docs/site/api/worker.md +0 -120
- package/docs/site/api/writers.md +0 -69
- package/docs/site/guide/getting-started.md +0 -143
- package/docs/site/guide/journey.md +0 -203
- package/docs/site/guide/migration-from-debug.md +0 -24
- package/docs/site/guide/spans.md +0 -139
- package/docs/site/guide/why.md +0 -55
- package/docs/site/guide/workers.md +0 -113
- package/docs/site/guide/zero-overhead.md +0 -87
- package/docs/site/index.md +0 -54
- package/tests/features.test.ts +0 -552
- package/tests/logger.test.ts +0 -944
- package/tests/tracing.test.ts +0 -618
- package/tests/universal.test.ts +0 -107
- package/tests/worker.test.ts +0 -590
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -10
package/tests/logger.test.ts
DELETED
|
@@ -1,944 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loggily Test Suite
|
|
3
|
-
*
|
|
4
|
-
* Tests for the logger-first observability system.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"
|
|
8
|
-
import {
|
|
9
|
-
createLogger,
|
|
10
|
-
enableSpans,
|
|
11
|
-
disableSpans,
|
|
12
|
-
setLogLevel,
|
|
13
|
-
getLogLevel,
|
|
14
|
-
spansAreEnabled,
|
|
15
|
-
setTraceFilter,
|
|
16
|
-
getTraceFilter,
|
|
17
|
-
setDebugFilter,
|
|
18
|
-
getDebugFilter,
|
|
19
|
-
setOutputMode,
|
|
20
|
-
resetIds,
|
|
21
|
-
type Logger,
|
|
22
|
-
type SpanLogger,
|
|
23
|
-
type ConditionalLogger,
|
|
24
|
-
} from "../src/index.ts"
|
|
25
|
-
|
|
26
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
-
// Test Helpers
|
|
28
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
interface CapturedLog {
|
|
31
|
-
level: string
|
|
32
|
-
message: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Create a mock console that captures output */
|
|
36
|
-
function createConsoleMock() {
|
|
37
|
-
const output: CapturedLog[] = []
|
|
38
|
-
const capture =
|
|
39
|
-
(level: string) =>
|
|
40
|
-
(msg: unknown): void => {
|
|
41
|
-
output.push({ level, message: String(msg) })
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
vi.spyOn(console, "debug").mockImplementation(capture("debug"))
|
|
45
|
-
vi.spyOn(console, "info").mockImplementation(capture("info"))
|
|
46
|
-
vi.spyOn(console, "warn").mockImplementation(capture("warn"))
|
|
47
|
-
vi.spyOn(console, "error").mockImplementation(capture("error"))
|
|
48
|
-
|
|
49
|
-
// Spans use process.stderr.write to bypass Ink's patchConsole
|
|
50
|
-
const origStderrWrite = process.stderr.write.bind(process.stderr)
|
|
51
|
-
vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string | Uint8Array) => {
|
|
52
|
-
output.push({ level: "stderr", message: String(chunk) })
|
|
53
|
-
return true
|
|
54
|
-
}) as typeof process.stderr.write)
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
output,
|
|
58
|
-
findSpan: () => output.find((o) => o.message.includes("SPAN")),
|
|
59
|
-
findSpans: () => output.filter((o) => o.message.includes("SPAN")),
|
|
60
|
-
origStderrWrite,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Console mock instance for all tests
|
|
65
|
-
let consoleMock: ReturnType<typeof createConsoleMock>
|
|
66
|
-
|
|
67
|
-
beforeEach(() => {
|
|
68
|
-
resetIds()
|
|
69
|
-
setLogLevel("trace") // Enable all levels
|
|
70
|
-
disableSpans() // Start with spans disabled
|
|
71
|
-
setTraceFilter(null) // Clear any trace filter
|
|
72
|
-
setDebugFilter(null) // Clear any debug filter
|
|
73
|
-
setOutputMode("console") // Reset output mode
|
|
74
|
-
consoleMock = createConsoleMock()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
vi.restoreAllMocks()
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe("createLogger", () => {
|
|
82
|
-
test("creates logger with name", () => {
|
|
83
|
-
const log = createLogger("myapp")
|
|
84
|
-
expect(log.name).toBe("myapp")
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("creates logger with props", () => {
|
|
88
|
-
const log = createLogger("myapp", { version: "1.0" })
|
|
89
|
-
expect(log.props).toEqual({ version: "1.0" })
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test("props are frozen", () => {
|
|
93
|
-
const log = createLogger("myapp", { version: "1.0" })
|
|
94
|
-
expect(() => {
|
|
95
|
-
// @ts-expect-error - testing immutability
|
|
96
|
-
log.props.version = "2.0"
|
|
97
|
-
}).toThrow()
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test("spanData is null for regular logger", () => {
|
|
101
|
-
const log = createLogger("myapp")
|
|
102
|
-
expect(log.spanData).toBeNull()
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe("logging methods", () => {
|
|
107
|
-
// Test all log levels with their expected console method
|
|
108
|
-
test.each([
|
|
109
|
-
["trace", "debug"], // trace uses console.debug
|
|
110
|
-
["debug", "debug"],
|
|
111
|
-
["info", "info"],
|
|
112
|
-
["warn", "warn"],
|
|
113
|
-
["error", "error"],
|
|
114
|
-
] as const)("%s level uses console.%s", (logLevel, consoleMethod) => {
|
|
115
|
-
const log = createLogger("test")
|
|
116
|
-
log[logLevel](`${logLevel} message`)
|
|
117
|
-
|
|
118
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
119
|
-
expect(consoleMock.output[0]!.level).toBe(consoleMethod)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test("includes data in output", () => {
|
|
123
|
-
const log = createLogger("test")
|
|
124
|
-
log.info("message", { key: "value" })
|
|
125
|
-
|
|
126
|
-
expect(consoleMock.output[0]!.message).toContain("key")
|
|
127
|
-
expect(consoleMock.output[0]!.message).toContain("value")
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test("inherits props in output", () => {
|
|
131
|
-
const log = createLogger("test", { app: "myapp" })
|
|
132
|
-
log.info("message")
|
|
133
|
-
|
|
134
|
-
expect(consoleMock.output[0]!.message).toContain("app")
|
|
135
|
-
expect(consoleMock.output[0]!.message).toContain("myapp")
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// Test log level filtering - levels below threshold are filtered out
|
|
139
|
-
// Note: createLogger returns ConditionalLogger where disabled levels are undefined
|
|
140
|
-
test.each([
|
|
141
|
-
["warn", ["warn", "error"], 2],
|
|
142
|
-
["error", ["error"], 1],
|
|
143
|
-
["info", ["info", "warn", "error"], 3],
|
|
144
|
-
] as const)("setLogLevel(%s) filters to %j", (threshold, expectedLevels, expectedCount) => {
|
|
145
|
-
setLogLevel(threshold)
|
|
146
|
-
const log = createLogger("test")
|
|
147
|
-
|
|
148
|
-
log.debug?.("d")
|
|
149
|
-
log.info?.("i")
|
|
150
|
-
log.warn?.("w")
|
|
151
|
-
log.error?.("e")
|
|
152
|
-
|
|
153
|
-
expect(consoleMock.output).toHaveLength(expectedCount)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test("error accepts Error object", () => {
|
|
157
|
-
const log = createLogger("test")
|
|
158
|
-
const err = new Error("Something went wrong")
|
|
159
|
-
|
|
160
|
-
log.error(err)
|
|
161
|
-
|
|
162
|
-
expect(consoleMock.output[0]!.message).toContain("Something went wrong")
|
|
163
|
-
expect(consoleMock.output[0]!.message).toContain("error_type")
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
describe("logger hierarchy", () => {
|
|
168
|
-
test(".logger() creates child with extended namespace", () => {
|
|
169
|
-
const parent = createLogger("app")
|
|
170
|
-
const child = parent.logger("import")
|
|
171
|
-
|
|
172
|
-
expect(child.name).toBe("app:import")
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test(".logger() inherits parent props", () => {
|
|
176
|
-
const parent = createLogger("app", { version: "1.0" })
|
|
177
|
-
const child = parent.logger("import")
|
|
178
|
-
|
|
179
|
-
expect(child.props).toEqual({ version: "1.0" })
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
test(".logger() merges additional props", () => {
|
|
183
|
-
const parent = createLogger("app", { version: "1.0" })
|
|
184
|
-
const child = parent.logger("import", { file: "data.csv" })
|
|
185
|
-
|
|
186
|
-
expect(child.props).toEqual({ version: "1.0", file: "data.csv" })
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test(".logger() without namespace keeps same name", () => {
|
|
190
|
-
const parent = createLogger("app")
|
|
191
|
-
const child = parent.logger(undefined, { extra: true })
|
|
192
|
-
|
|
193
|
-
expect(child.name).toBe("app")
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test(".child() is deprecated alias for .logger()", () => {
|
|
197
|
-
const parent = createLogger("app")
|
|
198
|
-
const child = parent.child("import")
|
|
199
|
-
|
|
200
|
-
expect(child.name).toBe("app:import")
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe("spans", () => {
|
|
205
|
-
test(".span() creates logger with spanData", () => {
|
|
206
|
-
const log = createLogger("app")
|
|
207
|
-
const span = log.span("import")
|
|
208
|
-
|
|
209
|
-
expect(span.spanData).not.toBeNull()
|
|
210
|
-
expect(span.spanData!.id).toBe("sp_1")
|
|
211
|
-
expect(span.spanData!.traceId).toBe("tr_1")
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
test("span extends namespace", () => {
|
|
215
|
-
const log = createLogger("app")
|
|
216
|
-
const span = log.span("import")
|
|
217
|
-
|
|
218
|
-
expect(span.name).toBe("app:import")
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test("span inherits props", () => {
|
|
222
|
-
const log = createLogger("app", { version: "1.0" })
|
|
223
|
-
const span = log.span("import", { file: "data.csv" })
|
|
224
|
-
|
|
225
|
-
expect(span.props).toEqual({ version: "1.0", file: "data.csv" })
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test("span has live duration", () => {
|
|
229
|
-
const log = createLogger("app")
|
|
230
|
-
const span = log.span("import")
|
|
231
|
-
|
|
232
|
-
const d1 = span.spanData!.duration
|
|
233
|
-
expect(d1).toBeGreaterThanOrEqual(0)
|
|
234
|
-
|
|
235
|
-
// Wait a bit
|
|
236
|
-
const start = Date.now()
|
|
237
|
-
while (Date.now() - start < 10) {}
|
|
238
|
-
|
|
239
|
-
const d2 = span.spanData!.duration
|
|
240
|
-
expect(d2).toBeGreaterThan(d1!)
|
|
241
|
-
|
|
242
|
-
span.end()
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test("span attributes can be set", () => {
|
|
246
|
-
const log = createLogger("app")
|
|
247
|
-
const span = log.span("import")
|
|
248
|
-
|
|
249
|
-
span.spanData.count = 42
|
|
250
|
-
span.spanData.name = "test"
|
|
251
|
-
|
|
252
|
-
expect(span.spanData.count).toBe(42)
|
|
253
|
-
expect(span.spanData.name).toBe("test")
|
|
254
|
-
|
|
255
|
-
span.end()
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
test("using keyword auto-disposes span", () => {
|
|
259
|
-
enableSpans()
|
|
260
|
-
const log = createLogger("app")
|
|
261
|
-
|
|
262
|
-
{
|
|
263
|
-
using span = log.span("import")
|
|
264
|
-
span.spanData.count = 42
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const spanOutput = consoleMock.findSpan()
|
|
268
|
-
expect(spanOutput).toBeDefined()
|
|
269
|
-
expect(spanOutput!.message).toContain("app:import")
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
test("nested spans have parent-child relationship", () => {
|
|
273
|
-
const log = createLogger("app")
|
|
274
|
-
|
|
275
|
-
const parent = log.span("import")
|
|
276
|
-
const child = parent.span("parse")
|
|
277
|
-
|
|
278
|
-
expect(child.spanData!.parentId).toBe(parent.spanData!.id)
|
|
279
|
-
expect(child.spanData!.traceId).toBe(parent.spanData!.traceId)
|
|
280
|
-
|
|
281
|
-
child.end()
|
|
282
|
-
parent.end()
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
test("nested spans share trace ID", () => {
|
|
286
|
-
const log = createLogger("app")
|
|
287
|
-
|
|
288
|
-
const span1 = log.span("import")
|
|
289
|
-
const span2 = span1.span("parse")
|
|
290
|
-
const span3 = span2.span("validate")
|
|
291
|
-
|
|
292
|
-
expect(span1.spanData!.traceId).toBe("tr_1")
|
|
293
|
-
expect(span2.spanData!.traceId).toBe("tr_1")
|
|
294
|
-
expect(span3.spanData!.traceId).toBe("tr_1")
|
|
295
|
-
|
|
296
|
-
span3.end()
|
|
297
|
-
span2.end()
|
|
298
|
-
span1.end()
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test(".end() can be called manually", () => {
|
|
302
|
-
enableSpans()
|
|
303
|
-
const log = createLogger("app")
|
|
304
|
-
const span = log.span("import")
|
|
305
|
-
|
|
306
|
-
span.end()
|
|
307
|
-
|
|
308
|
-
expect(span.spanData!.endTime).not.toBeNull()
|
|
309
|
-
expect(span.spanData!.duration).toBeGreaterThanOrEqual(0)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test("span output includes attributes", () => {
|
|
313
|
-
enableSpans()
|
|
314
|
-
const log = createLogger("app")
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
using span = log.span("import", { file: "data.csv" })
|
|
318
|
-
span.spanData.count = 42
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const spanOutput = consoleMock.findSpan()
|
|
322
|
-
expect(spanOutput!.message).toContain("file")
|
|
323
|
-
expect(spanOutput!.message).toContain("count")
|
|
324
|
-
expect(spanOutput!.message).toContain("42")
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe("span output control", () => {
|
|
329
|
-
test("spans disabled by default", () => {
|
|
330
|
-
const log = createLogger("app")
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
using span = log.span("import")
|
|
334
|
-
span.info("working")
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Only the info log, no span
|
|
338
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
339
|
-
expect(consoleMock.output[0]!.message).not.toContain("SPAN")
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
test("enableSpans() enables span output", () => {
|
|
343
|
-
enableSpans()
|
|
344
|
-
const log = createLogger("app")
|
|
345
|
-
|
|
346
|
-
{
|
|
347
|
-
using span = log.span("import")
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
expect(consoleMock.findSpan()).toBeDefined()
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
test("disableSpans() disables span output", () => {
|
|
354
|
-
enableSpans()
|
|
355
|
-
disableSpans()
|
|
356
|
-
const log = createLogger("app")
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
using span = log.span("import")
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
expect(consoleMock.findSpan()).toBeUndefined()
|
|
363
|
-
})
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
describe("console method usage (patchConsole compatibility)", () => {
|
|
367
|
-
// Consolidated: log level -> console method mapping (covered above in logging methods)
|
|
368
|
-
// This describe block focuses on patchConsole-specific behavior
|
|
369
|
-
|
|
370
|
-
test("span output uses process.stderr.write (bypasses Ink patchConsole)", () => {
|
|
371
|
-
enableSpans()
|
|
372
|
-
const log = createLogger("test")
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
using span = log.span("work")
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const spanOutput = consoleMock.findSpan()
|
|
379
|
-
expect(spanOutput!.level).toBe("stderr") // Spans bypass console, go directly to stderr
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
describe("createLogger", () => {
|
|
384
|
-
// Test enabled/disabled levels with parameterized tests
|
|
385
|
-
test.each([
|
|
386
|
-
["trace", { trace: true, debug: true, info: true, warn: true, error: true }],
|
|
387
|
-
["debug", { trace: false, debug: true, info: true, warn: true, error: true }],
|
|
388
|
-
["warn", { trace: false, debug: false, info: false, warn: true, error: true }],
|
|
389
|
-
["error", { trace: false, debug: false, info: false, warn: false, error: true }],
|
|
390
|
-
] as const)("at level %s, methods defined: %o", (level, expected) => {
|
|
391
|
-
setLogLevel(level)
|
|
392
|
-
const log = createLogger("test")
|
|
393
|
-
|
|
394
|
-
expect(log.trace !== undefined).toBe(expected.trace)
|
|
395
|
-
expect(log.debug !== undefined).toBe(expected.debug)
|
|
396
|
-
expect(log.info !== undefined).toBe(expected.info)
|
|
397
|
-
expect(log.warn !== undefined).toBe(expected.warn)
|
|
398
|
-
expect(log.error !== undefined).toBe(expected.error)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
test("optional chaining skips call when disabled", () => {
|
|
402
|
-
setLogLevel("error")
|
|
403
|
-
const log = createLogger("test")
|
|
404
|
-
|
|
405
|
-
log.debug?.("should not log")
|
|
406
|
-
log.info?.("should not log")
|
|
407
|
-
log.warn?.("should not log")
|
|
408
|
-
|
|
409
|
-
expect(consoleMock.output).toHaveLength(0)
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
test("optional chaining calls method when enabled", () => {
|
|
413
|
-
setLogLevel("debug")
|
|
414
|
-
const log = createLogger("test")
|
|
415
|
-
|
|
416
|
-
log.debug?.("should log")
|
|
417
|
-
|
|
418
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
419
|
-
expect(consoleMock.output[0]!.message).toContain("should log")
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
test("inherits props from base logger", () => {
|
|
423
|
-
setLogLevel("info")
|
|
424
|
-
const log = createLogger("test", { version: "1.0" })
|
|
425
|
-
|
|
426
|
-
expect(log.props).toEqual({ version: "1.0" })
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
test("can create child loggers and spans", () => {
|
|
430
|
-
setLogLevel("info")
|
|
431
|
-
const log = createLogger("test")
|
|
432
|
-
|
|
433
|
-
const child = log.logger("child")
|
|
434
|
-
expect(child.name).toBe("test:child")
|
|
435
|
-
|
|
436
|
-
const span = log.span("work")
|
|
437
|
-
expect(span.spanData).not.toBeNull()
|
|
438
|
-
span.end()
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
test("responds to log level changes", () => {
|
|
442
|
-
const log = createLogger("test")
|
|
443
|
-
|
|
444
|
-
setLogLevel("error")
|
|
445
|
-
expect(log.debug).toBeUndefined()
|
|
446
|
-
expect(log.info).toBeUndefined()
|
|
447
|
-
|
|
448
|
-
setLogLevel("debug")
|
|
449
|
-
expect(log.debug).toBeDefined()
|
|
450
|
-
expect(log.info).toBeDefined()
|
|
451
|
-
})
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
describe("configuration functions", () => {
|
|
455
|
-
test("getLogLevel returns current level", () => {
|
|
456
|
-
setLogLevel("warn")
|
|
457
|
-
expect(getLogLevel()).toBe("warn")
|
|
458
|
-
|
|
459
|
-
setLogLevel("debug")
|
|
460
|
-
expect(getLogLevel()).toBe("debug")
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
test("spansAreEnabled tracks span state", () => {
|
|
464
|
-
disableSpans()
|
|
465
|
-
expect(spansAreEnabled()).toBe(false)
|
|
466
|
-
|
|
467
|
-
enableSpans()
|
|
468
|
-
expect(spansAreEnabled()).toBe(true)
|
|
469
|
-
|
|
470
|
-
disableSpans()
|
|
471
|
-
expect(spansAreEnabled()).toBe(false)
|
|
472
|
-
})
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
describe("JSON format output", () => {
|
|
476
|
-
let originalNodeEnv: string | undefined
|
|
477
|
-
let originalTraceFormat: string | undefined
|
|
478
|
-
|
|
479
|
-
beforeEach(() => {
|
|
480
|
-
originalNodeEnv = process.env.NODE_ENV
|
|
481
|
-
originalTraceFormat = process.env.TRACE_FORMAT
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
afterEach(() => {
|
|
485
|
-
if (originalNodeEnv === undefined) {
|
|
486
|
-
delete process.env.NODE_ENV
|
|
487
|
-
} else {
|
|
488
|
-
process.env.NODE_ENV = originalNodeEnv
|
|
489
|
-
}
|
|
490
|
-
if (originalTraceFormat === undefined) {
|
|
491
|
-
delete process.env.TRACE_FORMAT
|
|
492
|
-
} else {
|
|
493
|
-
process.env.TRACE_FORMAT = originalTraceFormat
|
|
494
|
-
}
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
test("TRACE_FORMAT=json produces JSON output", () => {
|
|
498
|
-
process.env.TRACE_FORMAT = "json"
|
|
499
|
-
const log = createLogger("test")
|
|
500
|
-
|
|
501
|
-
log.info("test message", { key: "value" })
|
|
502
|
-
|
|
503
|
-
const output = consoleMock.output[0]!.message
|
|
504
|
-
const parsed = JSON.parse(output)
|
|
505
|
-
expect(parsed.level).toBe("info")
|
|
506
|
-
expect(parsed.name).toBe("test")
|
|
507
|
-
expect(parsed.msg).toBe("test message")
|
|
508
|
-
expect(parsed.key).toBe("value")
|
|
509
|
-
expect(parsed.time).toBeDefined()
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
test("NODE_ENV=production produces JSON output", () => {
|
|
513
|
-
process.env.NODE_ENV = "production"
|
|
514
|
-
delete process.env.TRACE_FORMAT
|
|
515
|
-
const log = createLogger("test")
|
|
516
|
-
|
|
517
|
-
log.info("prod message")
|
|
518
|
-
|
|
519
|
-
const output = consoleMock.output[0]!.message
|
|
520
|
-
const parsed = JSON.parse(output)
|
|
521
|
-
expect(parsed.level).toBe("info")
|
|
522
|
-
expect(parsed.msg).toBe("prod message")
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
test("JSON output includes all props", () => {
|
|
526
|
-
process.env.TRACE_FORMAT = "json"
|
|
527
|
-
const log = createLogger("test", { app: "myapp", version: "1.0" })
|
|
528
|
-
|
|
529
|
-
log.info("message")
|
|
530
|
-
|
|
531
|
-
const output = consoleMock.output[0]!.message
|
|
532
|
-
const parsed = JSON.parse(output)
|
|
533
|
-
expect(parsed.app).toBe("myapp")
|
|
534
|
-
expect(parsed.version).toBe("1.0")
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
test("JSON output handles errors", () => {
|
|
538
|
-
process.env.TRACE_FORMAT = "json"
|
|
539
|
-
const log = createLogger("test")
|
|
540
|
-
const err = new Error("test error")
|
|
541
|
-
|
|
542
|
-
log.error(err)
|
|
543
|
-
|
|
544
|
-
const output = consoleMock.output[0]!.message
|
|
545
|
-
const parsed = JSON.parse(output)
|
|
546
|
-
expect(parsed.msg).toBe("test error")
|
|
547
|
-
expect(parsed.error_type).toBe("Error")
|
|
548
|
-
expect(parsed.error_stack).toContain("Error: test error")
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
test("JSON span output includes duration", () => {
|
|
552
|
-
process.env.TRACE_FORMAT = "json"
|
|
553
|
-
enableSpans()
|
|
554
|
-
const log = createLogger("test")
|
|
555
|
-
|
|
556
|
-
{
|
|
557
|
-
using span = log.span("work")
|
|
558
|
-
span.spanData.count = 42
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const spanOutput = consoleMock.output.find((o) => {
|
|
562
|
-
try {
|
|
563
|
-
const parsed = JSON.parse(o.message)
|
|
564
|
-
return parsed.level === "span"
|
|
565
|
-
} catch {
|
|
566
|
-
return false
|
|
567
|
-
}
|
|
568
|
-
})
|
|
569
|
-
expect(spanOutput).toBeDefined()
|
|
570
|
-
|
|
571
|
-
const parsed = JSON.parse(spanOutput!.message)
|
|
572
|
-
expect(parsed.level).toBe("span")
|
|
573
|
-
expect(parsed.name).toBe("test:work")
|
|
574
|
-
expect(parsed.duration).toBeGreaterThanOrEqual(0)
|
|
575
|
-
expect(parsed.count).toBe(42)
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
test("JSON handles circular references", () => {
|
|
579
|
-
process.env.TRACE_FORMAT = "json"
|
|
580
|
-
const log = createLogger("test")
|
|
581
|
-
|
|
582
|
-
const circular: Record<string, unknown> = { name: "test" }
|
|
583
|
-
circular.self = circular
|
|
584
|
-
|
|
585
|
-
log.info("circular", circular)
|
|
586
|
-
|
|
587
|
-
const output = consoleMock.output[0]!.message
|
|
588
|
-
// Should not throw, should contain [Circular]
|
|
589
|
-
expect(output).toContain("[Circular]")
|
|
590
|
-
})
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
describe("console format output", () => {
|
|
594
|
-
test("includes timestamp", () => {
|
|
595
|
-
const log = createLogger("test")
|
|
596
|
-
log.info("message")
|
|
597
|
-
|
|
598
|
-
// Format: HH:MM:SS
|
|
599
|
-
expect(consoleMock.output[0]!.message).toMatch(/\d{2}:\d{2}:\d{2}/)
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
// Test level labels in console output
|
|
603
|
-
test.each([
|
|
604
|
-
["trace", "TRACE"],
|
|
605
|
-
["debug", "DEBUG"],
|
|
606
|
-
["info", "INFO"],
|
|
607
|
-
["warn", "WARN"],
|
|
608
|
-
["error", "ERROR"],
|
|
609
|
-
] as const)("%s level outputs %s label", (method, label) => {
|
|
610
|
-
const log = createLogger("test")
|
|
611
|
-
log[method]("msg")
|
|
612
|
-
|
|
613
|
-
expect(consoleMock.output[0]!.message).toContain(label)
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
test("includes namespace", () => {
|
|
617
|
-
const log = createLogger("myapp")
|
|
618
|
-
log.info("message")
|
|
619
|
-
|
|
620
|
-
expect(consoleMock.output[0]!.message).toContain("myapp")
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
test("span format includes SPAN label and duration", () => {
|
|
624
|
-
enableSpans()
|
|
625
|
-
const log = createLogger("test")
|
|
626
|
-
|
|
627
|
-
{
|
|
628
|
-
using span = log.span("work")
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const spanOutput = consoleMock.findSpan()
|
|
632
|
-
expect(spanOutput).toBeDefined()
|
|
633
|
-
expect(spanOutput!.message).toMatch(/\(\d+ms\)/)
|
|
634
|
-
})
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
describe("TRACE namespace filtering", () => {
|
|
638
|
-
test("setTraceFilter with namespaces enables spans and filtering", () => {
|
|
639
|
-
setTraceFilter(["myapp"])
|
|
640
|
-
|
|
641
|
-
expect(spansAreEnabled()).toBe(true)
|
|
642
|
-
expect(getTraceFilter()).toEqual(["myapp"])
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
// Test that setTraceFilter clears filter (but doesn't disable spans)
|
|
646
|
-
test.each([[null], [[]]] as const)("setTraceFilter(%j) clears filter", (filter) => {
|
|
647
|
-
setTraceFilter(["myapp"])
|
|
648
|
-
setTraceFilter(filter)
|
|
649
|
-
|
|
650
|
-
expect(getTraceFilter()).toBeNull()
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
test("filter allows exact namespace match", () => {
|
|
654
|
-
setTraceFilter(["myapp"])
|
|
655
|
-
const log = createLogger("myapp")
|
|
656
|
-
|
|
657
|
-
{
|
|
658
|
-
using span = log.span("work")
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
expect(consoleMock.findSpan()).toBeDefined()
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
test("filter allows child namespace match", () => {
|
|
665
|
-
setTraceFilter(["myapp"])
|
|
666
|
-
const log = createLogger("myapp")
|
|
667
|
-
|
|
668
|
-
{
|
|
669
|
-
using span = log.span("import") // myapp:import
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
expect(consoleMock.findSpan()).toBeDefined()
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
test("filter blocks non-matching namespace", () => {
|
|
676
|
-
setTraceFilter(["myapp"])
|
|
677
|
-
const log = createLogger("other")
|
|
678
|
-
|
|
679
|
-
{
|
|
680
|
-
using span = log.span("work")
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
expect(consoleMock.findSpan()).toBeUndefined()
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
test("filter supports multiple namespaces", () => {
|
|
687
|
-
setTraceFilter(["myapp", "other"])
|
|
688
|
-
|
|
689
|
-
const log1 = createLogger("myapp")
|
|
690
|
-
const log2 = createLogger("other")
|
|
691
|
-
const log3 = createLogger("blocked")
|
|
692
|
-
|
|
693
|
-
{
|
|
694
|
-
using span = log1.span("work")
|
|
695
|
-
}
|
|
696
|
-
{
|
|
697
|
-
using span = log2.span("work")
|
|
698
|
-
}
|
|
699
|
-
{
|
|
700
|
-
using span = log3.span("work")
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const spanOutputs = consoleMock.findSpans()
|
|
704
|
-
expect(spanOutputs).toHaveLength(2)
|
|
705
|
-
expect(spanOutputs[0]!.message).toContain("myapp")
|
|
706
|
-
expect(spanOutputs[1]!.message).toContain("other")
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
test("filter does not affect regular log messages", () => {
|
|
710
|
-
setTraceFilter(["myapp"])
|
|
711
|
-
const log = createLogger("other") // Not in filter
|
|
712
|
-
|
|
713
|
-
log.info("regular log")
|
|
714
|
-
|
|
715
|
-
// Regular logs still appear
|
|
716
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
717
|
-
expect(consoleMock.output[0]!.message).toContain("regular log")
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
test("no filter when spans enabled without setTraceFilter", () => {
|
|
721
|
-
enableSpans()
|
|
722
|
-
|
|
723
|
-
const log1 = createLogger("any")
|
|
724
|
-
const log2 = createLogger("namespace")
|
|
725
|
-
|
|
726
|
-
{
|
|
727
|
-
using span = log1.span("work")
|
|
728
|
-
}
|
|
729
|
-
{
|
|
730
|
-
using span = log2.span("work")
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Both should appear
|
|
734
|
-
expect(consoleMock.findSpans()).toHaveLength(2)
|
|
735
|
-
})
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
describe("DEBUG namespace filtering", () => {
|
|
739
|
-
test("setDebugFilter enables namespace filtering", () => {
|
|
740
|
-
setDebugFilter(["myapp"])
|
|
741
|
-
expect(getDebugFilter()).toEqual(["myapp"])
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
test("setDebugFilter(null) clears filter", () => {
|
|
745
|
-
setDebugFilter(["myapp"])
|
|
746
|
-
setDebugFilter(null)
|
|
747
|
-
expect(getDebugFilter()).toBeNull()
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
test("setDebugFilter([]) clears filter", () => {
|
|
751
|
-
setDebugFilter(["myapp"])
|
|
752
|
-
setDebugFilter([])
|
|
753
|
-
expect(getDebugFilter()).toBeNull()
|
|
754
|
-
})
|
|
755
|
-
|
|
756
|
-
test("filter allows exact namespace match", () => {
|
|
757
|
-
setDebugFilter(["myapp"])
|
|
758
|
-
const log = createLogger("myapp")
|
|
759
|
-
log.info("visible")
|
|
760
|
-
|
|
761
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
762
|
-
expect(consoleMock.output[0]!.message).toContain("visible")
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
test("filter allows child namespace match", () => {
|
|
766
|
-
setDebugFilter(["myapp"])
|
|
767
|
-
const log = createLogger("myapp")
|
|
768
|
-
const child = log.logger("db")
|
|
769
|
-
child.info("visible")
|
|
770
|
-
|
|
771
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
772
|
-
expect(consoleMock.output[0]!.message).toContain("visible")
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
test("filter blocks non-matching namespace", () => {
|
|
776
|
-
setDebugFilter(["myapp"])
|
|
777
|
-
const log = createLogger("other")
|
|
778
|
-
log.info("hidden")
|
|
779
|
-
|
|
780
|
-
expect(consoleMock.output).toHaveLength(0)
|
|
781
|
-
})
|
|
782
|
-
|
|
783
|
-
test("filter supports multiple namespaces", () => {
|
|
784
|
-
setDebugFilter(["myapp", "other"])
|
|
785
|
-
|
|
786
|
-
const log1 = createLogger("myapp")
|
|
787
|
-
const log2 = createLogger("other")
|
|
788
|
-
const log3 = createLogger("blocked")
|
|
789
|
-
|
|
790
|
-
log1.info("msg1")
|
|
791
|
-
log2.info("msg2")
|
|
792
|
-
log3.info("msg3")
|
|
793
|
-
|
|
794
|
-
expect(consoleMock.output).toHaveLength(2)
|
|
795
|
-
expect(consoleMock.output[0]!.message).toContain("myapp")
|
|
796
|
-
expect(consoleMock.output[1]!.message).toContain("other")
|
|
797
|
-
})
|
|
798
|
-
|
|
799
|
-
test("wildcard '*' allows all namespaces", () => {
|
|
800
|
-
setDebugFilter(["*"])
|
|
801
|
-
|
|
802
|
-
const log1 = createLogger("any")
|
|
803
|
-
const log2 = createLogger("namespace")
|
|
804
|
-
|
|
805
|
-
log1.info("msg1")
|
|
806
|
-
log2.info("msg2")
|
|
807
|
-
|
|
808
|
-
expect(consoleMock.output).toHaveLength(2)
|
|
809
|
-
})
|
|
810
|
-
|
|
811
|
-
test("negative pattern excludes matching namespace", () => {
|
|
812
|
-
setDebugFilter(["myapp", "-myapp:noisy"])
|
|
813
|
-
|
|
814
|
-
const log = createLogger("myapp")
|
|
815
|
-
const quiet = log.logger("db")
|
|
816
|
-
const noisy = log.logger("noisy")
|
|
817
|
-
|
|
818
|
-
log.info("root")
|
|
819
|
-
quiet.info("db msg")
|
|
820
|
-
noisy.info("noisy msg")
|
|
821
|
-
|
|
822
|
-
expect(consoleMock.output).toHaveLength(2)
|
|
823
|
-
expect(consoleMock.output[0]!.message).toContain("root")
|
|
824
|
-
expect(consoleMock.output[1]!.message).toContain("db msg")
|
|
825
|
-
})
|
|
826
|
-
|
|
827
|
-
test("negative pattern excludes children of excluded namespace", () => {
|
|
828
|
-
setDebugFilter(["*", "-km:storage:sql"])
|
|
829
|
-
|
|
830
|
-
const log = createLogger("km")
|
|
831
|
-
const storage = log.logger("storage")
|
|
832
|
-
const sql = storage.logger("sql")
|
|
833
|
-
const sqlChild = sql.logger("detail")
|
|
834
|
-
|
|
835
|
-
log.info("visible")
|
|
836
|
-
storage.info("visible")
|
|
837
|
-
sql.info("hidden")
|
|
838
|
-
sqlChild.info("also hidden")
|
|
839
|
-
|
|
840
|
-
expect(consoleMock.output).toHaveLength(2)
|
|
841
|
-
})
|
|
842
|
-
|
|
843
|
-
test("exclude-only pattern (no includes) blocks only excluded", () => {
|
|
844
|
-
setDebugFilter(["-km:noisy"])
|
|
845
|
-
|
|
846
|
-
const log1 = createLogger("km")
|
|
847
|
-
const log2 = createLogger("km").logger("noisy")
|
|
848
|
-
const log3 = createLogger("other")
|
|
849
|
-
|
|
850
|
-
log1.info("visible")
|
|
851
|
-
log2.info("hidden")
|
|
852
|
-
log3.info("visible")
|
|
853
|
-
|
|
854
|
-
expect(consoleMock.output).toHaveLength(2)
|
|
855
|
-
expect(consoleMock.output[0]!.message).toContain("km")
|
|
856
|
-
expect(consoleMock.output[1]!.message).toContain("other")
|
|
857
|
-
})
|
|
858
|
-
|
|
859
|
-
test("setDebugFilter auto-lowers log level to debug", () => {
|
|
860
|
-
setLogLevel("warn")
|
|
861
|
-
setDebugFilter(["myapp"])
|
|
862
|
-
|
|
863
|
-
expect(getLogLevel()).toBe("debug")
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
test("setDebugFilter preserves trace log level", () => {
|
|
867
|
-
setLogLevel("trace")
|
|
868
|
-
setDebugFilter(["myapp"])
|
|
869
|
-
|
|
870
|
-
expect(getLogLevel()).toBe("trace")
|
|
871
|
-
})
|
|
872
|
-
|
|
873
|
-
test("debug messages visible when filter matches", () => {
|
|
874
|
-
setLogLevel("warn") // Would normally hide debug
|
|
875
|
-
setDebugFilter(["myapp"]) // Auto-lowers to debug
|
|
876
|
-
|
|
877
|
-
const log = createLogger("myapp")
|
|
878
|
-
log.debug?.("debug visible")
|
|
879
|
-
|
|
880
|
-
expect(consoleMock.output).toHaveLength(1)
|
|
881
|
-
expect(consoleMock.output[0]!.message).toContain("debug visible")
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
test("getDebugFilter returns includes and excludes", () => {
|
|
885
|
-
setDebugFilter(["myapp", "-noisy"])
|
|
886
|
-
|
|
887
|
-
const filter = getDebugFilter()!
|
|
888
|
-
expect(filter).toContain("myapp")
|
|
889
|
-
expect(filter).toContain("-noisy")
|
|
890
|
-
})
|
|
891
|
-
|
|
892
|
-
test("filter also applies to spans", () => {
|
|
893
|
-
enableSpans()
|
|
894
|
-
setDebugFilter(["myapp"])
|
|
895
|
-
|
|
896
|
-
const log1 = createLogger("myapp")
|
|
897
|
-
const log2 = createLogger("other")
|
|
898
|
-
|
|
899
|
-
{
|
|
900
|
-
using span = log1.span("work")
|
|
901
|
-
}
|
|
902
|
-
{
|
|
903
|
-
using span = log2.span("work")
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const spans = consoleMock.findSpans()
|
|
907
|
-
expect(spans).toHaveLength(1)
|
|
908
|
-
expect(spans[0]!.message).toContain("myapp")
|
|
909
|
-
})
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
describe("setOutputMode", () => {
|
|
913
|
-
test("stderr mode routes writeLog to stderr", () => {
|
|
914
|
-
setOutputMode("stderr")
|
|
915
|
-
const log = createLogger("test")
|
|
916
|
-
log.info?.("hello")
|
|
917
|
-
|
|
918
|
-
const stderrOutput = consoleMock.output.filter((o) => o.level === "stderr")
|
|
919
|
-
expect(stderrOutput).toHaveLength(1)
|
|
920
|
-
expect(stderrOutput[0]!.message).toContain("hello")
|
|
921
|
-
|
|
922
|
-
// Should NOT appear in console output
|
|
923
|
-
const consoleOutput = consoleMock.output.filter((o) => o.level === "info")
|
|
924
|
-
expect(consoleOutput).toHaveLength(0)
|
|
925
|
-
})
|
|
926
|
-
|
|
927
|
-
test("writers-only mode suppresses all direct output", () => {
|
|
928
|
-
setOutputMode("writers-only")
|
|
929
|
-
const log = createLogger("test")
|
|
930
|
-
log.info?.("hello")
|
|
931
|
-
|
|
932
|
-
// No console or stderr output
|
|
933
|
-
expect(consoleMock.output).toHaveLength(0)
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
test("console mode (default) uses console methods", () => {
|
|
937
|
-
setOutputMode("console")
|
|
938
|
-
const log = createLogger("test")
|
|
939
|
-
log.info?.("hello")
|
|
940
|
-
|
|
941
|
-
const consoleOutput = consoleMock.output.filter((o) => o.level === "info")
|
|
942
|
-
expect(consoleOutput).toHaveLength(1)
|
|
943
|
-
})
|
|
944
|
-
})
|