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 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
- # loggily
1
+ # Loggily
2
2
 
3
- Structured logging with context, levels, and formatting — the logger that gets out of your way.
3
+ **Clarity without the clutter.**
4
4
 
5
- **Coming soon.**
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
+ [![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/)
9
+ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](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.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.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": "Bjørn Stabell <bjorn@stabell.org>",
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
- "main": "src/index.ts",
15
- "files": ["src", "README.md"]
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
+ }