loggily 0.0.1 → 0.3.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.
Files changed (48) hide show
  1. package/.github/workflows/docs.yml +58 -0
  2. package/.github/workflows/release.yml +31 -0
  3. package/.github/workflows/test.yml +20 -0
  4. package/CHANGELOG.md +45 -0
  5. package/CLAUDE.md +299 -0
  6. package/CONTRIBUTING.md +58 -0
  7. package/LICENSE +21 -0
  8. package/README.md +102 -3
  9. package/benchmarks/overhead.ts +267 -0
  10. package/bun.lock +479 -0
  11. package/docs/api-reference.md +400 -0
  12. package/docs/benchmarks.md +106 -0
  13. package/docs/comparison.md +315 -0
  14. package/docs/conditional-logging-research.md +159 -0
  15. package/docs/guide.md +205 -0
  16. package/docs/migration-from-debug.md +310 -0
  17. package/docs/migration-from-pino.md +178 -0
  18. package/docs/migration-from-winston.md +179 -0
  19. package/docs/site/.vitepress/config.ts +67 -0
  20. package/docs/site/api/configuration.md +94 -0
  21. package/docs/site/api/index.md +61 -0
  22. package/docs/site/api/logger.md +99 -0
  23. package/docs/site/api/worker.md +120 -0
  24. package/docs/site/api/writers.md +69 -0
  25. package/docs/site/guide/getting-started.md +143 -0
  26. package/docs/site/guide/journey.md +203 -0
  27. package/docs/site/guide/migration-from-debug.md +24 -0
  28. package/docs/site/guide/spans.md +139 -0
  29. package/docs/site/guide/why.md +55 -0
  30. package/docs/site/guide/workers.md +113 -0
  31. package/docs/site/guide/zero-overhead.md +87 -0
  32. package/docs/site/index.md +54 -0
  33. package/package.json +56 -8
  34. package/src/colors.ts +27 -0
  35. package/src/context.ts +155 -0
  36. package/src/core.ts +804 -0
  37. package/src/file-writer.ts +104 -0
  38. package/src/index.browser.ts +64 -0
  39. package/src/index.ts +10 -1
  40. package/src/tracing.ts +142 -0
  41. package/src/worker.ts +687 -0
  42. package/tests/features.test.ts +552 -0
  43. package/tests/logger.test.ts +944 -0
  44. package/tests/tracing.test.ts +618 -0
  45. package/tests/universal.test.ts +107 -0
  46. package/tests/worker.test.ts +590 -0
  47. package/tsconfig.json +20 -0
  48. package/vitest.config.ts +10 -0
