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,618 +0,0 @@
1
- /**
2
- * Tests for distributed tracing features:
3
- * 1. Configurable ID format (simple vs W3C)
4
- * 2. traceparent() header formatting
5
- * 3. AsyncLocalStorage context propagation
6
- * 4. Head-based sampling
7
- * 5. Auto-tagging logs with trace/span ID from context
8
- */
9
-
10
- import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"
11
- import {
12
- createLogger,
13
- enableSpans,
14
- disableSpans,
15
- setLogLevel,
16
- setLogFormat,
17
- setOutputMode,
18
- resetIds,
19
- setTraceFilter,
20
- setDebugFilter,
21
- setIdFormat,
22
- getIdFormat,
23
- traceparent,
24
- setSampleRate,
25
- getSampleRate,
26
- } from "../src/index.ts"
27
- import {
28
- enableContextPropagation,
29
- disableContextPropagation,
30
- getCurrentSpan,
31
- isContextPropagationEnabled,
32
- runInSpanContext,
33
- } from "../src/context.ts"
34
-
35
- // ─────────────────────────────────────────────────────────────────────────────
36
- // Test Helpers
37
- // ─────────────────────────────────────────────────────────────────────────────
38
-
39
- interface CapturedLog {
40
- level: string
41
- message: string
42
- }
43
-
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- const parseJSON = (s: string): Record<string, any> => JSON.parse(s)
46
-
47
- function createConsoleMock() {
48
- const output: CapturedLog[] = []
49
- const capture =
50
- (level: string) =>
51
- (msg: unknown): void => {
52
- output.push({ level, message: String(msg) })
53
- }
54
-
55
- vi.spyOn(console, "debug").mockImplementation(capture("debug"))
56
- vi.spyOn(console, "info").mockImplementation(capture("info"))
57
- vi.spyOn(console, "warn").mockImplementation(capture("warn"))
58
- vi.spyOn(console, "error").mockImplementation(capture("error"))
59
-
60
- vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string | Uint8Array) => {
61
- output.push({ level: "stderr", message: String(chunk) })
62
- return true
63
- }) as typeof process.stderr.write)
64
-
65
- return {
66
- output,
67
- findSpan: () => output.find((o) => o.message.includes("SPAN")),
68
- findSpans: () => output.filter((o) => o.message.includes("SPAN")),
69
- }
70
- }
71
-
72
- let consoleMock: ReturnType<typeof createConsoleMock>
73
-
74
- beforeEach(() => {
75
- resetIds()
76
- setLogLevel("trace")
77
- disableSpans()
78
- setTraceFilter(null)
79
- setDebugFilter(null)
80
- setOutputMode("console")
81
- setLogFormat("console")
82
- setIdFormat("simple")
83
- setSampleRate(1.0)
84
- disableContextPropagation()
85
- consoleMock = createConsoleMock()
86
- })
87
-
88
- afterEach(() => {
89
- vi.restoreAllMocks()
90
- })
91
-
92
- // ─────────────────────────────────────────────────────────────────────────────
93
- // 1. Configurable ID Format
94
- // ─────────────────────────────────────────────────────────────────────────────
95
-
96
- describe("ID format", () => {
97
- test("default format is simple", () => {
98
- expect(getIdFormat()).toBe("simple")
99
- })
100
-
101
- test("simple format produces sp_N and tr_N IDs", () => {
102
- setIdFormat("simple")
103
- const log = createLogger("test")
104
- const span = log.span("work")
105
-
106
- expect(span.spanData.id).toBe("sp_1")
107
- expect(span.spanData.traceId).toBe("tr_1")
108
- span.end()
109
- })
110
-
111
- test("W3C format produces hex IDs of correct length", () => {
112
- setIdFormat("w3c")
113
- const log = createLogger("test")
114
- const span = log.span("work")
115
-
116
- // Span ID: 16 hex chars
117
- expect(span.spanData.id).toMatch(/^[0-9a-f]{16}$/)
118
- // Trace ID: 32 hex chars
119
- expect(span.spanData.traceId).toMatch(/^[0-9a-f]{32}$/)
120
- span.end()
121
- })
122
-
123
- test("W3C IDs are unique", () => {
124
- setIdFormat("w3c")
125
- const log = createLogger("test")
126
- const span1 = log.span("a")
127
- const span2 = log.span("b")
128
-
129
- expect(span1.spanData.id).not.toBe(span2.spanData.id)
130
- // Different root spans get different trace IDs
131
- expect(span1.spanData.traceId).not.toBe(span2.spanData.traceId)
132
-
133
- span1.end()
134
- span2.end()
135
- })
136
-
137
- test("setIdFormat switches between formats", () => {
138
- setIdFormat("simple")
139
- expect(getIdFormat()).toBe("simple")
140
-
141
- setIdFormat("w3c")
142
- expect(getIdFormat()).toBe("w3c")
143
-
144
- const log = createLogger("test")
145
- const span = log.span("work")
146
- expect(span.spanData.id).toMatch(/^[0-9a-f]{16}$/)
147
- span.end()
148
-
149
- setIdFormat("simple")
150
- resetIds()
151
- const span2 = log.span("work2")
152
- expect(span2.spanData.id).toBe("sp_1")
153
- span2.end()
154
- })
155
-
156
- test("nested spans share trace ID in W3C format", () => {
157
- setIdFormat("w3c")
158
- const log = createLogger("test")
159
- const parent = log.span("parent")
160
- const child = parent.span("child")
161
-
162
- expect(child.spanData.traceId).toBe(parent.spanData.traceId)
163
- expect(child.spanData.parentId).toBe(parent.spanData.id)
164
-
165
- child.end()
166
- parent.end()
167
- })
168
- })
169
-
170
- // ─────────────────────────────────────────────────────────────────────────────
171
- // 2. traceparent() Header
172
- // ─────────────────────────────────────────────────────────────────────────────
173
-
174
- describe("traceparent()", () => {
175
- test("formats W3C traceparent header with W3C IDs", () => {
176
- setIdFormat("w3c")
177
- const log = createLogger("test")
178
- const span = log.span("request")
179
-
180
- const header = traceparent(span.spanData)
181
- // Format: 00-{32 hex}-{16 hex}-01
182
- expect(header).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/)
183
-
184
- // Verify it contains the actual IDs
185
- const parts = header.split("-")
186
- expect(parts[0]).toBe("00") // version
187
- expect(parts[1]).toBe(span.spanData.traceId)
188
- expect(parts[2]).toBe(span.spanData.id)
189
- expect(parts[3]).toBe("01") // sampled flag
190
-
191
- span.end()
192
- })
193
-
194
- test("formats traceparent from simple IDs (zero-padded)", () => {
195
- setIdFormat("simple")
196
- const log = createLogger("test")
197
- const span = log.span("request")
198
-
199
- const header = traceparent(span.spanData)
200
- // Should still produce valid traceparent format
201
- expect(header).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/)
202
-
203
- span.end()
204
- })
205
-
206
- test("traceparent can be used as HTTP header", () => {
207
- setIdFormat("w3c")
208
- const log = createLogger("test")
209
- const span = log.span("request")
210
-
211
- const header = traceparent(span.spanData)
212
-
213
- // Simulate setting as HTTP header
214
- const headers = new Headers()
215
- headers.set("traceparent", header)
216
- expect(headers.get("traceparent")).toBe(header)
217
-
218
- span.end()
219
- })
220
- })
221
-
222
- // ─────────────────────────────────────────────────────────────────────────────
223
- // 3. AsyncLocalStorage Context Propagation
224
- // ─────────────────────────────────────────────────────────────────────────────
225
-
226
- describe("context propagation", () => {
227
- test("disabled by default", () => {
228
- expect(isContextPropagationEnabled()).toBe(false)
229
- expect(getCurrentSpan()).toBeNull()
230
- })
231
-
232
- test("enableContextPropagation enables it", () => {
233
- enableContextPropagation()
234
- expect(isContextPropagationEnabled()).toBe(true)
235
- })
236
-
237
- test("disableContextPropagation disables it", () => {
238
- enableContextPropagation()
239
- disableContextPropagation()
240
- expect(isContextPropagationEnabled()).toBe(false)
241
- })
242
-
243
- test("getCurrentSpan returns null when no span is active", () => {
244
- enableContextPropagation()
245
- expect(getCurrentSpan()).toBeNull()
246
- })
247
-
248
- test("getCurrentSpan returns current span context within a span", () => {
249
- enableContextPropagation()
250
- const log = createLogger("test")
251
-
252
- {
253
- using span = log.span("request")
254
- const current = getCurrentSpan()
255
-
256
- expect(current).not.toBeNull()
257
- expect(current!.spanId).toBe(span.spanData.id)
258
- expect(current!.traceId).toBe(span.spanData.traceId)
259
- }
260
- })
261
-
262
- test("getCurrentSpan returns null after span ends", () => {
263
- enableContextPropagation()
264
- const log = createLogger("test")
265
-
266
- {
267
- using span = log.span("request")
268
- expect(getCurrentSpan()).not.toBeNull()
269
- }
270
-
271
- // After span disposal, context should be cleared
272
- expect(getCurrentSpan()).toBeNull()
273
- })
274
-
275
- test("nested spans auto-parent via context", () => {
276
- enableContextPropagation()
277
- const log = createLogger("test")
278
- // Create a separate logger that doesn't share span hierarchy
279
- const log2 = createLogger("other")
280
-
281
- {
282
- using parentSpan = log.span("parent")
283
- // A span created by a DIFFERENT logger still gets parented
284
- // because of AsyncLocalStorage context
285
- const childSpan = log2.span("child")
286
-
287
- expect(childSpan.spanData.parentId).toBe(parentSpan.spanData.id)
288
- expect(childSpan.spanData.traceId).toBe(parentSpan.spanData.traceId)
289
-
290
- childSpan.end()
291
- }
292
- })
293
-
294
- test("context propagation works across async boundaries", async () => {
295
- enableContextPropagation()
296
- const log = createLogger("test")
297
-
298
- const span = log.span("async-parent")
299
-
300
- // Simulate async work
301
- await new Promise<void>((resolve) => {
302
- setTimeout(() => {
303
- const current = getCurrentSpan()
304
- expect(current).not.toBeNull()
305
- expect(current!.spanId).toBe(span.spanData.id)
306
- resolve()
307
- }, 10)
308
- })
309
-
310
- span.end()
311
- })
312
-
313
- test("runInSpanContext scopes context to callback", () => {
314
- enableContextPropagation()
315
-
316
- const ctx = { spanId: "custom-span", traceId: "custom-trace", parentId: null }
317
-
318
- const result = runInSpanContext(ctx, () => {
319
- const current = getCurrentSpan()
320
- expect(current).not.toBeNull()
321
- expect(current!.spanId).toBe("custom-span")
322
- expect(current!.traceId).toBe("custom-trace")
323
- return 42
324
- })
325
-
326
- expect(result).toBe(42)
327
- })
328
-
329
- test("context propagation is no-op when disabled", () => {
330
- // Don't enable context propagation
331
- const log = createLogger("test")
332
-
333
- {
334
- using span = log.span("request")
335
- expect(getCurrentSpan()).toBeNull()
336
- }
337
- })
338
- })
339
-
340
- // ─────────────────────────────────────────────────────────────────────────────
341
- // 4. Head-Based Sampling
342
- // ─────────────────────────────────────────────────────────────────────────────
343
-
344
- describe("sampling", () => {
345
- test("default sample rate is 1.0 (everything sampled)", () => {
346
- expect(getSampleRate()).toBe(1.0)
347
- })
348
-
349
- test("setSampleRate validates range", () => {
350
- expect(() => setSampleRate(-0.1)).toThrow("between 0.0 and 1.0")
351
- expect(() => setSampleRate(1.1)).toThrow("between 0.0 and 1.0")
352
- })
353
-
354
- test("sample rate 0.0 suppresses all span output", () => {
355
- enableSpans()
356
- setSampleRate(0.0)
357
- const log = createLogger("test")
358
-
359
- for (let i = 0; i < 10; i++) {
360
- using span = log.span(`work-${i}`)
361
- }
362
-
363
- expect(consoleMock.findSpans()).toHaveLength(0)
364
- })
365
-
366
- test("sample rate 1.0 keeps all span output", () => {
367
- enableSpans()
368
- setSampleRate(1.0)
369
- const log = createLogger("test")
370
-
371
- for (let i = 0; i < 5; i++) {
372
- using span = log.span(`work-${i}`)
373
- }
374
-
375
- expect(consoleMock.findSpans()).toHaveLength(5)
376
- })
377
-
378
- test("sampling is head-based: decided at trace creation", () => {
379
- enableSpans()
380
- setSampleRate(0.0)
381
- const log = createLogger("test")
382
-
383
- // Create a root span — should be unsampled (rate=0)
384
- const root = log.span("root")
385
- // Reset rate — but sampling decision was already made
386
- setSampleRate(1.0)
387
- // Child spans inherit parent's sampling decision
388
- {
389
- using child = root.span("child")
390
- }
391
- root.end()
392
-
393
- // Even though rate is now 1.0, the root was created at 0.0
394
- expect(consoleMock.findSpans()).toHaveLength(0)
395
- })
396
-
397
- test("child spans are always sampled when parent is sampled", () => {
398
- enableSpans()
399
- setSampleRate(1.0)
400
- const log = createLogger("test")
401
-
402
- const root = log.span("root")
403
- // Lower rate after root creation — children should still be sampled
404
- setSampleRate(0.0)
405
- {
406
- using child = root.span("child")
407
- }
408
- root.end()
409
-
410
- // Root was sampled at 1.0, child inherits
411
- expect(consoleMock.findSpans()).toHaveLength(2)
412
- })
413
-
414
- test("partial sample rate produces some output", () => {
415
- enableSpans()
416
- setSampleRate(0.5)
417
-
418
- // Use seeded random for deterministic test
419
- let callCount = 0
420
- vi.spyOn(Math, "random").mockImplementation(() => {
421
- callCount++
422
- // Alternate: 0.3 (sampled), 0.7 (not sampled), 0.3, 0.7, ...
423
- return callCount % 2 === 1 ? 0.3 : 0.7
424
- })
425
-
426
- const log = createLogger("test")
427
-
428
- for (let i = 0; i < 4; i++) {
429
- using span = log.span(`work-${i}`)
430
- }
431
-
432
- // With alternating random values and 0.5 rate: 2 sampled, 2 not
433
- expect(consoleMock.findSpans()).toHaveLength(2)
434
- })
435
-
436
- test("span data is still available even when not sampled", () => {
437
- setSampleRate(0.0)
438
- const log = createLogger("test")
439
- const span = log.span("work")
440
-
441
- // spanData should still work — sampling only affects output
442
- span.spanData.count = 42
443
- expect(span.spanData.count).toBe(42)
444
- expect(span.spanData.id).toBeDefined()
445
- expect(span.spanData.traceId).toBeDefined()
446
-
447
- span.end()
448
- expect(span.spanData.duration).toBeGreaterThanOrEqual(0)
449
- })
450
- })
451
-
452
- // ─────────────────────────────────────────────────────────────────────────────
453
- // 5. Auto-Tagging Logs with Context
454
- // ─────────────────────────────────────────────────────────────────────────────
455
-
456
- describe("auto-tagging with context", () => {
457
- test("logs include trace_id and span_id when context is active", () => {
458
- enableContextPropagation()
459
- setLogFormat("json")
460
- const log = createLogger("test")
461
-
462
- {
463
- using span = log.span("request")
464
- log.info?.("inside span")
465
-
466
- const output = consoleMock.output.find((o) => {
467
- try {
468
- const parsed = parseJSON(o.message)
469
- return parsed.msg === "inside span"
470
- } catch {
471
- return false
472
- }
473
- })
474
- expect(output).toBeDefined()
475
-
476
- const parsed = parseJSON(output!.message)
477
- expect(parsed.trace_id).toBe(span.spanData.traceId)
478
- expect(parsed.span_id).toBe(span.spanData.id)
479
- }
480
- })
481
-
482
- test("logs do NOT include trace_id/span_id without context propagation", () => {
483
- // Context propagation disabled by default
484
- setLogFormat("json")
485
- const log = createLogger("test")
486
-
487
- {
488
- using span = log.span("request")
489
- log.info?.("no context")
490
-
491
- const output = consoleMock.output.find((o) => {
492
- try {
493
- const parsed = parseJSON(o.message)
494
- return parsed.msg === "no context"
495
- } catch {
496
- return false
497
- }
498
- })
499
- expect(output).toBeDefined()
500
-
501
- const parsed = parseJSON(output!.message)
502
- expect(parsed.trace_id).toBeUndefined()
503
- expect(parsed.span_id).toBeUndefined()
504
- }
505
- })
506
-
507
- test("logs outside a span have no trace tags", () => {
508
- enableContextPropagation()
509
- setLogFormat("json")
510
- const log = createLogger("test")
511
-
512
- log.info?.("outside span")
513
-
514
- const parsed = parseJSON(consoleMock.output[0]!.message)
515
- expect(parsed.trace_id).toBeUndefined()
516
- expect(parsed.span_id).toBeUndefined()
517
- })
518
-
519
- test("auto-tags work with console format too", () => {
520
- enableContextPropagation()
521
- const log = createLogger("test")
522
-
523
- {
524
- using span = log.span("request")
525
- log.info?.("tagged message")
526
-
527
- const output = consoleMock.output.find((o) => o.message.includes("tagged message"))
528
- expect(output).toBeDefined()
529
- expect(output!.message).toContain("trace_id")
530
- expect(output!.message).toContain("span_id")
531
- }
532
- })
533
-
534
- test("per-call data overrides context tags", () => {
535
- enableContextPropagation()
536
- setLogFormat("json")
537
- const log = createLogger("test")
538
-
539
- {
540
- using span = log.span("request")
541
- log.info?.("override test", { trace_id: "custom-trace" })
542
-
543
- const output = consoleMock.output.find((o) => {
544
- try {
545
- const parsed = parseJSON(o.message)
546
- return parsed.msg === "override test"
547
- } catch {
548
- return false
549
- }
550
- })
551
-
552
- const parsed = parseJSON(output!.message)
553
- // Per-call data wins over context
554
- expect(parsed.trace_id).toBe("custom-trace")
555
- }
556
- })
557
- })
558
-
559
- // ─────────────────────────────────────────────────────────────────────────────
560
- // Integration: Multiple features together
561
- // ─────────────────────────────────────────────────────────────────────────────
562
-
563
- describe("integration", () => {
564
- test("W3C IDs + traceparent + context propagation", () => {
565
- setIdFormat("w3c")
566
- enableContextPropagation()
567
- const log = createLogger("test")
568
-
569
- {
570
- using span = log.span("request")
571
- const header = traceparent(span.spanData)
572
-
573
- // Valid W3C traceparent
574
- expect(header).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/)
575
-
576
- // Context is set
577
- const current = getCurrentSpan()
578
- expect(current).not.toBeNull()
579
- expect(current!.spanId).toBe(span.spanData.id)
580
- }
581
- })
582
-
583
- test("sampling + context propagation", () => {
584
- enableContextPropagation()
585
- enableSpans()
586
- setSampleRate(1.0)
587
- setLogFormat("json")
588
- const log = createLogger("test")
589
-
590
- {
591
- using span = log.span("sampled")
592
- log.info?.("in sampled span")
593
- }
594
-
595
- // Span output exists (JSON format uses lowercase "span" as level)
596
- const spanOutput = consoleMock.output.find((o) => {
597
- try {
598
- return parseJSON(o.message).level === "span"
599
- } catch {
600
- return false
601
- }
602
- })
603
- expect(spanOutput).toBeDefined()
604
-
605
- // Log was auto-tagged
606
- const logOutput = consoleMock.output.find((o) => {
607
- try {
608
- return parseJSON(o.message).msg === "in sampled span"
609
- } catch {
610
- return false
611
- }
612
- })
613
- expect(logOutput).toBeDefined()
614
- const parsed = parseJSON(logOutput!.message)
615
- expect(parsed.trace_id).toBeDefined()
616
- expect(parsed.span_id).toBeDefined()
617
- })
618
- })