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.
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/release.yml +31 -0
- package/.github/workflows/test.yml +20 -0
- package/CHANGELOG.md +45 -0
- package/CLAUDE.md +299 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +21 -0
- package/README.md +102 -3
- package/benchmarks/overhead.ts +267 -0
- package/bun.lock +479 -0
- package/docs/api-reference.md +400 -0
- package/docs/benchmarks.md +106 -0
- package/docs/comparison.md +315 -0
- package/docs/conditional-logging-research.md +159 -0
- package/docs/guide.md +205 -0
- package/docs/migration-from-debug.md +310 -0
- package/docs/migration-from-pino.md +178 -0
- package/docs/migration-from-winston.md +179 -0
- package/docs/site/.vitepress/config.ts +67 -0
- package/docs/site/api/configuration.md +94 -0
- package/docs/site/api/index.md +61 -0
- package/docs/site/api/logger.md +99 -0
- package/docs/site/api/worker.md +120 -0
- package/docs/site/api/writers.md +69 -0
- package/docs/site/guide/getting-started.md +143 -0
- package/docs/site/guide/journey.md +203 -0
- package/docs/site/guide/migration-from-debug.md +24 -0
- package/docs/site/guide/spans.md +139 -0
- package/docs/site/guide/why.md +55 -0
- package/docs/site/guide/workers.md +113 -0
- package/docs/site/guide/zero-overhead.md +87 -0
- package/docs/site/index.md +54 -0
- package/package.json +56 -8
- package/src/colors.ts +27 -0
- package/src/context.ts +155 -0
- package/src/core.ts +804 -0
- package/src/file-writer.ts +104 -0
- package/src/index.browser.ts +64 -0
- package/src/index.ts +10 -1
- package/src/tracing.ts +142 -0
- package/src/worker.ts +687 -0
- package/tests/features.test.ts +552 -0
- package/tests/logger.test.ts +944 -0
- package/tests/tracing.test.ts +618 -0
- package/tests/universal.test.ts +107 -0
- package/tests/worker.test.ts +590 -0
- package/tsconfig.json +20 -0
- 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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
-
|
|
3
|
+
**Clarity without the clutter.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/beorn/loggily/actions/workflows/test.yml)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](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)
|