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/docs/api-reference.md
DELETED
|
@@ -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
|
-
```
|
package/docs/benchmarks.md
DELETED
|
@@ -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
|
-
```
|