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,400 +0,0 @@
1
- # API Reference
2
-
3
- Complete API documentation for loggily.
4
-
5
- ## Table of Contents
6
-
7
- - [createLogger](#createlogger)
8
- - [Logger Interface](#logger-interface)
9
- - [SpanLogger Interface](#spanlogger-interface)
10
- - [Configuration](#configuration)
11
- - [Writers](#writers)
12
- - [Types](#types)
13
- - [Span Collection](#span-collection)
14
-
15
- ---
16
-
17
- ## createLogger
18
-
19
- ```typescript
20
- function createLogger(name: string, props?: Record<string, unknown>): ConditionalLogger
21
- ```
22
-
23
- Create a logger. Disabled log levels return `undefined` -- use optional chaining (`?.`) to skip argument evaluation.
24
-
25
- ### Parameters
26
-
27
- | Parameter | Type | Required | Description |
28
- | --------- | ------------------------- | -------- | ------------------------------------------------ |
29
- | `name` | `string` | Yes | Logger namespace (e.g., `"myapp"`, `"myapp:db"`) |
30
- | `props` | `Record<string, unknown>` | No | Properties included in every log message |
31
-
32
- ### Returns
33
-
34
- `ConditionalLogger` -- A logger where disabled levels return `undefined`.
35
-
36
- ### Examples
37
-
38
- ```typescript
39
- const log = createLogger("myapp")
40
- log.info?.("starting")
41
-
42
- // With props (inherited by all children)
43
- const log = createLogger("myapp", { version: "1.0", env: "prod" })
44
- const dbLog = log.logger("db")
45
- // dbLog.props === { version: "1.0", env: "prod" }
46
- ```
47
-
48
- ---
49
-
50
- ## Logger Interface
51
-
52
- ```typescript
53
- interface Logger {
54
- readonly name: string
55
- readonly props: Readonly<Record<string, unknown>>
56
- readonly spanData: SpanData | null
57
-
58
- trace(message: LazyMessage, data?: Record<string, unknown>): void
59
- debug(message: LazyMessage, data?: Record<string, unknown>): void
60
- info(message: LazyMessage, data?: Record<string, unknown>): void
61
- warn(message: LazyMessage, data?: Record<string, unknown>): void
62
- error(message: LazyMessage, data?: Record<string, unknown>): void
63
- error(error: Error, data?: Record<string, unknown>): void
64
-
65
- logger(namespace?: string, props?: Record<string, unknown>): Logger
66
- span(namespace?: string, props?: Record<string, unknown>): SpanLogger
67
- child(context: Record<string, unknown>): Logger
68
- /** @deprecated Use .logger() instead */
69
- child(context: string): Logger
70
- end(): void
71
- }
72
- ```
73
-
74
- ### Properties
75
-
76
- | Property | Type | Description |
77
- | ---------- | ------------------------- | ------------------------------------------- |
78
- | `name` | `string` | Logger namespace (e.g., `"myapp:import"`) |
79
- | `props` | `Record<string, unknown>` | Frozen props (own + inherited from parent) |
80
- | `spanData` | `SpanData \| null` | Non-null for span loggers, null for regular |
81
-
82
- ### Logging Methods
83
-
84
- All methods accept a string, a lazy function `() => string`, or (for `.error()`) an Error object:
85
-
86
- ```typescript
87
- // String message
88
- log.info?.("server started", { port: 3000 })
89
-
90
- // Lazy message (function called only when level is enabled)
91
- log.debug?.(() => `tree: ${JSON.stringify(buildDebugTree())}`)
92
-
93
- // Error object (extracts message, stack, code automatically)
94
- log.error?.(new Error("connection failed"), { host: "db.example.com" })
95
- ```
96
-
97
- #### Log Levels
98
-
99
- | Level | Priority | Purpose |
100
- | ------- | -------- | -------------------------------------------- |
101
- | `trace` | 0 | Verbose debugging (hot paths, detailed flow) |
102
- | `debug` | 1 | Debug information (state changes, decisions) |
103
- | `info` | 2 | Normal operation (startup, completion) |
104
- | `warn` | 3 | Recoverable issues (deprecations, retries) |
105
- | `error` | 4 | Failures (exceptions, critical errors) |
106
-
107
- ### Child Creation
108
-
109
- #### `.logger(namespace?, props?)`
110
-
111
- Create a child logger that extends the namespace and inherits props.
112
-
113
- ```typescript
114
- const appLog = createLogger("myapp", { version: "1.0" })
115
- const dbLog = appLog.logger("db", { pool: "primary" })
116
- // namespace: "myapp:db"
117
- // props: { version: "1.0", pool: "primary" }
118
- ```
119
-
120
- #### `.span(namespace?, props?)`
121
-
122
- Create a timed span logger. Implements `Disposable` for use with `using`.
123
-
124
- ```typescript
125
- {
126
- using span = log.span("import", { file: "data.csv" })
127
- span.info?.("processing")
128
- span.spanData.rowCount = 1000
129
- }
130
- // SPAN myapp:import (234ms) {rowCount: 1000, file: "data.csv"}
131
- ```
132
-
133
- #### `.child(context)`
134
-
135
- Create a child logger with additional context fields in every message.
136
-
137
- ```typescript
138
- const reqLog = log.child({ requestId: "abc-123", userId: 42 })
139
- reqLog.info?.("handling request")
140
- // Includes requestId and userId in every log message
141
-
142
- // Context accumulates through nesting
143
- const dbLog = reqLog.child({ pool: "primary" })
144
- // Has: requestId, userId, pool
145
- ```
146
-
147
- #### `.end()`
148
-
149
- Manually end a span (alternative to `using`).
150
-
151
- ```typescript
152
- const span = log.span("operation")
153
- try {
154
- span.spanData.result = "success"
155
- } finally {
156
- span.end()
157
- }
158
- ```
159
-
160
- ---
161
-
162
- ## SpanLogger Interface
163
-
164
- ```typescript
165
- interface SpanLogger extends Logger, Disposable {
166
- readonly spanData: SpanData & { [key: string]: unknown }
167
- }
168
- ```
169
-
170
- SpanLogger extends Logger with non-null `spanData` and `Disposable` for automatic cleanup.
171
-
172
- ### SpanData Properties
173
-
174
- | Property | Type | Mutable | Description |
175
- | ----------- | ---------------- | ------- | ----------------------------------------- |
176
- | `id` | `string` | No | Unique span ID (`sp_1`, `sp_2`, ...) |
177
- | `traceId` | `string` | No | Trace ID (shared across nested spans) |
178
- | `parentId` | `string \| null` | No | Parent span ID (null for root spans) |
179
- | `startTime` | `number` | No | Start timestamp (ms since epoch) |
180
- | `endTime` | `number \| null` | No | End timestamp (null until span ends) |
181
- | `duration` | `number \| null` | No | Computed duration (live while active) |
182
- | `[custom]` | `unknown` | Yes | Set directly: `span.spanData.key = value` |
183
-
184
- ### Nested Spans
185
-
186
- Spans automatically track parent-child relationships and share trace IDs:
187
-
188
- ```typescript
189
- {
190
- using outer = log.span("request")
191
- {
192
- using inner = outer.span("db:query")
193
- // inner.spanData.parentId === outer.spanData.id
194
- // inner.spanData.traceId === outer.spanData.traceId
195
- }
196
- }
197
- ```
198
-
199
- ---
200
-
201
- ## Configuration
202
-
203
- ### Log Level
204
-
205
- ```typescript
206
- setLogLevel(level: LogLevel): void
207
- getLogLevel(): LogLevel
208
- ```
209
-
210
- ```typescript
211
- setLogLevel("warn") // Only warn and error
212
- setLogLevel("trace") // Everything
213
- setLogLevel("silent") // Nothing
214
- ```
215
-
216
- Default: `"info"`. Override with `LOG_LEVEL` env var.
217
-
218
- ### Log Format
219
-
220
- ```typescript
221
- setLogFormat(format: LogFormat): void
222
- getLogFormat(): LogFormat
223
- ```
224
-
225
- ```typescript
226
- setLogFormat("json") // Structured JSON output
227
- setLogFormat("console") // Human-readable console output
228
- ```
229
-
230
- Default: `"console"`. Override with `LOG_FORMAT` env var. Also auto-enabled by `NODE_ENV=production` or `TRACE_FORMAT=json`.
231
-
232
- ### Spans
233
-
234
- ```typescript
235
- enableSpans(): void
236
- disableSpans(): void
237
- spansAreEnabled(): boolean
238
- ```
239
-
240
- ### Trace Filter
241
-
242
- ```typescript
243
- setTraceFilter(namespaces: string[] | null): void
244
- getTraceFilter(): string[] | null
245
- ```
246
-
247
- Filter which namespaces produce span output:
248
-
249
- ```typescript
250
- setTraceFilter(["myapp"]) // Only myapp and myapp:* spans
251
- setTraceFilter(["db", "cache"]) // Only db:* and cache:* spans
252
- setTraceFilter(null) // All spans
253
- ```
254
-
255
- ### Debug Filter
256
-
257
- ```typescript
258
- setDebugFilter(namespaces: string[] | null): void
259
- getDebugFilter(): string[] | null
260
- ```
261
-
262
- Filter which namespaces produce log output (like the `DEBUG` env var):
263
-
264
- ```typescript
265
- setDebugFilter(["myapp"]) // Only myapp and myapp:*
266
- setDebugFilter(["myapp", "-myapp:sql"]) // myapp but not myapp:sql
267
- setDebugFilter(null) // All namespaces
268
- ```
269
-
270
- Auto-lowers log level to `debug` when set.
271
-
272
- ### Output Mode
273
-
274
- ```typescript
275
- setOutputMode(mode: OutputMode): void
276
- getOutputMode(): OutputMode
277
- setSuppressConsole(value: boolean): void
278
- ```
279
-
280
- | Mode | Console | Writers |
281
- | ---------------- | ------- | ------- |
282
- | `"console"` | Yes | Yes |
283
- | `"stderr"` | stderr | Yes |
284
- | `"writers-only"` | No | Yes |
285
-
286
- `setSuppressConsole(true)` suppresses console but writers still receive output.
287
-
288
- ### Environment Variables
289
-
290
- | Variable | Values | Effect |
291
- | -------------- | --------------------------------------- | ----------------------- |
292
- | `LOG_LEVEL` | trace, debug, info, warn, error, silent | Filter output by level |
293
- | `LOG_FORMAT` | console, json | Output format |
294
- | `DEBUG` | `*`, namespace prefixes, `-prefix` | Filter by namespace |
295
- | `TRACE` | `1`, `true`, or namespace prefixes | Enable span output |
296
- | `TRACE_FORMAT` | json | Force JSON output |
297
- | `NODE_ENV` | production | Auto-enable JSON format |
298
-
299
- ---
300
-
301
- ## Writers
302
-
303
- ### addWriter
304
-
305
- ```typescript
306
- function addWriter(writer: (formatted: string, level: string) => void): () => void
307
- ```
308
-
309
- Subscribe to all formatted log output. Returns an unsubscribe function.
310
-
311
- ```typescript
312
- const lines: string[] = []
313
- const unsub = addWriter((formatted) => lines.push(formatted))
314
- // ... later:
315
- unsub()
316
- ```
317
-
318
- ### createFileWriter
319
-
320
- ```typescript
321
- function createFileWriter(path: string, options?: FileWriterOptions): FileWriter
322
- ```
323
-
324
- Create a buffered file writer with automatic flushing.
325
-
326
- | Option | Type | Default | Description |
327
- | --------------- | -------- | ------- | -------------------------------------- |
328
- | `bufferSize` | `number` | 4096 | Flush when buffer exceeds this (bytes) |
329
- | `flushInterval` | `number` | 100 | Flush every N milliseconds |
330
-
331
- ```typescript
332
- const writer = createFileWriter("/tmp/app.log")
333
- const unsub = addWriter((formatted) => writer.write(formatted))
334
-
335
- // On shutdown:
336
- unsub()
337
- writer.close() // Flushes remaining buffer and closes fd
338
- ```
339
-
340
- The writer registers a `process.on("exit")` handler for data safety, and `unref()`s its interval timer so it won't keep the process alive.
341
-
342
- ---
343
-
344
- ## Types
345
-
346
- ```typescript
347
- type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent"
348
- type OutputLogLevel = "trace" | "debug" | "info" | "warn" | "error"
349
- type LogFormat = "console" | "json"
350
- type OutputMode = "console" | "stderr" | "writers-only"
351
- type LazyMessage = string | (() => string)
352
- ```
353
-
354
- ### ConditionalLogger
355
-
356
- The return type of `createLogger()`. Log methods are `undefined` when their level is disabled:
357
-
358
- ```typescript
359
- interface ConditionalLogger {
360
- readonly name: string
361
- readonly props: Readonly<Record<string, unknown>>
362
- readonly spanData: SpanData | null
363
-
364
- trace?: (message: LazyMessage, data?: Record<string, unknown>) => void
365
- debug?: (message: LazyMessage, data?: Record<string, unknown>) => void
366
- info?: (message: LazyMessage, data?: Record<string, unknown>) => void
367
- warn?: (message: LazyMessage, data?: Record<string, unknown>) => void
368
- error?: (message: LazyMessage | Error, data?: Record<string, unknown>) => void
369
-
370
- logger(namespace?: string, props?: Record<string, unknown>): Logger
371
- span(namespace?: string, props?: Record<string, unknown>): SpanLogger
372
- child(context: Record<string, unknown>): Logger
373
- end(): void
374
- }
375
- ```
376
-
377
- TypeScript enforces `?.` at compile time -- you can't call `log.debug()` without `?.` because the method may be undefined.
378
-
379
- ---
380
-
381
- ## Span Collection
382
-
383
- For testing and analysis, spans can be collected programmatically.
384
-
385
- ```typescript
386
- import { startCollecting, stopCollecting, getCollectedSpans, clearCollectedSpans, resetIds } from "loggily"
387
-
388
- resetIds() // Reset ID counters for deterministic tests
389
- startCollecting() // Enable span collection
390
-
391
- const log = createLogger("test")
392
- {
393
- using span = log.span("operation")
394
- span.spanData.items = 42
395
- }
396
-
397
- const spans = stopCollecting()
398
- // spans[0].id === "sp_1"
399
- // spans[0].duration === <measured>
400
- ```
@@ -1,106 +0,0 @@
1
- # Benchmarks
2
-
3
- Comparison of loggily against pino, winston, and debug.
4
-
5
- **Test environment**: Bun 1.3.9, macOS arm64 (Apple Silicon), 10M iterations per test.
6
-
7
- **Methodology**: All "enabled" benchmarks write to in-process noop sinks (no I/O syscalls) for a fair apples-to-apples comparison of formatting and serialization throughput:
8
-
9
- - beorn: `addWriter(noop)` + `setSuppressConsole(true)` + `setOutputMode("writers-only")`
10
- - pino: `pino(opts, noopWritableStream)`
11
- - winston: `Stream` transport with noop `Writable`
12
-
13
- ## Disabled Debug — Cheap Argument
14
-
15
- When debug logging is disabled and arguments are cheap (string literals):
16
-
17
- | Library | ops/s | ns/op | Relative |
18
- | --------------- | ----: | ----: | -------: |
19
- | noop (baseline) | 3B | 0.4 | 1.0x |
20
- | pino | 2B | 0.5 | 1.3x |
21
- | **loggily** | 383M | 2.6 | 6.5x |
22
- | debug | 43M | 23.4 | 59x |
23
- | winston | 3M | 391.2 | 978x |
24
-
25
- Pino wins here — its level check is a simple integer comparison without Proxy overhead. loggily's Proxy-based `?.` pattern adds ~2ns overhead for cheap args.
26
-
27
- ## Disabled Debug — Expensive Argument (the real story)
28
-
29
- When debug logging is disabled but arguments require evaluation (JSON.stringify):
30
-
31
- | Library | ops/s | ns/op | Relative |
32
- | --------------- | -------: | ------: | -------: |
33
- | noop (baseline) | 414M | 2.4 | 1.0x |
34
- | **loggily** | **248M** | **4.0** | **1.7x** |
35
- | pino | 8M | 133.1 | 55x |
36
- | debug | 7M | 153.3 | 64x |
37
- | winston | 1M | 774.6 | 323x |
38
-
39
- **loggily is 31x faster than pino** for disabled calls with expensive arguments. The `?.` pattern skips argument evaluation entirely — `log.debug?.(\`state: ${expensiveArg()}\`)`never calls`expensiveArg()` when debug is disabled.
40
-
41
- This is the key insight: real-world logging often involves string interpolation, `JSON.stringify`, or computed values. The `?.` pattern eliminates this cost entirely.
42
-
43
- ## Enabled Info — Cheap Argument
44
-
45
- When info logging is enabled, all loggers writing to noop sinks (fair comparison):
46
-
47
- | Library | ops/s | ns/op | Relative |
48
- | ----------- | ----: | ----: | -------: |
49
- | **loggily** | 3M | 371.4 | 1.0x |
50
- | pino | 2M | 471.7 | 1.3x |
51
- | winston | 1M | 748.3 | 2.0x |
52
-
53
- With a fair noop-sink comparison, loggily is the fastest for enabled string logging -- ~1.3x faster than pino and ~2x faster than winston.
54
-
55
- ## Enabled Info — Structured Data
56
-
57
- Logging with structured data (`{ key: "value", count: 42 }`), all to noop sinks:
58
-
59
- | Library | ops/s | ns/op | Relative |
60
- | ----------- | ----: | ------: | -------: |
61
- | **loggily** | 1M | 668.9 | 1.0x |
62
- | pino | 1M | 738.2 | 1.1x |
63
- | winston | 587K | 1,703.6 | 2.5x |
64
-
65
- Beorn and pino are neck-and-neck for structured data. Both are roughly 2.5x faster than winston.
66
-
67
- ## Enabled Warn — Error Object
68
-
69
- Logging with an Error object, all to noop sinks:
70
-
71
- | Library | ops/s | ns/op | Relative |
72
- | ----------- | ----: | ----: | -------: |
73
- | **loggily** | 1M | 990.9 | 1.0x |
74
- | winston | 839K | 1.2 | 1.2x |
75
- | pino | 541K | 1.8 | 1.9x |
76
-
77
- Beorn handles Error objects fastest, nearly 2x faster than pino. Pino's Error serialization is heavier due to its structured JSON pipeline.
78
-
79
- ## Span Creation
80
-
81
- Span create + dispose (no output):
82
-
83
- | Library | ops/s | ns/op |
84
- | ----------- | ----: | ----: |
85
- | **loggily** | 2M | 544.1 |
86
-
87
- ~544ns per span lifecycle including ID generation, timing, and disposal. No competitor offers built-in span support for comparison.
88
-
89
- ## Key Takeaways
90
-
91
- 1. **Disabled + expensive args**: loggily's `?.` pattern is 31x faster than pino, 194x faster than winston. This is the main differentiator.
92
- 2. **Disabled + cheap args**: Pino is faster due to no Proxy overhead. Both are sub-microsecond.
93
- 3. **Enabled + cheap args**: loggily is ~1.3x faster than pino when both write to the same kind of noop sink.
94
- 4. **Enabled + structured data**: loggily and pino are comparable; both are ~2x faster than winston.
95
- 5. **Enabled + Error objects**: loggily is fastest, ~1.9x faster than pino.
96
- 6. **The `?.` advantage grows with argument cost**: The more expensive your log arguments, the bigger the win.
97
-
98
- ## Reproducing
99
-
100
- ```bash
101
- # Install benchmark dependencies
102
- bun add -d pino winston debug @types/debug
103
-
104
- # Run benchmarks
105
- bun vendor/loggily/benchmarks/overhead.ts
106
- ```