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,552 +0,0 @@
1
- /**
2
- * Tests for new logger features:
3
- * 1. Lazy string interpolation
4
- * 2. Child loggers with context
5
- * 3. Structured logging (LOG_FORMAT=json)
6
- * 4. Async file writer
7
- */
8
-
9
- import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"
10
- import { existsSync, readFileSync, unlinkSync } from "fs"
11
- import { join } from "path"
12
- import { tmpdir } from "os"
13
- import {
14
- createLogger,
15
- setLogLevel,
16
- setLogFormat,
17
- getLogFormat,
18
- setOutputMode,
19
- resetIds,
20
- disableSpans,
21
- enableSpans,
22
- setTraceFilter,
23
- setDebugFilter,
24
- createFileWriter,
25
- addWriter,
26
- type FileWriter,
27
- } from "../src/index.ts"
28
-
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- const parseJSON = (s: string): Record<string, any> => JSON.parse(s)
31
-
32
- // ─────────────────────────────────────────────────────────────────────────────
33
- // Test Helpers
34
- // ─────────────────────────────────────────────────────────────────────────────
35
-
36
- interface CapturedLog {
37
- level: string
38
- message: string
39
- }
40
-
41
- /** Create a mock console that captures output */
42
- function createConsoleMock() {
43
- const output: CapturedLog[] = []
44
- const capture =
45
- (level: string) =>
46
- (msg: unknown): void => {
47
- output.push({ level, message: String(msg) })
48
- }
49
-
50
- vi.spyOn(console, "debug").mockImplementation(capture("debug"))
51
- vi.spyOn(console, "info").mockImplementation(capture("info"))
52
- vi.spyOn(console, "warn").mockImplementation(capture("warn"))
53
- vi.spyOn(console, "error").mockImplementation(capture("error"))
54
-
55
- vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string | Uint8Array) => {
56
- output.push({ level: "stderr", message: String(chunk) })
57
- return true
58
- }) as typeof process.stderr.write)
59
-
60
- return { output }
61
- }
62
-
63
- let consoleMock: ReturnType<typeof createConsoleMock>
64
-
65
- beforeEach(() => {
66
- resetIds()
67
- setLogLevel("trace")
68
- disableSpans()
69
- setTraceFilter(null)
70
- setDebugFilter(null)
71
- setOutputMode("console")
72
- setLogFormat("console")
73
- consoleMock = createConsoleMock()
74
- })
75
-
76
- afterEach(() => {
77
- vi.restoreAllMocks()
78
- })
79
-
80
- // ─────────────────────────────────────────────────────────────────────────────
81
- // 1. Lazy String Interpolation
82
- // ─────────────────────────────────────────────────────────────────────────────
83
-
84
- describe("lazy string interpolation", () => {
85
- test("accepts a function that returns a string", () => {
86
- const log = createLogger("test")
87
- log.info?.(() => "lazy message")
88
-
89
- expect(consoleMock.output).toHaveLength(1)
90
- expect(consoleMock.output[0]!.message).toContain("lazy message")
91
- })
92
-
93
- test("function is called when level is enabled", () => {
94
- const fn = vi.fn(() => "computed value")
95
- const log = createLogger("test")
96
- log.info?.(fn)
97
-
98
- expect(fn).toHaveBeenCalledTimes(1)
99
- expect(consoleMock.output[0]!.message).toContain("computed value")
100
- })
101
-
102
- test("function is NOT called when level is disabled", () => {
103
- setLogLevel("error")
104
- const fn = vi.fn(() => "expensive computation")
105
- const log = createLogger("test")
106
-
107
- // debug is disabled at error level, so fn should never be called
108
- log.debug?.(fn)
109
-
110
- expect(fn).not.toHaveBeenCalled()
111
- expect(consoleMock.output).toHaveLength(0)
112
- })
113
-
114
- test("function is NOT called when namespace is filtered out", () => {
115
- setDebugFilter(["allowed"])
116
- const fn = vi.fn(() => "expensive computation")
117
- const log = createLogger("blocked")
118
-
119
- log.info?.(fn)
120
-
121
- expect(fn).not.toHaveBeenCalled()
122
- expect(consoleMock.output).toHaveLength(0)
123
- })
124
-
125
- test("string messages still work unchanged", () => {
126
- const log = createLogger("test")
127
- log.info?.("plain string")
128
-
129
- expect(consoleMock.output).toHaveLength(1)
130
- expect(consoleMock.output[0]!.message).toContain("plain string")
131
- })
132
-
133
- test("lazy messages work with data parameter", () => {
134
- const log = createLogger("test")
135
- log.info?.(() => "lazy with data", { key: "value" })
136
-
137
- expect(consoleMock.output).toHaveLength(1)
138
- expect(consoleMock.output[0]!.message).toContain("lazy with data")
139
- expect(consoleMock.output[0]!.message).toContain("key")
140
- })
141
-
142
- test("lazy messages work with all log levels", () => {
143
- const log = createLogger("test")
144
-
145
- log.trace?.(() => "trace lazy")
146
- log.debug?.(() => "debug lazy")
147
- log.info?.(() => "info lazy")
148
- log.warn?.(() => "warn lazy")
149
- log.error?.(() => "error lazy")
150
-
151
- expect(consoleMock.output).toHaveLength(5)
152
- expect(consoleMock.output[0]!.message).toContain("trace lazy")
153
- expect(consoleMock.output[4]!.message).toContain("error lazy")
154
- })
155
-
156
- test("lazy messages work in JSON format", () => {
157
- setLogFormat("json")
158
- const log = createLogger("test")
159
- log.info?.(() => "json lazy")
160
-
161
- const parsed = parseJSON(consoleMock.output[0]!.message)
162
- expect(parsed.msg).toBe("json lazy")
163
- })
164
- })
165
-
166
- // ─────────────────────────────────────────────────────────────────────────────
167
- // 2. Child Loggers with Context
168
- // ─────────────────────────────────────────────────────────────────────────────
169
-
170
- describe("child loggers with context", () => {
171
- test("child({...}) creates logger with context fields", () => {
172
- const log = createLogger("app")
173
- const child = log.child({ requestId: "abc-123" })
174
-
175
- child.info?.("handling request")
176
-
177
- expect(consoleMock.output).toHaveLength(1)
178
- expect(consoleMock.output[0]!.message).toContain("requestId")
179
- expect(consoleMock.output[0]!.message).toContain("abc-123")
180
- })
181
-
182
- test("child keeps parent namespace", () => {
183
- const log = createLogger("app")
184
- const child = log.child({ requestId: "abc" })
185
-
186
- expect(child.name).toBe("app")
187
- })
188
-
189
- test("child inherits parent props", () => {
190
- const log = createLogger("app", { version: "1.0" })
191
- const child = log.child({ requestId: "abc" })
192
-
193
- expect(child.props).toEqual({ version: "1.0", requestId: "abc" })
194
- })
195
-
196
- test("child context is included in every log message", () => {
197
- setLogFormat("json")
198
- const log = createLogger("app")
199
- const child = log.child({ requestId: "abc" })
200
-
201
- child.info?.("first")
202
- child.warn?.("second")
203
-
204
- const first = parseJSON(consoleMock.output[0]!.message)
205
- const second = parseJSON(consoleMock.output[1]!.message)
206
- expect(first.requestId).toBe("abc")
207
- expect(second.requestId).toBe("abc")
208
- })
209
-
210
- test("child context merges with per-call data", () => {
211
- setLogFormat("json")
212
- const log = createLogger("app")
213
- const child = log.child({ requestId: "abc" })
214
-
215
- child.info?.("msg", { extra: "data" })
216
-
217
- const parsed = parseJSON(consoleMock.output[0]!.message)
218
- expect(parsed.requestId).toBe("abc")
219
- expect(parsed.extra).toBe("data")
220
- })
221
-
222
- test("nested children accumulate context", () => {
223
- setLogFormat("json")
224
- const log = createLogger("app")
225
- const child1 = log.child({ requestId: "abc" })
226
- const child2 = child1.child({ userId: "user-1" })
227
-
228
- child2.info?.("nested context")
229
-
230
- const parsed = parseJSON(consoleMock.output[0]!.message)
231
- expect(parsed.requestId).toBe("abc")
232
- expect(parsed.userId).toBe("user-1")
233
- })
234
-
235
- test("child context overrides parent props on conflict", () => {
236
- setLogFormat("json")
237
- const log = createLogger("app", { env: "prod" })
238
- const child = log.child({ env: "test" })
239
-
240
- child.info?.("override")
241
-
242
- const parsed = parseJSON(consoleMock.output[0]!.message)
243
- expect(parsed.env).toBe("test")
244
- })
245
-
246
- test("deprecated string child still works", () => {
247
- const log = createLogger("app")
248
- const child = log.child("import")
249
-
250
- expect(child.name).toBe("app:import")
251
- })
252
-
253
- test("child can create spans", () => {
254
- enableSpans()
255
- const log = createLogger("app")
256
- const child = log.child({ requestId: "abc" })
257
-
258
- {
259
- using span = child.span("work")
260
- span.info?.("working")
261
- }
262
-
263
- // Check span output includes the context
264
- const spanOutput = consoleMock.output.find((o) => o.message.includes("SPAN"))
265
- expect(spanOutput).toBeDefined()
266
- expect(spanOutput!.message).toContain("requestId")
267
- })
268
-
269
- test("child can create further children via .logger()", () => {
270
- const log = createLogger("app")
271
- const child = log.child({ requestId: "abc" })
272
- const subLogger = child.logger("db")
273
-
274
- expect(subLogger.name).toBe("app:db")
275
- expect(subLogger.props).toEqual({ requestId: "abc" })
276
- })
277
- })
278
-
279
- // ─────────────────────────────────────────────────────────────────────────────
280
- // 3. Structured Logging (LOG_FORMAT=json)
281
- // ─────────────────────────────────────────────────────────────────────────────
282
-
283
- describe("LOG_FORMAT configuration", () => {
284
- test("setLogFormat('json') produces JSON output", () => {
285
- setLogFormat("json")
286
- const log = createLogger("test")
287
-
288
- log.info?.("json message", { key: "value" })
289
-
290
- const parsed = parseJSON(consoleMock.output[0]!.message)
291
- expect(parsed.level).toBe("info")
292
- expect(parsed.name).toBe("test")
293
- expect(parsed.msg).toBe("json message")
294
- expect(parsed.key).toBe("value")
295
- expect(parsed.time).toBeDefined()
296
- })
297
-
298
- test("setLogFormat('console') produces human-readable output", () => {
299
- setLogFormat("console")
300
- const log = createLogger("test")
301
-
302
- log.info?.("console message")
303
-
304
- const output = consoleMock.output[0]!.message
305
- expect(output).toContain("INFO")
306
- expect(output).toContain("test")
307
- expect(output).toContain("console message")
308
- // Should not be valid JSON
309
- expect(() => parseJSON(output)).toThrow()
310
- })
311
-
312
- test("getLogFormat returns current format", () => {
313
- expect(getLogFormat()).toBe("console")
314
-
315
- setLogFormat("json")
316
- expect(getLogFormat()).toBe("json")
317
-
318
- setLogFormat("console")
319
- expect(getLogFormat()).toBe("console")
320
- })
321
-
322
- test("JSON format includes all props", () => {
323
- setLogFormat("json")
324
- const log = createLogger("test", { app: "myapp", version: "1.0" })
325
-
326
- log.info?.("message")
327
-
328
- const parsed = parseJSON(consoleMock.output[0]!.message)
329
- expect(parsed.app).toBe("myapp")
330
- expect(parsed.version).toBe("1.0")
331
- })
332
-
333
- test("JSON format handles errors", () => {
334
- setLogFormat("json")
335
- const log = createLogger("test")
336
- const err = new Error("json error")
337
-
338
- log.error?.(err)
339
-
340
- const parsed = parseJSON(consoleMock.output[0]!.message)
341
- expect(parsed.msg).toBe("json error")
342
- expect(parsed.error_type).toBe("Error")
343
- })
344
-
345
- test("JSON format works with spans", () => {
346
- setLogFormat("json")
347
- enableSpans()
348
- const log = createLogger("test")
349
-
350
- {
351
- using span = log.span("work")
352
- span.spanData.items = 5
353
- }
354
-
355
- const spanOutput = consoleMock.output.find((o) => {
356
- try {
357
- const parsed = parseJSON(o.message)
358
- return parsed.level === "span"
359
- } catch {
360
- return false
361
- }
362
- })
363
- expect(spanOutput).toBeDefined()
364
-
365
- const parsed = parseJSON(spanOutput!.message)
366
- expect(parsed.level).toBe("span")
367
- expect(parsed.items).toBe(5)
368
- })
369
-
370
- test("JSON output has standard fields: time, level, name, msg", () => {
371
- setLogFormat("json")
372
- const log = createLogger("myapp")
373
-
374
- log.info?.("request handled")
375
-
376
- const parsed = parseJSON(consoleMock.output[0]!.message)
377
- expect(parsed).toHaveProperty("time")
378
- expect(parsed).toHaveProperty("level", "info")
379
- expect(parsed).toHaveProperty("name", "myapp")
380
- expect(parsed).toHaveProperty("msg", "request handled")
381
- // time should be ISO format
382
- expect(new Date(parsed.time).toISOString()).toBe(parsed.time)
383
- })
384
-
385
- describe("LOG_FORMAT env var", () => {
386
- let originalLogFormat: string | undefined
387
-
388
- beforeEach(() => {
389
- originalLogFormat = process.env.LOG_FORMAT
390
- })
391
-
392
- afterEach(() => {
393
- if (originalLogFormat === undefined) {
394
- delete process.env.LOG_FORMAT
395
- } else {
396
- process.env.LOG_FORMAT = originalLogFormat
397
- }
398
- })
399
-
400
- test("LOG_FORMAT=json is respected at init time (tested via setLogFormat)", () => {
401
- // The env var is read at module load time, so we test the API directly
402
- setLogFormat("json")
403
- const log = createLogger("test")
404
-
405
- log.info?.("env json")
406
-
407
- const parsed = parseJSON(consoleMock.output[0]!.message)
408
- expect(parsed.msg).toBe("env json")
409
- })
410
- })
411
- })
412
-
413
- // ─────────────────────────────────────────────────────────────────────────────
414
- // 4. Async File Writer
415
- // ─────────────────────────────────────────────────────────────────────────────
416
-
417
- describe("createFileWriter", () => {
418
- let testFile: string
419
- let writer: FileWriter | null = null
420
-
421
- beforeEach(() => {
422
- testFile = join(tmpdir(), `logger-test-${Date.now()}-${Math.random().toString(36).slice(2)}.log`)
423
- })
424
-
425
- afterEach(() => {
426
- writer?.close()
427
- writer = null
428
- if (existsSync(testFile)) {
429
- unlinkSync(testFile)
430
- }
431
- })
432
-
433
- test("writes lines to file", () => {
434
- writer = createFileWriter(testFile, { bufferSize: 1 }) // tiny buffer = immediate flush
435
- writer.write("line one")
436
- writer.write("line two")
437
- writer.flush()
438
-
439
- const content = readFileSync(testFile, "utf-8")
440
- expect(content).toContain("line one\n")
441
- expect(content).toContain("line two\n")
442
- })
443
-
444
- test("flushes on buffer size threshold", () => {
445
- writer = createFileWriter(testFile, { bufferSize: 10, flushInterval: 60000 })
446
-
447
- // Write enough to exceed 10 bytes
448
- writer.write("hello world this is a long line")
449
-
450
- // Should have flushed automatically
451
- const content = readFileSync(testFile, "utf-8")
452
- expect(content).toContain("hello world")
453
- })
454
-
455
- test("flush() writes buffer to disk", () => {
456
- writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 60000 })
457
-
458
- writer.write("buffered line")
459
- // Not yet flushed (buffer is large, interval is long)
460
- const before = existsSync(testFile) ? readFileSync(testFile, "utf-8") : ""
461
-
462
- writer.flush()
463
- const after = readFileSync(testFile, "utf-8")
464
- expect(after).toContain("buffered line\n")
465
- })
466
-
467
- test("close() flushes remaining buffer and closes fd", () => {
468
- writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 60000 })
469
-
470
- writer.write("final line")
471
- writer.close()
472
- writer = null // prevent double close in afterEach
473
-
474
- const content = readFileSync(testFile, "utf-8")
475
- expect(content).toContain("final line\n")
476
- })
477
-
478
- test("writes are ignored after close", () => {
479
- writer = createFileWriter(testFile, { bufferSize: 1 })
480
- writer.write("before close")
481
- writer.close()
482
-
483
- // This should not throw or write
484
- writer.write("after close")
485
- writer = null
486
-
487
- const content = readFileSync(testFile, "utf-8")
488
- expect(content).toContain("before close")
489
- expect(content).not.toContain("after close")
490
- })
491
-
492
- test("integrates with addWriter", () => {
493
- writer = createFileWriter(testFile, { bufferSize: 1 })
494
- const unsubscribe = addWriter((formatted) => writer!.write(formatted))
495
-
496
- const log = createLogger("test")
497
- log.info?.("writer integration")
498
-
499
- writer.flush()
500
- unsubscribe()
501
-
502
- const content = readFileSync(testFile, "utf-8")
503
- expect(content).toContain("writer integration")
504
- })
505
-
506
- test("flushes on interval", async () => {
507
- writer = createFileWriter(testFile, { bufferSize: 999999, flushInterval: 50 })
508
-
509
- writer.write("interval line")
510
-
511
- // Wait for the flush interval to fire
512
- await new Promise((resolve) => setTimeout(resolve, 100))
513
-
514
- const content = readFileSync(testFile, "utf-8")
515
- expect(content).toContain("interval line\n")
516
- })
517
-
518
- test("multiple close calls are safe", () => {
519
- writer = createFileWriter(testFile, { bufferSize: 1 })
520
- writer.write("data")
521
- writer.close()
522
- // Should not throw
523
- writer.close()
524
- writer = null
525
- })
526
-
527
- test("creates file if it does not exist", () => {
528
- expect(existsSync(testFile)).toBe(false)
529
- writer = createFileWriter(testFile)
530
- writer.write("new file")
531
- writer.flush()
532
-
533
- expect(existsSync(testFile)).toBe(true)
534
- expect(readFileSync(testFile, "utf-8")).toContain("new file")
535
- })
536
-
537
- test("appends to existing file", () => {
538
- // Create file with initial content
539
- const w1 = createFileWriter(testFile, { bufferSize: 1 })
540
- w1.write("first")
541
- w1.close()
542
-
543
- // Open again and append
544
- writer = createFileWriter(testFile, { bufferSize: 1 })
545
- writer.write("second")
546
- writer.flush()
547
-
548
- const content = readFileSync(testFile, "utf-8")
549
- expect(content).toContain("first\n")
550
- expect(content).toContain("second\n")
551
- })
552
- })