loggily 0.0.1 → 0.3.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/.github/workflows/docs.yml +58 -0
- package/.github/workflows/release.yml +31 -0
- package/.github/workflows/test.yml +20 -0
- package/CHANGELOG.md +45 -0
- package/CLAUDE.md +299 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +21 -0
- package/README.md +102 -3
- package/benchmarks/overhead.ts +267 -0
- package/bun.lock +479 -0
- package/docs/api-reference.md +400 -0
- package/docs/benchmarks.md +106 -0
- package/docs/comparison.md +315 -0
- package/docs/conditional-logging-research.md +159 -0
- package/docs/guide.md +205 -0
- package/docs/migration-from-debug.md +310 -0
- package/docs/migration-from-pino.md +178 -0
- package/docs/migration-from-winston.md +179 -0
- package/docs/site/.vitepress/config.ts +67 -0
- package/docs/site/api/configuration.md +94 -0
- package/docs/site/api/index.md +61 -0
- package/docs/site/api/logger.md +99 -0
- package/docs/site/api/worker.md +120 -0
- package/docs/site/api/writers.md +69 -0
- package/docs/site/guide/getting-started.md +143 -0
- package/docs/site/guide/journey.md +203 -0
- package/docs/site/guide/migration-from-debug.md +24 -0
- package/docs/site/guide/spans.md +139 -0
- package/docs/site/guide/why.md +55 -0
- package/docs/site/guide/workers.md +113 -0
- package/docs/site/guide/zero-overhead.md +87 -0
- package/docs/site/index.md +54 -0
- package/package.json +56 -8
- package/src/colors.ts +27 -0
- package/src/context.ts +155 -0
- package/src/core.ts +804 -0
- package/src/file-writer.ts +104 -0
- package/src/index.browser.ts +64 -0
- package/src/index.ts +10 -1
- package/src/tracing.ts +142 -0
- package/src/worker.ts +687 -0
- package/tests/features.test.ts +552 -0
- package/tests/logger.test.ts +944 -0
- package/tests/tracing.test.ts +618 -0
- package/tests/universal.test.ts +107 -0
- package/tests/worker.test.ts +590 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +10 -0
package/package.json
CHANGED
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loggily",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Structured logging with
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Structured logging with spans. ~3KB, zero-overhead disabled logging via optional chaining.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"logger",
|
|
7
|
+
"logging",
|
|
8
|
+
"optional-chaining",
|
|
9
|
+
"spans",
|
|
10
|
+
"structured",
|
|
11
|
+
"tracing",
|
|
12
|
+
"typescript",
|
|
13
|
+
"zero-overhead"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://beorn.codes/decant/",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/beorn/decant/issues"
|
|
18
|
+
},
|
|
6
19
|
"license": "MIT",
|
|
7
|
-
"author": "
|
|
8
|
-
"homepage": "https://github.com/beorn/loggily",
|
|
20
|
+
"author": "Beorn",
|
|
9
21
|
"repository": {
|
|
10
22
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/beorn/
|
|
23
|
+
"url": "https://github.com/beorn/decant.git"
|
|
12
24
|
},
|
|
13
25
|
"type": "module",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
26
|
+
"module": "src/index.ts",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"browser": "./src/index.browser.ts",
|
|
32
|
+
"default": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"./file-writer": {
|
|
35
|
+
"types": "./dist/file-writer.d.ts",
|
|
36
|
+
"default": "./src/file-writer.ts"
|
|
37
|
+
},
|
|
38
|
+
"./worker": {
|
|
39
|
+
"types": "./dist/worker.d.ts",
|
|
40
|
+
"default": "./src/worker.ts"
|
|
41
|
+
},
|
|
42
|
+
"./context": {
|
|
43
|
+
"types": "./dist/context.d.ts",
|
|
44
|
+
"default": "./src/context.ts"
|
|
45
|
+
},
|
|
46
|
+
"./tracing": {
|
|
47
|
+
"types": "./dist/tracing.d.ts",
|
|
48
|
+
"default": "./src/tracing.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "bunx --bun vitest run",
|
|
55
|
+
"docs:dev": "vitepress dev docs/site",
|
|
56
|
+
"docs:build": "vitepress build docs/site",
|
|
57
|
+
"docs:preview": "vitepress preview docs/site"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^25.3.3",
|
|
61
|
+
"vitepress": "^1.6.3",
|
|
62
|
+
"vitest": "^4.0.18"
|
|
63
|
+
}
|
|
16
64
|
}
|
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,155 @@
|
|
|
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
|
+
// ============ API ============
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Enable AsyncLocalStorage-based context propagation.
|
|
48
|
+
* Once enabled, new spans automatically parent to the current context span,
|
|
49
|
+
* and log messages are auto-tagged with trace_id/span_id.
|
|
50
|
+
*
|
|
51
|
+
* **Node.js/Bun only** — not available in browser environments.
|
|
52
|
+
*/
|
|
53
|
+
export function enableContextPropagation(): void {
|
|
54
|
+
if (!storage) {
|
|
55
|
+
storage = new AsyncLocalStorage<SpanContext>()
|
|
56
|
+
}
|
|
57
|
+
contextEnabled = true
|
|
58
|
+
|
|
59
|
+
// Register hooks with core.ts
|
|
60
|
+
_setContextHooks({
|
|
61
|
+
getContextTags,
|
|
62
|
+
getContextParent() {
|
|
63
|
+
const span = getCurrentSpan()
|
|
64
|
+
if (!span) return null
|
|
65
|
+
return { spanId: span.spanId, traceId: span.traceId }
|
|
66
|
+
},
|
|
67
|
+
enterContext: enterSpanContext,
|
|
68
|
+
exitContext: exitSpanContext,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Disable context propagation.
|
|
74
|
+
* Existing spans continue to work, but new spans won't auto-parent.
|
|
75
|
+
*/
|
|
76
|
+
export function disableContextPropagation(): void {
|
|
77
|
+
contextEnabled = false
|
|
78
|
+
_clearContextHooks()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Check if context propagation is enabled */
|
|
82
|
+
export function isContextPropagationEnabled(): boolean {
|
|
83
|
+
return contextEnabled
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the current span context from AsyncLocalStorage.
|
|
88
|
+
* Returns null if no span is active in the current async context,
|
|
89
|
+
* or if context propagation is not enabled.
|
|
90
|
+
*/
|
|
91
|
+
export function getCurrentSpan(): SpanContext | null {
|
|
92
|
+
if (!contextEnabled || !storage) return null
|
|
93
|
+
return storage.getStore() ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enter a span context for the remainder of the current synchronous execution
|
|
98
|
+
* and any async operations started from it. Used by the logger when creating
|
|
99
|
+
* spans with `using` — since `using` doesn't wrap user code in a callback,
|
|
100
|
+
* `enterWith()` is the right primitive.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
export function enterSpanContext(spanId: string, traceId: string, parentId: string | null): void {
|
|
105
|
+
if (!contextEnabled || !storage) return
|
|
106
|
+
storage.enterWith({ spanId, traceId, parentId })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Restore the parent span context (called when a span ends).
|
|
111
|
+
* Re-enters the parent's context, or clears the context if there is no parent.
|
|
112
|
+
*
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
export function exitSpanContext(parentId: string | null, parentTraceId: string | null): void {
|
|
116
|
+
if (!contextEnabled || !storage) return
|
|
117
|
+
if (parentId && parentTraceId) {
|
|
118
|
+
// Restore parent context — note: we don't have the parent's parentId
|
|
119
|
+
// but that's fine since this context is only used for auto-tagging and
|
|
120
|
+
// auto-parenting new child spans (which will read spanId and traceId).
|
|
121
|
+
storage.enterWith({ spanId: parentId, traceId: parentTraceId, parentId: null })
|
|
122
|
+
} else {
|
|
123
|
+
// No parent — exit the context entirely
|
|
124
|
+
// enterWith(undefined as any) is not ideal, but there's no "exitWith"
|
|
125
|
+
// We use a sentinel to indicate "no active span"
|
|
126
|
+
storage.enterWith(undefined as unknown as SpanContext)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run a function within a span context.
|
|
132
|
+
* Used for explicit context scoping (e.g., in request handlers).
|
|
133
|
+
*
|
|
134
|
+
* @param context - The span context to set
|
|
135
|
+
* @param fn - The function to run within the context
|
|
136
|
+
* @returns The return value of fn
|
|
137
|
+
*/
|
|
138
|
+
export function runInSpanContext<T>(context: SpanContext, fn: () => T): T {
|
|
139
|
+
if (!contextEnabled || !storage) return fn()
|
|
140
|
+
return storage.run(context, fn)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the context tags (trace_id, span_id) for the current async context.
|
|
145
|
+
* Used by writeLog() to auto-tag log messages.
|
|
146
|
+
* Returns empty object if context propagation is disabled or no span is active.
|
|
147
|
+
*/
|
|
148
|
+
export function getContextTags(): Record<string, string> {
|
|
149
|
+
const span = getCurrentSpan()
|
|
150
|
+
if (!span) return {}
|
|
151
|
+
return {
|
|
152
|
+
trace_id: span.traceId,
|
|
153
|
+
span_id: span.spanId,
|
|
154
|
+
}
|
|
155
|
+
}
|