@@ -0,0 +1,58 @@
1
+ name: Deploy Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "src/**"
9
+ - ".github/workflows/docs.yml"
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+ pages: write
15
+ id-token: write
16
+
17
+ concurrency:
18
+ group: pages
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+ with:
28
+ fetch-depth: 0
29
+
30
+ - name: Setup Bun
31
+ uses: oven-sh/setup-bun@v2
32
+ with:
33
+ bun-version: latest
34
+
35
+ - name: Setup Pages
36
+ uses: actions/configure-pages@v4
37
+
38
+ - name: Install dependencies
39
+ run: bun install
40
+
41
+ - name: Build docs
42
+ run: bun run docs:build
43
+
44
+ - name: Upload artifact
45
+ uses: actions/upload-pages-artifact@v3
46
+ with:
47
+ path: docs/site/.vitepress/dist
48
+
49
+ deploy:
50
+ environment:
51
+ name: github-pages
52
+ url: ${{ steps.deployment.outputs.page_url }}
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - name: Deploy to GitHub Pages
57
+ id: deployment
58
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: oven-sh/setup-bun@v2
17
+ with:
18
+ bun-version: latest
19
+ - run: bun install
20
+ - run: bun test
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '22'
24
+ registry-url: 'https://registry.npmjs.org'
25
+ - run: npm publish --access public
26
+ env:
27
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28
+ - name: Create GitHub Release
29
+ uses: softprops/action-gh-release@v2
30
+ with:
31
+ generate_release_notes: true
@@ -0,0 +1,20 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: oven-sh/setup-bun@v2
16
+ with:
17
+ bun-version: latest
18
+ - run: bun install
19
+ - run: bun run typecheck
20
+ - run: bun test
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to loggily will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-03-04
9
+
10
+ ### Added
11
+
12
+ - **Lazy messages** -- Pass `() => string` functions that are only called when the level is enabled
13
+ - **Child context loggers** -- `log.child({ requestId: "abc" })` creates a logger with structured context fields in every message
14
+ - **LOG_FORMAT env var** -- `LOG_FORMAT=json` explicitly enables structured JSON output
15
+ - `setLogFormat()` / `getLogFormat()` -- Programmatic log format control
16
+ - `setDebugFilter()` / `getDebugFilter()` -- Programmatic namespace filtering (like `DEBUG` env var)
17
+ - **File writer** -- `createFileWriter(path, opts?)` for buffered file output with auto-flush
18
+ - **Writer system** -- `addWriter(fn)` to subscribe to all formatted log output
19
+ - `setOutputMode()` / `getOutputMode()` -- Control output destination (`console`, `stderr`, `writers-only`)
20
+ - `setSuppressConsole()` -- Suppress console output while writers still receive
21
+ - Comprehensive test suite (153 tests)
22
+
23
+ ### Changed
24
+
25
+ - `createLogger()` now returns a `ConditionalLogger` directly (no separate function needed)
26
+ - Improved documentation with full API reference and comparison guides
27
+
28
+ ## [0.1.0] - 2026-01-15
29
+
30
+ ### Added
31
+
32
+ - Initial release
33
+ - `createLogger(name, props?)` -- Create structured logger
34
+ - Logger methods: `trace`, `debug`, `info`, `warn`, `error`
35
+ - Child loggers with `.logger(namespace, props?)`
36
+ - Span timing with `.span(namespace, props?)` and `using` keyword support
37
+ - `SpanData` with id, traceId, parentId, startTime, endTime, duration
38
+ - Custom span attributes via `span.spanData.key = value`
39
+ - Configuration via environment variables: `LOG_LEVEL`, `TRACE`, `TRACE_FORMAT`
40
+ - Programmatic configuration: `setLogLevel`, `getLogLevel`, `enableSpans`, `disableSpans`, `spansAreEnabled`
41
+ - `setTraceFilter()` / `getTraceFilter()` -- Namespace-based span output control
42
+ - Dual output format: pretty console (dev) and JSON (production)
43
+ - Worker thread support: `createWorkerLogger`, `createWorkerLogHandler`, `forwardConsole`
44
+ - Span collection for testing: `startCollecting`, `stopCollecting`, `getCollectedSpans`, `clearCollectedSpans`
45
+ - `resetIds()` for deterministic tests
package/CLAUDE.md ADDED
@@ -0,0 +1,299 @@
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
@@ -0,0 +1,58 @@
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bjørn Stabell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,9 +1,108 @@
1
1
  # loggily
2
2
 
3
- Structured logging with context, levels, and formatting — the logger that gets out of your way.
3
+ **Clarity without the clutter.**
4
4
 
