loggily 0.6.0 → 0.6.2
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 +70 -90
- package/dist/index.d.mts +333 -4
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +733 -3
- package/dist/index.mjs.map +1 -0
- package/package.json +22 -39
- package/dist/context.d.mts +0 -91
- package/dist/context.d.mts.map +0 -1
- package/dist/context.mjs +0 -145
- package/dist/context.mjs.map +0 -1
- package/dist/core-7D7sstHl.d.mts +0 -239
- package/dist/core-7D7sstHl.d.mts.map +0 -1
- package/dist/core-BDFU50FQ.mjs +0 -570
- package/dist/core-BDFU50FQ.mjs.map +0 -1
- package/dist/file-writer-BuQGFGRs.d.mts +0 -46
- package/dist/file-writer-BuQGFGRs.d.mts.map +0 -1
- package/dist/file-writer.d.mts +0 -2
- package/dist/file-writer.mjs +0 -75
- package/dist/file-writer.mjs.map +0 -1
- package/dist/metrics.d.mts +0 -48
- package/dist/metrics.d.mts.map +0 -1
- package/dist/metrics.mjs +0 -130
- package/dist/metrics.mjs.map +0 -1
- package/dist/tracing-2kv3HZ07.d.mts +0 -65
- package/dist/tracing-2kv3HZ07.d.mts.map +0 -1
- package/dist/tracing.d.mts +0 -2
- package/dist/tracing.mjs +0 -96
- package/dist/tracing.mjs.map +0 -1
- package/dist/worker.d.mts +0 -173
- package/dist/worker.d.mts.map +0 -1
- package/dist/worker.mjs +0 -468
- package/dist/worker.mjs.map +0 -1
- package/src/colors.ts +0 -27
- package/src/context.ts +0 -170
- package/src/core.ts +0 -880
- package/src/file-writer.ts +0 -110
- package/src/index.browser.ts +0 -72
- package/src/index.ts +0 -18
- package/src/metrics.ts +0 -201
- package/src/tracing.ts +0 -150
- package/src/worker.ts +0 -674
package/src/core.ts
DELETED
|
@@ -1,880 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loggily - Structured logging with spans
|
|
3
|
-
*
|
|
4
|
-
* Logger-first architecture: Span = Logger + Duration
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* const log = createLogger('myapp')
|
|
8
|
-
*
|
|
9
|
-
* // Simple logging
|
|
10
|
-
* log.info('starting')
|
|
11
|
-
*
|
|
12
|
-
* // Lazy messages (function not called when level is disabled)
|
|
13
|
-
* log.debug?.(() => `expensive: ${computeState()}`)
|
|
14
|
-
*
|
|
15
|
-
* // Child loggers with context fields
|
|
16
|
-
* const reqLog = log.child({ requestId: 'abc' })
|
|
17
|
-
* reqLog.info('handling request') // includes requestId in every message
|
|
18
|
-
*
|
|
19
|
-
* // With timing (span)
|
|
20
|
-
* {
|
|
21
|
-
* using task = log.span('import', { file: 'data.csv' })
|
|
22
|
-
* task.info('importing')
|
|
23
|
-
* task.spanData.count = 42 // Set span attributes
|
|
24
|
-
* // Auto-disposal on block exit → SPAN myapp:import (15ms)
|
|
25
|
-
* }
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { colors as pc } from "./colors.js"
|
|
29
|
-
|
|
30
|
-
// ============ Metrics ============
|
|
31
|
-
|
|
32
|
-
/** Data passed to span recorders on disposal */
|
|
33
|
-
export interface SpanRecord {
|
|
34
|
-
readonly name: string
|
|
35
|
-
readonly durationMs: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Interface for span duration recording — implemented by createMetricsCollector() in metrics.ts */
|
|
39
|
-
export interface SpanRecorder {
|
|
40
|
-
recordSpan(data: SpanRecord): void
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Ambient span recorder — auto-records when TRACE is active.
|
|
45
|
-
* Set by metrics.ts on import; can be replaced for testing.
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
export let _ambientRecorder: SpanRecorder | null = null
|
|
49
|
-
export function _setAmbientRecorder(recorder: SpanRecorder | null): void {
|
|
50
|
-
_ambientRecorder = recorder
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ============ Runtime Detection ============
|
|
54
|
-
|
|
55
|
-
/** Cached process reference — undefined in browser/edge runtimes */
|
|
56
|
-
const _process = typeof process !== "undefined" ? process : undefined
|
|
57
|
-
|
|
58
|
-
/** Read an environment variable, returning undefined in non-Node runtimes */
|
|
59
|
-
function getEnv(key: string): string | undefined {
|
|
60
|
-
return _process?.env?.[key]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Write to stderr with console.error fallback for non-Node runtimes */
|
|
64
|
-
function writeStderr(text: string): void {
|
|
65
|
-
if (_process?.stderr?.write) {
|
|
66
|
-
_process.stderr.write(text + "\n")
|
|
67
|
-
} else {
|
|
68
|
-
console.error(text)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ============ Types ============
|
|
73
|
-
|
|
74
|
-
/** Log levels that produce output */
|
|
75
|
-
export type OutputLogLevel = "trace" | "debug" | "info" | "warn" | "error"
|
|
76
|
-
|
|
77
|
-
/** All log levels including silent (for filtering) */
|
|
78
|
-
export type LogLevel = OutputLogLevel | "silent"
|
|
79
|
-
|
|
80
|
-
/** Message can be a string or a lazy function that returns a string */
|
|
81
|
-
export type LazyMessage = string | (() => string)
|
|
82
|
-
|
|
83
|
-
/** Span props can be an object or a lazy function (skipped entirely via ?. when tracing is off) */
|
|
84
|
-
export type LazyProps = Record<string, unknown> | (() => Record<string, unknown>)
|
|
85
|
-
|
|
86
|
-
/** Span data accessible via logger.spanData */
|
|
87
|
-
export interface SpanData {
|
|
88
|
-
readonly id: string
|
|
89
|
-
readonly traceId: string
|
|
90
|
-
readonly parentId: string | null
|
|
91
|
-
readonly startTime: number
|
|
92
|
-
readonly endTime: number | null
|
|
93
|
-
readonly duration: number | null
|
|
94
|
-
/** Custom attributes - set via direct property assignment */
|
|
95
|
-
[key: string]: unknown
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Logger interface */
|
|
99
|
-
export interface Logger {
|
|
100
|
-
/** Logger namespace (e.g., 'myapp:import') */
|
|
101
|
-
readonly name: string
|
|
102
|
-
/** Props inherited from parent + own props */
|
|
103
|
-
readonly props: Readonly<Record<string, unknown>>
|
|
104
|
-
/** Span data (non-null for span loggers, null for regular loggers) */
|
|
105
|
-
readonly spanData: SpanData | null
|
|
106
|
-
|
|
107
|
-
// Logging methods (accept string or lazy () => string)
|
|
108
|
-
trace(message: LazyMessage, data?: Record<string, unknown>): void
|
|
109
|
-
debug(message: LazyMessage, data?: Record<string, unknown>): void
|
|
110
|
-
info(message: LazyMessage, data?: Record<string, unknown>): void
|
|
111
|
-
warn(message: LazyMessage, data?: Record<string, unknown>): void
|
|
112
|
-
error(message: LazyMessage, data?: Record<string, unknown>): void
|
|
113
|
-
/** Error overload - extracts message, stack, code from Error */
|
|
114
|
-
error(error: Error, data?: Record<string, unknown>): void
|
|
115
|
-
|
|
116
|
-
// Create children
|
|
117
|
-
/** Create child logger (extends namespace, inherits props) */
|
|
118
|
-
logger(namespace?: string, props?: Record<string, unknown>): Logger
|
|
119
|
-
/** Create child span (extends namespace, inherits props, adds timing). Props can be lazy. */
|
|
120
|
-
span(namespace?: string, props?: LazyProps): SpanLogger
|
|
121
|
-
|
|
122
|
-
/** Create child logger with context fields merged into every message */
|
|
123
|
-
child(context: Record<string, unknown>): Logger
|
|
124
|
-
/** @deprecated Use .logger() instead for namespace-based children */
|
|
125
|
-
child(context: string): Logger
|
|
126
|
-
|
|
127
|
-
/** End span manually (alternative to using keyword) */
|
|
128
|
-
end(): void
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Span logger - Logger with active span (spanData is non-null, implements Disposable) */
|
|
132
|
-
export interface SpanLogger extends Logger, Disposable {
|
|
133
|
-
readonly spanData: SpanData & {
|
|
134
|
-
/** Mutable attributes - set directly */
|
|
135
|
-
[key: string]: unknown
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ============ Writers ============
|
|
140
|
-
|
|
141
|
-
type LogWriter = (formatted: string, level: string) => void
|
|
142
|
-
const writers: LogWriter[] = []
|
|
143
|
-
|
|
144
|
-
/** Add a writer that receives all formatted log output. Returns unsubscribe. */
|
|
145
|
-
export function addWriter(writer: LogWriter): () => void {
|
|
146
|
-
writers.push(writer)
|
|
147
|
-
return () => {
|
|
148
|
-
const idx = writers.indexOf(writer)
|
|
149
|
-
if (idx !== -1) writers.splice(idx, 1)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let suppressConsole = false
|
|
154
|
-
|
|
155
|
-
/** Suppress console output from the logger (writers still receive output). */
|
|
156
|
-
export function setSuppressConsole(value: boolean): void {
|
|
157
|
-
suppressConsole = value
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Output mode for writeLog */
|
|
161
|
-
export type OutputMode = "console" | "stderr" | "writers-only"
|
|
162
|
-
let outputMode: OutputMode = "console"
|
|
163
|
-
|
|
164
|
-
/** Set output mode for log messages (not spans — spans always use stderr). */
|
|
165
|
-
export function setOutputMode(mode: OutputMode): void {
|
|
166
|
-
outputMode = mode
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Get current output mode */
|
|
170
|
-
export function getOutputMode(): OutputMode {
|
|
171
|
-
return outputMode
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ============ Configuration ============
|
|
175
|
-
|
|
176
|
-
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
177
|
-
trace: 0,
|
|
178
|
-
debug: 1,
|
|
179
|
-
info: 2,
|
|
180
|
-
warn: 3,
|
|
181
|
-
error: 4,
|
|
182
|
-
silent: 5,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Initialize from environment
|
|
186
|
-
const envLogLevel = getEnv("LOG_LEVEL")?.toLowerCase()
|
|
187
|
-
let currentLogLevel: LogLevel =
|
|
188
|
-
envLogLevel === "trace" ||
|
|
189
|
-
envLogLevel === "debug" ||
|
|
190
|
-
envLogLevel === "info" ||
|
|
191
|
-
envLogLevel === "warn" ||
|
|
192
|
-
envLogLevel === "error" ||
|
|
193
|
-
envLogLevel === "silent"
|
|
194
|
-
? envLogLevel
|
|
195
|
-
: "info"
|
|
196
|
-
|
|
197
|
-
// Span output control (TRACE=1 or TRACE=myapp,other)
|
|
198
|
-
const traceEnv = getEnv("TRACE")
|
|
199
|
-
let spansEnabled = traceEnv === "1" || traceEnv === "true"
|
|
200
|
-
let traceFilter: Set<string> | null = null
|
|
201
|
-
if (traceEnv && traceEnv !== "1" && traceEnv !== "true") {
|
|
202
|
-
traceFilter = new Set(traceEnv.split(",").map((s) => s.trim()))
|
|
203
|
-
spansEnabled = true
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Debug namespace filter (DEBUG=myapp or DEBUG=myapp,-myapp:noisy or DEBUG=*)
|
|
207
|
-
// Supports negative patterns with `-` prefix (like the `debug` npm package)
|
|
208
|
-
|
|
209
|
-
/** Parse a comma-separated namespace filter into include/exclude sets */
|
|
210
|
-
function parseNamespaceFilter(input: string[]): {
|
|
211
|
-
includes: Set<string> | null
|
|
212
|
-
excludes: Set<string> | null
|
|
213
|
-
} {
|
|
214
|
-
const includeList: string[] = []
|
|
215
|
-
const excludeList: string[] = []
|
|
216
|
-
for (const part of input) {
|
|
217
|
-
if (part.startsWith("-")) {
|
|
218
|
-
excludeList.push(part.slice(1))
|
|
219
|
-
} else {
|
|
220
|
-
includeList.push(part)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
includes: includeList.length > 0 ? new Set(includeList) : null,
|
|
225
|
-
excludes: excludeList.length > 0 ? new Set(excludeList) : null,
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const debugEnv = getEnv("DEBUG")
|
|
230
|
-
let debugIncludes: Set<string> | null = null
|
|
231
|
-
let debugExcludes: Set<string> | null = null
|
|
232
|
-
if (debugEnv) {
|
|
233
|
-
const parts = debugEnv.split(",").map((s) => s.trim())
|
|
234
|
-
const parsed = parseNamespaceFilter(parts)
|
|
235
|
-
debugIncludes = parsed.includes
|
|
236
|
-
// Normalize wildcard variants
|
|
237
|
-
if (debugIncludes && [...debugIncludes].some((p) => p === "*" || p === "1" || p === "true")) {
|
|
238
|
-
debugIncludes = new Set(["*"])
|
|
239
|
-
}
|
|
240
|
-
debugExcludes = parsed.excludes
|
|
241
|
-
// Auto-lower log level to at least debug when DEBUG is set
|
|
242
|
-
if (LOG_LEVEL_PRIORITY[currentLogLevel] > LOG_LEVEL_PRIORITY.debug) {
|
|
243
|
-
currentLogLevel = "debug"
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Set minimum log level */
|
|
248
|
-
export function setLogLevel(level: LogLevel): void {
|
|
249
|
-
currentLogLevel = level
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Get current log level */
|
|
253
|
-
export function getLogLevel(): LogLevel {
|
|
254
|
-
return currentLogLevel
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Enable span output */
|
|
258
|
-
export function enableSpans(): void {
|
|
259
|
-
spansEnabled = true
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Disable span output */
|
|
263
|
-
export function disableSpans(): void {
|
|
264
|
-
spansEnabled = false
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Check if spans are enabled */
|
|
268
|
-
export function spansAreEnabled(): boolean {
|
|
269
|
-
return spansEnabled
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Set trace filter for namespace-based span output control.
|
|
274
|
-
* Only spans matching these namespace prefixes will be output.
|
|
275
|
-
* @param namespaces - Array of namespace prefixes, or null to disable filtering
|
|
276
|
-
*/
|
|
277
|
-
export function setTraceFilter(namespaces: string[] | null): void {
|
|
278
|
-
if (namespaces === null || namespaces.length === 0) {
|
|
279
|
-
traceFilter = null
|
|
280
|
-
} else {
|
|
281
|
-
traceFilter = new Set(namespaces)
|
|
282
|
-
spansEnabled = true
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Get current trace filter (null means no filtering) */
|
|
287
|
-
export function getTraceFilter(): string[] | null {
|
|
288
|
-
return traceFilter ? [...traceFilter] : null
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Set debug namespace filter (like the `debug` npm package).
|
|
293
|
-
* When set, only loggers matching these namespace prefixes produce output.
|
|
294
|
-
* Supports negative patterns with `-` prefix (e.g., ["-km:noisy"]).
|
|
295
|
-
* Also ensures log level is at least `debug`.
|
|
296
|
-
* @param namespaces - Array of namespace prefixes (prefix with `-` to exclude), or null to disable
|
|
297
|
-
*/
|
|
298
|
-
export function setDebugFilter(namespaces: string[] | null): void {
|
|
299
|
-
if (namespaces === null || namespaces.length === 0) {
|
|
300
|
-
debugIncludes = null
|
|
301
|
-
debugExcludes = null
|
|
302
|
-
} else {
|
|
303
|
-
const parsed = parseNamespaceFilter(namespaces)
|
|
304
|
-
debugIncludes = parsed.includes
|
|
305
|
-
debugExcludes = parsed.excludes
|
|
306
|
-
if (LOG_LEVEL_PRIORITY[currentLogLevel] > LOG_LEVEL_PRIORITY.debug) {
|
|
307
|
-
currentLogLevel = "debug"
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** Get current debug namespace filter (null means no filtering) */
|
|
313
|
-
export function getDebugFilter(): string[] | null {
|
|
314
|
-
if (!debugIncludes && !debugExcludes) return null
|
|
315
|
-
const result: string[] = []
|
|
316
|
-
if (debugIncludes) result.push(...debugIncludes)
|
|
317
|
-
if (debugExcludes) result.push(...[...debugExcludes].map((e) => `-${e}`))
|
|
318
|
-
return result
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ============ Log Format ============
|
|
322
|
-
|
|
323
|
-
/** Output format: human-readable console or structured JSON */
|
|
324
|
-
export type LogFormat = "console" | "json"
|
|
325
|
-
|
|
326
|
-
// Initialize from LOG_FORMAT env var, falling back to auto-detect
|
|
327
|
-
const envLogFormat = getEnv("LOG_FORMAT")?.toLowerCase()
|
|
328
|
-
let currentLogFormat: LogFormat = envLogFormat === "json" ? "json" : envLogFormat === "console" ? "console" : "console"
|
|
329
|
-
|
|
330
|
-
/** Set log output format */
|
|
331
|
-
export function setLogFormat(format: LogFormat): void {
|
|
332
|
-
currentLogFormat = format
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Get current log output format */
|
|
336
|
-
export function getLogFormat(): LogFormat {
|
|
337
|
-
return currentLogFormat
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** Determine whether to use JSON formatting for the current call */
|
|
341
|
-
function useJsonFormat(): boolean {
|
|
342
|
-
return currentLogFormat === "json" || getEnv("NODE_ENV") === "production" || getEnv("TRACE_FORMAT") === "json"
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ============ ID Generation (delegated to tracing.ts) ============
|
|
346
|
-
|
|
347
|
-
import { generateSpanId, generateTraceId, resetIdCounters, shouldSample } from "./tracing.js"
|
|
348
|
-
|
|
349
|
-
// Reset for testing
|
|
350
|
-
export function resetIds(): void {
|
|
351
|
-
resetIdCounters()
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ============ Context Propagation Hooks ============
|
|
355
|
-
|
|
356
|
-
// These are set by context.ts when enableContextPropagation() is called.
|
|
357
|
-
// Kept as nullable callbacks to avoid importing AsyncLocalStorage in browser.
|
|
358
|
-
|
|
359
|
-
/** Hook to get current span context tags (trace_id, span_id) for auto-tagging logs */
|
|
360
|
-
let _getContextTags: (() => Record<string, string>) | null = null
|
|
361
|
-
|
|
362
|
-
/** Hook to get parent span info from async context */
|
|
363
|
-
let _getContextParent: (() => { spanId: string; traceId: string } | null) | null = null
|
|
364
|
-
|
|
365
|
-
/** Hook to enter a span context (sets AsyncLocalStorage for the current async scope) */
|
|
366
|
-
let _enterContext: ((spanId: string, traceId: string, parentId: string | null) => void) | null = null
|
|
367
|
-
|
|
368
|
-
/** Hook to exit a span context (restores previous context snapshot) */
|
|
369
|
-
let _exitContext: ((spanId: string) => void) | null = null
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Register context propagation hooks (called by context.ts).
|
|
373
|
-
* @internal
|
|
374
|
-
*/
|
|
375
|
-
export function _setContextHooks(hooks: {
|
|
376
|
-
getContextTags: () => Record<string, string>
|
|
377
|
-
getContextParent: () => { spanId: string; traceId: string } | null
|
|
378
|
-
enterContext: (spanId: string, traceId: string, parentId: string | null) => void
|
|
379
|
-
exitContext: (spanId: string) => void
|
|
380
|
-
}): void {
|
|
381
|
-
_getContextTags = hooks.getContextTags
|
|
382
|
-
_getContextParent = hooks.getContextParent
|
|
383
|
-
_enterContext = hooks.enterContext
|
|
384
|
-
_exitContext = hooks.exitContext
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Clear context propagation hooks (called by disableContextPropagation).
|
|
389
|
-
* @internal
|
|
390
|
-
*/
|
|
391
|
-
export function _clearContextHooks(): void {
|
|
392
|
-
_getContextTags = null
|
|
393
|
-
_getContextParent = null
|
|
394
|
-
_enterContext = null
|
|
395
|
-
_exitContext = null
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ============ Formatting ============
|
|
399
|
-
|
|
400
|
-
function shouldLog(level: LogLevel): boolean {
|
|
401
|
-
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLogLevel]
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function shouldTraceNamespace(namespace: string): boolean {
|
|
405
|
-
if (!spansEnabled) return false
|
|
406
|
-
if (!traceFilter) return true
|
|
407
|
-
return matchesNamespaceSet(namespace, traceFilter)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Safe JSON.stringify that handles bigint, circular refs, symbols, and Error objects.
|
|
412
|
-
* Prevents crashes from non-serializable values in log data.
|
|
413
|
-
*/
|
|
414
|
-
function safeStringify(value: unknown): string {
|
|
415
|
-
const seen = new WeakSet()
|
|
416
|
-
return JSON.stringify(value, (_key, val) => {
|
|
417
|
-
if (typeof val === "bigint") return val.toString()
|
|
418
|
-
if (typeof val === "symbol") return val.toString()
|
|
419
|
-
if (val instanceof Error) return { message: val.message, stack: val.stack, name: val.name }
|
|
420
|
-
if (typeof val === "object" && val !== null) {
|
|
421
|
-
if (seen.has(val)) return "[Circular]"
|
|
422
|
-
seen.add(val)
|
|
423
|
-
}
|
|
424
|
-
return val
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function formatConsole(namespace: string, level: string, message: string, data?: Record<string, unknown>): string {
|
|
429
|
-
const time = pc.dim(new Date().toISOString().split("T")[1]?.split(".")[0] || "")
|
|
430
|
-
|
|
431
|
-
let levelStr = ""
|
|
432
|
-
switch (level) {
|
|
433
|
-
case "trace":
|
|
434
|
-
levelStr = pc.dim("TRACE")
|
|
435
|
-
break
|
|
436
|
-
case "debug":
|
|
437
|
-
levelStr = pc.dim("DEBUG")
|
|
438
|
-
break
|
|
439
|
-
case "info":
|
|
440
|
-
levelStr = pc.blue("INFO")
|
|
441
|
-
break
|
|
442
|
-
case "warn":
|
|
443
|
-
levelStr = pc.yellow("WARN")
|
|
444
|
-
break
|
|
445
|
-
case "error":
|
|
446
|
-
levelStr = pc.red("ERROR")
|
|
447
|
-
break
|
|
448
|
-
case "span":
|
|
449
|
-
levelStr = pc.magenta("SPAN")
|
|
450
|
-
break
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const ns = pc.cyan(namespace)
|
|
454
|
-
let output = `${time} ${levelStr} ${ns} ${message}`
|
|
455
|
-
|
|
456
|
-
if (data && Object.keys(data).length > 0) {
|
|
457
|
-
output += ` ${pc.dim(safeStringify(data))}`
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return output
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function formatJSON(namespace: string, level: string, message: string, data?: Record<string, unknown>): string {
|
|
464
|
-
const entry = {
|
|
465
|
-
time: new Date().toISOString(),
|
|
466
|
-
level,
|
|
467
|
-
name: namespace,
|
|
468
|
-
msg: message,
|
|
469
|
-
...data,
|
|
470
|
-
}
|
|
471
|
-
return safeStringify(entry)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function matchesNamespaceSet(namespace: string, set: Set<string>): boolean {
|
|
475
|
-
if (set.has("*")) return true
|
|
476
|
-
for (const filter of set) {
|
|
477
|
-
if (namespace === filter || namespace.startsWith(filter + ":")) {
|
|
478
|
-
return true
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return false
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function shouldDebugNamespace(namespace: string): boolean {
|
|
485
|
-
if (!debugIncludes && !debugExcludes) return true
|
|
486
|
-
// Excludes take priority
|
|
487
|
-
if (debugExcludes && matchesNamespaceSet(namespace, debugExcludes)) {
|
|
488
|
-
return false
|
|
489
|
-
}
|
|
490
|
-
// If includes are set, namespace must match
|
|
491
|
-
if (debugIncludes) return matchesNamespaceSet(namespace, debugIncludes)
|
|
492
|
-
return true
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/** Resolve a lazy message: if it's a function, call it; otherwise return the string */
|
|
496
|
-
function resolveMessage(msg: LazyMessage): string {
|
|
497
|
-
return typeof msg === "function" ? msg() : msg
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function writeLog(
|
|
501
|
-
namespace: string,
|
|
502
|
-
level: OutputLogLevel,
|
|
503
|
-
message: LazyMessage,
|
|
504
|
-
data?: Record<string, unknown>,
|
|
505
|
-
): void {
|
|
506
|
-
if (!shouldLog(level)) return
|
|
507
|
-
if (!shouldDebugNamespace(namespace)) return
|
|
508
|
-
|
|
509
|
-
// Resolve lazy message only after level/namespace checks pass
|
|
510
|
-
const resolved = resolveMessage(message)
|
|
511
|
-
|
|
512
|
-
// Auto-tag with trace/span context when context propagation is enabled
|
|
513
|
-
const contextTags = _getContextTags?.()
|
|
514
|
-
const mergedData = contextTags && Object.keys(contextTags).length > 0 ? { ...contextTags, ...data } : data
|
|
515
|
-
|
|
516
|
-
const formatted = useJsonFormat()
|
|
517
|
-
? formatJSON(namespace, level, resolved, mergedData)
|
|
518
|
-
: formatConsole(namespace, level, resolved, mergedData)
|
|
519
|
-
|
|
520
|
-
for (const w of writers) w(formatted, level)
|
|
521
|
-
|
|
522
|
-
if (suppressConsole || outputMode === "writers-only") return
|
|
523
|
-
|
|
524
|
-
if (outputMode === "stderr") {
|
|
525
|
-
writeStderr(formatted)
|
|
526
|
-
return
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Default: use console methods (captured by Ink's patchConsole for TUI panel)
|
|
530
|
-
switch (level) {
|
|
531
|
-
case "trace":
|
|
532
|
-
case "debug":
|
|
533
|
-
console.debug(formatted)
|
|
534
|
-
break
|
|
535
|
-
case "info":
|
|
536
|
-
console.info(formatted)
|
|
537
|
-
break
|
|
538
|
-
case "warn":
|
|
539
|
-
console.warn(formatted)
|
|
540
|
-
break
|
|
541
|
-
case "error":
|
|
542
|
-
console.error(formatted)
|
|
543
|
-
break
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export function writeSpan(namespace: string, duration: number, attrs: Record<string, unknown>): void {
|
|
548
|
-
if (!shouldTraceNamespace(namespace)) return
|
|
549
|
-
if (!shouldDebugNamespace(namespace)) return
|
|
550
|
-
|
|
551
|
-
const message = `(${duration}ms)`
|
|
552
|
-
const formatted = useJsonFormat()
|
|
553
|
-
? formatJSON(namespace, "span", message, { duration, ...attrs })
|
|
554
|
-
: formatConsole(namespace, "span", message, { duration, ...attrs })
|
|
555
|
-
|
|
556
|
-
for (const w of writers) w(formatted, "span")
|
|
557
|
-
if (!suppressConsole) writeStderr(formatted)
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// ============ Shared SpanData Proxy ============
|
|
561
|
-
|
|
562
|
-
interface SpanDataFields {
|
|
563
|
-
id: string
|
|
564
|
-
traceId: string
|
|
565
|
-
parentId: string | null
|
|
566
|
-
startTime: number
|
|
567
|
-
endTime: number | null
|
|
568
|
-
duration: number | null
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Create a proxy that exposes span metadata as readonly and custom attributes as writable.
|
|
573
|
-
* Shared between core logger spans and worker logger spans.
|
|
574
|
-
*/
|
|
575
|
-
export function createSpanDataProxy(getFields: () => SpanDataFields, attrs: Record<string, unknown>): SpanData {
|
|
576
|
-
const READONLY_KEYS = new Set(["id", "traceId", "parentId", "startTime", "endTime", "duration"])
|
|
577
|
-
return new Proxy(attrs, {
|
|
578
|
-
get(_target, prop) {
|
|
579
|
-
if (READONLY_KEYS.has(prop as string)) {
|
|
580
|
-
return getFields()[prop as keyof SpanDataFields]
|
|
581
|
-
}
|
|
582
|
-
return attrs[prop as string]
|
|
583
|
-
},
|
|
584
|
-
set(_target, prop, value) {
|
|
585
|
-
if (READONLY_KEYS.has(prop as string)) {
|
|
586
|
-
return false
|
|
587
|
-
}
|
|
588
|
-
attrs[prop as string] = value
|
|
589
|
-
return true
|
|
590
|
-
},
|
|
591
|
-
}) as SpanData
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ============ Implementation ============
|
|
595
|
-
|
|
596
|
-
interface MutableSpanData {
|
|
597
|
-
id: string
|
|
598
|
-
traceId: string
|
|
599
|
-
parentId: string | null
|
|
600
|
-
startTime: number
|
|
601
|
-
endTime: number | null
|
|
602
|
-
duration: number | null
|
|
603
|
-
attrs: Record<string, unknown>
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function createLoggerImpl(
|
|
607
|
-
name: string,
|
|
608
|
-
props: Record<string, unknown>,
|
|
609
|
-
spanMeta: MutableSpanData | null,
|
|
610
|
-
parentSpanId: string | null,
|
|
611
|
-
traceId: string | null,
|
|
612
|
-
traceSampled: boolean = true,
|
|
613
|
-
): Logger {
|
|
614
|
-
const log = (level: OutputLogLevel, msgOrError: LazyMessage | Error, data?: Record<string, unknown>): void => {
|
|
615
|
-
if (msgOrError instanceof Error) {
|
|
616
|
-
const err = msgOrError
|
|
617
|
-
writeLog(name, level, err.message, {
|
|
618
|
-
...props,
|
|
619
|
-
...data,
|
|
620
|
-
error_type: err.name,
|
|
621
|
-
error_stack: err.stack,
|
|
622
|
-
error_code: (err as { code?: string }).code,
|
|
623
|
-
})
|
|
624
|
-
} else {
|
|
625
|
-
writeLog(name, level, msgOrError, { ...props, ...data })
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const logger: Logger = {
|
|
630
|
-
name,
|
|
631
|
-
props: Object.freeze({ ...props }),
|
|
632
|
-
|
|
633
|
-
get spanData(): SpanData | null {
|
|
634
|
-
if (!spanMeta) return null
|
|
635
|
-
return createSpanDataProxy(
|
|
636
|
-
() => ({
|
|
637
|
-
id: spanMeta.id,
|
|
638
|
-
traceId: spanMeta.traceId,
|
|
639
|
-
parentId: spanMeta.parentId,
|
|
640
|
-
startTime: spanMeta.startTime,
|
|
641
|
-
endTime: spanMeta.endTime,
|
|
642
|
-
duration: spanMeta.endTime !== null ? spanMeta.endTime - spanMeta.startTime : Date.now() - spanMeta.startTime,
|
|
643
|
-
}),
|
|
644
|
-
spanMeta.attrs,
|
|
645
|
-
)
|
|
646
|
-
},
|
|
647
|
-
|
|
648
|
-
trace: (msg, data) => log("trace", msg, data),
|
|
649
|
-
debug: (msg, data) => log("debug", msg, data),
|
|
650
|
-
info: (msg, data) => log("info", msg, data),
|
|
651
|
-
warn: (msg, data) => log("warn", msg, data),
|
|
652
|
-
error: (msgOrError, data) => log("error", msgOrError as string, data),
|
|
653
|
-
|
|
654
|
-
logger(namespace?: string, childProps?: Record<string, unknown>): Logger {
|
|
655
|
-
const childName = namespace ? `${name}:${namespace}` : name
|
|
656
|
-
const mergedProps = { ...props, ...childProps }
|
|
657
|
-
return createLoggerImpl(childName, mergedProps, null, parentSpanId, traceId, traceSampled)
|
|
658
|
-
},
|
|
659
|
-
|
|
660
|
-
span(namespace?: string, childProps?: LazyProps): SpanLogger {
|
|
661
|
-
const childName = namespace ? `${name}:${namespace}` : name
|
|
662
|
-
const resolvedChildProps = typeof childProps === "function" ? childProps() : childProps
|
|
663
|
-
const mergedProps = { ...props, ...resolvedChildProps }
|
|
664
|
-
const newSpanId = generateSpanId()
|
|
665
|
-
|
|
666
|
-
// Resolve parent from context propagation if not explicitly set
|
|
667
|
-
let resolvedParentId = parentSpanId
|
|
668
|
-
let resolvedTraceId = traceId
|
|
669
|
-
|
|
670
|
-
if (!resolvedParentId && _getContextParent) {
|
|
671
|
-
const ctxParent = _getContextParent()
|
|
672
|
-
if (ctxParent) {
|
|
673
|
-
resolvedParentId = ctxParent.spanId
|
|
674
|
-
resolvedTraceId = resolvedTraceId || ctxParent.traceId
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Determine trace ID — generate new one if starting a new trace
|
|
679
|
-
const isNewTrace = !resolvedTraceId
|
|
680
|
-
const finalTraceId = resolvedTraceId || generateTraceId()
|
|
681
|
-
|
|
682
|
-
// Head-based sampling: inherit from parent, or decide at trace creation
|
|
683
|
-
const sampled = isNewTrace ? shouldSample() : traceSampled
|
|
684
|
-
|
|
685
|
-
const newSpanData: MutableSpanData = {
|
|
686
|
-
id: newSpanId,
|
|
687
|
-
traceId: finalTraceId,
|
|
688
|
-
parentId: resolvedParentId,
|
|
689
|
-
startTime: Date.now(),
|
|
690
|
-
endTime: null,
|
|
691
|
-
duration: null,
|
|
692
|
-
attrs: {},
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const spanLogger = createLoggerImpl(
|
|
696
|
-
childName,
|
|
697
|
-
mergedProps,
|
|
698
|
-
newSpanData,
|
|
699
|
-
newSpanId,
|
|
700
|
-
finalTraceId,
|
|
701
|
-
sampled,
|
|
702
|
-
) as SpanLogger
|
|
703
|
-
|
|
704
|
-
// Enter span context for async propagation (if enabled)
|
|
705
|
-
_enterContext?.(newSpanId, finalTraceId, resolvedParentId)
|
|
706
|
-
|
|
707
|
-
// Add disposal
|
|
708
|
-
;(spanLogger as unknown as { [Symbol.dispose]: () => void })[Symbol.dispose] = () => {
|
|
709
|
-
if (newSpanData.endTime !== null) return // Already disposed
|
|
710
|
-
|
|
711
|
-
newSpanData.endTime = Date.now()
|
|
712
|
-
newSpanData.duration = newSpanData.endTime - newSpanData.startTime
|
|
713
|
-
|
|
714
|
-
// Collect span data if collection is active
|
|
715
|
-
if (collectSpans) {
|
|
716
|
-
collectedSpans.push(
|
|
717
|
-
createSpanDataProxy(
|
|
718
|
-
() => ({
|
|
719
|
-
id: newSpanData.id,
|
|
720
|
-
traceId: newSpanData.traceId,
|
|
721
|
-
parentId: newSpanData.parentId,
|
|
722
|
-
startTime: newSpanData.startTime,
|
|
723
|
-
endTime: newSpanData.endTime,
|
|
724
|
-
duration: newSpanData.duration,
|
|
725
|
-
}),
|
|
726
|
-
{ ...newSpanData.attrs },
|
|
727
|
-
),
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Exit span context (restore previous context snapshot)
|
|
732
|
-
_exitContext?.(newSpanId)
|
|
733
|
-
|
|
734
|
-
// Record to ambient recorder (auto-active when TRACE is on, set by metrics.ts)
|
|
735
|
-
_ambientRecorder?.recordSpan({ name: childName, durationMs: newSpanData.duration })
|
|
736
|
-
|
|
737
|
-
// Only emit span if sampled
|
|
738
|
-
if (sampled) {
|
|
739
|
-
writeSpan(childName, newSpanData.duration, {
|
|
740
|
-
span_id: newSpanData.id,
|
|
741
|
-
trace_id: newSpanData.traceId,
|
|
742
|
-
parent_id: newSpanData.parentId,
|
|
743
|
-
...mergedProps,
|
|
744
|
-
...newSpanData.attrs,
|
|
745
|
-
})
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return spanLogger
|
|
750
|
-
},
|
|
751
|
-
|
|
752
|
-
child(context: string | Record<string, unknown>): Logger {
|
|
753
|
-
if (typeof context === "string") {
|
|
754
|
-
// Deprecated string overload - use .logger() instead
|
|
755
|
-
return this.logger(context)
|
|
756
|
-
}
|
|
757
|
-
// Context object overload: merge context fields into props
|
|
758
|
-
return createLoggerImpl(name, { ...props, ...context }, null, parentSpanId, traceId, traceSampled)
|
|
759
|
-
},
|
|
760
|
-
|
|
761
|
-
end(): void {
|
|
762
|
-
if (spanMeta?.endTime === null) {
|
|
763
|
-
;(this as unknown as { [Symbol.dispose]: () => void })[Symbol.dispose]?.()
|
|
764
|
-
}
|
|
765
|
-
},
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return logger
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Create a plain logger for a component (internal use).
|
|
773
|
-
* For application code, use createLogger() instead which returns undefined for disabled levels.
|
|
774
|
-
*/
|
|
775
|
-
function createPlainLogger(name: string, props?: Record<string, unknown>): Logger {
|
|
776
|
-
return createLoggerImpl(name, props || {}, null, null, null)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// ============ Collected Spans (for analysis) ============
|
|
780
|
-
|
|
781
|
-
const collectedSpans: SpanData[] = []
|
|
782
|
-
let collectSpans = false
|
|
783
|
-
|
|
784
|
-
/** Enable span collection for analysis */
|
|
785
|
-
export function startCollecting(): void {
|
|
786
|
-
collectSpans = true
|
|
787
|
-
collectedSpans.length = 0
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/** Stop collecting and return collected spans */
|
|
791
|
-
export function stopCollecting(): SpanData[] {
|
|
792
|
-
collectSpans = false
|
|
793
|
-
return [...collectedSpans]
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/** Get collected spans */
|
|
797
|
-
export function getCollectedSpans(): SpanData[] {
|
|
798
|
-
return [...collectedSpans]
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/** Clear collected spans */
|
|
802
|
-
export function clearCollectedSpans(): void {
|
|
803
|
-
collectedSpans.length = 0
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// ============ Conditional Logger (Zero-Overhead Pattern) ============
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Logger with optional methods — returns undefined for disabled levels.
|
|
810
|
-
* Use with optional chaining: `log.debug?.("msg")` for zero-overhead when disabled.
|
|
811
|
-
*
|
|
812
|
-
* Defined as an explicit interface (not Omit<Logger,...>) so that
|
|
813
|
-
* oxlint's type-aware mode can resolve it without advanced type inference.
|
|
814
|
-
*/
|
|
815
|
-
export interface ConditionalLogger {
|
|
816
|
-
readonly name: string
|
|
817
|
-
readonly props: Readonly<Record<string, unknown>>
|
|
818
|
-
readonly spanData: SpanData | null
|
|
819
|
-
|
|
820
|
-
trace?: (message: LazyMessage, data?: Record<string, unknown>) => void
|
|
821
|
-
debug?: (message: LazyMessage, data?: Record<string, unknown>) => void
|
|
822
|
-
info?: (message: LazyMessage, data?: Record<string, unknown>) => void
|
|
823
|
-
warn?: (message: LazyMessage, data?: Record<string, unknown>) => void
|
|
824
|
-
error?: {
|
|
825
|
-
(message: LazyMessage, data?: Record<string, unknown>): void
|
|
826
|
-
(error: Error, data?: Record<string, unknown>): void
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
logger(namespace?: string, props?: Record<string, unknown>): Logger
|
|
830
|
-
span(namespace?: string, props?: LazyProps): SpanLogger
|
|
831
|
-
child(context: Record<string, unknown>): Logger
|
|
832
|
-
child(context: string): Logger
|
|
833
|
-
end(): void
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* Create a logger for a component.
|
|
838
|
-
* Returns undefined for disabled levels - use with optional chaining for zero overhead.
|
|
839
|
-
*
|
|
840
|
-
* Log levels (most → least verbose): trace < debug < info < warn < error < silent
|
|
841
|
-
* Default level: info (trace and debug disabled)
|
|
842
|
-
*
|
|
843
|
-
* @example
|
|
844
|
-
* const log = createLogger('myapp')
|
|
845
|
-
*
|
|
846
|
-
* // All methods support ?. for zero-overhead when disabled
|
|
847
|
-
* log.trace?.(`very verbose: ${expensiveDebug()}`) // Skipped at info level
|
|
848
|
-
* log.debug?.(`debug: ${getState()}`) // Skipped at info level
|
|
849
|
-
* log.info?.('starting') // Enabled at info level
|
|
850
|
-
* log.warn?.('deprecated') // Enabled at info level
|
|
851
|
-
* log.error?.('failed') // Enabled at info level
|
|
852
|
-
*
|
|
853
|
-
* // With -q flag or LOG_LEVEL=warn:
|
|
854
|
-
* log.info?.('starting') // Now skipped - info < warn
|
|
855
|
-
*
|
|
856
|
-
* // With initial props
|
|
857
|
-
* const log = createLogger('myapp', { version: '1.0' })
|
|
858
|
-
*
|
|
859
|
-
* // Create spans
|
|
860
|
-
* {
|
|
861
|
-
* using task = log.span('import', { file: 'data.csv' })
|
|
862
|
-
* task.info?.('importing')
|
|
863
|
-
* task.spanData.count = 42
|
|
864
|
-
* }
|
|
865
|
-
*/
|
|
866
|
-
export function createLogger(name: string, props?: Record<string, unknown>): ConditionalLogger {
|
|
867
|
-
const baseLog = createPlainLogger(name, props)
|
|
868
|
-
|
|
869
|
-
return new Proxy(baseLog as ConditionalLogger, {
|
|
870
|
-
get(target, prop: string) {
|
|
871
|
-
if (prop in LOG_LEVEL_PRIORITY && prop !== "silent") {
|
|
872
|
-
const current = LOG_LEVEL_PRIORITY[currentLogLevel]
|
|
873
|
-
if (LOG_LEVEL_PRIORITY[prop as keyof typeof LOG_LEVEL_PRIORITY] < current) {
|
|
874
|
-
return undefined
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
return (target as unknown as Record<string, unknown>)[prop]
|
|
878
|
-
},
|
|
879
|
-
})
|
|
880
|
-
}
|