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 CHANGED
@@ -1,175 +1,88 @@
1
- # Loggily
2
-
3
- **Clarity without the clutter.**
4
-
5
- Debugs, logs, and spans -- one API.
6
-
7
1
  [![Tests](https://github.com/beorn/loggily/actions/workflows/test.yml/badge.svg)](https://github.com/beorn/loggily/actions/workflows/test.yml)
8
2
  [![npm version](https://img.shields.io/npm/v/loggily.svg)](https://www.npmjs.com/package/loggily)
9
3
  [![size](https://img.shields.io/bundlephobia/minzip/loggily)](https://bundlephobia.com/package/loggily)
10
4
  [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
11
5
 
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.
6
+ # Loggily Clarity without the clutter
13
7
 
14
- Loggily replaces all three with one namespace tree and one output pipeline. Pure TypeScript, zero dependencies, ~3 KB.
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", [{ level: "debug" }, console])
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
- Readable, colorized output in development:
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
- In benchmarks with expensive disabled log arguments, this is [~22x faster](https://beorn.codes/loggily/guide/benchmarks) than a conventional noop logger.
24
+ ## Getting Started
59
25
 
60
- ## Install
26
+ ```console
27
+ $ npm install loggily
61
28
 
62
- ```bash
63
- npm install loggily
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
- // Objects configure, arrays branch, values write
40
+ // Config pipeline — objects configure, arrays branch, values write
94
41
  const log = createLogger("myapp", [
95
- { level: "debug", ns: "-sql" },
96
- console,
97
- { file: "/tmp/app.log", level: "error", format: "json" },
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
- When no config array is provided, loggily reads `LOG_LEVEL`, `DEBUG`, `LOG_FORMAT`, and `NODE_ENV` from the environment.
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
- ### Spans
55
+ // Child loggers — extend namespace, add context
56
+ const dbLog = log.child("db", { pool: "main" }) // namespace: "myapp:db"
104
57
 
105
- ```typescript
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 = log.span("db:query", { table: "users" })
108
- const users = await db.query("SELECT * FROM users")
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
- Key types exported for power users:
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
- | Type | Description |
138
- | ------------------- | ---------------------------------------------------------------- |
139
- | `LogEvent` | A log message event (kind, level, namespace, message, props) |
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
- Loggily was built while developing a terminal UI where disabled debug logs inside the render loop were eating frame time. No existing logger solved the "disabled calls should cost nothing" problem at the language level, so `?.` became the foundation.
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
- > **Status:** Early release (0.x). The core API is stable, but details may evolve before 1.0.
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
- ## When not to use Loggily
78
+ ## About
162
79
 
163
- - **You need worker-thread transport pipelines with log rotation and dozens of plugins.** [Pino](https://getpino.io/) has a mature transport ecosystem for this.
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
- ## Documentation
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
- - **[Get Started](https://beorn.codes/loggily/guide/journey)** -- progressive guide from first log to full observability
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
- ## License
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)
@@ -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 }
@@ -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;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"}
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 { i as _setContextHooks, n as _clearContextHooks } from "./core-Du3sIje6.mjs";
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 }
@@ -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, 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"}
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"}