loggily 0.0.1 → 0.4.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/LICENSE +21 -0
- package/README.md +148 -4
- package/package.json +68 -7
- package/src/colors.ts +27 -0
- package/src/context.ts +170 -0
- package/src/core.ts +850 -0
- package/src/file-writer.ts +110 -0
- package/src/index.browser.ts +72 -0
- package/src/index.ts +18 -1
- package/src/tracing.ts +150 -0
- package/src/worker.ts +674 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bjørn Stabell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,9 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Loggily
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Clarity without the clutter.**
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/beorn/loggily/actions/workflows/test.yml)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
> Early release (0.x) -- API may evolve before 1.0.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install loggily
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Requirement | Version |
|
|
20
|
+
| ------------- | ------------------------------------------------- |
|
|
21
|
+
| Node.js | 18+ |
|
|
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
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createLogger } from "loggily"
|
|
31
|
+
|
|
32
|
+
const log = createLogger("myapp")
|
|
33
|
+
|
|
34
|
+
// ?. skips the entire call — including argument evaluation — when the level is disabled
|
|
35
|
+
log.info?.("server started", { port: 3000 })
|
|
36
|
+
log.debug?.("cache hit", { key: "user:42" })
|
|
37
|
+
log.error?.(new Error("connection lost"))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Output in development (colorized with timestamps and clickable source lines):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
14:32:15 INFO myapp server started {port: 3000}
|
|
44
|
+
14:32:15 DEBUG myapp cache hit {key: "user:42"}
|
|
45
|
+
14:32:15 ERROR myapp connection lost
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Set `NODE_ENV=production` or `LOG_FORMAT=json` and the same code emits structured JSON:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "time": "2024-01-15T14:32:15.123Z", "level": "info", "name": "myapp", "msg": "server started", "port": 3000 }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Spans
|
|
55
|
+
|
|
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:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// With `using` (TS 5.2+, Bun 1.0+, Node 22+)
|
|
60
|
+
{
|
|
61
|
+
using span = log.span("db:query", { table: "users" })
|
|
62
|
+
const users = await db.query("SELECT * FROM users")
|
|
63
|
+
span.spanData.count = users.length
|
|
64
|
+
}
|
|
65
|
+
// Output: SPAN myapp:db:query (45ms) {count: 100, table: "users"}
|
|
66
|
+
|
|
67
|
+
// Without `using` — works on any runtime
|
|
68
|
+
const span = log.span("db:query", { table: "users" })
|
|
69
|
+
try {
|
|
70
|
+
const users = await db.query("SELECT * FROM users")
|
|
71
|
+
span.spanData.count = users.length
|
|
72
|
+
} finally {
|
|
73
|
+
span.end()
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Why Loggily?
|
|
78
|
+
|
|
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.
|
|
80
|
+
|
|
81
|
+
### Near-zero cost for disabled logs
|
|
82
|
+
|
|
83
|
+
Most loggers waste work when logging is disabled. Even with a noop function, arguments are still evaluated:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Traditional — args are ALWAYS evaluated, even when debug is off
|
|
87
|
+
log.debug(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Loggily uses optional chaining to skip the entire call — including argument evaluation:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Loggily — args are NOT evaluated when disabled
|
|
94
|
+
log.debug?.(`state: ${JSON.stringify(computeExpensiveState())}`)
|
|
95
|
+
```
|
|
96
|
+
|
|
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.
|
|
98
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
## Features
|
|
102
|
+
|
|
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).
|
|
111
|
+
|
|
112
|
+
## When Not to Use Loggily
|
|
113
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
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.
|
|
6
150
|
|
|
7
151
|
## License
|
|
8
152
|
|
|
9
|
-
MIT
|
|
153
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,16 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loggily",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "TypeScript logger with debug-style namespaces, structured JSON output, and lightweight spans. Disabled logs skip argument evaluation via optional chaining.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"browser",
|
|
7
|
+
"bun",
|
|
8
|
+
"debug",
|
|
9
|
+
"json-logger",
|
|
10
|
+
"logger",
|
|
11
|
+
"logging",
|
|
12
|
+
"namespace",
|
|
13
|
+
"node",
|
|
14
|
+
"observability",
|
|
15
|
+
"optional-chaining",
|
|
16
|
+
"spans",
|
|
17
|
+
"structured-logging",
|
|
18
|
+
"tracing",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://beorn.codes/loggily/",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/beorn/loggily/issues"
|
|
24
|
+
},
|
|
6
25
|
"license": "MIT",
|
|
7
|
-
"author": "
|
|
8
|
-
"homepage": "https://github.com/beorn/loggily",
|
|
26
|
+
"author": "Beorn",
|
|
9
27
|
"repository": {
|
|
10
28
|
"type": "git",
|
|
11
29
|
"url": "https://github.com/beorn/loggily.git"
|
|
12
30
|
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
13
35
|
"type": "module",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
36
|
+
"module": "src/index.ts",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"browser": "./src/index.browser.ts",
|
|
42
|
+
"default": "./src/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"./file-writer": {
|
|
45
|
+
"types": "./dist/file-writer.d.ts",
|
|
46
|
+
"default": "./src/file-writer.ts"
|
|
47
|
+
},
|
|
48
|
+
"./worker": {
|
|
49
|
+
"types": "./dist/worker.d.ts",
|
|
50
|
+
"default": "./src/worker.ts"
|
|
51
|
+
},
|
|
52
|
+
"./context": {
|
|
53
|
+
"types": "./dist/context.d.ts",
|
|
54
|
+
"default": "./src/context.ts"
|
|
55
|
+
},
|
|
56
|
+
"./tracing": {
|
|
57
|
+
"types": "./dist/tracing.d.ts",
|
|
58
|
+
"default": "./src/tracing.ts"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "bunx --bun vitest run",
|
|
68
|
+
"docs:dev": "vitepress dev docs",
|
|
69
|
+
"docs:build": "vitepress build docs",
|
|
70
|
+
"docs:preview": "vitepress preview docs"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^25.3.3",
|
|
74
|
+
"vitepress": "^1.6.3",
|
|
75
|
+
"vitest": "^4.0.18"
|
|
76
|
+
}
|
|
16
77
|
}
|
package/src/colors.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored ANSI color functions — replaces picocolors dependency.
|
|
3
|
+
* Supports NO_COLOR, FORCE_COLOR, and TTY detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const _process = typeof process !== "undefined" ? process : undefined
|
|
7
|
+
|
|
8
|
+
const enabled =
|
|
9
|
+
_process?.env?.["FORCE_COLOR"] !== undefined && _process?.env?.["FORCE_COLOR"] !== "0"
|
|
10
|
+
? true
|
|
11
|
+
: _process?.env?.["NO_COLOR"] !== undefined
|
|
12
|
+
? false
|
|
13
|
+
: (_process?.stdout?.isTTY ?? false)
|
|
14
|
+
|
|
15
|
+
function wrap(open: string, close: string): (str: string) => string {
|
|
16
|
+
if (!enabled) return (str) => str
|
|
17
|
+
return (str) => open + str + close
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const colors = {
|
|
21
|
+
dim: wrap("\x1b[2m", "\x1b[22m"),
|
|
22
|
+
blue: wrap("\x1b[34m", "\x1b[39m"),
|
|
23
|
+
yellow: wrap("\x1b[33m", "\x1b[39m"),
|
|
24
|
+
red: wrap("\x1b[31m", "\x1b[39m"),
|
|
25
|
+
magenta: wrap("\x1b[35m", "\x1b[39m"),
|
|
26
|
+
cyan: wrap("\x1b[36m", "\x1b[39m"),
|
|
27
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncLocalStorage-based context propagation for loggily — Node.js/Bun only.
|
|
3
|
+
*
|
|
4
|
+
* Separated from core logger to allow tree-shaking in browser bundles.
|
|
5
|
+
* When enabled, new spans automatically parent to the current context span,
|
|
6
|
+
* and writeLog() auto-tags with trace_id/span_id from context.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { enableContextPropagation, getCurrentSpan } from "loggily/context"
|
|
11
|
+
*
|
|
12
|
+
* enableContextPropagation()
|
|
13
|
+
*
|
|
14
|
+
* const log = createLogger("myapp")
|
|
15
|
+
* {
|
|
16
|
+
* using span = log.span("request")
|
|
17
|
+
* // All logs and child spans within this async context
|
|
18
|
+
* // automatically inherit trace_id and span_id
|
|
19
|
+
* log.info("inside span") // auto-tagged with trace_id, span_id
|
|
20
|
+
*
|
|
21
|
+
* const current = getCurrentSpan()
|
|
22
|
+
* // current === { spanId: "sp_1", traceId: "tr_1", parentId: null }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
28
|
+
import { _setContextHooks, _clearContextHooks } from "./core.js"
|
|
29
|
+
|
|
30
|
+
// ============ Types ============
|
|
31
|
+
|
|
32
|
+
/** Minimal span context stored in AsyncLocalStorage */
|
|
33
|
+
export interface SpanContext {
|
|
34
|
+
readonly spanId: string
|
|
35
|
+
readonly traceId: string
|
|
36
|
+
readonly parentId: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============ State ============
|
|
40
|
+
|
|
41
|
+
let storage: AsyncLocalStorage<SpanContext> | null = null
|
|
42
|
+
let contextEnabled = false
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map from spanId → the SpanContext that was active when the span was entered.
|
|
46
|
+
* Used to restore the exact previous context on exit, avoiding corruption
|
|
47
|
+
* from non-LIFO end() ordering.
|
|
48
|
+
*/
|
|
49
|
+
const previousContexts = new Map<string, SpanContext | null>()
|
|
50
|
+
|
|
51
|
+
// ============ API ============
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enable AsyncLocalStorage-based context propagation.
|
|
55
|
+
* Once enabled, new spans automatically parent to the current context span,
|
|
56
|
+
* and log messages are auto-tagged with trace_id/span_id.
|
|
57
|
+
*
|
|
58
|
+
* **Node.js/Bun only** — not available in browser environments.
|
|
59
|
+
*/
|
|
60
|
+
export function enableContextPropagation(): void {
|
|
61
|
+
if (!storage) {
|
|
62
|
+
storage = new AsyncLocalStorage<SpanContext>()
|
|
63
|
+
}
|
|
64
|
+
contextEnabled = true
|
|
65
|
+
|
|
66
|
+
// Register hooks with core.ts
|
|
67
|
+
_setContextHooks({
|
|
68
|
+
getContextTags,
|
|
69
|
+
getContextParent() {
|
|
70
|
+
const span = getCurrentSpan()
|
|
71
|
+
if (!span) return null
|
|
72
|
+
return { spanId: span.spanId, traceId: span.traceId }
|
|
73
|
+
},
|
|
74
|
+
enterContext: enterSpanContext,
|
|
75
|
+
exitContext: exitSpanContext,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Disable context propagation.
|
|
81
|
+
* Existing spans continue to work, but new spans won't auto-parent.
|
|
82
|
+
*/
|
|
83
|
+
export function disableContextPropagation(): void {
|
|
84
|
+
contextEnabled = false
|
|
85
|
+
_clearContextHooks()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Check if context propagation is enabled */
|
|
89
|
+
export function isContextPropagationEnabled(): boolean {
|
|
90
|
+
return contextEnabled
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the current span context from AsyncLocalStorage.
|
|
95
|
+
* Returns null if no span is active in the current async context,
|
|
96
|
+
* or if context propagation is not enabled.
|
|
97
|
+
*/
|
|
98
|
+
export function getCurrentSpan(): SpanContext | null {
|
|
99
|
+
if (!contextEnabled || !storage) return null
|
|
100
|
+
return storage.getStore() ?? null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Enter a span context for the remainder of the current synchronous execution
|
|
105
|
+
* and any async operations started from it. Used by the logger when creating
|
|
106
|
+
* spans with `using` — since `using` doesn't wrap user code in a callback,
|
|
107
|
+
* `enterWith()` is the right primitive.
|
|
108
|
+
*
|
|
109
|
+
* Captures the full previous SpanContext snapshot so it can be restored
|
|
110
|
+
* exactly on exit, even with non-LIFO end() ordering.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function enterSpanContext(spanId: string, traceId: string, parentId: string | null): void {
|
|
115
|
+
if (!contextEnabled || !storage) return
|
|
116
|
+
|
|
117
|
+
// Capture the full previous context before overwriting
|
|
118
|
+
const previous = storage.getStore() ?? null
|
|
119
|
+
previousContexts.set(spanId, previous)
|
|
120
|
+
|
|
121
|
+
storage.enterWith({ spanId, traceId, parentId })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Restore the previous span context (called when a span ends).
|
|
126
|
+
* Restores the exact SpanContext snapshot captured at enter time,
|
|
127
|
+
* preventing corruption from non-LIFO end() ordering.
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export function exitSpanContext(spanId: string): void {
|
|
132
|
+
if (!contextEnabled || !storage) return
|
|
133
|
+
|
|
134
|
+
const previous = previousContexts.get(spanId)
|
|
135
|
+
previousContexts.delete(spanId)
|
|
136
|
+
|
|
137
|
+
if (previous) {
|
|
138
|
+
storage.enterWith(previous)
|
|
139
|
+
} else {
|
|
140
|
+
// No previous context — exit entirely
|
|
141
|
+
storage.enterWith(undefined as unknown as SpanContext)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a function within a span context.
|
|
147
|
+
* Used for explicit context scoping (e.g., in request handlers).
|
|
148
|
+
*
|
|
149
|
+
* @param context - The span context to set
|
|
150
|
+
* @param fn - The function to run within the context
|
|
151
|
+
* @returns The return value of fn
|
|
152
|
+
*/
|
|
153
|
+
export function runInSpanContext<T>(context: SpanContext, fn: () => T): T {
|
|
154
|
+
if (!contextEnabled || !storage) return fn()
|
|
155
|
+
return storage.run(context, fn)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the context tags (trace_id, span_id) for the current async context.
|
|
160
|
+
* Used by writeLog() to auto-tag log messages.
|
|
161
|
+
* Returns empty object if context propagation is disabled or no span is active.
|
|
162
|
+
*/
|
|
163
|
+
export function getContextTags(): Record<string, string> {
|
|
164
|
+
const span = getCurrentSpan()
|
|
165
|
+
if (!span) return {}
|
|
166
|
+
return {
|
|
167
|
+
trace_id: span.traceId,
|
|
168
|
+
span_id: span.spanId,
|
|
169
|
+
}
|
|
170
|
+
}
|