loggily 0.6.1 → 0.7.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
@@ -2,42 +2,28 @@
2
2
 
3
3
  **Clarity without the clutter.**
4
4
 
5
- One library. One namespace tree. One output pipeline. For logs (structured JSON or console), debug(), and tracing spans. Near-zero overhead from disabled log levels. Pure TypeScript. ~3KB. Zero dependencies.
5
+ Debugs, logs, and spans -- one API.
6
6
 
7
7
  [![Tests](https://github.com/beorn/loggily/actions/workflows/test.yml/badge.svg)](https://github.com/beorn/loggily/actions/workflows/test.yml)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.2+-blue.svg)](https://www.typescriptlang.org/)
8
+ [![npm version](https://img.shields.io/npm/v/loggily.svg)](https://www.npmjs.com/package/loggily)
9
+ [![size](https://img.shields.io/bundlephobia/minzip/loggily)](https://bundlephobia.com/package/loggily)
9
10
  [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
10
11
 
11
- > Early release (0.x) -- API may evolve before 1.0.
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.
12
13
 
13
- ## Install
14
-
15
- ```bash
16
- npm install loggily
17
- ```
18
-
19
- | Requirement | Version |
20
- | ------------- | ------------------------------------------------- |
21
- | Node.js | >= 23.6 |
22
- | Bun | 1.0+ |
23
- | TypeScript | 5.2+ (for `using`; `.end()` works on any version) |
24
- | Module format | ESM-only |
25
- | Browser | Supported via conditional export |
26
-
27
- ## Quick Start
14
+ Loggily replaces all three with one namespace tree and one output pipeline. Pure TypeScript, zero dependencies, ~3 KB.
28
15
 
29
16
  ```typescript
30
17
  import { createLogger } from "loggily"
31
18
 
32
- const log = createLogger("myapp")
19
+ const log = createLogger("myapp", [{ level: "debug" }, console])
33
20
 
34
- // ?. skips the entire call — including argument evaluation — when the level is disabled
35
21
  log.info?.("server started", { port: 3000 })
36
22
  log.debug?.("cache hit", { key: "user:42" })
37
23
  log.error?.(new Error("connection lost"))
38
24
  ```
39
25
 
40
- Output in development (colorized with timestamps and clickable source lines):
26
+ Readable, colorized output in development:
41
27
 
42
28
  ```
43
29
  14:32:15 INFO myapp server started {port: 3000}
@@ -45,108 +31,144 @@ Output in development (colorized with timestamps and clickable source lines):
45
31
  14:32:15 ERROR myapp connection lost
46
32
  ```
47
33
 
48
- Set `NODE_ENV=production` or `LOG_FORMAT=json` and the same code emits structured JSON:
34
+ Set `NODE_ENV=production` and the same calls emit structured JSON:
49
35
 
50
36
  ```json
51
37
  { "time": "2024-01-15T14:32:15.123Z", "level": "info", "name": "myapp", "msg": "server started", "port": 3000 }
52
38
  ```
53
39
 
54
- ### Spans
40
+ ## Why the `?.`
41
+
42
+ Disabled logs should not build strings, serialize objects, or compute snapshots just to throw them away.
55
43
 
56
- Time operations with lightweight spans. Uses TC39 [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) (`using` requires TypeScript 5.2+ and runtime support). For other environments, call `.end()` manually:
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
+ ```
57
+
58
+ In benchmarks with expensive disabled log arguments, this is [~22x faster](https://beorn.codes/loggily/guide/benchmarks) than a conventional noop logger.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ npm install loggily
64
+ ```
65
+
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
+ ```typescript
91
+ import { createLogger } from "loggily"
92
+
93
+ // Objects configure, arrays branch, values write
94
+ const log = createLogger("myapp", [
95
+ { level: "debug", ns: "-sql" },
96
+ console,
97
+ { file: "/tmp/app.log", level: "error", format: "json" },
98
+ ])
99
+ ```
100
+
101
+ When no config array is provided, loggily reads `LOG_LEVEL`, `DEBUG`, `LOG_FORMAT`, and `NODE_ENV` from the environment.
102
+
103
+ ### Spans
57
104
 
58
105
  ```typescript
59
- // With `using` (TS 5.2+, Bun 1.0+, Node 22+)
60
106
  {
61
107
  using span = log.span("db:query", { table: "users" })
62
108
  const users = await db.query("SELECT * FROM users")
63
109
  span.spanData.count = users.length
64
110
  }
65
- // Output: SPAN myapp:db:query (45ms) {count: 100, table: "users"}
111
+ // SPAN myapp:db:query (45ms) {count: 100, table: "users"}
66
112
 
67
- // Without `using` — works on any runtime
68
- const span = log.span("db:query", { table: "users" })
113
+ // Without `using` — call .end() manually
114
+ const span = log.span("db:query")
69
115
  try {
70
- const users = await db.query("SELECT * FROM users")
71
- span.spanData.count = users.length
116
+ /* ... */
72
117
  } finally {
73
118
  span.end()
74
119
  }
75
120
  ```
76
121
 
77
- ## Why Loggily?
122
+ ### Common configuration
78
123
 
79
- One API for debug-style namespace logging, structured JSON output, and lightweight spans. Many projects end up with separate tools for these -- **debug** for conditional output, **pino/winston** for production logs, a tracing SDK for timings -- with separate configs, formats, and APIs. Loggily integrates all three into one namespace tree, one output pipeline, one `?.` pattern.
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 |
80
130
 
81
- ### Near-zero cost for disabled logs
131
+ See the [full environment variable reference](https://beorn.codes/loggily/api/configuration).
82
132
 
83
- Most loggers waste work when logging is disabled. Even with a noop function, arguments are still evaluated:
133
+ ### Types
84
134
 
85
- ```typescript
86
- // Traditional — args are ALWAYS evaluated, even when debug is off
87
- log.debug(`state: ${JSON.stringify(computeExpensiveState())}`)
88
- ```
135
+ Key types exported for power users:
89
136
 
90
- Loggily uses optional chaining to skip the entire call — including argument evaluation:
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 |
91
145
 
92
- ```typescript
93
- // Loggily — args are NOT evaluated when disabled
94
- log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
95
- ```
146
+ `buildPipeline()` and `defaultPipeline()` are exported for direct pipeline construction.
96
147
 
97
- 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.
148
+ ## Compatibility
98
149
 
99
- > **Note**: The big performance advantage is specifically for disabled logging with expensive arguments, not universal logger throughput. Pino is optimized for high-throughput enabled JSON logging; Loggily's biggest advantage is skipping work when logs are disabled. See [benchmarks](https://beorn.codes/loggily/guide/benchmarks) for detailed numbers per scenario.
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`
100
154
 
101
- ## Features
155
+ ## Why this exists
156
+
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.
102
158
 
103
- - **Namespace hierarchy** organize logs with `:` separators. `log.logger("db")` creates `myapp:db`. Children inherit parent context.
104
- - **Lightweight spans and trace IDs** — time any operation with `using span = log.span("name")`. Automatic duration, parent-child tracking, and trace IDs. For full OpenTelemetry interoperability with exporters and propagation, use OpenTelemetry.
105
- - **Lazy messages** — `log.debug?.(() => expensiveString())` skips the function entirely when disabled.
106
- - **Child context** — `log.child({ requestId })` adds structured fields to every message in the chain.
107
- - **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.
108
- - **File writer** — `addWriter()` + `createFileWriter()` for buffered file output with auto-flush.
109
- - **Worker threads** — forward logs from workers to the main thread with full type safety (`loggily/worker`).
110
- - **debug-compatible namespace filtering** — reads `DEBUG=myapp:*` just like the debug package. Easy migration from debug — see the [migration guide](https://beorn.codes/loggily/guide/migration-from-debug).
159
+ > **Status:** Early release (0.x). The core API is stable, but details may evolve before 1.0.
111
160
 
112
- ## When Not to Use Loggily
161
+ ## When not to use Loggily
113
162
 
114
- - **Max-throughput transport pipelines** use [Pino](https://getpino.io/) for worker-thread transports, custom serializers, and log rotation.
115
- - **Vendor/exporter interop** use [OpenTelemetry](https://opentelemetry.io/) for distributed tracing with propagation, semantic conventions, and backend integrations.
116
- - **Tiny dev-only namespace logs** — use [debug](https://github.com/debug-js/debug) if all you need is conditional dev output with zero ceremony.
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.
117
165
 
118
166
  ## Documentation
119
167
 
120
- - **[Get Started](https://beorn.codes/loggily/guide/journey)** progressive guide from first log to full observability
121
- - **[Full docs site](https://beorn.codes/loggily/)** guides, API reference, migration guides
122
- - [Comparison](https://beorn.codes/loggily/guide/comparison) vs Pino, Winston, Bunyan, debug
123
- - [Migration from debug](https://beorn.codes/loggily/guide/migration-from-debug) step-by-step migration guide
124
-
125
- ## Environment Variables
126
-
127
- | Variable | Values | Effect |
128
- | -------------- | --------------------------------------- | --------------------------------------- |
129
- | `LOG_LEVEL` | trace, debug, info, warn, error, silent | Minimum output level |
130
- | `LOG_FORMAT` | console, json | Output format |
131
- | `DEBUG` | `*`, namespace prefixes, `-prefix` | Namespace filter (like `debug` package) |
132
- | `TRACE` | `1`, `true`, or namespace prefixes | Enable span output |
133
- | `TRACE_FORMAT` | json | Force JSON for spans |
134
- | `NODE_ENV` | production | Auto-enable JSON format |
135
-
136
- ## API
137
-
138
- | Function | Description |
139
- | ---------------------------------------------------------------------- | ------------------------------------------------------------- |
140
- | `createLogger(name, props?)` | Create a logger (disabled levels return `undefined` for `?.`) |
141
- | `.trace?.()` / `.debug?.()` / `.info?.()` / `.warn?.()` / `.error?.()` | Log at level (message + optional data) |
142
- | `.logger(namespace)` | Create child logger with extended namespace |
143
- | `.span(namespace, props?)` | Create timed span (implements `Disposable`) |
144
- | `.child(context)` | Create child with structured context fields |
145
- | `addWriter(fn)` / `createFileWriter(path)` | Custom output writers |
146
- | `setLogLevel()` / `setLogFormat()` / `enableSpans()` | Runtime configuration |
147
- | `createWorkerLogger()` / `createWorkerLogHandler()` | Worker thread support (`loggily/worker`) |
148
-
149
- See the [full API reference](https://beorn.codes/loggily/api/) for all functions and options.
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
150
172
 
151
173
  ## License
152
174
 
@@ -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"}
@@ -0,0 +1,145 @@
1
+ import { i as _setContextHooks, n as _clearContextHooks } from "./core-Du3sIje6.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"}