loggily 0.7.0 → 0.10.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 +46 -133
- package/dist/context.d.mts +2 -2
- package/dist/context.d.mts.map +1 -1
- package/dist/context.mjs +3 -3
- package/dist/context.mjs.map +1 -1
- package/dist/core-Byd3DY71.mjs +1324 -0
- package/dist/core-Byd3DY71.mjs.map +1 -0
- package/dist/core-DHB4KaG1.d.mts +260 -0
- package/dist/core-DHB4KaG1.d.mts.map +1 -0
- package/dist/{index-Co4jC3mx.d.mts → file-writer-BhJzFNKE.d.mts} +68 -50
- package/dist/file-writer-BhJzFNKE.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 -3
- package/dist/index.mjs +7 -2
- package/dist/index.mjs.map +1 -0
- package/dist/metrics.d.mts +2 -48
- package/dist/metrics.mjs +13 -43
- package/dist/metrics.mjs.map +1 -1
- 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-D0omS7eF.d.mts +85 -0
- package/dist/pipeline-D0omS7eF.d.mts.map +1 -0
- package/dist/worker.d.mts +64 -94
- package/dist/worker.d.mts.map +1 -1
- package/dist/worker.mjs +107 -281
- package/dist/worker.mjs.map +1 -1
- package/package.json +26 -4
- package/dist/core-DAFH-huv.d.mts +0 -199
- package/dist/core-DAFH-huv.d.mts.map +0 -1
- package/dist/core-Du3sIje6.mjs +0 -900
- package/dist/core-Du3sIje6.mjs.map +0 -1
- package/dist/index-Co4jC3mx.d.mts.map +0 -1
- package/dist/metrics.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -1,175 +1,88 @@
|
|
|
1
|
-
# Loggily
|
|
2
|
-
|
|
3
|
-
**Clarity without the clutter.**
|
|
4
|
-
|
|
5
|
-
Debugs, logs, and spans -- one API.
|
|
6
|
-
|
|
7
1
|
[](https://github.com/beorn/loggily/actions/workflows/test.yml)
|
|
8
2
|
[](https://www.npmjs.com/package/loggily)
|
|
9
3
|
[](https://bundlephobia.com/package/loggily)
|
|
10
4
|
[](LICENSE)
|
|
11
5
|
|
|
12
|
-
|
|
6
|
+
# Loggily — Clarity without the clutter
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
**Debugs, logs, and spans — structured and dev, server and browser — one API.**
|
|
9
|
+
|
|
10
|
+
Replace `debug` + your JSON logger + ad-hoc timers with one namespace tree and one output pipeline. Pure TypeScript, zero dependencies, ~3 KB.
|
|
15
11
|
|
|
16
12
|
```typescript
|
|
17
13
|
import { createLogger } from "loggily"
|
|
18
14
|
|
|
19
|
-
const log = createLogger("myapp"
|
|
15
|
+
const log = createLogger("myapp") // zero config — reads LOG_LEVEL, DEBUG from env
|
|
20
16
|
|
|
21
17
|
log.info?.("server started", { port: 3000 })
|
|
22
18
|
log.debug?.("cache hit", { key: "user:42" })
|
|
23
19
|
log.error?.(new Error("connection lost"))
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
14:32:15 INFO myapp server started {port: 3000}
|
|
30
|
-
14:32:15 DEBUG myapp cache hit {key: "user:42"}
|
|
31
|
-
14:32:15 ERROR myapp connection lost
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Set `NODE_ENV=production` and the same calls emit structured JSON:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{ "time": "2024-01-15T14:32:15.123Z", "level": "info", "name": "myapp", "msg": "server started", "port": 3000 }
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Why the `?.`
|
|
41
|
-
|
|
42
|
-
Disabled logs should not build strings, serialize objects, or compute snapshots just to throw them away.
|
|
43
|
-
|
|
44
|
-
With most loggers, this work still happens:
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
log.debug(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
48
|
-
// computeExpensiveState() runs even when debug is off
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
With Loggily, optional chaining short-circuits the entire call:
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
55
|
-
// nothing runs when debug is off — not the function, not the stringify, not the template
|
|
56
|
-
```
|
|
22
|
+
**The `?.` optional chaining trick** short-circuits the entire call when a log level is disabled, so nothing evaluates — not the string interpolation, not the function calls, nothing. In benchmarks, that's [~22x faster](https://loggily.dev/guide/benchmarks) than conventional noop loggers. [See how Loggily compares →](https://loggily.dev/guide/comparison)
|
|
57
23
|
|
|
58
|
-
|
|
24
|
+
## Getting Started
|
|
59
25
|
|
|
60
|
-
|
|
26
|
+
```console
|
|
27
|
+
$ npm install loggily
|
|
61
28
|
|
|
62
|
-
|
|
63
|
-
|
|
29
|
+
$ DEBUG='*' node app # show all debug output
|
|
30
|
+
$ DEBUG='myapp:db' node app # only database logs
|
|
31
|
+
$ LOG_FILE=/tmp/app.log node app # write to file
|
|
32
|
+
$ NODE_ENV=production node app # structured JSON output
|
|
33
|
+
$ TRACE=1 node app # enable span timing
|
|
64
34
|
```
|
|
65
35
|
|
|
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
|
-
|
|
74
|
-
Loggily uses `Symbol.dispose` (TC39 Explicit Resource Management) for span cleanup, which requires a modern runtime.
|
|
75
|
-
|
|
76
|
-
## Features
|
|
77
|
-
|
|
78
|
-
- **Config pipeline** -- second arg to `createLogger` is a config array: objects configure (`{ level, ns, format }`), arrays branch, values write. Pass `console` for terminal output, `{ file: "/path" }` for file output, or functions for custom stages.
|
|
79
|
-
- **Namespace hierarchy** -- organize logs with `:` separators. `DEBUG=myapp:db` shows only database output, compatible with the same patterns as the `debug` package.
|
|
80
|
-
- **Lightweight spans** -- time any operation with `using span = log.span("name")`. Automatic duration, parent-child tracking, and trace IDs.
|
|
81
|
-
- **Dev & production** -- colorized console in development, structured JSON in production. Same code, zero config.
|
|
82
|
-
- **Child context** -- `log.child({ requestId })` adds structured fields to every message in the chain.
|
|
83
|
-
- **Automatic async context** -- enable `AsyncLocalStorage`-based propagation and every log in a request's async chain inherits trace/span IDs without passing loggers around.
|
|
84
|
-
- **Lazy messages** -- `log.debug?.(() => expensiveString())` skips the function entirely when disabled.
|
|
85
|
-
- **Error overloads** -- `log.error?.(err)`, `log.error?.(err, "msg")`, and `log.error?.(err, "msg", data)`.
|
|
86
|
-
- **Worker threads** -- forward logs from workers to the main thread with full type safety.
|
|
87
|
-
|
|
88
|
-
### Config Array
|
|
89
|
-
|
|
90
36
|
```typescript
|
|
91
37
|
import { createLogger } from "loggily"
|
|
38
|
+
import { toOtel } from "loggily/otel"
|
|
92
39
|
|
|
93
|
-
//
|
|
40
|
+
// Config pipeline — objects configure, arrays branch, values write
|
|
94
41
|
const log = createLogger("myapp", [
|
|
95
|
-
{ level: "debug",
|
|
96
|
-
|
|
97
|
-
|
|
42
|
+
{ level: "debug", metrics: true }, // config object — sets scope
|
|
43
|
+
toOtel({ api: otelApi }), // stage — forwards to Jaeger/Grafana/Datadog
|
|
44
|
+
pinoTransport, // writable — { write } receives events
|
|
45
|
+
{ file: "...", format: "json" }, // file sink — formatted strings
|
|
46
|
+
[{ level: "warn" }, { file: "..." }], // branch — sub-pipeline with own scope
|
|
47
|
+
console, // colorized dev output, JSON in production
|
|
98
48
|
])
|
|
99
|
-
```
|
|
100
49
|
|
|
101
|
-
|
|
50
|
+
// Structured logging
|
|
51
|
+
log.info?.("server started", { port: 3000 })
|
|
52
|
+
log.debug?.(`state: ${expensiveFunc()}`) // skipped if debug off
|
|
53
|
+
log.error?.(new Error("connection lost"))
|
|
102
54
|
|
|
103
|
-
|
|
55
|
+
// Child loggers — extend namespace, add context
|
|
56
|
+
const dbLog = log.child("db", { pool: "main" }) // namespace: "myapp:db"
|
|
104
57
|
|
|
105
|
-
|
|
58
|
+
// Spans — time any operation, auto-track parent/child + trace IDs
|
|
59
|
+
// AsyncLocalStorage propagation: logs in async chains inherit span context
|
|
106
60
|
{
|
|
107
|
-
using span =
|
|
108
|
-
const users = await
|
|
109
|
-
span.spanData.count = users.length
|
|
110
|
-
}
|
|
111
|
-
// SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
112
|
-
|
|
113
|
-
// Without `using` — call .end() manually
|
|
114
|
-
const span = log.span("db:query")
|
|
115
|
-
try {
|
|
116
|
-
/* ... */
|
|
117
|
-
} finally {
|
|
118
|
-
span.end()
|
|
61
|
+
using span = dbLog.span?.("query", { table: "users" })
|
|
62
|
+
const users = await queryUsers() // logs inside queryUsers() get trace IDs
|
|
63
|
+
if (span) span.spanData.count = users.length
|
|
119
64
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
### Common configuration
|
|
123
|
-
|
|
124
|
-
| Variable | Example | Effect |
|
|
125
|
-
| ------------ | ------------------------- | ------------------------------------------------------ |
|
|
126
|
-
| `DEBUG` | `myapp:db,-myapp:sql` | Namespace filter (compatible with the `debug` package) |
|
|
127
|
-
| `LOG_LEVEL` | `debug`, `info`, `warn` | Minimum output level |
|
|
128
|
-
| `LOG_FORMAT` | `console`, `json` | Override output format |
|
|
129
|
-
| `TRACE` | `1` or namespace prefixes | Enable span output |
|
|
130
|
-
|
|
131
|
-
See the [full environment variable reference](https://beorn.codes/loggily/api/configuration).
|
|
132
|
-
|
|
133
|
-
### Types
|
|
65
|
+
// → SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
134
66
|
|
|
135
|
-
|
|
67
|
+
// Metrics — p50/p95/p99 from spans
|
|
68
|
+
log.metrics.summary() // myapp:db:query: 42 spans, mean=3.2ms, p95=8.4ms
|
|
136
69
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
| `SpanEvent` | A span timing event (kind, namespace, duration, spanId, traceId) |
|
|
141
|
-
| `Event` | `LogEvent \| SpanEvent` |
|
|
142
|
-
| `Stage` | `(event: Event) => Event \| null \| void` |
|
|
143
|
-
| `Pipeline` | `{ dispatch, level, dispose }` |
|
|
144
|
-
| `ConditionalLogger` | Logger with `?.`-compatible methods |
|
|
145
|
-
|
|
146
|
-
`buildPipeline()` and `defaultPipeline()` are exported for direct pipeline construction.
|
|
147
|
-
|
|
148
|
-
## Compatibility
|
|
149
|
-
|
|
150
|
-
- **`DEBUG=` compatible** -- uses the same namespace filter patterns as the `debug` package
|
|
151
|
-
- **Works with Pino transports** -- custom stage functions can forward events to any sink
|
|
152
|
-
- **W3C Trace Context** -- `traceparent()` generates standard trace headers
|
|
153
|
-
- **OpenTelemetry compatible** -- span events include `spanId`, `traceId`, `parentId`
|
|
154
|
-
|
|
155
|
-
## Why this exists
|
|
70
|
+
// Composable — build custom factories
|
|
71
|
+
const myCreateLogger = pipe(baseCreateLogger, withSpans(), myPlugin())
|
|
72
|
+
```
|
|
156
73
|
|
|
157
|
-
|
|
74
|
+
Also supports [async context propagation](https://loggily.dev/api/context), [worker threads](https://loggily.dev/guide/workers), and [browser](https://loggily.dev/guide/getting-started#browser-support).
|
|
158
75
|
|
|
159
|
-
|
|
76
|
+
**Works with:** [OpenTelemetry](https://loggily.dev/api/otel) (Jaeger, Grafana, Datadog, any OTLP backend) · [Pino transports](https://loggily.dev/guide/destinations#pino) · Sentry · Elasticsearch · AWS CloudWatch · Prometheus · [W3C Trace Context](https://loggily.dev/api/tracing) · [`DEBUG=` patterns](https://loggily.dev/guide/journey#namespaces) · [See all destinations →](https://loggily.dev/guide/destinations)
|
|
160
77
|
|
|
161
|
-
##
|
|
78
|
+
## About
|
|
162
79
|
|
|
163
|
-
|
|
164
|
-
- **You need distributed tracing with vendor exporters and auto-instrumentation.** [OpenTelemetry](https://opentelemetry.io/) is the industry standard.
|
|
80
|
+
Born from the frustration of juggling separate systems for debug logging, structured production logs, metrics, and spans — each with its own API, config, and propagation — and then duplicating the whole setup again because browser and terminal needed completely different pipelines and destinations. Loggily unifies it all: one API, one config, one pipeline that works everywhere, without the overhead when logs are off.
|
|
165
81
|
|
|
166
|
-
|
|
82
|
+
**Requirements:** Node.js ≥ 23.6 or Bun ≥ 1.0. ESM-only. TypeScript 5.2+ for `using` (`.end()` works on any version). [Browser supported](https://loggily.dev/guide/getting-started#browser-support) via conditional export.
|
|
167
83
|
|
|
168
|
-
-
|
|
169
|
-
- **[Full docs site](https://beorn.codes/loggily/)** -- guides, API reference, migration guides
|
|
170
|
-
- [Comparison](https://beorn.codes/loggily/guide/comparison) -- what Loggily does, compatibility, when to use something else
|
|
171
|
-
- [Migration from debug](https://beorn.codes/loggily/guide/migration-from-debug) -- step-by-step migration guide
|
|
84
|
+
**When not to use Loggily:** if you need auto-instrumentation (HTTP, database, gRPC) use OpenTelemetry's SDK directly; if you need log rotation or dozens of transport plugins, Pino's ecosystem is deeper.
|
|
172
85
|
|
|
173
|
-
|
|
86
|
+
**Docs:** [Get Started](https://loggily.dev/guide/getting-started) · [Full docs](https://loggily.dev/) · [Comparison](https://loggily.dev/guide/comparison) · [Why Loggily](https://loggily.dev/guide/why)
|
|
174
87
|
|
|
175
88
|
[MIT](LICENSE)
|
package/dist/context.d.mts
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* const log = createLogger("myapp")
|
|
16
16
|
* {
|
|
17
|
-
* using span = log.span("request")
|
|
17
|
+
* using span = log.span?.("request")
|
|
18
18
|
* // All logs and child spans within this async context
|
|
19
19
|
* // automatically inherit trace_id and span_id
|
|
20
|
-
* log.info("inside span") // auto-tagged with trace_id, span_id
|
|
20
|
+
* log.info?.("inside span") // auto-tagged with trace_id, span_id
|
|
21
21
|
*
|
|
22
22
|
* const current = getCurrentSpan()
|
|
23
23
|
* // current === { spanId: "sp_1", traceId: "tr_1", parentId: null }
|
package/dist/context.d.mts.map
CHANGED
|
@@ -1 +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;
|
|
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;AA2EhB;;;;AAAA,iBApDgB,yBAAA,CAAA;AA0EhB;AAAA,iBApEgB,2BAAA,CAAA;;;;;;iBASA,cAAA,CAAA,GAAkB,WAAA;;;;;;;;;AAqElC;;;iBArDgB,gBAAA,CACd,MAAA,UACA,OAAA,UACA,QAAA;;;;;;;;iBAkBc,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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as _setContextHooks, t as _clearContextHooks } from "./core-Byd3DY71.mjs";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
//#region src/context.ts
|
|
4
4
|
/**
|
|
@@ -16,10 +16,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
16
16
|
*
|
|
17
17
|
* const log = createLogger("myapp")
|
|
18
18
|
* {
|
|
19
|
-
* using span = log.span("request")
|
|
19
|
+
* using span = log.span?.("request")
|
|
20
20
|
* // All logs and child spans within this async context
|
|
21
21
|
* // automatically inherit trace_id and span_id
|
|
22
|
-
* log.info("inside span") // auto-tagged with trace_id, span_id
|
|
22
|
+
* log.info?.("inside span") // auto-tagged with trace_id, span_id
|
|
23
23
|
*
|
|
24
24
|
* const current = getCurrentSpan()
|
|
25
25
|
* // current === { spanId: "sp_1", traceId: "tr_1", parentId: null }
|
package/dist/context.mjs.map
CHANGED
|
@@ -1 +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
|
|
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(\n spanId: string,\n traceId: string,\n parentId: string | null,\n): 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,iBACd,QACA,SACA,UACM;AACN,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"}
|