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
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# The Journey
|
|
2
|
-
|
|
3
|
-
> Clarity without the clutter.
|
|
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:
|
|
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 Get
|
|
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
|
-
- **Structured logging** with levels, namespaces, colorized dev output, JSON production output
|
|
195
|
-
- **Debug output** with `DEBUG=namespace:*` filtering — the `debug` package's power, integrated
|
|
196
|
-
- **Span timing** with `using` keyword, nested traces, and independent `TRACE=` control
|
|
197
|
-
- **Flexible output** via writers — file, HTTP, tracing backends, anything
|
|
198
|
-
- **Worker thread support** with automatic forwarding
|
|
199
|
-
- **Context propagation** via child loggers
|
|
200
|
-
|
|
201
|
-
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.
|
|
202
|
-
|
|
203
|
-
~3KB. Zero dependencies. Modern TypeScript.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Migration from debug
|
|
2
|
-
|
|
3
|
-
See the [full migration guide](https://github.com/beorn/loggily/blob/main/docs/migration-from-debug.md) for step-by-step instructions.
|
|
4
|
-
|
|
5
|
-
## Quick Overview
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// Before (debug)
|
|
9
|
-
import createDebug from "debug"
|
|
10
|
-
const debug = createDebug("myapp")
|
|
11
|
-
debug("user %s logged in from %s", username, ip)
|
|
12
|
-
|
|
13
|
-
// After (loggily)
|
|
14
|
-
import { createLogger } from "loggily"
|
|
15
|
-
const log = createLogger("myapp")
|
|
16
|
-
log.info?.("user logged in", { username, ip })
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
The `DEBUG` environment variable works the same way:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
DEBUG=myapp bun run app
|
|
23
|
-
DEBUG='myapp,-myapp:noisy' bun run app
|
|
24
|
-
```
|
package/docs/site/guide/spans.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# Spans
|
|
2
|
-
|
|
3
|
-
Spans are loggers with timing. Call `.span()` and it creates a child logger that tracks how long the block runs.
|
|
4
|
-
|
|
5
|
-
## Basic Usage
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
{
|
|
9
|
-
using span = log.span("import", { file: "data.csv" })
|
|
10
|
-
span.info?.("parsing rows")
|
|
11
|
-
const rows = await parseFile()
|
|
12
|
-
span.spanData.rowCount = rows.length
|
|
13
|
-
}
|
|
14
|
-
// SPAN myapp:import (1234ms) {rowCount: 500, file: "data.csv"}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The `using` keyword (TC39 Explicit Resource Management) calls `span[Symbol.dispose]()` when the block exits, which records the end time and emits the span event.
|
|
18
|
-
|
|
19
|
-
## Enabling Spans
|
|
20
|
-
|
|
21
|
-
Span output is off by default. Enable via environment or code:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
TRACE=1 bun run app # All spans
|
|
25
|
-
TRACE=myapp:db bun run app # Only db spans
|
|
26
|
-
TRACE=myapp,other bun run app # Multiple namespaces
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { enableSpans, setTraceFilter } from "loggily"
|
|
31
|
-
|
|
32
|
-
enableSpans() // All spans
|
|
33
|
-
setTraceFilter(["myapp:db"]) // Only db spans
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Nested Spans
|
|
37
|
-
|
|
38
|
-
Spans automatically track parent-child relationships and share trace IDs:
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
{
|
|
42
|
-
using request = log.span("request", { path: "/api/users" })
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
using auth = request.span("auth")
|
|
46
|
-
await verifyToken()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
using db = request.span("db:query")
|
|
51
|
-
// db.spanData.parentId === request.spanData.id
|
|
52
|
-
// db.spanData.traceId === request.spanData.traceId
|
|
53
|
-
await fetchUsers()
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Output:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
SPAN myapp:auth (12ms) {}
|
|
62
|
-
SPAN myapp:db:query (45ms) {}
|
|
63
|
-
SPAN myapp:request (62ms) {path: "/api/users"}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Span Data
|
|
67
|
-
|
|
68
|
-
Set custom attributes via `span.spanData`:
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
{
|
|
72
|
-
using span = log.span("batch")
|
|
73
|
-
span.spanData.total = items.length
|
|
74
|
-
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
await process(item)
|
|
77
|
-
span.spanData.processed = ((span.spanData.processed as number) ?? 0) + 1
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
span.spanData.status = "complete"
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Read-only Properties
|
|
85
|
-
|
|
86
|
-
| Property | Type | Description |
|
|
87
|
-
| ----------- | ---------------- | ------------------------------------ |
|
|
88
|
-
| `id` | `string` | Unique span ID (`sp_1`, `sp_2`, ...) |
|
|
89
|
-
| `traceId` | `string` | Shared across nested spans |
|
|
90
|
-
| `parentId` | `string \| null` | Parent span ID |
|
|
91
|
-
| `startTime` | `number` | Start timestamp (ms since epoch) |
|
|
92
|
-
| `endTime` | `number \| null` | End timestamp (null while running) |
|
|
93
|
-
| `duration` | `number` | Live duration (computed on access) |
|
|
94
|
-
|
|
95
|
-
## Manual End
|
|
96
|
-
|
|
97
|
-
For environments without `using` support:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const span = log.span("operation")
|
|
101
|
-
try {
|
|
102
|
-
await doWork()
|
|
103
|
-
span.spanData.result = "success"
|
|
104
|
-
} finally {
|
|
105
|
-
span.end()
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Logging Within Spans
|
|
110
|
-
|
|
111
|
-
Spans are full loggers -- you can call `.info?.()`, `.debug?.()`, etc:
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
{
|
|
115
|
-
using span = log.span("import")
|
|
116
|
-
span.info?.("starting import")
|
|
117
|
-
span.debug?.("reading file")
|
|
118
|
-
span.warn?.("skipping malformed row", { row: 42 })
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## JSON Output
|
|
123
|
-
|
|
124
|
-
Spans respect the output format:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
TRACE=1 LOG_FORMAT=json bun run app
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```json
|
|
131
|
-
{
|
|
132
|
-
"time": "2026-01-15T14:32:16.456Z",
|
|
133
|
-
"level": "span",
|
|
134
|
-
"name": "myapp:import",
|
|
135
|
-
"msg": "(1234ms)",
|
|
136
|
-
"duration": 1234,
|
|
137
|
-
"rowCount": 500
|
|
138
|
-
}
|
|
139
|
-
```
|
package/docs/site/guide/why.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Why loggily?
|
|
2
|
-
|
|
3
|
-
## The Problem
|
|
4
|
-
|
|
5
|
-
Most loggers waste work when logging is disabled. Even when `debug` level is off:
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// Pino, Winston, Bunyan
|
|
9
|
-
log.debug(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
`computeExpensiveState()` runs, `JSON.stringify()` runs, the string is concatenated -- all discarded because debug is off. In hot code paths (rendering loops, per-node operations), this adds up.
|
|
13
|
-
|
|
14
|
-
## The Solution
|
|
15
|
-
|
|
16
|
-
loggily uses optional chaining to skip argument evaluation entirely:
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
When `debug` is disabled, `log.debug` is `undefined`. JavaScript's `?.` operator short-circuits: `computeExpensiveState()` never runs, `JSON.stringify()` never runs, the string is never built.
|
|
23
|
-
|
|
24
|
-
## Benchmarks
|
|
25
|
-
|
|
26
|
-
10M iterations, Bun 1.1.x, M1 Mac:
|
|
27
|
-
|
|
28
|
-
| Scenario | ops/s | ns/op |
|
|
29
|
-
| -------------------------------------- | -------- | ------- |
|
|
30
|
-
| Traditional noop (cheap args) | 2168M | 0.5 |
|
|
31
|
-
| Optional chaining (cheap args) | 1406M | 0.7 |
|
|
32
|
-
| Traditional noop (expensive args) | 17M | 57.6 |
|
|
33
|
-
| **Optional chaining (expensive args)** | **408M** | **2.5** |
|
|
34
|
-
|
|
35
|
-
For cheap arguments the overhead is ~0.2ns -- negligible. For expensive arguments, **22x faster**.
|
|
36
|
-
|
|
37
|
-
## Compared to Others
|
|
38
|
-
|
|
39
|
-
| Feature | loggily | Pino | Winston | debug |
|
|
40
|
-
| ------------------ | ---------- | ----- | ------- | ----- |
|
|
41
|
-
| Zero-cost disabled | `?.` (22x) | noop | noop | check |
|
|
42
|
-
| Built-in spans | Yes | No | No | No |
|
|
43
|
-
| Bundle size | ~3KB | ~17KB | ~200KB+ | ~2KB |
|
|
44
|
-
| TypeScript native | Yes | Types | Types | Types |
|
|
45
|
-
| Worker threads | Yes | No | No | No |
|
|
46
|
-
|
|
47
|
-
See [Comparison](https://github.com/beorn/loggily/blob/main/docs/comparison.md) for detailed analysis of each.
|
|
48
|
-
|
|
49
|
-
## Design Principles
|
|
50
|
-
|
|
51
|
-
1. **Logger = Span**: Every logger can become a span. No separate tracing library needed.
|
|
52
|
-
2. **Zero cost**: Disabled levels skip everything, including argument evaluation.
|
|
53
|
-
3. **Minimal surface**: Few functions, each does one thing well.
|
|
54
|
-
4. **Type enforced**: TypeScript makes `?.` mandatory -- you can't accidentally call a disabled level.
|
|
55
|
-
5. **Structured**: JSON in production, readable console in development.
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Worker Thread Support
|
|
2
|
-
|
|
3
|
-
loggily provides typed message protocols for forwarding logs from worker threads to the main thread.
|
|
4
|
-
|
|
5
|
-
## Full Logger Forwarding
|
|
6
|
-
|
|
7
|
-
### Worker Side
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import { createWorkerLogger } from "loggily/worker"
|
|
11
|
-
|
|
12
|
-
const log = createWorkerLogger(postMessage, "myapp:worker")
|
|
13
|
-
|
|
14
|
-
log.info?.("processing", { file: "data.csv" })
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
using span = log.span("parse")
|
|
18
|
-
span.info?.("parsing rows")
|
|
19
|
-
span.spanData.lines = 100
|
|
20
|
-
}
|
|
21
|
-
// Span events forwarded to main thread automatically
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Main Thread Side
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
import { createWorkerLogHandler, isWorkerMessage } from "loggily/worker"
|
|
28
|
-
|
|
29
|
-
const handle = createWorkerLogHandler()
|
|
30
|
-
|
|
31
|
-
worker.onmessage = (e) => {
|
|
32
|
-
if (isWorkerMessage(e.data)) {
|
|
33
|
-
handle(e.data)
|
|
34
|
-
} else {
|
|
35
|
-
// Handle other message types
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Console Forwarding
|
|
41
|
-
|
|
42
|
-
For simpler cases, forward `console.*` calls:
|
|
43
|
-
|
|
44
|
-
### Worker Side
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
import { forwardConsole } from "loggily/worker"
|
|
48
|
-
|
|
49
|
-
forwardConsole(postMessage, "myapp:worker")
|
|
50
|
-
|
|
51
|
-
// All console.* calls are now forwarded
|
|
52
|
-
console.log("processing", { file: "data.csv" })
|
|
53
|
-
console.error(new Error("failed"))
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Main Thread Side
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { createWorkerConsoleHandler } from "loggily/worker"
|
|
60
|
-
|
|
61
|
-
const handle = createWorkerConsoleHandler({
|
|
62
|
-
defaultNamespace: "myapp:worker",
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
worker.onmessage = (e) => {
|
|
66
|
-
if (e.data.type === "console") {
|
|
67
|
-
handle(e.data)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Message Types
|
|
73
|
-
|
|
74
|
-
All messages are fully typed. Use the type guards to safely handle them:
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import {
|
|
78
|
-
isWorkerConsoleMessage,
|
|
79
|
-
isWorkerLogMessage,
|
|
80
|
-
isWorkerSpanMessage,
|
|
81
|
-
isWorkerMessage,
|
|
82
|
-
type WorkerMessage,
|
|
83
|
-
} from "loggily/worker"
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
| Type Guard | Message Type |
|
|
87
|
-
| ------------------------ | ----------------------- |
|
|
88
|
-
| `isWorkerConsoleMessage` | `console.*` forwarding |
|
|
89
|
-
| `isWorkerLogMessage` | Structured log messages |
|
|
90
|
-
| `isWorkerSpanMessage` | Span start/end events |
|
|
91
|
-
| `isWorkerMessage` | Any of the above |
|
|
92
|
-
|
|
93
|
-
## Serialization
|
|
94
|
-
|
|
95
|
-
Arguments are automatically serialized for `postMessage`:
|
|
96
|
-
|
|
97
|
-
- Functions become `"[Function: name]"`
|
|
98
|
-
- Symbols become their `toString()` representation
|
|
99
|
-
- BigInts become `"123n"` strings
|
|
100
|
-
- Circular references become `"[Circular]"`
|
|
101
|
-
- Errors become `{ name, message, stack }` objects
|
|
102
|
-
- Depth is capped at 5 levels
|
|
103
|
-
|
|
104
|
-
## Restoring Console
|
|
105
|
-
|
|
106
|
-
If you need to disable console forwarding:
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
import { restoreConsole } from "loggily/worker"
|
|
110
|
-
|
|
111
|
-
// Later:
|
|
112
|
-
restoreConsole() // Original console methods restored
|
|
113
|
-
```
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Zero-Overhead Logging
|
|
2
|
-
|
|
3
|
-
## How It Works
|
|
4
|
-
|
|
5
|
-
`createLogger()` returns a `ConditionalLogger` -- a Proxy where disabled log levels return `undefined`:
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
const log = createLogger("myapp")
|
|
9
|
-
|
|
10
|
-
// At default level (info):
|
|
11
|
-
typeof log.trace // undefined
|
|
12
|
-
typeof log.debug // undefined
|
|
13
|
-
typeof log.info // function
|
|
14
|
-
typeof log.warn // function
|
|
15
|
-
typeof log.error // function
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Using `?.` means the entire call is skipped when the method is undefined:
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
log.debug?.(`tree: ${JSON.stringify(buildTree())}`)
|
|
22
|
-
// buildTree() and JSON.stringify() NEVER run when debug is off
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Lazy Messages
|
|
26
|
-
|
|
27
|
-
For even more control, pass a function:
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
log.debug?.(() => {
|
|
31
|
-
const state = gatherComplexState()
|
|
32
|
-
return `state: ${JSON.stringify(state)}`
|
|
33
|
-
})
|
|
34
|
-
// Function only called when debug is enabled
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Type: `LazyMessage = string | (() => string)`
|
|
38
|
-
|
|
39
|
-
Both patterns work with all log levels and with structured data:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
log.trace?.(() => `verbose: ${expensiveComputation()}`, { extra: "data" })
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Dynamic Levels
|
|
46
|
-
|
|
47
|
-
The logger responds to level changes in real-time:
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import { createLogger, setLogLevel } from "loggily"
|
|
51
|
-
|
|
52
|
-
const log = createLogger("myapp")
|
|
53
|
-
|
|
54
|
-
setLogLevel("error")
|
|
55
|
-
log.debug // undefined
|
|
56
|
-
log.info // undefined
|
|
57
|
-
|
|
58
|
-
setLogLevel("debug")
|
|
59
|
-
log.debug // function (now available)
|
|
60
|
-
log.info // function
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## TypeScript Enforcement
|
|
64
|
-
|
|
65
|
-
TypeScript's type system makes `?.` mandatory:
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
const log = createLogger("myapp")
|
|
69
|
-
|
|
70
|
-
log.debug("msg") // Type error: Object is possibly 'undefined'
|
|
71
|
-
log.debug?.("msg") // Correct
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Performance Numbers
|
|
75
|
-
|
|
76
|
-
10M iterations, Bun 1.1.x, M1 Mac:
|
|
77
|
-
|
|
78
|
-
| Pattern | Cheap args | Expensive args |
|
|
79
|
-
| ----------------- | ---------- | -------------- |
|
|
80
|
-
| Noop function | 0.5 ns/op | 57.6 ns/op |
|
|
81
|
-
| Optional chaining | 0.7 ns/op | **2.5 ns/op** |
|
|
82
|
-
| Proxy + noop | 2.8 ns/op | 65.3 ns/op |
|
|
83
|
-
| Proxy + `?.` | 1.8 ns/op | 5.9 ns/op |
|
|
84
|
-
|
|
85
|
-
The Proxy overhead (~1ns) comes from `createLogger()` wrapping the base logger. For logging operations this is negligible compared to the actual I/O.
|
|
86
|
-
|
|
87
|
-
See [Conditional Logging Research](https://github.com/beorn/loggily/blob/main/docs/conditional-logging-research.md) for methodology and external references.
|
package/docs/site/index.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
layout: home
|
|
3
|
-
|
|
4
|
-
hero:
|
|
5
|
-
name: "loggily"
|
|
6
|
-
text: "Clarity without the clutter"
|
|
7
|
-
tagline: "Debug logging, structured logs, and distributed tracing — integrated into one ~3KB library. Zero dependencies, zero-overhead via optional chaining."
|
|
8
|
-
actions:
|
|
9
|
-
- theme: brand
|
|
10
|
-
text: The Journey
|
|
11
|
-
link: /guide/journey
|
|
12
|
-
- theme: alt
|
|
13
|
-
text: View on GitHub
|
|
14
|
-
link: https://github.com/beorn/loggily
|
|
15
|
-
|
|
16
|
-
features:
|
|
17
|
-
- title: "Debug Logging"
|
|
18
|
-
details: "Namespace filtering with DEBUG=myapp,-myapp:noisy — same ergonomics as the debug package. Uses native console methods so source lines stay clickable in DevTools."
|
|
19
|
-
- title: "Structured Logs"
|
|
20
|
-
details: "Colorized console with timestamps and clickable source lines in development. Structured JSON in production. Same code, same API — output format switches automatically."
|
|
21
|
-
- title: "Distributed Tracing"
|
|
22
|
-
details: "Built-in spans with automatic timing, parent-child tracking, trace IDs, and traceparent headers. All integrated — no separate SDK to wire up."
|
|
23
|
-
- title: Zero-Overhead via ?.
|
|
24
|
-
details: "Optional chaining skips the entire call — including argument evaluation — when a level is disabled. Typically 10x+ faster for real-world logging with string interpolation and serialization."
|
|
25
|
-
- title: ~3KB, Zero Dependencies
|
|
26
|
-
details: "No external dependencies. Native TypeScript, ESM-only. Runs on Node, Bun, and Deno."
|
|
27
|
-
- title: One Unified Pipeline
|
|
28
|
-
details: "Most projects wire together debug, pino, and OpenTelemetry — three configs, three formats, three APIs. loggily integrates all three: one namespace tree, one output pipeline, one import instead of three."
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Quick Start
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
bun add loggily
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { createLogger } from "loggily"
|
|
39
|
-
|
|
40
|
-
const log = createLogger("myapp")
|
|
41
|
-
|
|
42
|
-
// ?. skips the entire call — including argument evaluation — when the level is disabled (near-zero cost)
|
|
43
|
-
log.info?.("server started", { port: 3000 })
|
|
44
|
-
log.debug?.("cache hit", { key: "user:42" })
|
|
45
|
-
log.error?.(new Error("connection lost"))
|
|
46
|
-
|
|
47
|
-
// Spans time operations automatically
|
|
48
|
-
{
|
|
49
|
-
using span = log.span("db:query", { table: "users" })
|
|
50
|
-
const users = await db.query("SELECT * FROM users")
|
|
51
|
-
span.spanData.count = users.length
|
|
52
|
-
}
|
|
53
|
-
// Output: SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
54
|
-
```
|