loggily 0.6.2 → 0.8.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/README.md +175 -42
- package/dist/context.d.mts +91 -0
- package/dist/context.d.mts.map +1 -0
- package/dist/context.mjs +145 -0
- package/dist/context.mjs.map +1 -0
- package/dist/core-B3pox577.mjs +1074 -0
- package/dist/core-B3pox577.mjs.map +1 -0
- package/dist/core-Dm2PQUoS.d.mts +191 -0
- package/dist/core-Dm2PQUoS.d.mts.map +1 -0
- package/dist/file-writer-DtaY8Njt.d.mts +109 -0
- package/dist/file-writer-DtaY8Njt.d.mts.map +1 -0
- package/dist/index.browser.d.mts +10 -0
- package/dist/index.browser.d.mts.map +1 -0
- package/dist/index.browser.mjs +10 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +4 -333
- package/dist/index.mjs +4 -731
- package/dist/index.mjs.map +1 -1
- package/dist/metrics.d.mts +2 -0
- package/dist/metrics.mjs +100 -0
- package/dist/metrics.mjs.map +1 -0
- package/dist/otel.d.mts +63 -0
- package/dist/otel.d.mts.map +1 -0
- package/dist/otel.mjs +82 -0
- package/dist/otel.mjs.map +1 -0
- package/dist/pipeline-Cl9-wCmt.d.mts +73 -0
- package/dist/pipeline-Cl9-wCmt.d.mts.map +1 -0
- package/dist/worker.d.mts +143 -0
- package/dist/worker.d.mts.map +1 -0
- package/dist/worker.mjs +297 -0
- package/dist/worker.mjs.map +1 -0
- package/package.json +45 -9
- package/dist/index.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -2,28 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
**Clarity without the clutter.**
|
|
4
4
|
|
|
5
|
-
Debugs, logs, and spans
|
|
5
|
+
Debugs, logs, and spans -- one API.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/beorn/loggily/actions/workflows/test.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/loggily)
|
|
9
9
|
[](https://bundlephobia.com/package/loggily)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
Most apps end up with three logging tools: `debug` for local troubleshooting, a JSON logger
|
|
12
|
+
Most apps end up with three logging tools: `debug` for local troubleshooting, a JSON logger for production, and ad-hoc timers or a tracing SDK for performance. Three APIs, three configs, three output formats.
|
|
13
13
|
|
|
14
14
|
Loggily replaces all three with one namespace tree and one output pipeline. Pure TypeScript, zero dependencies, ~3 KB.
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
17
|
import { createLogger } from "loggily"
|
|
18
18
|
|
|
19
|
-
const log = createLogger("myapp")
|
|
19
|
+
const log = createLogger("myapp", [{ level: "debug" }, console])
|
|
20
20
|
|
|
21
21
|
log.info?.("server started", { port: 3000 })
|
|
22
22
|
log.debug?.("cache hit", { key: "user:42" })
|
|
23
23
|
log.error?.(new Error("connection lost"))
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Readable, colorized output in development:
|
|
26
|
+
Readable, colorized output in development (colors don't render on GitHub -- run it in a terminal):
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
14:32:15 INFO myapp server started {port: 3000}
|
|
@@ -55,7 +55,7 @@ log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
|
55
55
|
// nothing runs when debug is off — not the function, not the stringify, not the template
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
In benchmarks with expensive disabled log arguments, this is [~22x faster](https://
|
|
58
|
+
In benchmarks with expensive disabled log arguments, this is [~22x faster](https://loggily.dev/guide/benchmarks) than a conventional noop logger.
|
|
59
59
|
|
|
60
60
|
## Install
|
|
61
61
|
|
|
@@ -63,52 +63,184 @@ In benchmarks with expensive disabled log arguments, this is [~22x faster](https
|
|
|
63
63
|
npm install loggily
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
| Requirement
|
|
67
|
-
|
|
68
|
-
| Node.js
|
|
69
|
-
| Bun
|
|
70
|
-
| TypeScript
|
|
71
|
-
| Module format | ESM-only
|
|
72
|
-
| Browser
|
|
66
|
+
| Requirement | Version |
|
|
67
|
+
| ------------- | ----------------------------------------------- |
|
|
68
|
+
| Node.js | >= 23.6 |
|
|
69
|
+
| Bun | 1.0+ |
|
|
70
|
+
| TypeScript | 5.2+ for `using`; `.end()` works on any version |
|
|
71
|
+
| Module format | ESM-only |
|
|
72
|
+
| Browser | Supported via conditional export |
|
|
73
73
|
|
|
74
74
|
Loggily uses `Symbol.dispose` (TC39 Explicit Resource Management) for span cleanup, which requires a modern runtime.
|
|
75
75
|
|
|
76
76
|
## Features
|
|
77
77
|
|
|
78
|
-
- **
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
- **Zero-cost disabled logs** -- `?.` short-circuits the entire call: no string interpolation, no JSON.stringify, no function evaluation. [~22x faster](https://loggily.dev/guide/benchmarks) than noop loggers.
|
|
79
|
+
- **Config pipeline** -- `createLogger("app", [config, console, { file }, stage, [branch]])`. Objects configure, arrays branch, values write.
|
|
80
|
+
- **Namespace hierarchy** -- `DEBUG=myapp:db` shows only database output. Same patterns as the `debug` package.
|
|
81
|
+
- **Spans** -- `using span = log.span("name")`. Duration, parent-child tracking, trace IDs, custom data. Control per-pipeline with `{ spans: true/false }`.
|
|
82
|
+
- **Dev & production** -- colorized console in development, structured JSON in production. Same code.
|
|
83
|
+
- **Child loggers** -- `log.child("auth")` extends namespace, `log.child({ requestId })` adds context.
|
|
84
|
+
- **Async context** -- `AsyncLocalStorage` propagation: every log in a request's async chain inherits trace/span IDs automatically.
|
|
85
|
+
- **Error cause chains** -- `log.error?.(err)` serializes `Error.cause` recursively (up to 3 levels).
|
|
86
|
+
- **Worker threads** -- pipeline-based: `createWorkerLogger(postMessage, "ns")` on worker, `createWorkerLogHandler()` on main. Same events, same pipeline.
|
|
87
|
+
- **OpenTelemetry bridge** -- `toOtel({ api })` stage forwards events to any OTLP backend. Transparent: events pass through to subsequent pipeline elements.
|
|
88
|
+
- **Pino transport compatible** -- works with object-mode writable sinks (compatible with Pino transport interface). Events use Loggily's record shape.
|
|
89
|
+
- **Span metrics** -- `{ metrics: true }` in config auto-creates a collector on `log.metrics` (p50/p95/p99). Or use `withMetrics(collector)` for shared/custom collectors.
|
|
90
|
+
- **Head-based sampling** -- `setSampleRate(0.1)` to sample 10% of traces.
|
|
91
|
+
- **Composable plugins** -- `pipe(baseCreateLogger, withSpans(), myPlugin())` to build custom factories.
|
|
92
|
+
- **Browser support** -- bundlers auto-select the browser entry point via `browser` condition.
|
|
93
|
+
- **~3 KB, zero dependencies.**
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
88
96
|
|
|
89
97
|
```typescript
|
|
98
|
+
import { createLogger } from "loggily"
|
|
99
|
+
|
|
100
|
+
const log = createLogger("myapp", [{ level: "debug" }, console])
|
|
101
|
+
|
|
102
|
+
log.info?.("server started", { port: 3000 })
|
|
103
|
+
log.debug?.("cache hit", { key: "user:42" })
|
|
104
|
+
log.error?.(new Error("timeout"), "request failed", { url: "/api" })
|
|
105
|
+
|
|
106
|
+
// Child loggers
|
|
107
|
+
const dbLog = log.child("db", { pool: "main" }) // namespace: "myapp:db"
|
|
108
|
+
|
|
109
|
+
// Spans -- time any operation
|
|
90
110
|
{
|
|
91
|
-
using span =
|
|
92
|
-
const users = await
|
|
111
|
+
using span = dbLog.span("query", { table: "users" })
|
|
112
|
+
const users = await queryUsers() // your DB call
|
|
93
113
|
span.spanData.count = users.length
|
|
94
114
|
}
|
|
95
|
-
// SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
115
|
+
// → SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
116
|
+
```
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
## Complete Example
|
|
119
|
+
|
|
120
|
+
The config array accepts six element types -- here they are in one pipeline:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { createLogger } from "loggily"
|
|
124
|
+
import { toOtel } from "loggily/otel"
|
|
125
|
+
import * as otelApi from "@opentelemetry/api"
|
|
126
|
+
|
|
127
|
+
const log = createLogger("myapp", [
|
|
128
|
+
// "myapp" -- namespace, filter with DEBUG=myapp
|
|
129
|
+
{ level: "debug", metrics: true }, // config object -- sets scope
|
|
130
|
+
toOtel({ api: otelApi }), // stage -- transforms/forwards events
|
|
131
|
+
pinoTransport, // writable -- { write } receives raw Events
|
|
132
|
+
{ file: "/tmp/app.log", format: "json" }, // file sink -- writes formatted strings
|
|
133
|
+
[{ level: "error" }, { file: "/tmp/err.log" }], // branch -- sub-pipeline with own scope
|
|
134
|
+
console, // console -- colorized, human-readable
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
// Custom writable -- any { write } receives raw Event objects
|
|
138
|
+
const log2 = createLogger("ingest", [
|
|
139
|
+
{ write: (event) => fetch("/ingest", { method: "POST", body: JSON.stringify(event) }) },
|
|
140
|
+
console,
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
// Custom stage -- functions transform, filter, or enrich events
|
|
144
|
+
const log3 = createLogger("filtered", [
|
|
145
|
+
(event) => (event.kind === "log" && event.message.includes("secret") ? null : event),
|
|
146
|
+
(event) => ({ ...event, props: { ...event.props, host: os.hostname() } }),
|
|
147
|
+
console,
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
// Metrics -- { metrics: true } auto-creates a collector on log.metrics
|
|
151
|
+
for (const [name, s] of log.metrics.all()) {
|
|
152
|
+
if (s.p95 > 100) console.warn(`${name} is slow: p95=${s.p95}ms`)
|
|
153
|
+
}
|
|
100
154
|
```
|
|
101
155
|
|
|
102
|
-
###
|
|
156
|
+
### Composition with plugins
|
|
157
|
+
|
|
158
|
+
`createLogger` is `pipe(baseCreateLogger, withEnvDefaults(), withSpans(), withConfigMetrics())`. For full manual control:
|
|
103
159
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
160
|
+
```typescript
|
|
161
|
+
import { baseCreateLogger, pipe, withSpans, withEnvDefaults } from "loggily"
|
|
162
|
+
|
|
163
|
+
// Custom factory — choose exactly which plugins to include
|
|
164
|
+
const myCreateLogger = pipe(baseCreateLogger, withSpans(), myPlugin())
|
|
165
|
+
const log = myCreateLogger("myapp", [console])
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`baseCreateLogger` does NOT include `withSpans()` or `withEnvDefaults()` — loggers it creates cannot create spans and do not read environment variables.
|
|
169
|
+
|
|
170
|
+
### Test helper
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { createTestLogger } from "loggily"
|
|
174
|
+
const log = createTestLogger("test") // all levels enabled, console output
|
|
175
|
+
```
|
|
110
176
|
|
|
111
|
-
|
|
177
|
+
### Environment variables
|
|
178
|
+
|
|
179
|
+
| Variable | Values | Default |
|
|
180
|
+
| ------------------- | --------------------------------------- | --------- |
|
|
181
|
+
| `LOG_LEVEL` | trace, debug, info, warn, error, silent | `info` |
|
|
182
|
+
| `LOG_FORMAT` | console, json | `console` |
|
|
183
|
+
| `LOG_FILE` | file path | (none) |
|
|
184
|
+
| `DEBUG` | `*`, namespace prefixes, `-prefix` | (none) |
|
|
185
|
+
| `TRACE` | `1`, `true`, namespace prefixes | (none) |
|
|
186
|
+
| `TRACE_FORMAT` | json | (none) |
|
|
187
|
+
| `TRACE_ID_FORMAT` | simple, w3c | `simple` |
|
|
188
|
+
| `TRACE_SAMPLE_RATE` | 0.0 -- 1.0 | `1.0` |
|
|
189
|
+
| `NODE_ENV` | production | (none) |
|
|
190
|
+
|
|
191
|
+
### Namespace filter patterns
|
|
192
|
+
|
|
193
|
+
| Pattern | Matches |
|
|
194
|
+
| ------------------ | -------------------------------------------------------------- |
|
|
195
|
+
| `*` | Everything |
|
|
196
|
+
| `myapp` | Exact match + children (`myapp`, `myapp:db`, `myapp:db:query`) |
|
|
197
|
+
| `myapp:*` | Same as `myapp` — explicit wildcard |
|
|
198
|
+
| `-myapp:sql` | Exclude `myapp:sql` and its children |
|
|
199
|
+
| `myapp,-myapp:sql` | Include myapp, exclude sql subtree |
|
|
200
|
+
|
|
201
|
+
### Types
|
|
202
|
+
|
|
203
|
+
Key types exported for power users:
|
|
204
|
+
|
|
205
|
+
| Type | Description |
|
|
206
|
+
| ------------------- | ---------------------------------------------------------------- |
|
|
207
|
+
| `LogEvent` | A log message event (kind, level, namespace, message, props) |
|
|
208
|
+
| `SpanEvent` | A span timing event (kind, namespace, duration, spanId, traceId) |
|
|
209
|
+
| `Event` | `LogEvent \| SpanEvent` |
|
|
210
|
+
| `Stage` | `(event: Event) => Event \| null \| void` |
|
|
211
|
+
| `Pipeline` | `{ dispatch, level, dispose }` |
|
|
212
|
+
| `ConditionalLogger` | Logger with `?.`-compatible methods |
|
|
213
|
+
| `ConfigElement` | Union of all valid config array elements |
|
|
214
|
+
| `ConfigObject` | Scope config: `{ level?, ns?, format?, spans? }` |
|
|
215
|
+
| `FileDescriptor` | File output: `{ file, level?, ns?, format? }` |
|
|
216
|
+
| `Writable` | Any object with `{ write, objectMode? }` |
|
|
217
|
+
|
|
218
|
+
`buildPipeline()` is exported for direct pipeline construction.
|
|
219
|
+
|
|
220
|
+
### Subpath exports
|
|
221
|
+
|
|
222
|
+
| Import path | Contents |
|
|
223
|
+
| ----------------- | -------------------------------------------------------------------- |
|
|
224
|
+
| `loggily` | Core API, types, pipeline builder |
|
|
225
|
+
| `loggily/context` | AsyncLocalStorage context propagation (Node.js) |
|
|
226
|
+
| `loggily/worker` | Worker thread logger + message handlers |
|
|
227
|
+
| `loggily/otel` | OpenTelemetry bridge (`toOtel` stage) |
|
|
228
|
+
| `loggily/metrics` | Span metrics collection (`{ metrics: true }` or explicit collectors) |
|
|
229
|
+
|
|
230
|
+
## Compatibility & Destinations
|
|
231
|
+
|
|
232
|
+
- **OpenTelemetry** -- `toOtel({ api })` forwards to Jaeger, Grafana, Datadog, or any OTLP backend
|
|
233
|
+
- **Sentry** -- capture errors via a 3-line stage function
|
|
234
|
+
- **Pino transports** -- works with object-mode writable sinks (compatible with Pino transport interface)
|
|
235
|
+
- **Elasticsearch / OpenSearch** -- post JSON events directly
|
|
236
|
+
- **AWS CloudWatch** -- writable calling `putLogEvents`
|
|
237
|
+
- **Prometheus** -- expose `log.metrics` as a `/metrics` endpoint
|
|
238
|
+
- **`DEBUG=` patterns** -- same namespace filter syntax as the `debug` package
|
|
239
|
+
- **W3C Trace Context** -- `traceparent()` generates standard headers
|
|
240
|
+
- **Browser** -- bundlers auto-select the browser entry point
|
|
241
|
+
- **Worker threads** -- pipeline-based forwarding via `postMessage`
|
|
242
|
+
|
|
243
|
+
See the [Destinations guide](https://loggily.dev/guide/destinations) for copy-paste recipes.
|
|
112
244
|
|
|
113
245
|
## Why this exists
|
|
114
246
|
|
|
@@ -118,15 +250,16 @@ Loggily was built while developing a terminal UI where disabled debug logs insid
|
|
|
118
250
|
|
|
119
251
|
## When not to use Loggily
|
|
120
252
|
|
|
121
|
-
- **You need
|
|
122
|
-
- **You need
|
|
253
|
+
- **You need auto-instrumentation** (HTTP, database, gRPC). Use OpenTelemetry's SDK directly -- Loggily's OTEL bridge forwards events but doesn't instrument frameworks.
|
|
254
|
+
- **You need log rotation, file compression, or dozens of transport plugins.** Pino's transport ecosystem is deeper. (You can use Pino transports with Loggily's `objectMode` writables, but Pino owns that ecosystem.)
|
|
255
|
+
- **You're not on a modern runtime.** Loggily requires Node.js >= 23.6 or Bun >= 1.0.
|
|
123
256
|
|
|
124
257
|
## Documentation
|
|
125
258
|
|
|
126
|
-
- **[Get Started](https://
|
|
127
|
-
- **[Full docs site](https://
|
|
128
|
-
- [Comparison](https://
|
|
129
|
-
- [Migration from debug](https://
|
|
259
|
+
- **[Get Started](https://loggily.dev/guide/journey)** -- progressive guide from first log to full observability
|
|
260
|
+
- **[Full docs site](https://loggily.dev/)** -- guides, API reference, migration guides
|
|
261
|
+
- [Comparison](https://loggily.dev/guide/comparison) -- what Loggily does, compatibility, when to use something else
|
|
262
|
+
- [Migration from debug](https://loggily.dev/guide/migration-from-debug) -- step-by-step migration guide
|
|
130
263
|
|
|
131
264
|
## License
|
|
132
265
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/context.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* AsyncLocalStorage-based context propagation for loggily — Node.js/Bun only.
|
|
4
|
+
*
|
|
5
|
+
* Separated from core logger to allow tree-shaking in browser bundles.
|
|
6
|
+
* When enabled, new spans automatically parent to the current context span,
|
|
7
|
+
* and writeLog() auto-tags with trace_id/span_id from context.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { enableContextPropagation, getCurrentSpan } from "loggily/context"
|
|
12
|
+
*
|
|
13
|
+
* enableContextPropagation()
|
|
14
|
+
*
|
|
15
|
+
* const log = createLogger("myapp")
|
|
16
|
+
* {
|
|
17
|
+
* using span = log.span("request")
|
|
18
|
+
* // All logs and child spans within this async context
|
|
19
|
+
* // automatically inherit trace_id and span_id
|
|
20
|
+
* log.info("inside span") // auto-tagged with trace_id, span_id
|
|
21
|
+
*
|
|
22
|
+
* const current = getCurrentSpan()
|
|
23
|
+
* // current === { spanId: "sp_1", traceId: "tr_1", parentId: null }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
/** Minimal span context stored in AsyncLocalStorage */
|
|
28
|
+
interface SpanContext {
|
|
29
|
+
readonly spanId: string;
|
|
30
|
+
readonly traceId: string;
|
|
31
|
+
readonly parentId: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Enable AsyncLocalStorage-based context propagation.
|
|
35
|
+
* Once enabled, new spans automatically parent to the current context span,
|
|
36
|
+
* and log messages are auto-tagged with trace_id/span_id.
|
|
37
|
+
*
|
|
38
|
+
* **Node.js/Bun only** — not available in browser environments.
|
|
39
|
+
*/
|
|
40
|
+
declare function enableContextPropagation(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Disable context propagation.
|
|
43
|
+
* Existing spans continue to work, but new spans won't auto-parent.
|
|
44
|
+
*/
|
|
45
|
+
declare function disableContextPropagation(): void;
|
|
46
|
+
/** Check if context propagation is enabled */
|
|
47
|
+
declare function isContextPropagationEnabled(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Get the current span context from AsyncLocalStorage.
|
|
50
|
+
* Returns null if no span is active in the current async context,
|
|
51
|
+
* or if context propagation is not enabled.
|
|
52
|
+
*/
|
|
53
|
+
declare function getCurrentSpan(): SpanContext | null;
|
|
54
|
+
/**
|
|
55
|
+
* Enter a span context for the remainder of the current synchronous execution
|
|
56
|
+
* and any async operations started from it. Used by the logger when creating
|
|
57
|
+
* spans with `using` — since `using` doesn't wrap user code in a callback,
|
|
58
|
+
* `enterWith()` is the right primitive.
|
|
59
|
+
*
|
|
60
|
+
* Captures the full previous SpanContext snapshot so it can be restored
|
|
61
|
+
* exactly on exit, even with non-LIFO end() ordering.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
declare function enterSpanContext(spanId: string, traceId: string, parentId: string | null): void;
|
|
66
|
+
/**
|
|
67
|
+
* Restore the previous span context (called when a span ends).
|
|
68
|
+
* Restores the exact SpanContext snapshot captured at enter time,
|
|
69
|
+
* preventing corruption from non-LIFO end() ordering.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
declare function exitSpanContext(spanId: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Run a function within a span context.
|
|
76
|
+
* Used for explicit context scoping (e.g., in request handlers).
|
|
77
|
+
*
|
|
78
|
+
* @param context - The span context to set
|
|
79
|
+
* @param fn - The function to run within the context
|
|
80
|
+
* @returns The return value of fn
|
|
81
|
+
*/
|
|
82
|
+
declare function runInSpanContext<T>(context: SpanContext, fn: () => T): T;
|
|
83
|
+
/**
|
|
84
|
+
* Get the context tags (trace_id, span_id) for the current async context.
|
|
85
|
+
* Used by writeLog() to auto-tag log messages.
|
|
86
|
+
* Returns empty object if context propagation is disabled or no span is active.
|
|
87
|
+
*/
|
|
88
|
+
declare function getContextTags(): Record<string, string>;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { SpanContext, disableContextPropagation, enableContextPropagation, enterSpanContext, exitSpanContext, getContextTags, getCurrentSpan, isContextPropagationEnabled, runInSpanContext };
|
|
91
|
+
//# sourceMappingURL=context.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.mts","names":[],"sources":["../src/context.ts"],"mappings":";;AAgCA;;;;;;;;;AA2BA;;;;;AAuBA;;;;;AAMA;;;;;AASA;AAAA,UAjEiB,WAAA;EAAA,SACN,MAAA;EAAA,SACA,OAAA;EAAA,SACA,QAAA;AAAA;;;;;;;;iBAwBK,wBAAA,CAAA;AAuEhB;;;;AAAA,iBAhDgB,yBAAA,CAAA;AAsEhB;AAAA,iBAhEgB,2BAAA,CAAA;;;;;;iBASA,cAAA,CAAA,GAAkB,WAAA;;;;;;;;;AAiElC;;;iBAjDgB,gBAAA,CAAiB,MAAA,UAAgB,OAAA,UAAiB,QAAA;;;;;;;;iBAiBlD,eAAA,CAAgB,MAAA;;;;;;;;;iBAsBhB,gBAAA,GAAA,CAAoB,OAAA,EAAS,WAAA,EAAa,EAAA,QAAU,CAAA,GAAI,CAAA;;;;;;iBAUxD,cAAA,CAAA,GAAkB,MAAA"}
|
package/dist/context.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { n as _setContextHooks, t as _clearContextHooks } from "./core-B3pox577.mjs";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/context.ts
|
|
4
|
+
/**
|
|
5
|
+
* AsyncLocalStorage-based context propagation for loggily — Node.js/Bun only.
|
|
6
|
+
*
|
|
7
|
+
* Separated from core logger to allow tree-shaking in browser bundles.
|
|
8
|
+
* When enabled, new spans automatically parent to the current context span,
|
|
9
|
+
* and writeLog() auto-tags with trace_id/span_id from context.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { enableContextPropagation, getCurrentSpan } from "loggily/context"
|
|
14
|
+
*
|
|
15
|
+
* enableContextPropagation()
|
|
16
|
+
*
|
|
17
|
+
* const log = createLogger("myapp")
|
|
18
|
+
* {
|
|
19
|
+
* using span = log.span("request")
|
|
20
|
+
* // All logs and child spans within this async context
|
|
21
|
+
* // automatically inherit trace_id and span_id
|
|
22
|
+
* log.info("inside span") // auto-tagged with trace_id, span_id
|
|
23
|
+
*
|
|
24
|
+
* const current = getCurrentSpan()
|
|
25
|
+
* // current === { spanId: "sp_1", traceId: "tr_1", parentId: null }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
let storage = null;
|
|
30
|
+
let contextEnabled = false;
|
|
31
|
+
/**
|
|
32
|
+
* Map from spanId → the SpanContext that was active when the span was entered.
|
|
33
|
+
* Used to restore the exact previous context on exit, avoiding corruption
|
|
34
|
+
* from non-LIFO end() ordering.
|
|
35
|
+
*/
|
|
36
|
+
const previousContexts = /* @__PURE__ */ new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Enable AsyncLocalStorage-based context propagation.
|
|
39
|
+
* Once enabled, new spans automatically parent to the current context span,
|
|
40
|
+
* and log messages are auto-tagged with trace_id/span_id.
|
|
41
|
+
*
|
|
42
|
+
* **Node.js/Bun only** — not available in browser environments.
|
|
43
|
+
*/
|
|
44
|
+
function enableContextPropagation() {
|
|
45
|
+
if (!storage) storage = new AsyncLocalStorage();
|
|
46
|
+
contextEnabled = true;
|
|
47
|
+
_setContextHooks({
|
|
48
|
+
getContextTags,
|
|
49
|
+
getContextParent() {
|
|
50
|
+
const span = getCurrentSpan();
|
|
51
|
+
if (!span) return null;
|
|
52
|
+
return {
|
|
53
|
+
spanId: span.spanId,
|
|
54
|
+
traceId: span.traceId
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
enterContext: enterSpanContext,
|
|
58
|
+
exitContext: exitSpanContext
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Disable context propagation.
|
|
63
|
+
* Existing spans continue to work, but new spans won't auto-parent.
|
|
64
|
+
*/
|
|
65
|
+
function disableContextPropagation() {
|
|
66
|
+
contextEnabled = false;
|
|
67
|
+
_clearContextHooks();
|
|
68
|
+
}
|
|
69
|
+
/** Check if context propagation is enabled */
|
|
70
|
+
function isContextPropagationEnabled() {
|
|
71
|
+
return contextEnabled;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the current span context from AsyncLocalStorage.
|
|
75
|
+
* Returns null if no span is active in the current async context,
|
|
76
|
+
* or if context propagation is not enabled.
|
|
77
|
+
*/
|
|
78
|
+
function getCurrentSpan() {
|
|
79
|
+
if (!contextEnabled || !storage) return null;
|
|
80
|
+
return storage.getStore() ?? null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Enter a span context for the remainder of the current synchronous execution
|
|
84
|
+
* and any async operations started from it. Used by the logger when creating
|
|
85
|
+
* spans with `using` — since `using` doesn't wrap user code in a callback,
|
|
86
|
+
* `enterWith()` is the right primitive.
|
|
87
|
+
*
|
|
88
|
+
* Captures the full previous SpanContext snapshot so it can be restored
|
|
89
|
+
* exactly on exit, even with non-LIFO end() ordering.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
function enterSpanContext(spanId, traceId, parentId) {
|
|
94
|
+
if (!contextEnabled || !storage) return;
|
|
95
|
+
const previous = storage.getStore() ?? null;
|
|
96
|
+
previousContexts.set(spanId, previous);
|
|
97
|
+
storage.enterWith({
|
|
98
|
+
spanId,
|
|
99
|
+
traceId,
|
|
100
|
+
parentId
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Restore the previous span context (called when a span ends).
|
|
105
|
+
* Restores the exact SpanContext snapshot captured at enter time,
|
|
106
|
+
* preventing corruption from non-LIFO end() ordering.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
function exitSpanContext(spanId) {
|
|
111
|
+
if (!contextEnabled || !storage) return;
|
|
112
|
+
const previous = previousContexts.get(spanId);
|
|
113
|
+
previousContexts.delete(spanId);
|
|
114
|
+
if (previous) storage.enterWith(previous);
|
|
115
|
+
else storage.enterWith(void 0);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run a function within a span context.
|
|
119
|
+
* Used for explicit context scoping (e.g., in request handlers).
|
|
120
|
+
*
|
|
121
|
+
* @param context - The span context to set
|
|
122
|
+
* @param fn - The function to run within the context
|
|
123
|
+
* @returns The return value of fn
|
|
124
|
+
*/
|
|
125
|
+
function runInSpanContext(context, fn) {
|
|
126
|
+
if (!contextEnabled || !storage) return fn();
|
|
127
|
+
return storage.run(context, fn);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the context tags (trace_id, span_id) for the current async context.
|
|
131
|
+
* Used by writeLog() to auto-tag log messages.
|
|
132
|
+
* Returns empty object if context propagation is disabled or no span is active.
|
|
133
|
+
*/
|
|
134
|
+
function getContextTags() {
|
|
135
|
+
const span = getCurrentSpan();
|
|
136
|
+
if (!span) return {};
|
|
137
|
+
return {
|
|
138
|
+
trace_id: span.traceId,
|
|
139
|
+
span_id: span.spanId
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { disableContextPropagation, enableContextPropagation, enterSpanContext, exitSpanContext, getContextTags, getCurrentSpan, isContextPropagationEnabled, runInSpanContext };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=context.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.mjs","names":[],"sources":["../src/context.ts"],"sourcesContent":["/**\n * AsyncLocalStorage-based context propagation for loggily — Node.js/Bun only.\n *\n * Separated from core logger to allow tree-shaking in browser bundles.\n * When enabled, new spans automatically parent to the current context span,\n * and writeLog() auto-tags with trace_id/span_id from context.\n *\n * @example\n * ```typescript\n * import { enableContextPropagation, getCurrentSpan } from \"loggily/context\"\n *\n * enableContextPropagation()\n *\n * const log = createLogger(\"myapp\")\n * {\n * using span = log.span(\"request\")\n * // All logs and child spans within this async context\n * // automatically inherit trace_id and span_id\n * log.info(\"inside span\") // auto-tagged with trace_id, span_id\n *\n * const current = getCurrentSpan()\n * // current === { spanId: \"sp_1\", traceId: \"tr_1\", parentId: null }\n * }\n * ```\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\"\nimport { _setContextHooks, _clearContextHooks } from \"./core.js\"\n\n// ============ Types ============\n\n/** Minimal span context stored in AsyncLocalStorage */\nexport interface SpanContext {\n readonly spanId: string\n readonly traceId: string\n readonly parentId: string | null\n}\n\n// ============ State ============\n\nlet storage: AsyncLocalStorage<SpanContext> | null = null\nlet contextEnabled = false\n\n/**\n * Map from spanId → the SpanContext that was active when the span was entered.\n * Used to restore the exact previous context on exit, avoiding corruption\n * from non-LIFO end() ordering.\n */\nconst previousContexts = new Map<string, SpanContext | null>()\n\n// ============ API ============\n\n/**\n * Enable AsyncLocalStorage-based context propagation.\n * Once enabled, new spans automatically parent to the current context span,\n * and log messages are auto-tagged with trace_id/span_id.\n *\n * **Node.js/Bun only** — not available in browser environments.\n */\nexport function enableContextPropagation(): void {\n if (!storage) {\n storage = new AsyncLocalStorage<SpanContext>()\n }\n contextEnabled = true\n\n // Register hooks with core.ts\n _setContextHooks({\n getContextTags,\n getContextParent() {\n const span = getCurrentSpan()\n if (!span) return null\n return { spanId: span.spanId, traceId: span.traceId }\n },\n enterContext: enterSpanContext,\n exitContext: exitSpanContext,\n })\n}\n\n/**\n * Disable context propagation.\n * Existing spans continue to work, but new spans won't auto-parent.\n */\nexport function disableContextPropagation(): void {\n contextEnabled = false\n _clearContextHooks()\n}\n\n/** Check if context propagation is enabled */\nexport function isContextPropagationEnabled(): boolean {\n return contextEnabled\n}\n\n/**\n * Get the current span context from AsyncLocalStorage.\n * Returns null if no span is active in the current async context,\n * or if context propagation is not enabled.\n */\nexport function getCurrentSpan(): SpanContext | null {\n if (!contextEnabled || !storage) return null\n return storage.getStore() ?? null\n}\n\n/**\n * Enter a span context for the remainder of the current synchronous execution\n * and any async operations started from it. Used by the logger when creating\n * spans with `using` — since `using` doesn't wrap user code in a callback,\n * `enterWith()` is the right primitive.\n *\n * Captures the full previous SpanContext snapshot so it can be restored\n * exactly on exit, even with non-LIFO end() ordering.\n *\n * @internal\n */\nexport function enterSpanContext(spanId: string, traceId: string, parentId: string | null): void {\n if (!contextEnabled || !storage) return\n\n // Capture the full previous context before overwriting\n const previous = storage.getStore() ?? null\n previousContexts.set(spanId, previous)\n\n storage.enterWith({ spanId, traceId, parentId })\n}\n\n/**\n * Restore the previous span context (called when a span ends).\n * Restores the exact SpanContext snapshot captured at enter time,\n * preventing corruption from non-LIFO end() ordering.\n *\n * @internal\n */\nexport function exitSpanContext(spanId: string): void {\n if (!contextEnabled || !storage) return\n\n const previous = previousContexts.get(spanId)\n previousContexts.delete(spanId)\n\n if (previous) {\n storage.enterWith(previous)\n } else {\n // No previous context — exit entirely\n storage.enterWith(undefined as unknown as SpanContext)\n }\n}\n\n/**\n * Run a function within a span context.\n * Used for explicit context scoping (e.g., in request handlers).\n *\n * @param context - The span context to set\n * @param fn - The function to run within the context\n * @returns The return value of fn\n */\nexport function runInSpanContext<T>(context: SpanContext, fn: () => T): T {\n if (!contextEnabled || !storage) return fn()\n return storage.run(context, fn)\n}\n\n/**\n * Get the context tags (trace_id, span_id) for the current async context.\n * Used by writeLog() to auto-tag log messages.\n * Returns empty object if context propagation is disabled or no span is active.\n */\nexport function getContextTags(): Record<string, string> {\n const span = getCurrentSpan()\n if (!span) return {}\n return {\n trace_id: span.traceId,\n span_id: span.spanId,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAI,UAAiD;AACrD,IAAI,iBAAiB;;;;;;AAOrB,MAAM,mCAAmB,IAAI,KAAiC;;;;;;;;AAW9D,SAAgB,2BAAiC;AAC/C,KAAI,CAAC,QACH,WAAU,IAAI,mBAAgC;AAEhD,kBAAiB;AAGjB,kBAAiB;EACf;EACA,mBAAmB;GACjB,MAAM,OAAO,gBAAgB;AAC7B,OAAI,CAAC,KAAM,QAAO;AAClB,UAAO;IAAE,QAAQ,KAAK;IAAQ,SAAS,KAAK;IAAS;;EAEvD,cAAc;EACd,aAAa;EACd,CAAC;;;;;;AAOJ,SAAgB,4BAAkC;AAChD,kBAAiB;AACjB,qBAAoB;;;AAItB,SAAgB,8BAAuC;AACrD,QAAO;;;;;;;AAQT,SAAgB,iBAAqC;AACnD,KAAI,CAAC,kBAAkB,CAAC,QAAS,QAAO;AACxC,QAAO,QAAQ,UAAU,IAAI;;;;;;;;;;;;;AAc/B,SAAgB,iBAAiB,QAAgB,SAAiB,UAA+B;AAC/F,KAAI,CAAC,kBAAkB,CAAC,QAAS;CAGjC,MAAM,WAAW,QAAQ,UAAU,IAAI;AACvC,kBAAiB,IAAI,QAAQ,SAAS;AAEtC,SAAQ,UAAU;EAAE;EAAQ;EAAS;EAAU,CAAC;;;;;;;;;AAUlD,SAAgB,gBAAgB,QAAsB;AACpD,KAAI,CAAC,kBAAkB,CAAC,QAAS;CAEjC,MAAM,WAAW,iBAAiB,IAAI,OAAO;AAC7C,kBAAiB,OAAO,OAAO;AAE/B,KAAI,SACF,SAAQ,UAAU,SAAS;KAG3B,SAAQ,UAAU,KAAA,EAAoC;;;;;;;;;;AAY1D,SAAgB,iBAAoB,SAAsB,IAAgB;AACxE,KAAI,CAAC,kBAAkB,CAAC,QAAS,QAAO,IAAI;AAC5C,QAAO,QAAQ,IAAI,SAAS,GAAG;;;;;;;AAQjC,SAAgB,iBAAyC;CACvD,MAAM,OAAO,gBAAgB;AAC7B,KAAI,CAAC,KAAM,QAAO,EAAE;AACpB,QAAO;EACL,UAAU,KAAK;EACf,SAAS,KAAK;EACf"}
|