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/CLAUDE.md
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
# loggily
|
|
2
|
-
|
|
3
|
-
Structured logging with spans. Logger-first architecture: Span = Logger + Duration.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { createLogger } from "loggily"
|
|
9
|
-
const log = createLogger("myapp")
|
|
10
|
-
|
|
11
|
-
log.info("starting")
|
|
12
|
-
log.error(new Error("failed"))
|
|
13
|
-
|
|
14
|
-
// Spans for timing (implements Disposable)
|
|
15
|
-
{
|
|
16
|
-
using span = log.span("import", { file: "data.csv" })
|
|
17
|
-
span.info("working...")
|
|
18
|
-
span.spanData.count = 42
|
|
19
|
-
}
|
|
20
|
-
// → SPAN myapp:import (15ms) {count: 42, file: "data.csv"}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Environment Variables
|
|
24
|
-
|
|
25
|
-
| Variable | Values | Effect |
|
|
26
|
-
| ------------ | --------------------------------------- | -------------------------- |
|
|
27
|
-
| LOG_LEVEL | trace, debug, info, warn, error, silent | Filter output by level |
|
|
28
|
-
| DEBUG | \*, namespace prefixes, -prefix | Filter output by namespace |
|
|
29
|
-
| TRACE | 1, true, or namespace prefixes | Enable span output |
|
|
30
|
-
| TRACE_FORMAT | json | Force JSON output |
|
|
31
|
-
| NODE_ENV | production | Auto-enable JSON format |
|
|
32
|
-
|
|
33
|
-
### Examples
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
LOG_LEVEL=debug bun run app # Enable debug logging
|
|
37
|
-
DEBUG=km:storage bun run app # Only show km:storage (+ children), auto-enables debug level
|
|
38
|
-
DEBUG='km:*,-km:sql' bun run app # Show all km namespaces except km:sql
|
|
39
|
-
DEBUG='*' bun run app # Show all namespaces at debug level
|
|
40
|
-
TRACE=1 bun run app # Enable all span timing output
|
|
41
|
-
TRACE=myapp:import bun run app # Enable spans for specific namespace
|
|
42
|
-
TRACE=myapp,other bun run app # Enable spans for multiple prefixes
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## API
|
|
46
|
-
|
|
47
|
-
### createLogger(name, props?)
|
|
48
|
-
|
|
49
|
-
Create a logger. Props are inherited by children.
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
const log = createLogger("myapp", { version: "1.0" })
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Logger Methods
|
|
56
|
-
|
|
57
|
-
| Method | Purpose |
|
|
58
|
-
| ----------------------------- | ------------------ |
|
|
59
|
-
| `.trace(msg, data?)` | Verbose debugging |
|
|
60
|
-
| `.debug(msg, data?)` | Debug information |
|
|
61
|
-
| `.info(msg, data?)` | Normal operation |
|
|
62
|
-
| `.warn(msg, data?)` | Recoverable issues |
|
|
63
|
-
| `.error(msg \| Error, data?)` | Failures |
|
|
64
|
-
|
|
65
|
-
### Child Loggers
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// Extend namespace, inherit props
|
|
69
|
-
const child = log.logger("import", { file: "data.csv" })
|
|
70
|
-
// → namespace: "myapp:import", props: { version: "1.0", file: "data.csv" }
|
|
71
|
-
|
|
72
|
-
// Create span (child with timing)
|
|
73
|
-
{
|
|
74
|
-
using span = log.span("import")
|
|
75
|
-
span.info("working...")
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Spans
|
|
80
|
-
|
|
81
|
-
Spans are loggers with timing. They implement `Disposable` for use with `using`:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
{
|
|
85
|
-
using span = log.span("operation", { context: "value" })
|
|
86
|
-
span.debug("step 1")
|
|
87
|
-
span.spanData.processed = 100 // Set custom attributes
|
|
88
|
-
}
|
|
89
|
-
// On block exit: SPAN myapp:operation (15ms) {processed: 100, context: "value"}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
For environments without `using` support, call `.end()` manually:
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const span = log.span("operation")
|
|
96
|
-
try {
|
|
97
|
-
span.info("working...")
|
|
98
|
-
span.spanData.count = 42
|
|
99
|
-
} finally {
|
|
100
|
-
span.end()
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Span Data
|
|
105
|
-
|
|
106
|
-
| Property | Type | Description |
|
|
107
|
-
| -------------------- | ------------------------- | ------------------------------------- |
|
|
108
|
-
| `spanData.id` | string (readonly) | Unique span ID (sp_1, sp_2...) |
|
|
109
|
-
| `spanData.traceId` | string (readonly) | Trace ID (shared across nested spans) |
|
|
110
|
-
| `spanData.parentId` | string \| null (readonly) | Parent span ID |
|
|
111
|
-
| `spanData.startTime` | number (readonly) | Start timestamp (ms) |
|
|
112
|
-
| `spanData.duration` | number (readonly) | Live duration since start |
|
|
113
|
-
| `spanData.custom` | any (writable) | Set custom attributes |
|
|
114
|
-
|
|
115
|
-
### Configuration Functions
|
|
116
|
-
|
|
117
|
-
```typescript
|
|
118
|
-
import {
|
|
119
|
-
setLogLevel,
|
|
120
|
-
getLogLevel,
|
|
121
|
-
enableSpans,
|
|
122
|
-
disableSpans,
|
|
123
|
-
spansAreEnabled,
|
|
124
|
-
setTraceFilter,
|
|
125
|
-
getTraceFilter,
|
|
126
|
-
setDebugFilter,
|
|
127
|
-
getDebugFilter,
|
|
128
|
-
} from "loggily"
|
|
129
|
-
|
|
130
|
-
setLogLevel("debug") // Set minimum level
|
|
131
|
-
getLogLevel() // Get current level: "debug"
|
|
132
|
-
enableSpans() // Enable span output
|
|
133
|
-
disableSpans() // Disable span output
|
|
134
|
-
spansAreEnabled() // Check if spans are enabled
|
|
135
|
-
setTraceFilter(["myapp"]) // Only output spans for "myapp" and "myapp:*"
|
|
136
|
-
setTraceFilter(null) // Clear filter, output all spans
|
|
137
|
-
getTraceFilter() // Get current filter: ["myapp"] or null
|
|
138
|
-
setDebugFilter(["myapp"]) // Only show output for "myapp" and "myapp:*"
|
|
139
|
-
setDebugFilter(["myapp", "-myapp:sql"]) // Show myapp but exclude myapp:sql
|
|
140
|
-
setDebugFilter(null) // Clear filter, show all namespaces
|
|
141
|
-
getDebugFilter() // Get current filter: ["myapp", "-myapp:sql"] or null
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Distributed Tracing (opt-in)
|
|
145
|
-
|
|
146
|
-
### ID Format
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
import { setIdFormat, getIdFormat } from "loggily"
|
|
150
|
-
|
|
151
|
-
setIdFormat("simple") // sp_1, tr_1 (default)
|
|
152
|
-
setIdFormat("w3c") // 16-char hex span, 32-char hex trace (W3C Trace Context)
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### traceparent Header
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
import { traceparent } from "loggily"
|
|
159
|
-
|
|
160
|
-
const span = log.span("http-request")
|
|
161
|
-
const header = traceparent(span.spanData)
|
|
162
|
-
// → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
|
|
163
|
-
fetch(url, { headers: { traceparent: header } })
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Sampling
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
import { setSampleRate, getSampleRate } from "loggily"
|
|
170
|
-
|
|
171
|
-
setSampleRate(0.1) // Sample 10% of traces (head-based)
|
|
172
|
-
setSampleRate(1.0) // Sample everything (default)
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Context Propagation (Node.js/Bun only)
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { enableContextPropagation, getCurrentSpan } from "loggily/context"
|
|
179
|
-
|
|
180
|
-
enableContextPropagation()
|
|
181
|
-
|
|
182
|
-
const log = createLogger("myapp")
|
|
183
|
-
{
|
|
184
|
-
using span = log.span("request")
|
|
185
|
-
// Logs auto-tagged with trace_id/span_id
|
|
186
|
-
log.info("handling") // includes trace_id, span_id in output
|
|
187
|
-
|
|
188
|
-
// Child spans from ANY logger auto-parent via AsyncLocalStorage
|
|
189
|
-
const other = createLogger("db")
|
|
190
|
-
const dbSpan = other.span("query") // parentId = span.id
|
|
191
|
-
|
|
192
|
-
getCurrentSpan() // { spanId, traceId, parentId }
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## Output Format
|
|
197
|
-
|
|
198
|
-
### Console (development)
|
|
199
|
-
|
|
200
|
-
```
|
|
201
|
-
14:32:15 INFO myapp starting
|
|
202
|
-
14:32:15 DEBUG myapp:import loading {file: "data.csv"}
|
|
203
|
-
14:32:16 SPAN myapp:import (1234ms) {count: 42}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### JSON (production / TRACE_FORMAT=json)
|
|
207
|
-
|
|
208
|
-
```json
|
|
209
|
-
{"time":"2024-01-15T14:32:15.123Z","level":"info","name":"myapp","msg":"starting"}
|
|
210
|
-
{"time":"2024-01-15T14:32:16.456Z","level":"span","name":"myapp:import","msg":"(1234ms)","duration":1234,"count":42}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## Zero-Overhead Pattern (Optional Chaining)
|
|
214
|
-
|
|
215
|
-
`createLogger` returns `undefined` for disabled log levels, enabling zero-overhead logging.
|
|
216
|
-
|
|
217
|
-
**Log levels** (most → least verbose): `trace < debug < info < warn < error < silent`
|
|
218
|
-
**Default level**: `warn` for km CLI (trace, debug, and info disabled)
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { createLogger } from "loggily"
|
|
222
|
-
|
|
223
|
-
const log = createLogger("km:tui")
|
|
224
|
-
|
|
225
|
-
// All methods support ?. for zero-overhead when their level is disabled
|
|
226
|
-
log.trace?.(`very verbose: ${expensiveDebug()}`) // Skipped at default (warn)
|
|
227
|
-
log.debug?.(`state: ${getState()}`) // Skipped at default (warn)
|
|
228
|
-
log.info?.("starting") // Skipped at default (warn)
|
|
229
|
-
log.warn?.("deprecated") // Enabled at default (warn)
|
|
230
|
-
log.error?.("failed") // Enabled at default
|
|
231
|
-
|
|
232
|
-
// With -v flag or LOG_LEVEL=info, info is enabled:
|
|
233
|
-
log.info?.("starting") // Enabled when level=info
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### Why optional chaining?
|
|
237
|
-
|
|
238
|
-
**Benchmark results** (10M iterations, Bun 1.1.x):
|
|
239
|
-
|
|
240
|
-
| Scenario | ops/s | ns/op | Notes |
|
|
241
|
-
| ------------------------- | -------- | ------- | ----------------------------------- |
|
|
242
|
-
| noop (cheap args) | 2168M | 0.5 | Fastest for trivial args |
|
|
243
|
-
| `?.` (cheap args) | 1406M | 0.7 | ~0.2ns overhead - negligible |
|
|
244
|
-
| noop (expensive args) | 17M | 57.6 | Args still evaluated - wasted! |
|
|
245
|
-
| **`?.` (expensive args)** | **408M** | **2.5** | Args NOT evaluated - **22x faster** |
|
|
246
|
-
|
|
247
|
-
**Key insight**: Optional chaining is only ~0.2ns slower for cheap args, but **22x faster** for expensive args because it skips argument evaluation entirely.
|
|
248
|
-
|
|
249
|
-
- `log.debug?.()` skips the entire call including argument evaluation when debug is disabled
|
|
250
|
-
- TypeScript enforces `?.` at compile time (methods are typed as possibly undefined)
|
|
251
|
-
- Main benefit: expensive string formatting and function calls are completely skipped
|
|
252
|
-
|
|
253
|
-
See [docs/conditional-logging-research.md](docs/conditional-logging-research.md) for detailed research and external references.
|
|
254
|
-
|
|
255
|
-
## Lazy Messages
|
|
256
|
-
|
|
257
|
-
Messages can be functions — only called when the log level is enabled:
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
log.debug?.(() => `expensive: ${JSON.stringify(bigObject)}`)
|
|
261
|
-
// Function never called when debug is disabled
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Type: `LazyMessage = string | (() => string)`
|
|
265
|
-
|
|
266
|
-
## Child Context Loggers
|
|
267
|
-
|
|
268
|
-
Create child loggers with additional structured context (not just namespace):
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
const child = log.child({ requestId: "abc-123", userId: 42 })
|
|
272
|
-
child.info?.("handling request")
|
|
273
|
-
// → 14:32:15 INFO myapp handling request {requestId: "abc-123", userId: 42}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## JSON Output Format
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
LOG_FORMAT=json bun run app # Force JSON output in any environment
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
In addition to `TRACE_FORMAT=json` and `NODE_ENV=production`, `LOG_FORMAT=json` explicitly enables structured JSON output.
|
|
283
|
-
|
|
284
|
-
## File Writer
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
import { createFileWriter } from "loggily"
|
|
288
|
-
|
|
289
|
-
const writer = createFileWriter("/tmp/app.log")
|
|
290
|
-
const log = createLogger("myapp", { writer })
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
## Best Practices
|
|
294
|
-
|
|
295
|
-
1. **Namespace hierarchy**: Use `:` to create hierarchy (`myapp:db:query`)
|
|
296
|
-
2. **Props for context**: Pass structured data, not string interpolation
|
|
297
|
-
3. **Spans for timing**: Wrap operations you want to measure
|
|
298
|
-
4. **Level filtering**: Use `LOG_LEVEL` to control verbosity
|
|
299
|
-
5. **Conditional logging**: Use `?.` pattern in hot paths to skip arg evaluation
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Contributing to loggily
|
|
2
|
-
|
|
3
|
-
## Development Setup
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
git clone https://github.com/beorn/loggily.git
|
|
7
|
-
cd logger
|
|
8
|
-
bun install
|
|
9
|
-
bun run test # Run tests
|
|
10
|
-
bun run typecheck # Type check
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Code Style
|
|
14
|
-
|
|
15
|
-
- TypeScript strict mode
|
|
16
|
-
- ESM imports only (`import`/`export`, never `require`)
|
|
17
|
-
- Factory functions over classes
|
|
18
|
-
- Zero external dependencies
|
|
19
|
-
|
|
20
|
-
## Testing
|
|
21
|
-
|
|
22
|
-
All changes should include tests. Run the test suite before submitting:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
bun run test
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Tests must be silent on success. Use `vi.spyOn(console, 'log').mockImplementation(() => {})` to suppress output in tests.
|
|
29
|
-
|
|
30
|
-
## Pull Requests
|
|
31
|
-
|
|
32
|
-
1. Fork the repository
|
|
33
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
34
|
-
3. Make your changes with tests
|
|
35
|
-
4. Ensure `bun run test` and `bun run typecheck` pass
|
|
36
|
-
5. Commit with [conventional commit](https://conventionalcommits.org/) messages
|
|
37
|
-
6. Push and open a pull request
|
|
38
|
-
|
|
39
|
-
## Commit Messages
|
|
40
|
-
|
|
41
|
-
- `feat:` -- New features
|
|
42
|
-
- `fix:` -- Bug fixes
|
|
43
|
-
- `docs:` -- Documentation only
|
|
44
|
-
- `test:` -- Test additions
|
|
45
|
-
- `refactor:` -- Code changes that neither fix bugs nor add features
|
|
46
|
-
- `chore:` -- Maintenance tasks
|
|
47
|
-
|
|
48
|
-
## Design Principles
|
|
49
|
-
|
|
50
|
-
1. **Logger-first** -- Spans are loggers with timing, not separate concepts
|
|
51
|
-
2. **Minimal surface** -- Few, well-designed functions
|
|
52
|
-
3. **Type safe** -- TypeScript enforces correct usage (e.g., `?.` for disabled levels)
|
|
53
|
-
4. **Zero-cost** -- Optional chaining skips argument evaluation when disabled
|
|
54
|
-
5. **Structured** -- JSON in production, readable console in development
|
|
55
|
-
|
|
56
|
-
## Questions?
|
|
57
|
-
|
|
58
|
-
Open an issue for discussion before starting large changes.
|
package/benchmarks/overhead.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loggily Benchmark Suite
|
|
3
|
-
*
|
|
4
|
-
* Compares zero-overhead disabled logging and enabled logging performance
|
|
5
|
-
* against popular alternatives: pino, winston, debug.
|
|
6
|
-
*
|
|
7
|
-
* All "enabled" benchmarks use the same kind of sink (noop writer) for a fair
|
|
8
|
-
* apples-to-apples comparison of formatting + serialization throughput.
|
|
9
|
-
*
|
|
10
|
-
* Run: bun benchmarks/overhead.ts
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { addWriter, createLogger, setLogLevel, setOutputMode, setSuppressConsole, disableSpans } from "../src/index.ts"
|
|
14
|
-
|
|
15
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
function measure(
|
|
18
|
-
name: string,
|
|
19
|
-
fn: () => void,
|
|
20
|
-
iterations: number,
|
|
21
|
-
): { name: string; opsPerSec: number; nsPerOp: number } {
|
|
22
|
-
// Warmup
|
|
23
|
-
for (let i = 0; i < 1000; i++) fn()
|
|
24
|
-
|
|
25
|
-
const start = Bun.nanoseconds()
|
|
26
|
-
for (let i = 0; i < iterations; i++) fn()
|
|
27
|
-
const elapsed = Bun.nanoseconds() - start
|
|
28
|
-
|
|
29
|
-
const nsPerOp = elapsed / iterations
|
|
30
|
-
const opsPerSec = 1e9 / nsPerOp
|
|
31
|
-
|
|
32
|
-
return { name, opsPerSec, nsPerOp }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatOps(ops: number): string {
|
|
36
|
-
if (ops >= 1e9) return `${(ops / 1e9).toFixed(0)}B`
|
|
37
|
-
if (ops >= 1e6) return `${(ops / 1e6).toFixed(0)}M`
|
|
38
|
-
if (ops >= 1e3) return `${(ops / 1e3).toFixed(0)}K`
|
|
39
|
-
return `${ops.toFixed(0)}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function formatNs(ns: number): string {
|
|
43
|
-
if (ns >= 1e6) return `${(ns / 1e6).toFixed(1)}ms`
|
|
44
|
-
if (ns >= 1e3) return `${(ns / 1e3).toFixed(1)}µs`
|
|
45
|
-
return `${ns.toFixed(1)}ns`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function printResults(title: string, results: Array<{ name: string; opsPerSec: number; nsPerOp: number }>) {
|
|
49
|
-
console.log(`\n${title}`)
|
|
50
|
-
console.log("─".repeat(70))
|
|
51
|
-
|
|
52
|
-
const maxNameLen = Math.max(...results.map((r) => r.name.length))
|
|
53
|
-
|
|
54
|
-
for (const r of results) {
|
|
55
|
-
const name = r.name.padEnd(maxNameLen)
|
|
56
|
-
const ops = formatOps(r.opsPerSec).padStart(6)
|
|
57
|
-
const ns = formatNs(r.nsPerOp).padStart(8)
|
|
58
|
-
console.log(` ${name} ${ops} ops/s ${ns}/op`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ── Expensive argument simulation ────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function expensiveArg(): string {
|
|
65
|
-
return JSON.stringify({ a: 1, b: 2, c: [3, 4, 5], d: { e: "hello", f: true } })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Noop stream (shared sink type for fair enabled comparisons) ──────────────
|
|
69
|
-
|
|
70
|
-
const { Writable } = await import("stream")
|
|
71
|
-
const noopStream = () =>
|
|
72
|
-
new Writable({
|
|
73
|
-
write(_chunk: unknown, _encoding: string, callback: () => void) {
|
|
74
|
-
callback()
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// ── loggily setup ──────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
const beornLog = createLogger("bench")
|
|
81
|
-
// Route all output to noop writer, suppress console
|
|
82
|
-
setSuppressConsole(true)
|
|
83
|
-
setOutputMode("writers-only")
|
|
84
|
-
addWriter(() => {}) // noop writer — receives formatted output, discards it
|
|
85
|
-
disableSpans()
|
|
86
|
-
|
|
87
|
-
// ── Pino setup ───────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
type LogFn = {
|
|
90
|
-
(msg: string): void
|
|
91
|
-
(obj: Record<string, unknown>, msg: string): void
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface BenchLogger {
|
|
95
|
-
debug: LogFn
|
|
96
|
-
info: LogFn
|
|
97
|
-
warn: LogFn
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Disabled pino (level=warn, debug/info disabled) — second arg = noop stream
|
|
101
|
-
// Enabled pino (level=debug, all levels active) — second arg = noop stream
|
|
102
|
-
let pinoDisabled: BenchLogger
|
|
103
|
-
let pinoEnabled: BenchLogger
|
|
104
|
-
try {
|
|
105
|
-
const pino = (await import("pino")).default
|
|
106
|
-
pinoDisabled = pino({ level: "warn" }, noopStream())
|
|
107
|
-
pinoEnabled = pino({ level: "debug" }, noopStream())
|
|
108
|
-
} catch {
|
|
109
|
-
console.log("pino not installed — install with: bun add -d pino")
|
|
110
|
-
const stub: BenchLogger = { debug: () => {}, info: () => {}, warn: () => {} }
|
|
111
|
-
pinoDisabled = stub
|
|
112
|
-
pinoEnabled = stub
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Winston setup ────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
// Disabled winston (level=warn, debug/info disabled)
|
|
118
|
-
// Enabled winston (level=debug, all levels active) — noop stream transport
|
|
119
|
-
let winstonDisabled: BenchLogger
|
|
120
|
-
let winstonEnabled: BenchLogger
|
|
121
|
-
try {
|
|
122
|
-
const winston = await import("winston")
|
|
123
|
-
winstonDisabled = winston.createLogger({
|
|
124
|
-
level: "warn",
|
|
125
|
-
transports: [new winston.transports.Console({ silent: true })],
|
|
126
|
-
})
|
|
127
|
-
winstonEnabled = winston.createLogger({
|
|
128
|
-
level: "debug",
|
|
129
|
-
transports: [new winston.transports.Stream({ stream: noopStream() })],
|
|
130
|
-
})
|
|
131
|
-
} catch {
|
|
132
|
-
console.log("winston not installed — install with: bun add -d winston")
|
|
133
|
-
const stub: BenchLogger = { debug: () => {}, info: () => {}, warn: () => {} }
|
|
134
|
-
winstonDisabled = stub
|
|
135
|
-
winstonEnabled = stub
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Debug setup ──────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
let debugFn: (msg: string) => void
|
|
141
|
-
try {
|
|
142
|
-
const debug = (await import("debug")).default
|
|
143
|
-
debugFn = debug("bench") // DEBUG env not set → disabled
|
|
144
|
-
} catch {
|
|
145
|
-
console.log("debug not installed — install with: bun add -d debug")
|
|
146
|
-
debugFn = () => {}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Baseline: noop ───────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
const noop = (): void => {}
|
|
152
|
-
|
|
153
|
-
// ── Benchmarks ───────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
const N = 10_000_000
|
|
156
|
-
|
|
157
|
-
console.log("loggily Benchmark Suite")
|
|
158
|
-
console.log(`Iterations: ${(N / 1e6).toFixed(0)}M per test`)
|
|
159
|
-
console.log(`Runtime: Bun ${Bun.version}`)
|
|
160
|
-
console.log(`Platform: ${process.platform} ${process.arch}`)
|
|
161
|
-
|
|
162
|
-
// ─── PART 1: DISABLED LOGGING ────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
// 1. Disabled debug call — cheap args
|
|
165
|
-
{
|
|
166
|
-
setLogLevel("warn") // debug disabled
|
|
167
|
-
|
|
168
|
-
const results = [
|
|
169
|
-
measure("noop()", () => noop(), N),
|
|
170
|
-
measure("beorn: log.debug?.(str)", () => beornLog.debug?.("hello"), N),
|
|
171
|
-
measure("pino: log.debug(str)", () => pinoDisabled.debug("hello"), N),
|
|
172
|
-
measure("winston: log.debug(str)", () => winstonDisabled.debug("hello"), N),
|
|
173
|
-
measure('debug: debug("hello")', () => debugFn("hello"), N),
|
|
174
|
-
]
|
|
175
|
-
|
|
176
|
-
printResults("DISABLED DEBUG — cheap argument (string literal)", results)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 2. Disabled debug call — expensive args
|
|
180
|
-
{
|
|
181
|
-
setLogLevel("warn") // debug disabled
|
|
182
|
-
|
|
183
|
-
const results = [
|
|
184
|
-
measure("noop(expensive)", () => noop(), N),
|
|
185
|
-
measure("beorn: log.debug?.(expensive)", () => beornLog.debug?.(`state: ${expensiveArg()}`), N),
|
|
186
|
-
measure("pino: log.debug(expensive)", () => pinoDisabled.debug(`state: ${expensiveArg()}`), N),
|
|
187
|
-
measure("winston: log.debug(expensive)", () => winstonDisabled.debug(`state: ${expensiveArg()}`), N),
|
|
188
|
-
measure("debug: debug(expensive)", () => debugFn(`state: ${expensiveArg()}`), N),
|
|
189
|
-
]
|
|
190
|
-
|
|
191
|
-
printResults("DISABLED DEBUG — expensive argument (JSON.stringify)", results)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ─── PART 2: ENABLED LOGGING (all to noop writers) ───────────────────────────
|
|
195
|
-
// Fair comparison: all loggers format + serialize, all write to noop sinks.
|
|
196
|
-
// beorn: addWriter(noop) + setSuppressConsole(true) + setOutputMode("writers-only")
|
|
197
|
-
// pino: pino(opts, noopWritableStream)
|
|
198
|
-
// winston: Stream transport with noop Writable
|
|
199
|
-
|
|
200
|
-
// 3. Enabled info — cheap args (string literal)
|
|
201
|
-
{
|
|
202
|
-
setLogLevel("info") // info enabled
|
|
203
|
-
|
|
204
|
-
const results = [
|
|
205
|
-
measure("beorn: log.info?.(str)", () => beornLog.info?.("hello"), N / 10),
|
|
206
|
-
measure("pino: log.info(str)", () => pinoEnabled.info("hello"), N / 10),
|
|
207
|
-
measure("winston: log.info(str)", () => winstonEnabled.info("hello"), N / 10),
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
printResults("ENABLED INFO — cheap argument (string literal) — all to noop sink", results)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 4. Enabled info — structured data
|
|
214
|
-
{
|
|
215
|
-
setLogLevel("info") // info enabled
|
|
216
|
-
|
|
217
|
-
const structuredData = { key: "value", count: 42 }
|
|
218
|
-
|
|
219
|
-
const results = [
|
|
220
|
-
measure("beorn: log.info?.(str, data)", () => beornLog.info?.("request", structuredData), N / 10),
|
|
221
|
-
measure("pino: log.info(obj, str)", () => pinoEnabled.info(structuredData, "request"), N / 10),
|
|
222
|
-
measure("winston: log.info(str, data)", () => winstonEnabled.info("request", structuredData), N / 10),
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
printResults("ENABLED INFO — structured data ({ key, count }) — all to noop sink", results)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// 5. Enabled warn — with Error object
|
|
229
|
-
{
|
|
230
|
-
setLogLevel("warn") // warn enabled
|
|
231
|
-
|
|
232
|
-
const err = new Error("something broke")
|
|
233
|
-
|
|
234
|
-
const results = [
|
|
235
|
-
measure("beorn: log.warn?.(Error)", () => beornLog.warn?.(err), N / 10),
|
|
236
|
-
measure("pino: log.warn(Error)", () => pinoEnabled.warn({ err }, "something broke"), N / 10),
|
|
237
|
-
measure("winston: log.warn(str, Error)", () => winstonEnabled.warn("something broke", { error: err }), N / 10),
|
|
238
|
-
]
|
|
239
|
-
|
|
240
|
-
printResults("ENABLED WARN — Error object — all to noop sink", results)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ─── PART 3: SPANS ──────────────────────────────────────────────────────────
|
|
244
|
-
|
|
245
|
-
// 6. Span creation + disposal
|
|
246
|
-
{
|
|
247
|
-
setLogLevel("warn")
|
|
248
|
-
disableSpans()
|
|
249
|
-
|
|
250
|
-
const results = [
|
|
251
|
-
measure(
|
|
252
|
-
"beorn: span create+dispose",
|
|
253
|
-
() => {
|
|
254
|
-
using _s = beornLog.span("op")
|
|
255
|
-
},
|
|
256
|
-
N / 10,
|
|
257
|
-
),
|
|
258
|
-
]
|
|
259
|
-
|
|
260
|
-
printResults("SPAN — create + dispose (no output)", results)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
console.log("\n" + "─".repeat(70))
|
|
264
|
-
console.log("Key: ops/s = operations per second, /op = time per operation")
|
|
265
|
-
console.log("beorn uses ?. for zero-overhead: disabled calls skip argument evaluation")
|
|
266
|
-
console.log("Enabled benchmarks: all loggers write to noop sinks (fair comparison)")
|
|
267
|
-
console.log("")
|