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.
Files changed (46) hide show
  1. package/README.md +67 -22
  2. package/package.json +37 -21
  3. package/src/context.ts +26 -11
  4. package/src/core.ts +118 -72
  5. package/src/file-writer.ts +12 -6
  6. package/src/index.browser.ts +9 -1
  7. package/src/index.ts +9 -1
  8. package/src/tracing.ts +11 -3
  9. package/src/worker.ts +119 -132
  10. package/.github/workflows/docs.yml +0 -58
  11. package/.github/workflows/release.yml +0 -31
  12. package/.github/workflows/test.yml +0 -20
  13. package/CHANGELOG.md +0 -45
  14. package/CLAUDE.md +0 -299
  15. package/CONTRIBUTING.md +0 -58
  16. package/benchmarks/overhead.ts +0 -267
  17. package/bun.lock +0 -479
  18. package/docs/api-reference.md +0 -400
  19. package/docs/benchmarks.md +0 -106
  20. package/docs/comparison.md +0 -315
  21. package/docs/conditional-logging-research.md +0 -159
  22. package/docs/guide.md +0 -205
  23. package/docs/migration-from-debug.md +0 -310
  24. package/docs/migration-from-pino.md +0 -178
  25. package/docs/migration-from-winston.md +0 -179
  26. package/docs/site/.vitepress/config.ts +0 -67
  27. package/docs/site/api/configuration.md +0 -94
  28. package/docs/site/api/index.md +0 -61
  29. package/docs/site/api/logger.md +0 -99
  30. package/docs/site/api/worker.md +0 -120
  31. package/docs/site/api/writers.md +0 -69
  32. package/docs/site/guide/getting-started.md +0 -143
  33. package/docs/site/guide/journey.md +0 -203
  34. package/docs/site/guide/migration-from-debug.md +0 -24
  35. package/docs/site/guide/spans.md +0 -139
  36. package/docs/site/guide/why.md +0 -55
  37. package/docs/site/guide/workers.md +0 -113
  38. package/docs/site/guide/zero-overhead.md +0 -87
  39. package/docs/site/index.md +0 -54
  40. package/tests/features.test.ts +0 -552
  41. package/tests/logger.test.ts +0 -944
  42. package/tests/tracing.test.ts +0 -618
  43. package/tests/universal.test.ts +0 -107
  44. package/tests/worker.test.ts +0 -590
  45. package/tsconfig.json +0 -20
  46. package/vitest.config.ts +0 -10
@@ -1,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
- ```
@@ -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
- ```
@@ -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.
@@ -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
- ```