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/tracing.test.ts
DELETED
|
@@ -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
|
-
})
|