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.
Files changed (48) hide show
  1. package/.github/workflows/docs.yml +58 -0
  2. package/.github/workflows/release.yml +31 -0
  3. package/.github/workflows/test.yml +20 -0
  4. package/CHANGELOG.md +45 -0
  5. package/CLAUDE.md +299 -0
  6. package/CONTRIBUTING.md +58 -0
  7. package/LICENSE +21 -0
  8. package/README.md +102 -3
  9. package/benchmarks/overhead.ts +267 -0
  10. package/bun.lock +479 -0
  11. package/docs/api-reference.md +400 -0
  12. package/docs/benchmarks.md +106 -0
  13. package/docs/comparison.md +315 -0
  14. package/docs/conditional-logging-research.md +159 -0
  15. package/docs/guide.md +205 -0
  16. package/docs/migration-from-debug.md +310 -0
  17. package/docs/migration-from-pino.md +178 -0
  18. package/docs/migration-from-winston.md +179 -0
  19. package/docs/site/.vitepress/config.ts +67 -0
  20. package/docs/site/api/configuration.md +94 -0
  21. package/docs/site/api/index.md +61 -0
  22. package/docs/site/api/logger.md +99 -0
  23. package/docs/site/api/worker.md +120 -0
  24. package/docs/site/api/writers.md +69 -0
  25. package/docs/site/guide/getting-started.md +143 -0
  26. package/docs/site/guide/journey.md +203 -0
  27. package/docs/site/guide/migration-from-debug.md +24 -0
  28. package/docs/site/guide/spans.md +139 -0
  29. package/docs/site/guide/why.md +55 -0
  30. package/docs/site/guide/workers.md +113 -0
  31. package/docs/site/guide/zero-overhead.md +87 -0
  32. package/docs/site/index.md +54 -0
  33. package/package.json +56 -8
  34. package/src/colors.ts +27 -0
  35. package/src/context.ts +155 -0
  36. package/src/core.ts +804 -0
  37. package/src/file-writer.ts +104 -0
  38. package/src/index.browser.ts +64 -0
  39. package/src/index.ts +10 -1
  40. package/src/tracing.ts +142 -0
  41. package/src/worker.ts +687 -0
  42. package/tests/features.test.ts +552 -0
  43. package/tests/logger.test.ts +944 -0
  44. package/tests/tracing.test.ts +618 -0
  45. package/tests/universal.test.ts +107 -0
  46. package/tests/worker.test.ts +590 -0
  47. package/tsconfig.json +20 -0
  48. package/vitest.config.ts +10 -0
package/package.json CHANGED
@@ -1,16 +1,64 @@
1
1
  {
2
2
  "name": "loggily",
3
- "version": "0.0.1",
4
- "description": "Structured logging with context, levels, and formatting the logger that gets out of your way",
5
- "keywords": ["logging", "logger", "structured", "debug", "log"],
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": "Bjørn Stabell <bjorn@stabell.org>",
8
- "homepage": "https://github.com/beorn/loggily",
20
+ "author": "Beorn",
9
21
  "repository": {
10
22
  "type": "git",
11
- "url": "https://github.com/beorn/loggily.git"
23
+ "url": "https://github.com/beorn/decant.git"
12
24
  },
13
25
  "type": "module",
14
- "main": "src/index.ts",
15
- "files": ["src", "README.md"]
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
+ }