loggily 0.3.0 → 0.4.1
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 +37 -21
- 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/comparison.md
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
# Comparison with Other Loggers
|
|
2
|
-
|
|
3
|
-
How loggily compares to popular Node.js logging libraries.
|
|
4
|
-
|
|
5
|
-
## Feature Comparison Table
|
|
6
|
-
|
|
7
|
-
| Feature | loggily | Pino | Winston | Bunyan | debug |
|
|
8
|
-
| ---------------------- | ----------- | ------- | --------- | --------- | --------- |
|
|
9
|
-
| **Log Levels** | Yes (5) | Yes (6) | Yes (7) | Yes (6) | No |
|
|
10
|
-
| **Structured Logging** | Yes | Yes | Yes | Yes | No |
|
|
11
|
-
| **JSON Output** | Yes | Yes | Yes | Yes | No |
|
|
12
|
-
| **Spans/Tracing** | Built-in | No | No | No | No |
|
|
13
|
-
| **Zero-cost Disabled** | Yes (`?.`) | No | No | No | No |
|
|
14
|
-
| **Child Loggers** | Yes | Yes | Yes | Yes | Manual |
|
|
15
|
-
| **Transports** | File writer | Yes | Yes | Yes | No |
|
|
16
|
-
| **Pretty Print** | Auto (dev) | Plugin | Plugin | Plugin | Yes |
|
|
17
|
-
| **Browser Support** | Partial | Yes | Yes | Yes | Yes |
|
|
18
|
-
| **Bundle Size** | ~3KB | ~17KB | ~200KB+ | ~30KB | ~2KB |
|
|
19
|
-
| **TypeScript** | Native | Yes | Types pkg | Types pkg | Types pkg |
|
|
20
|
-
|
|
21
|
-
## vs Pino
|
|
22
|
-
|
|
23
|
-
[Pino](https://github.com/pinojs/pino) is the gold standard for high-performance Node.js logging.
|
|
24
|
-
|
|
25
|
-
### Similarities
|
|
26
|
-
|
|
27
|
-
- Performance-focused design
|
|
28
|
-
- JSON output in production
|
|
29
|
-
- Child loggers with inherited context
|
|
30
|
-
- Minimal overhead
|
|
31
|
-
|
|
32
|
-
### Differences
|
|
33
|
-
|
|
34
|
-
| Aspect | Pino | loggily |
|
|
35
|
-
| ------------------ | ------------------------------ | ---------------------------------- |
|
|
36
|
-
| Zero-cost disabled | Noop function (args evaluated) | Optional chaining (args skipped) |
|
|
37
|
-
| Spans | External (pino-opentelemetry) | Built-in |
|
|
38
|
-
| Transports | Built-in (worker threads) | File writer + custom via addWriter |
|
|
39
|
-
| Formatters | Plugin system | Console/JSON auto-switch |
|
|
40
|
-
| Serializers | Configurable | Fixed (Error auto-handled) |
|
|
41
|
-
|
|
42
|
-
### When to Choose
|
|
43
|
-
|
|
44
|
-
**Choose Pino if:**
|
|
45
|
-
|
|
46
|
-
- You need transport plugins (file rotation, remote logging)
|
|
47
|
-
- You need custom serializers for complex objects
|
|
48
|
-
- You're building a large production system with multiple log destinations
|
|
49
|
-
|
|
50
|
-
**Choose loggily if:**
|
|
51
|
-
|
|
52
|
-
- You want zero-cost disabled logging via optional chaining
|
|
53
|
-
- You need built-in span timing
|
|
54
|
-
- You prefer simplicity over configuration
|
|
55
|
-
- Bundle size matters
|
|
56
|
-
|
|
57
|
-
### Code Comparison
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
// Pino
|
|
61
|
-
import pino from "pino"
|
|
62
|
-
const log = pino({ level: "debug" })
|
|
63
|
-
const child = log.child({ requestId: "123" })
|
|
64
|
-
child.info({ user: "alice" }, "logged in")
|
|
65
|
-
|
|
66
|
-
// loggily
|
|
67
|
-
import { createLogger } from "loggily"
|
|
68
|
-
const log = createLogger("myapp")
|
|
69
|
-
const child = log.logger("request", { requestId: "123" })
|
|
70
|
-
child.info("logged in", { user: "alice" })
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## vs Winston
|
|
76
|
-
|
|
77
|
-
[Winston](https://github.com/winstonjs/winston) is the most popular Node.js logger with extensive transport support.
|
|
78
|
-
|
|
79
|
-
### Similarities
|
|
80
|
-
|
|
81
|
-
- Multiple log levels
|
|
82
|
-
- Structured logging support
|
|
83
|
-
- Child loggers
|
|
84
|
-
|
|
85
|
-
### Differences
|
|
86
|
-
|
|
87
|
-
| Aspect | Winston | loggily |
|
|
88
|
-
| ------------- | ---------------------- | ------------------- |
|
|
89
|
-
| Philosophy | Flexible, configurable | Simple, opinionated |
|
|
90
|
-
| Transports | 10+ built-in | stdout only |
|
|
91
|
-
| Configuration | Extensive | Minimal (env vars) |
|
|
92
|
-
| Performance | Moderate | High |
|
|
93
|
-
| Bundle Size | ~200KB+ | ~3KB |
|
|
94
|
-
| Spans | No | Built-in |
|
|
95
|
-
|
|
96
|
-
### When to Choose
|
|
97
|
-
|
|
98
|
-
**Choose Winston if:**
|
|
99
|
-
|
|
100
|
-
- You need multiple transports (file, HTTP, database)
|
|
101
|
-
- You need custom formatters and filters
|
|
102
|
-
- You have complex logging requirements
|
|
103
|
-
|
|
104
|
-
**Choose loggily if:**
|
|
105
|
-
|
|
106
|
-
- You want minimal configuration
|
|
107
|
-
- Performance is critical
|
|
108
|
-
- You're logging to stdout (12-factor app)
|
|
109
|
-
- You need built-in timing spans
|
|
110
|
-
|
|
111
|
-
### Code Comparison
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// Winston
|
|
115
|
-
import winston from "winston"
|
|
116
|
-
const log = winston.createLogger({
|
|
117
|
-
level: "info",
|
|
118
|
-
format: winston.format.json(),
|
|
119
|
-
transports: [new winston.transports.Console()],
|
|
120
|
-
})
|
|
121
|
-
log.info("starting", { port: 3000 })
|
|
122
|
-
|
|
123
|
-
// loggily
|
|
124
|
-
import { createLogger } from "loggily"
|
|
125
|
-
const log = createLogger("myapp")
|
|
126
|
-
log.info("starting", { port: 3000 })
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## vs Bunyan
|
|
132
|
-
|
|
133
|
-
[Bunyan](https://github.com/trentm/node-bunyan) focuses on JSON logging with built-in CLI tools.
|
|
134
|
-
|
|
135
|
-
### Similarities
|
|
136
|
-
|
|
137
|
-
- JSON-first output
|
|
138
|
-
- Child loggers
|
|
139
|
-
- Structured data
|
|
140
|
-
|
|
141
|
-
### Differences
|
|
142
|
-
|
|
143
|
-
| Aspect | Bunyan | loggily |
|
|
144
|
-
| ------------- | ---------------------- | --------------------------- |
|
|
145
|
-
| Output Format | JSON only | Console (dev) / JSON (prod) |
|
|
146
|
-
| CLI Tools | bunyan CLI for viewing | None |
|
|
147
|
-
| Streams | Multiple streams | stdout only |
|
|
148
|
-
| Spans | No | Built-in |
|
|
149
|
-
| API | Verbose | Simple |
|
|
150
|
-
|
|
151
|
-
### When to Choose
|
|
152
|
-
|
|
153
|
-
**Choose Bunyan if:**
|
|
154
|
-
|
|
155
|
-
- You want the bunyan CLI for log viewing
|
|
156
|
-
- You need multiple output streams
|
|
157
|
-
- JSON-only output is fine for development
|
|
158
|
-
|
|
159
|
-
**Choose loggily if:**
|
|
160
|
-
|
|
161
|
-
- You want readable console output in development
|
|
162
|
-
- You need built-in spans
|
|
163
|
-
- You prefer a simpler API
|
|
164
|
-
|
|
165
|
-
### Code Comparison
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// Bunyan
|
|
169
|
-
import bunyan from "bunyan"
|
|
170
|
-
const log = bunyan.createLogger({ name: "myapp" })
|
|
171
|
-
const child = log.child({ requestId: "123" })
|
|
172
|
-
child.info({ user: "alice" }, "logged in")
|
|
173
|
-
|
|
174
|
-
// loggily
|
|
175
|
-
import { createLogger } from "loggily"
|
|
176
|
-
const log = createLogger("myapp")
|
|
177
|
-
const child = log.logger("request", { requestId: "123" })
|
|
178
|
-
child.info("logged in", { user: "alice" })
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## vs debug
|
|
184
|
-
|
|
185
|
-
[debug](https://github.com/debug-js/debug) is a tiny debugging utility.
|
|
186
|
-
|
|
187
|
-
### Similarities
|
|
188
|
-
|
|
189
|
-
- Minimal footprint
|
|
190
|
-
- Namespace-based organization
|
|
191
|
-
- Environment variable control
|
|
192
|
-
|
|
193
|
-
### Differences
|
|
194
|
-
|
|
195
|
-
| Aspect | debug | loggily |
|
|
196
|
-
| ------------- | ----------------- | ----------------- |
|
|
197
|
-
| Log Levels | No (on/off) | Yes (5 levels) |
|
|
198
|
-
| Output Format | printf-style | Structured JSON |
|
|
199
|
-
| Spans | No | Built-in |
|
|
200
|
-
| Conditional | `.enabled` check | Optional chaining |
|
|
201
|
-
| Data | Inline in message | Separate object |
|
|
202
|
-
|
|
203
|
-
### When to Choose
|
|
204
|
-
|
|
205
|
-
**Choose debug if:**
|
|
206
|
-
|
|
207
|
-
- You only need simple debugging output
|
|
208
|
-
- You don't need log levels
|
|
209
|
-
- You don't need structured data
|
|
210
|
-
|
|
211
|
-
**Choose loggily if:**
|
|
212
|
-
|
|
213
|
-
- You need log levels
|
|
214
|
-
- You need structured data
|
|
215
|
-
- You need timing spans
|
|
216
|
-
- You want zero-cost disabled logging
|
|
217
|
-
|
|
218
|
-
### Code Comparison
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
// debug
|
|
222
|
-
import createDebug from "debug"
|
|
223
|
-
const debug = createDebug("myapp")
|
|
224
|
-
debug("user %s logged in", username)
|
|
225
|
-
|
|
226
|
-
// loggily
|
|
227
|
-
import { createLogger } from "loggily"
|
|
228
|
-
const log = createLogger("myapp")
|
|
229
|
-
log.info("user logged in", { username })
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
See [migration-from-debug.md](./migration-from-debug.md) for a detailed migration guide.
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## Unique Features of loggily
|
|
237
|
-
|
|
238
|
-
### 1. Zero-cost Disabled Logging
|
|
239
|
-
|
|
240
|
-
Optional chaining skips argument evaluation entirely:
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
// Other loggers - args always evaluated
|
|
244
|
-
pino.debug(`expensive: ${computeState()}`) // computeState() runs even if disabled
|
|
245
|
-
|
|
246
|
-
// loggily - args skipped when disabled
|
|
247
|
-
log.debug?.(`expensive: ${computeState()}`) // computeState() NOT called if disabled
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Benchmark (10M iterations):**
|
|
251
|
-
|
|
252
|
-
- Noop with expensive args: 17M ops/s (57.6ns)
|
|
253
|
-
- Optional chaining with expensive args: 408M ops/s (2.5ns) - **22x faster**
|
|
254
|
-
|
|
255
|
-
### 2. Built-in Spans
|
|
256
|
-
|
|
257
|
-
No external tracing library needed:
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
{
|
|
261
|
-
using span = log.span("db:query", { table: "users" })
|
|
262
|
-
const users = await db.query("SELECT * FROM users")
|
|
263
|
-
span.spanData.count = users.length
|
|
264
|
-
}
|
|
265
|
-
// → SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Features:
|
|
269
|
-
|
|
270
|
-
- Automatic timing on block exit
|
|
271
|
-
- Parent-child relationships tracked
|
|
272
|
-
- Custom attributes via `spanData`
|
|
273
|
-
- Trace ID for request correlation
|
|
274
|
-
|
|
275
|
-
### 3. Disposable Pattern
|
|
276
|
-
|
|
277
|
-
Uses JavaScript's `using` keyword for automatic cleanup:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
{
|
|
281
|
-
using span = log.span("operation")
|
|
282
|
-
// ... work ...
|
|
283
|
-
} // Span automatically ends and emits timing
|
|
284
|
-
|
|
285
|
-
// No need for try/finally or .end() calls
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### 4. Auto-format Switching
|
|
289
|
-
|
|
290
|
-
Console output in development, JSON in production:
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
# Development (pretty console)
|
|
294
|
-
bun run app
|
|
295
|
-
# → 14:32:15 INFO myapp starting {port: 3000}
|
|
296
|
-
|
|
297
|
-
# Production (JSON)
|
|
298
|
-
NODE_ENV=production bun run app
|
|
299
|
-
# → {"time":"2024-01-15T14:32:15.123Z","level":"info","name":"myapp","msg":"starting","port":3000}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## Summary
|
|
305
|
-
|
|
306
|
-
| Use Case | Recommended |
|
|
307
|
-
| --------------------------------------- | ---------------- |
|
|
308
|
-
| High-performance with optional chaining | loggily |
|
|
309
|
-
| Built-in span timing | loggily |
|
|
310
|
-
| Multiple transports | Pino or Winston |
|
|
311
|
-
| Extensive configuration | Winston |
|
|
312
|
-
| JSON CLI tools | Bunyan |
|
|
313
|
-
| Simple debugging only | debug |
|
|
314
|
-
| Minimal bundle size | debug or loggily |
|
|
315
|
-
| TypeScript-first | loggily or Pino |
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# Conditional Logging Research
|
|
2
|
-
|
|
3
|
-
Background research for the optional chaining pattern in loggily.
|
|
4
|
-
|
|
5
|
-
## The Problem
|
|
6
|
-
|
|
7
|
-
When logging is disabled, traditional loggers still evaluate arguments:
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
// Even when debug is disabled, expensiveState() is STILL called
|
|
11
|
-
log.debug("state: %o", computeExpensiveState())
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
This is wasted work. In hot code paths (rendering loops, per-node operations), this overhead adds up.
|
|
15
|
-
|
|
16
|
-
## Solution: Optional Chaining
|
|
17
|
-
|
|
18
|
-
JavaScript's optional chaining (`?.`) skips argument evaluation entirely when the method is undefined:
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
// When log.debug is undefined, the entire call is skipped
|
|
22
|
-
// computeExpensiveState() is NEVER called
|
|
23
|
-
log.debug?.("state: %o", computeExpensiveState())
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Benchmark Results (Bun 1.1.x, M1 Mac)
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
DISABLED LOGGING (no arguments evaluated)
|
|
30
|
-
1. noop function call 2168M ops/s 0.5ns/op
|
|
31
|
-
2. optional chaining (?.) - undefined 1406M ops/s 0.7ns/op
|
|
32
|
-
3. proxy returning undefined + ?. 545M ops/s 1.8ns/op
|
|
33
|
-
4. proxy returning noop 352M ops/s 2.8ns/op
|
|
34
|
-
|
|
35
|
-
DISABLED LOGGING (with expensive argument)
|
|
36
|
-
1. noop - args evaluated (wastes work) 17M ops/s 57.6ns/op
|
|
37
|
-
2. optional chaining - args NOT evaluated 408M ops/s 2.5ns/op ← 22x faster
|
|
38
|
-
3. proxy + ?. - args NOT evaluated 168M ops/s 5.9ns/op
|
|
39
|
-
4. proxy + noop - args evaluated (wastes work) 15M ops/s 65.3ns/op
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Key insight**: For cheap arguments, noop is only ~0.2ns faster. For expensive arguments, optional chaining is **22x faster** because it skips argument evaluation entirely.
|
|
43
|
-
|
|
44
|
-
## External Research
|
|
45
|
-
|
|
46
|
-
### Matteo Collina's Analysis (2025)
|
|
47
|
-
|
|
48
|
-
Article: [Noop Functions vs Optional Chaining: A Performance Deep Dive](https://adventures.nodeland.dev/archive/noop-functions-vs-optional-chaining-a-performance/)
|
|
49
|
-
|
|
50
|
-
Collina (Pino maintainer) benchmarked that noop functions outperform optional chaining in raw call overhead. However, his test used cheap arguments. The benchmark above shows that the **real benefit** of `?.` is skipping expensive argument evaluation, which Collina's test didn't measure.
|
|
51
|
-
|
|
52
|
-
Quote: "Discover why noop functions are faster than optional chaining in JavaScript"
|
|
53
|
-
|
|
54
|
-
**Our finding**: This is only true for cheap args. When args are expensive, `?.` wins decisively.
|
|
55
|
-
|
|
56
|
-
### Lazy Logging in JavaScript (2023)
|
|
57
|
-
|
|
58
|
-
Article: [Lazy/conditional logging in JavaScript](https://tonisives.com/blog/2023/05/28/lazy-conditional-logging-in-javascript/)
|
|
59
|
-
|
|
60
|
-
Toni Sives describes passing functions to delay evaluation:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
Logger.trace(() => `long task took ${longRunningTask()}`)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
This works but requires wrapping every call in an arrow function. Optional chaining is more ergonomic:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
log.trace?.(`long task took ${longRunningTask()}`)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### TC39 Explicit Resource Management
|
|
73
|
-
|
|
74
|
-
The `using` keyword (Stage 3 as of June 2024) enables automatic span disposal:
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
{
|
|
78
|
-
using span = log.span("operation")
|
|
79
|
-
span.debug("working...")
|
|
80
|
-
} // Automatically calls span[Symbol.dispose]()
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Reference: [TC39 Proposal](https://tc39.es/proposal-explicit-resource-management/)
|
|
84
|
-
|
|
85
|
-
### Proxy Performance (Historical)
|
|
86
|
-
|
|
87
|
-
Valeri Karpov's 2016 benchmarks showed Proxies were ~10x slower than direct property access. Modern V8 has improved significantly, but Proxy still adds overhead (~1-3ns per access). For logging, this is negligible compared to actual I/O.
|
|
88
|
-
|
|
89
|
-
Reference: [Thoughts on ES6 Proxies Performance](https://thecodebarbarian.com/thoughts-on-es6-proxies-performance)
|
|
90
|
-
|
|
91
|
-
## Implementation Approach
|
|
92
|
-
|
|
93
|
-
### Proxy Wrapper
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
import { createLogger, getLogLevel } from "loggily"
|
|
97
|
-
|
|
98
|
-
const baseLog = createLogger("myapp")
|
|
99
|
-
|
|
100
|
-
const LEVEL_PRIORITY = {
|
|
101
|
-
trace: 0,
|
|
102
|
-
debug: 1,
|
|
103
|
-
info: 2,
|
|
104
|
-
warn: 3,
|
|
105
|
-
error: 4,
|
|
106
|
-
silent: 5,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export const log = new Proxy(baseLog, {
|
|
110
|
-
get(target, prop: string) {
|
|
111
|
-
// For level methods, return undefined if disabled
|
|
112
|
-
if (prop in LEVEL_PRIORITY) {
|
|
113
|
-
const current = LEVEL_PRIORITY[getLogLevel() as keyof typeof LEVEL_PRIORITY]
|
|
114
|
-
if (LEVEL_PRIORITY[prop as keyof typeof LEVEL_PRIORITY] < current) {
|
|
115
|
-
return undefined
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return (target as any)[prop]
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### TypeScript Types
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
type ConditionalLogger = {
|
|
127
|
-
trace?: (msg: string, data?: object) => void
|
|
128
|
-
debug?: (msg: string, data?: object) => void
|
|
129
|
-
info?: (msg: string, data?: object) => void
|
|
130
|
-
warn?: (msg: string, data?: object) => void
|
|
131
|
-
error: (msg: string, data?: object) => void // Always available
|
|
132
|
-
logger: (ns?: string, props?: object) => ConditionalLogger
|
|
133
|
-
span?: (ns?: string, props?: object) => SpanLogger | undefined
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
TypeScript enforces the `?.` pattern at compile time - you can't call `log.debug()` without `?.` because the method may be undefined.
|
|
138
|
-
|
|
139
|
-
## Design Decision: Always Use `?.`
|
|
140
|
-
|
|
141
|
-
Given the benchmark results:
|
|
142
|
-
|
|
143
|
-
- Overhead of `?.` vs noop for cheap args: ~0.2ns (negligible)
|
|
144
|
-
- Benefit of `?.` for expensive args: ~55ns saved (22x faster)
|
|
145
|
-
|
|
146
|
-
**Conclusion**: Always use `log.debug?.()`. The ergonomic cost is minimal (just add `?.`), and the performance benefit is significant when arguments involve any computation.
|
|
147
|
-
|
|
148
|
-
## Alternative: Lazy Evaluation
|
|
149
|
-
|
|
150
|
-
For very expensive argument preparation, use a function:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
log.debug?.(() => {
|
|
154
|
-
const state = gatherComplexState()
|
|
155
|
-
return ["state: %o", state]
|
|
156
|
-
})
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
The logger calls the function only if debug is enabled. This pattern is useful when you need to prepare multiple pieces of data that depend on each other.
|
package/docs/guide.md
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# The Guide
|
|
2
|
-
|
|
3
|
-
> Clarity without the clutter. Ergonomic unified logs, spans, metrics, and debugs for modern TypeScript.
|
|
4
|
-
|
|
5
|
-
Your first app uses `console.log`. That's enough for a script, a prototype, a small server. Then your app grows. You need structured logs for production, the `debug` package for conditional verbose output, a tracing library for timings, maybe OpenTelemetry for distributed traces — and suddenly you're juggling three tools with three APIs, three configuration schemes, and three output formats.
|
|
6
|
-
|
|
7
|
-
loggily is one library where structured logging, debug-style conditional output, timed spans, and metrics all share the same namespace tree, the same output pipeline, and the same zero-overhead pattern. You adopt each capability when you need it. Nothing is wasted, nothing conflicts, nothing clutters your code.
|
|
8
|
-
|
|
9
|
-
## Level 1: Just Log
|
|
10
|
-
|
|
11
|
-
You need structured logging with levels. One import, one function.
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { createLogger } from "loggily"
|
|
15
|
-
|
|
16
|
-
const log = createLogger("myapp")
|
|
17
|
-
|
|
18
|
-
log.info?.("server started", { port: 3000 })
|
|
19
|
-
log.warn?.("disk space low", { free: "2GB" })
|
|
20
|
-
log.error?.(new Error("connection failed"))
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Notice the `?.` — if a log level is disabled, the entire call is skipped, including argument evaluation. In benchmarks, this makes disabled log calls ~22x faster than a traditional logger that still evaluates its arguments. You get zero-cost logging for disabled levels, not just low-cost.
|
|
24
|
-
|
|
25
|
-
Colorized in your terminal, with source locations:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
14:32:15 INFO myapp server started {port: 3000}
|
|
29
|
-
14:32:15 WARN myapp disk space low {free: "2GB"}
|
|
30
|
-
14:32:15 ERROR myapp connection failed
|
|
31
|
-
Error: connection failed
|
|
32
|
-
at server.ts:42
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Set `LOG_FORMAT=json` or `NODE_ENV=production` and the same calls produce structured JSON — same data, machine-parseable, ready for Datadog or Elastic or whatever your ops team uses:
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{ "time": "2024-01-15T14:32:15.123Z", "level": "info", "name": "myapp", "msg": "server started", "port": 3000 }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
You never choose between human-readable and machine-parseable. You get both from the same call.
|
|
42
|
-
|
|
43
|
-
**The wall**: Your app has 20 modules. You need verbose output from the database layer but not from the HTTP layer. `LOG_LEVEL=debug` turns on everything.
|
|
44
|
-
|
|
45
|
-
## Level 2: Namespaces
|
|
46
|
-
|
|
47
|
-
Loggers form a tree. Child loggers inherit their parent's namespace and props:
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
const log = createLogger("myapp")
|
|
51
|
-
const db = log.logger("db") // myapp:db
|
|
52
|
-
const http = log.logger("http") // myapp:http
|
|
53
|
-
const query = db.logger("query") // myapp:db:query
|
|
54
|
-
|
|
55
|
-
db.debug?.("connecting") // myapp:db
|
|
56
|
-
query.debug?.("SELECT * FROM...") // myapp:db:query
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Now you can target output. `DEBUG` auto-lowers the log level to `debug` and restricts all output to matching namespaces:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
DEBUG=myapp:db bun run app # Only myapp:db namespace (all levels)
|
|
63
|
-
DEBUG='myapp:*,-myapp:http' bun run app # Everything except HTTP
|
|
64
|
-
LOG_LEVEL=debug bun run app # Debug level globally, all namespaces
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
`DEBUG` is a namespace visibility filter inspired by the `debug` package — same patterns, same muscle memory — but as part of a full logging system with levels, structured data, and JSON output. Use `LOG_LEVEL` when you want to change the verbosity floor without restricting namespaces.
|
|
68
|
-
|
|
69
|
-
**The wall**: A request takes 3 seconds. You know it's slow, but you don't know which part.
|
|
70
|
-
|
|
71
|
-
## Level 3: Spans
|
|
72
|
-
|
|
73
|
-
A span is a logger with a timer. It measures how long a block takes, and every log inside it inherits its context:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
{
|
|
77
|
-
using span = log.span("import", { file: "data.csv" })
|
|
78
|
-
span.info?.("parsing rows")
|
|
79
|
-
span.spanData.count = 42
|
|
80
|
-
}
|
|
81
|
-
// -> SPAN myapp:import (1234ms) {count: 42, file: "data.csv"}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
The `using` keyword (TC39 Explicit Resource Management) automatically calls `span[Symbol.dispose]()` at block exit. The span measures its duration and reports it along with any attributes you set. No try/finally, no manual timing, no separate tracing SDK.
|
|
85
|
-
|
|
86
|
-
Spans nest. Each span gets a unique ID and shares its parent's trace ID, so you can correlate events across a request:
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
{
|
|
90
|
-
using req = log.span("request", { path: "/api/users" })
|
|
91
|
-
{
|
|
92
|
-
using db = req.span("db-query")
|
|
93
|
-
// db.spanData.traceId === req.spanData.traceId
|
|
94
|
-
// db.spanData.parentId === req.spanData.id
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Control span output independently from logs:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
TRACE=1 bun run app # All spans
|
|
103
|
-
TRACE=myapp:db bun run app # Only database spans
|
|
104
|
-
TRACE=myapp:db,myapp:cache bun run app # Database + cache spans
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**The wall**: Now you need logs sent elsewhere — a file, Datadog, your tracing backend — not just the console.
|
|
108
|
-
|
|
109
|
-
## Level 4: Writers
|
|
110
|
-
|
|
111
|
-
The writer system is a simple function interface. Write once, send anywhere:
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import { addWriter, createFileWriter } from "loggily"
|
|
115
|
-
|
|
116
|
-
// File writer with buffered auto-flush
|
|
117
|
-
const file = createFileWriter("/var/log/app.log")
|
|
118
|
-
addWriter((formatted, level) => file.write(formatted))
|
|
119
|
-
|
|
120
|
-
// Send to an HTTP endpoint
|
|
121
|
-
addWriter((formatted, level) => {
|
|
122
|
-
if (level === "error") fetch("/api/alerts", { method: "POST", body: formatted })
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// Send spans to your tracing backend
|
|
126
|
-
addWriter((formatted, level) => {
|
|
127
|
-
if (level === "span") sendToJaeger(JSON.parse(formatted))
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
You can attach multiple writers — each one receives every log and span. The logger doesn't care where the output goes; it just produces structured data. You decide where to send it.
|
|
132
|
-
|
|
133
|
-
Output modes let you control the default output:
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
import { setOutputMode } from "loggily"
|
|
137
|
-
setOutputMode("writers-only") // Only writers, no console
|
|
138
|
-
setOutputMode("stderr") // Bypass Ink/React console capture
|
|
139
|
-
setOutputMode("console") // Default: console.log/warn/error
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**The wall**: You spawn worker threads for heavy processing, but their logs vanish from the main output.
|
|
143
|
-
|
|
144
|
-
## Level 5: Workers
|
|
145
|
-
|
|
146
|
-
Worker threads get their own loggers that forward to the main thread:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// worker.ts
|
|
150
|
-
import { createWorkerLogger } from "loggily/worker"
|
|
151
|
-
|
|
152
|
-
const log = createWorkerLogger(postMessage, "myapp:worker")
|
|
153
|
-
log.info?.("processing chunk", { size: 1000 })
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
using span = log.span("process")
|
|
157
|
-
// ...
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// main.ts
|
|
163
|
-
import { createWorkerLogHandler } from "loggily/worker"
|
|
164
|
-
|
|
165
|
-
const handler = createWorkerLogHandler()
|
|
166
|
-
worker.on("message", (msg) => handler(msg))
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Logs and spans from workers appear in the same output stream with the same formatting. No interleaving, no lost messages.
|
|
170
|
-
|
|
171
|
-
**The wall**: You need child loggers that carry request context through async call chains without passing the logger everywhere.
|
|
172
|
-
|
|
173
|
-
## Level 6: Context
|
|
174
|
-
|
|
175
|
-
Child loggers carry structured context through async call chains. Create one at the request boundary, and every downstream log inherits its fields:
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
const reqLog = log.child({ requestId: "abc-123", userId: 42 })
|
|
179
|
-
|
|
180
|
-
reqLog.info?.("handling request")
|
|
181
|
-
// -> 14:32:15 INFO myapp handling request {requestId: "abc-123", userId: 42}
|
|
182
|
-
|
|
183
|
-
// Pass reqLog to downstream functions -- context propagates
|
|
184
|
-
await handleAuth(reqLog)
|
|
185
|
-
await handleQuery(reqLog)
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Every log from `reqLog` and its descendants carries `requestId` and `userId` without manual field-passing. In JSON mode, these become top-level fields — perfect for filtering in your log aggregator.
|
|
189
|
-
|
|
190
|
-
## What You Have
|
|
191
|
-
|
|
192
|
-
Normally, you'd pull in one library for logs, another for debug prints, a tracing SDK for spans — and struggle to tie them together. With loggily, these aren't separate concerns. They're modes of the same tool.
|
|
193
|
-
|
|
194
|
-
At this point you've replaced that patchwork with a single library:
|
|
195
|
-
|
|
196
|
-
- **Structured logging** with levels, namespaces, colorized dev output, JSON production output, and source locations
|
|
197
|
-
- **Debug output** with `DEBUG=namespace:*` filtering — the `debug` package's power, integrated
|
|
198
|
-
- **Span timing** with `using` keyword, nested traces, and independent `TRACE=` control
|
|
199
|
-
- **Flexible output** via writers — file, HTTP, tracing backends, anything
|
|
200
|
-
- **Worker thread support** with automatic forwarding
|
|
201
|
-
- **Context propagation** via child loggers
|
|
202
|
-
|
|
203
|
-
All sharing one namespace tree. All respecting the same log levels. All using the same `?.` pattern — disabled calls are skipped entirely, including argument evaluation. There when you need it, invisible when you don't.
|
|
204
|
-
|
|
205
|
-
~3KB. Zero dependencies. Modern TypeScript.
|