5
- **Coming soon.**
5
+ [![Tests](https://github.com/beorn/loggily/actions/workflows/test.yml/badge.svg)](https://github.com/beorn/loggily/actions/workflows/test.yml)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ Debug logging, structured logs, and distributed tracing — integrated into one **~3KB** library with a single API. Zero dependencies.
10
+
11
+ Most projects wire together three separate tools that don't talk to each other: **debug** for conditional output, **pino/winston** for production logs, **OpenTelemetry** for tracing. loggily integrates all three into one unified system — same namespace tree, same output pipeline, same `?.` zero-overhead pattern. Every logger is a potential span: call `.span()` and it becomes one, with automatic timing, parent-child tracking, and trace IDs. Nothing to sync, nothing to configure separately.
12
+
13
+ In development, you get colorized console output with timestamps, level colors, and clickable source lines — loggily uses native `console` methods so stack traces stay intact in DevTools. In production, the same code emits structured JSON. No config change needed.
14
+
15
+ Read **[The Journey](docs/guide.md)** for the full story.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ bun add loggily # or: npm install loggily
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { createLogger } from "loggily"
27
+
28
+ const log = createLogger("myapp")
29
+
30
+ // ?. skips the entire call — including argument evaluation — when the level is disabled (near-zero cost)
31
+ log.info?.("server started", { port: 3000 })
32
+ log.debug?.("cache hit", { key: "user:42" })
33
+ log.error?.(new Error("connection lost"))
34
+
35
+ // Spans time operations automatically
36
+ {
37
+ using span = log.span("db:query", { table: "users" })
38
+ const users = await db.query("SELECT * FROM users")
39
+ span.spanData.count = users.length
40
+ }
41
+ // Output: SPAN myapp:db:query (45ms) {count: 100, table: "users"}
42
+ ```
43
+
44
+ ## Why Another Logger?
45
+
46
+ Beyond the integration story above, most loggers also waste work when logging is disabled. Even with a noop function, arguments are still evaluated:
47
+
48
+ ```typescript
49
+ // Traditional — args are ALWAYS evaluated, even when debug is off
50
+ log.debug(`state: ${JSON.stringify(computeExpensiveState())}`)
51
+ ```
52
+
53
+ loggily uses optional chaining to skip the entire call — including argument evaluation:
54
+
55
+ ```typescript
56
+ // loggily — args are NOT evaluated when disabled
57
+ log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
58
+ ```
59
+
60
+ For trivial arguments the difference is negligible. But for real-world logging — string interpolation, JSON serialization, state snapshots — optional chaining is typically **10x+ faster** because it skips the work entirely. The more expensive your arguments, the bigger the win.
61
+
62
+ ## Features
63
+
64
+ - **Namespace hierarchy** — organize logs with `:` separators. `log.logger("db")` creates `myapp:db`. Children inherit parent context.
65
+ - **Spans** — time any operation with `using span = log.span("name")`. Automatic duration, parent-child tracking, and trace IDs. _(Uses TC39 [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management); call `span.end()` manually if your runtime doesn't support `using` yet.)_
66
+ - **Lazy messages** — `log.debug?.(() => expensiveString())` skips the function entirely when disabled.
67
+ - **Child context** — `log.child({ requestId })` adds structured fields to every message in the chain.
68
+ - **Dev & production** — colorized console with timestamps, level colors, and clickable source lines in development. Structured JSON in production. Switches automatically via `NODE_ENV` — same code, zero config.
69
+ - **File writer** — `addWriter()` + `createFileWriter()` for buffered file output with auto-flush.
70
+ - **Worker threads** — forward logs from workers to the main thread with full type safety (`loggily/worker`).
71
+ - **Drop-in debug replacement** — reads `DEBUG=myapp:*` just like the debug package. Swap your imports in minutes.
72
+
73
+ ## Documentation
74
+
75
+ - **[The Journey](docs/guide.md)** — progressive guide from first log to full observability
76
+ - **[Full docs site](https://beorn.codes/loggily/)** — guides, API reference, migration guides
77
+ - [Comparison](docs/comparison.md) — vs Pino, Winston, Bunyan, debug
78
+ - [Migration from debug](docs/migration-from-debug.md) — step-by-step migration guide
79
+
80
+ ## Environment Variables
81
+
82
+ | Variable | Values | Effect |
83
+ | -------------- | --------------------------------------- | --------------------------------------- |
84
+ | `LOG_LEVEL` | trace, debug, info, warn, error, silent | Minimum output level |
85
+ | `LOG_FORMAT` | console, json | Output format |
86
+ | `DEBUG` | `*`, namespace prefixes, `-prefix` | Namespace filter (like `debug` package) |
87
+ | `TRACE` | `1`, `true`, or namespace prefixes | Enable span output |
88
+ | `TRACE_FORMAT` | json | Force JSON for spans |
89
+ | `NODE_ENV` | production | Auto-enable JSON format |
90
+
91
+ ## API
92
+
93
+ | Function | Description |
94
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------- |
95
+ | `createLogger(name, props?)` | Create a logger (disabled levels return `undefined` for `?.`) |
96
+ | `.trace?.()` / `.debug?.()` / `.info?.()` / `.warn?.()` / `.error?.()` | Log at level (message + optional data) |
97
+ | `.logger(namespace)` | Create child logger with extended namespace |
98
+ | `.span(namespace, props?)` | Create timed span (implements `Disposable`) |
99
+ | `.child(context)` | Create child with structured context fields |
100
+ | `addWriter(fn)` / `createFileWriter(path)` | Custom output writers |
101
+ | `setLogLevel()` / `setLogFormat()` / `enableSpans()` | Runtime configuration |
102
+ | `createWorkerLogger()` / `createWorkerLogHandler()` | Worker thread support (`loggily/worker`) |
103
+
104
+ See the [full API reference](docs/api-reference.md) for all functions and options.
6
105
 
7
106
  ## License
8
107
 
9
- MIT
108
+ [MIT](LICENSE)