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.
Files changed (46) hide show
  1. package/README.md +67 -22
  2. package/package.json +24 -11
  3. package/src/context.ts +26 -11
  4. package/src/core.ts +118 -72
  5. package/src/file-writer.ts +12 -6
  6. package/src/index.browser.ts +9 -1
  7. package/src/index.ts +9 -1
  8. package/src/tracing.ts +11 -3
  9. package/src/worker.ts +119 -132
  10. package/.github/workflows/docs.yml +0 -58
  11. package/.github/workflows/release.yml +0 -31
  12. package/.github/workflows/test.yml +0 -20
  13. package/CHANGELOG.md +0 -45
  14. package/CLAUDE.md +0 -299
  15. package/CONTRIBUTING.md +0 -58
  16. package/benchmarks/overhead.ts +0 -267
  17. package/bun.lock +0 -479
  18. package/docs/api-reference.md +0 -400
  19. package/docs/benchmarks.md +0 -106
  20. package/docs/comparison.md +0 -315
  21. package/docs/conditional-logging-research.md +0 -159
  22. package/docs/guide.md +0 -205
  23. package/docs/migration-from-debug.md +0 -310
  24. package/docs/migration-from-pino.md +0 -178
  25. package/docs/migration-from-winston.md +0 -179
  26. package/docs/site/.vitepress/config.ts +0 -67
  27. package/docs/site/api/configuration.md +0 -94
  28. package/docs/site/api/index.md +0 -61
  29. package/docs/site/api/logger.md +0 -99
  30. package/docs/site/api/worker.md +0 -120
  31. package/docs/site/api/writers.md +0 -69
  32. package/docs/site/guide/getting-started.md +0 -143
  33. package/docs/site/guide/journey.md +0 -203
  34. package/docs/site/guide/migration-from-debug.md +0 -24
  35. package/docs/site/guide/spans.md +0 -139
  36. package/docs/site/guide/why.md +0 -55
  37. package/docs/site/guide/workers.md +0 -113
  38. package/docs/site/guide/zero-overhead.md +0 -87
  39. package/docs/site/index.md +0 -54
  40. package/tests/features.test.ts +0 -552
  41. package/tests/logger.test.ts +0 -944
  42. package/tests/tracing.test.ts +0 -618
  43. package/tests/universal.test.ts +0 -107
  44. package/tests/worker.test.ts +0 -590
  45. package/tsconfig.json +0 -20
  46. package/vitest.config.ts +0 -10
@@ -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
- })