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/src/worker.ts
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Thread Logger/Console Forwarding
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities to forward loggily and console.* output from worker threads
|
|
5
|
+
* to the main thread, ensuring proper integration with DEBUG_LOG and log files.
|
|
6
|
+
*
|
|
7
|
+
* ## Full Logger Forwarding (Recommended)
|
|
8
|
+
*
|
|
9
|
+
* @example Worker side:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createWorkerLogger } from "loggily/worker"
|
|
12
|
+
* const log = createWorkerLogger(postMessage, "km:worker:parse")
|
|
13
|
+
*
|
|
14
|
+
* log.info("processing", { file: "test.md" })
|
|
15
|
+
* {
|
|
16
|
+
* using span = log.span("parse")
|
|
17
|
+
* // ... work ...
|
|
18
|
+
* span.spanData.lines = 100
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Main thread side:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { createWorkerLogHandler } from "loggily/worker"
|
|
25
|
+
*
|
|
26
|
+
* const handleLog = createWorkerLogHandler()
|
|
27
|
+
* worker.onmessage = (e) => {
|
|
28
|
+
* if (e.data.type === "log" || e.data.type === "span") handleLog(e.data)
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ## Console Forwarding (Simple)
|
|
33
|
+
*
|
|
34
|
+
* @example Worker side:
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { forwardConsole } from "loggily/worker"
|
|
37
|
+
* forwardConsole(postMessage)
|
|
38
|
+
*
|
|
39
|
+
* console.log("message") // Forwarded to main thread
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
createLogger,
|
|
45
|
+
enableSpans,
|
|
46
|
+
type ConditionalLogger,
|
|
47
|
+
type LazyMessage,
|
|
48
|
+
type Logger,
|
|
49
|
+
type SpanLogger,
|
|
50
|
+
type SpanData,
|
|
51
|
+
} from "./index.ts"
|
|
52
|
+
|
|
53
|
+
// ============ Message Protocol ============
|
|
54
|
+
|
|
55
|
+
/** Message sent from worker to main thread for console output */
|
|
56
|
+
export interface WorkerConsoleMessage {
|
|
57
|
+
type: "console"
|
|
58
|
+
level: "log" | "debug" | "info" | "warn" | "error" | "trace"
|
|
59
|
+
namespace?: string
|
|
60
|
+
args: unknown[]
|
|
61
|
+
timestamp: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Message sent from worker to main thread for structured log output */
|
|
65
|
+
export interface WorkerLogMessage {
|
|
66
|
+
type: "log"
|
|
67
|
+
level: "trace" | "debug" | "info" | "warn" | "error"
|
|
68
|
+
namespace: string
|
|
69
|
+
message: string
|
|
70
|
+
data?: Record<string, unknown>
|
|
71
|
+
timestamp: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Message sent from worker to main thread for span events */
|
|
75
|
+
export interface WorkerSpanMessage {
|
|
76
|
+
type: "span"
|
|
77
|
+
event: "start" | "end"
|
|
78
|
+
namespace: string
|
|
79
|
+
spanId: string
|
|
80
|
+
traceId: string
|
|
81
|
+
parentId: string | null
|
|
82
|
+
startTime: number
|
|
83
|
+
endTime?: number
|
|
84
|
+
duration?: number
|
|
85
|
+
props: Record<string, unknown>
|
|
86
|
+
spanData: Record<string, unknown>
|
|
87
|
+
timestamp: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Union type for all worker messages */
|
|
91
|
+
export type WorkerMessage = WorkerConsoleMessage | WorkerLogMessage | WorkerSpanMessage
|
|
92
|
+
|
|
93
|
+
/** Type guard for WorkerConsoleMessage */
|
|
94
|
+
export function isWorkerConsoleMessage(msg: unknown): msg is WorkerConsoleMessage {
|
|
95
|
+
return (
|
|
96
|
+
typeof msg === "object" &&
|
|
97
|
+
(msg as WorkerConsoleMessage)?.type === "console" &&
|
|
98
|
+
typeof (msg as WorkerConsoleMessage).level === "string" &&
|
|
99
|
+
Array.isArray((msg as WorkerConsoleMessage).args)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Type guard for WorkerLogMessage */
|
|
104
|
+
export function isWorkerLogMessage(msg: unknown): msg is WorkerLogMessage {
|
|
105
|
+
return (
|
|
106
|
+
typeof msg === "object" &&
|
|
107
|
+
(msg as WorkerLogMessage)?.type === "log" &&
|
|
108
|
+
typeof (msg as WorkerLogMessage).level === "string" &&
|
|
109
|
+
typeof (msg as WorkerLogMessage).namespace === "string"
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Type guard for WorkerSpanMessage */
|
|
114
|
+
export function isWorkerSpanMessage(msg: unknown): msg is WorkerSpanMessage {
|
|
115
|
+
return (
|
|
116
|
+
typeof msg === "object" &&
|
|
117
|
+
(msg as WorkerSpanMessage)?.type === "span" &&
|
|
118
|
+
typeof (msg as WorkerSpanMessage).event === "string"
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Type guard for any worker message */
|
|
123
|
+
export function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
|
124
|
+
return isWorkerConsoleMessage(msg) || isWorkerLogMessage(msg) || isWorkerSpanMessage(msg)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============ Worker Side ============
|
|
128
|
+
|
|
129
|
+
type PostMessageFn = (message: WorkerConsoleMessage) => void
|
|
130
|
+
|
|
131
|
+
/** Store original console methods for restoration */
|
|
132
|
+
let originalConsole: typeof console | null = null
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Serialize a value for transmission via postMessage.
|
|
136
|
+
* Handles non-serializable values like functions and circular references.
|
|
137
|
+
*/
|
|
138
|
+
function serializeArg(arg: unknown, depth = 0): unknown {
|
|
139
|
+
// Prevent infinite recursion
|
|
140
|
+
if (depth > 5) return "[max depth]"
|
|
141
|
+
|
|
142
|
+
if (arg === null || arg === undefined) return arg
|
|
143
|
+
if (typeof arg === "function") return `[Function: ${arg.name || "anonymous"}]`
|
|
144
|
+
if (typeof arg === "symbol") return arg.toString()
|
|
145
|
+
if (typeof arg === "bigint") return arg.toString() + "n"
|
|
146
|
+
|
|
147
|
+
if (arg instanceof Error) {
|
|
148
|
+
return {
|
|
149
|
+
name: arg.name,
|
|
150
|
+
message: arg.message,
|
|
151
|
+
stack: arg.stack,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (Array.isArray(arg)) {
|
|
156
|
+
return arg.map((v) => serializeArg(v, depth + 1))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof arg === "object") {
|
|
160
|
+
try {
|
|
161
|
+
// Try structured clone first (handles most cases)
|
|
162
|
+
structuredClone(arg)
|
|
163
|
+
return arg
|
|
164
|
+
} catch {
|
|
165
|
+
// Fall back to manual serialization
|
|
166
|
+
const result: Record<string, unknown> = {}
|
|
167
|
+
const seen = new Set<object>()
|
|
168
|
+
seen.add(arg)
|
|
169
|
+
|
|
170
|
+
for (const [key, value] of Object.entries(arg)) {
|
|
171
|
+
if (typeof value === "object" && value !== null && seen.has(value)) {
|
|
172
|
+
result[key] = "[Circular]"
|
|
173
|
+
} else {
|
|
174
|
+
result[key] = serializeArg(value, depth + 1)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return arg
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Forward console.* calls from worker to main thread.
|
|
186
|
+
*
|
|
187
|
+
* Monkey-patches console methods to send messages via postMessage.
|
|
188
|
+
* Call this at the start of your worker script.
|
|
189
|
+
*
|
|
190
|
+
* @param postMessage - The worker's postMessage function
|
|
191
|
+
* @param namespace - Optional namespace for log messages (e.g., "km:worker:parse")
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* // At top of worker file:
|
|
196
|
+
* import { forwardConsole } from "loggily/worker"
|
|
197
|
+
* forwardConsole(postMessage, "km:worker:parse")
|
|
198
|
+
*
|
|
199
|
+
* // Now all console.* calls are forwarded:
|
|
200
|
+
* console.log("processing", { file: "test.md" })
|
|
201
|
+
* console.error(new Error("failed"))
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export function forwardConsole(postMessage: PostMessageFn, namespace?: string): void {
|
|
205
|
+
// Store original console for restoration
|
|
206
|
+
if (!originalConsole) {
|
|
207
|
+
originalConsole = { ...console }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const levels = ["log", "debug", "info", "warn", "error", "trace"] as const
|
|
211
|
+
|
|
212
|
+
for (const level of levels) {
|
|
213
|
+
console[level] = (...args: unknown[]) => {
|
|
214
|
+
const serializedArgs = args.map((arg) => serializeArg(arg))
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
postMessage({
|
|
218
|
+
type: "console",
|
|
219
|
+
level,
|
|
220
|
+
namespace,
|
|
221
|
+
args: serializedArgs,
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
})
|
|
224
|
+
} catch {
|
|
225
|
+
// postMessage might fail if worker is shutting down
|
|
226
|
+
// Fall back to original console
|
|
227
|
+
originalConsole?.[level](...args)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Restore original console methods.
|
|
235
|
+
* Call this if you need to disable console forwarding.
|
|
236
|
+
*/
|
|
237
|
+
export function restoreConsole(): void {
|
|
238
|
+
if (originalConsole) {
|
|
239
|
+
Object.assign(console, originalConsole)
|
|
240
|
+
originalConsole = null
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============ Worker Logger (Full API) ============
|
|
245
|
+
|
|
246
|
+
type PostMessageAnyFn = (message: WorkerMessage) => void
|
|
247
|
+
|
|
248
|
+
let workerSpanIdCounter = 0
|
|
249
|
+
let workerTraceIdCounter = 0
|
|
250
|
+
|
|
251
|
+
function generateWorkerSpanId(): string {
|
|
252
|
+
return `wsp_${(++workerSpanIdCounter).toString(36)}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateWorkerTraceId(): string {
|
|
256
|
+
return `wtr_${(++workerTraceIdCounter).toString(36)}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Reset worker ID counters (for testing) */
|
|
260
|
+
export function resetWorkerIds(): void {
|
|
261
|
+
workerSpanIdCounter = 0
|
|
262
|
+
workerTraceIdCounter = 0
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface WorkerLoggerOptions {
|
|
266
|
+
/** Parent span ID for nested spans */
|
|
267
|
+
parentSpanId?: string | null
|
|
268
|
+
/** Trace ID for distributed tracing */
|
|
269
|
+
traceId?: string | null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a logger instance for use in a worker thread.
|
|
274
|
+
*
|
|
275
|
+
* All log calls and span events are forwarded to the main thread via postMessage.
|
|
276
|
+
* The main thread should use createWorkerLogHandler to process these messages.
|
|
277
|
+
*
|
|
278
|
+
* @param postMessage - The worker's postMessage function
|
|
279
|
+
* @param namespace - Logger namespace (e.g., "km:worker:parse")
|
|
280
|
+
* @param props - Optional initial props
|
|
281
|
+
* @param options - Optional configuration
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* import { createWorkerLogger } from "loggily/worker"
|
|
286
|
+
*
|
|
287
|
+
* const log = createWorkerLogger(postMessage, "km:worker:parse")
|
|
288
|
+
*
|
|
289
|
+
* log.info("starting parse", { file: "test.md" })
|
|
290
|
+
*
|
|
291
|
+
* {
|
|
292
|
+
* using span = log.span("process")
|
|
293
|
+
* span.info("processing...")
|
|
294
|
+
* span.spanData.lineCount = 100
|
|
295
|
+
* }
|
|
296
|
+
* // Span end event automatically sent to main thread
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export function createWorkerLogger(
|
|
300
|
+
postMessage: PostMessageAnyFn,
|
|
301
|
+
namespace: string,
|
|
302
|
+
props: Record<string, unknown> = {},
|
|
303
|
+
options: WorkerLoggerOptions = {},
|
|
304
|
+
): Logger {
|
|
305
|
+
const { parentSpanId = null, traceId = null } = options
|
|
306
|
+
|
|
307
|
+
function log(
|
|
308
|
+
level: "trace" | "debug" | "info" | "warn" | "error",
|
|
309
|
+
message: LazyMessage,
|
|
310
|
+
data?: Record<string, unknown>,
|
|
311
|
+
): void {
|
|
312
|
+
const resolved = typeof message === "function" ? message() : message
|
|
313
|
+
try {
|
|
314
|
+
postMessage({
|
|
315
|
+
type: "log",
|
|
316
|
+
level,
|
|
317
|
+
namespace,
|
|
318
|
+
message: resolved,
|
|
319
|
+
data: data ? { ...props, ...data } : Object.keys(props).length > 0 ? props : undefined,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
})
|
|
322
|
+
} catch {
|
|
323
|
+
// Worker might be shutting down
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function createSpan(spanNamespace?: string, spanProps?: Record<string, unknown>): SpanLogger {
|
|
328
|
+
const fullNamespace = spanNamespace ? `${namespace}:${spanNamespace}` : namespace
|
|
329
|
+
const mergedProps = { ...props, ...spanProps }
|
|
330
|
+
const spanId = generateWorkerSpanId()
|
|
331
|
+
const spanTraceId = traceId || generateWorkerTraceId()
|
|
332
|
+
const startTime = Date.now()
|
|
333
|
+
|
|
334
|
+
// Mutable span data that can be set by the user
|
|
335
|
+
const customSpanData: Record<string, unknown> = {}
|
|
336
|
+
|
|
337
|
+
// Send span start event
|
|
338
|
+
try {
|
|
339
|
+
postMessage({
|
|
340
|
+
type: "span",
|
|
341
|
+
event: "start",
|
|
342
|
+
namespace: fullNamespace,
|
|
343
|
+
spanId,
|
|
344
|
+
traceId: spanTraceId,
|
|
345
|
+
parentId: parentSpanId,
|
|
346
|
+
startTime,
|
|
347
|
+
props: mergedProps,
|
|
348
|
+
spanData: {},
|
|
349
|
+
timestamp: Date.now(),
|
|
350
|
+
})
|
|
351
|
+
} catch {
|
|
352
|
+
// Worker might be shutting down
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let ended = false
|
|
356
|
+
|
|
357
|
+
const spanData: SpanData = new Proxy(customSpanData as SpanData, {
|
|
358
|
+
get(_target, prop) {
|
|
359
|
+
if (prop === "id") return spanId
|
|
360
|
+
if (prop === "traceId") return spanTraceId
|
|
361
|
+
if (prop === "parentId") return parentSpanId
|
|
362
|
+
if (prop === "startTime") return startTime
|
|
363
|
+
if (prop === "endTime") return ended ? Date.now() : null
|
|
364
|
+
if (prop === "duration") return Date.now() - startTime
|
|
365
|
+
return customSpanData[prop as string]
|
|
366
|
+
},
|
|
367
|
+
set(_target, prop, value) {
|
|
368
|
+
if (
|
|
369
|
+
prop !== "id" &&
|
|
370
|
+
prop !== "traceId" &&
|
|
371
|
+
prop !== "parentId" &&
|
|
372
|
+
prop !== "startTime" &&
|
|
373
|
+
prop !== "endTime" &&
|
|
374
|
+
prop !== "duration"
|
|
375
|
+
) {
|
|
376
|
+
customSpanData[prop as string] = value
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
return false
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
function endSpan(): void {
|
|
384
|
+
if (ended) return
|
|
385
|
+
ended = true
|
|
386
|
+
|
|
387
|
+
const endTime = Date.now()
|
|
388
|
+
const duration = endTime - startTime
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
postMessage({
|
|
392
|
+
type: "span",
|
|
393
|
+
event: "end",
|
|
394
|
+
namespace: fullNamespace,
|
|
395
|
+
spanId,
|
|
396
|
+
traceId: spanTraceId,
|
|
397
|
+
parentId: parentSpanId,
|
|
398
|
+
startTime,
|
|
399
|
+
endTime,
|
|
400
|
+
duration,
|
|
401
|
+
props: mergedProps,
|
|
402
|
+
spanData: customSpanData,
|
|
403
|
+
timestamp: Date.now(),
|
|
404
|
+
})
|
|
405
|
+
} catch {
|
|
406
|
+
// Worker might be shutting down
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Create child logger for the span
|
|
411
|
+
const childLogger = createWorkerLogger(postMessage, fullNamespace, mergedProps, {
|
|
412
|
+
parentSpanId: spanId,
|
|
413
|
+
traceId: spanTraceId,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const spanLogger: SpanLogger = {
|
|
417
|
+
...childLogger,
|
|
418
|
+
spanData,
|
|
419
|
+
end: endSpan,
|
|
420
|
+
[Symbol.dispose]: endSpan,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return spanLogger
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const logger: Logger = {
|
|
427
|
+
name: namespace,
|
|
428
|
+
props: Object.freeze({ ...props }),
|
|
429
|
+
spanData: null,
|
|
430
|
+
|
|
431
|
+
trace: (msg, data) => log("trace", msg, data),
|
|
432
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
433
|
+
info: (msg, data) => log("info", msg, data),
|
|
434
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
435
|
+
error: (msgOrError: LazyMessage | Error, data?: Record<string, unknown>) => {
|
|
436
|
+
if (msgOrError instanceof Error) {
|
|
437
|
+
log("error", msgOrError.message, {
|
|
438
|
+
...data,
|
|
439
|
+
error_type: msgOrError.name,
|
|
440
|
+
error_stack: msgOrError.stack,
|
|
441
|
+
error_code: (msgOrError as NodeJS.ErrnoException).code,
|
|
442
|
+
})
|
|
443
|
+
} else {
|
|
444
|
+
log("error", msgOrError, data)
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
logger(childNamespace?: string, childProps?: Record<string, unknown>): Logger {
|
|
449
|
+
const fullNamespace = childNamespace ? `${namespace}:${childNamespace}` : namespace
|
|
450
|
+
return createWorkerLogger(postMessage, fullNamespace, { ...props, ...childProps }, options)
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
span: createSpan,
|
|
454
|
+
|
|
455
|
+
child(context: Record<string, unknown> | string): Logger {
|
|
456
|
+
if (typeof context === "string") {
|
|
457
|
+
return this.logger(context)
|
|
458
|
+
}
|
|
459
|
+
return createWorkerLogger(postMessage, namespace, { ...props, ...context }, options)
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
end(): void {
|
|
463
|
+
// No-op for non-span logger
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return logger
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============ Main Thread Side ============
|
|
471
|
+
|
|
472
|
+
export interface WorkerConsoleHandlerOptions {
|
|
473
|
+
/** Default namespace if message doesn't include one */
|
|
474
|
+
defaultNamespace?: string
|
|
475
|
+
/** Custom logger to use (defaults to creating one with the namespace) */
|
|
476
|
+
logger?: Logger
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a handler for worker console messages.
|
|
481
|
+
*
|
|
482
|
+
* Use this on the main thread to receive and output messages from workers.
|
|
483
|
+
*
|
|
484
|
+
* @param options - Handler options
|
|
485
|
+
* @returns Handler function to call with worker messages
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```typescript
|
|
489
|
+
* import { createWorkerConsoleHandler } from "loggily/worker"
|
|
490
|
+
*
|
|
491
|
+
* const handleConsole = createWorkerConsoleHandler({
|
|
492
|
+
* defaultNamespace: "km:worker:parse"
|
|
493
|
+
* })
|
|
494
|
+
*
|
|
495
|
+
* worker.onmessage = (e) => {
|
|
496
|
+
* if (e.data.type === "console") {
|
|
497
|
+
* handleConsole(e.data)
|
|
498
|
+
* } else {
|
|
499
|
+
* // Handle other message types
|
|
500
|
+
* }
|
|
501
|
+
* }
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
export function createWorkerConsoleHandler(
|
|
505
|
+
options: WorkerConsoleHandlerOptions = {},
|
|
506
|
+
): (message: WorkerConsoleMessage) => void {
|
|
507
|
+
const loggers = new Map<string, ConditionalLogger>()
|
|
508
|
+
|
|
509
|
+
function getLogger(namespace?: string): ConditionalLogger {
|
|
510
|
+
const ns = namespace || options.defaultNamespace || "worker"
|
|
511
|
+
|
|
512
|
+
let logger = loggers.get(ns)
|
|
513
|
+
if (!logger) {
|
|
514
|
+
logger = options.logger ? (options.logger as ConditionalLogger) : createLogger(ns)
|
|
515
|
+
loggers.set(ns, logger)
|
|
516
|
+
}
|
|
517
|
+
return logger
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return (message: WorkerConsoleMessage) => {
|
|
521
|
+
const logger = getLogger(message.namespace)
|
|
522
|
+
const args = message.args
|
|
523
|
+
|
|
524
|
+
// Format args into a message string
|
|
525
|
+
const formattedMessage =
|
|
526
|
+
args.length === 0
|
|
527
|
+
? ""
|
|
528
|
+
: args.length === 1 && typeof args[0] === "string"
|
|
529
|
+
? args[0]
|
|
530
|
+
: args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
|
|
531
|
+
|
|
532
|
+
// Extract data object if present (last arg is object and not a string)
|
|
533
|
+
const lastArg = args[args.length - 1]
|
|
534
|
+
const data =
|
|
535
|
+
args.length > 1 && typeof lastArg === "object" && lastArg !== null && !Array.isArray(lastArg)
|
|
536
|
+
? (lastArg as Record<string, unknown>)
|
|
537
|
+
: undefined
|
|
538
|
+
|
|
539
|
+
// Log at the appropriate level (use ?. since level might be disabled)
|
|
540
|
+
switch (message.level) {
|
|
541
|
+
case "trace":
|
|
542
|
+
logger.trace?.(formattedMessage, data)
|
|
543
|
+
break
|
|
544
|
+
case "debug":
|
|
545
|
+
logger.debug?.(formattedMessage, data)
|
|
546
|
+
break
|
|
547
|
+
case "info":
|
|
548
|
+
case "log":
|
|
549
|
+
logger.info?.(formattedMessage, data)
|
|
550
|
+
break
|
|
551
|
+
case "warn":
|
|
552
|
+
logger.warn?.(formattedMessage, data)
|
|
553
|
+
break
|
|
554
|
+
case "error":
|
|
555
|
+
logger.error?.(formattedMessage, data)
|
|
556
|
+
break
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ============ Full Logger Handler ============
|
|
562
|
+
|
|
563
|
+
export interface WorkerLogHandlerOptions {
|
|
564
|
+
/** Enable span output (default: uses spansAreEnabled()) */
|
|
565
|
+
enableSpans?: boolean
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Create a handler for worker logger messages (logs and spans).
|
|
570
|
+
*
|
|
571
|
+
* Use this on the main thread to receive and output messages from workers
|
|
572
|
+
* that use createWorkerLogger.
|
|
573
|
+
*
|
|
574
|
+
* @param options - Handler options
|
|
575
|
+
* @returns Handler function to call with worker messages
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```typescript
|
|
579
|
+
* import { createWorkerLogHandler, isWorkerMessage } from "loggily/worker"
|
|
580
|
+
*
|
|
581
|
+
* const handleLog = createWorkerLogHandler()
|
|
582
|
+
*
|
|
583
|
+
* worker.onmessage = (e) => {
|
|
584
|
+
* if (isWorkerMessage(e.data)) {
|
|
585
|
+
* handleLog(e.data)
|
|
586
|
+
* } else {
|
|
587
|
+
* // Handle other message types
|
|
588
|
+
* }
|
|
589
|
+
* }
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
export function createWorkerLogHandler(options: WorkerLogHandlerOptions = {}): (message: WorkerMessage) => void {
|
|
593
|
+
const loggers = new Map<string, ConditionalLogger>()
|
|
594
|
+
|
|
595
|
+
// Enable spans if requested
|
|
596
|
+
if (options.enableSpans) {
|
|
597
|
+
enableSpans()
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function getLogger(namespace: string): ConditionalLogger {
|
|
601
|
+
let logger = loggers.get(namespace)
|
|
602
|
+
if (!logger) {
|
|
603
|
+
logger = createLogger(namespace)
|
|
604
|
+
loggers.set(namespace, logger)
|
|
605
|
+
}
|
|
606
|
+
return logger
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return (message: WorkerMessage) => {
|
|
610
|
+
if (isWorkerConsoleMessage(message)) {
|
|
611
|
+
// Handle console messages
|
|
612
|
+
const logger = getLogger(message.namespace || "worker")
|
|
613
|
+
const args = message.args
|
|
614
|
+
const formattedMessage =
|
|
615
|
+
args.length === 0
|
|
616
|
+
? ""
|
|
617
|
+
: args.length === 1 && typeof args[0] === "string"
|
|
618
|
+
? args[0]
|
|
619
|
+
: args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")
|
|
620
|
+
|
|
621
|
+
const lastArg = args[args.length - 1]
|
|
622
|
+
const data =
|
|
623
|
+
args.length > 1 && typeof lastArg === "object" && lastArg !== null && !Array.isArray(lastArg)
|
|
624
|
+
? (lastArg as Record<string, unknown>)
|
|
625
|
+
: undefined
|
|
626
|
+
|
|
627
|
+
// Use ?. since level might be disabled
|
|
628
|
+
switch (message.level) {
|
|
629
|
+
case "trace":
|
|
630
|
+
logger.trace?.(formattedMessage, data)
|
|
631
|
+
break
|
|
632
|
+
case "debug":
|
|
633
|
+
logger.debug?.(formattedMessage, data)
|
|
634
|
+
break
|
|
635
|
+
case "info":
|
|
636
|
+
case "log":
|
|
637
|
+
logger.info?.(formattedMessage, data)
|
|
638
|
+
break
|
|
639
|
+
case "warn":
|
|
640
|
+
logger.warn?.(formattedMessage, data)
|
|
641
|
+
break
|
|
642
|
+
case "error":
|
|
643
|
+
logger.error?.(formattedMessage, data)
|
|
644
|
+
break
|
|
645
|
+
}
|
|
646
|
+
} else if (isWorkerLogMessage(message)) {
|
|
647
|
+
// Handle structured log messages
|
|
648
|
+
const logger = getLogger(message.namespace)
|
|
649
|
+
|
|
650
|
+
// Use ?. since level might be disabled
|
|
651
|
+
switch (message.level) {
|
|
652
|
+
case "trace":
|
|
653
|
+
logger.trace?.(message.message, message.data)
|
|
654
|
+
break
|
|
655
|
+
case "debug":
|
|
656
|
+
logger.debug?.(message.message, message.data)
|
|
657
|
+
break
|
|
658
|
+
case "info":
|
|
659
|
+
logger.info?.(message.message, message.data)
|
|
660
|
+
break
|
|
661
|
+
case "warn":
|
|
662
|
+
logger.warn?.(message.message, message.data)
|
|
663
|
+
break
|
|
664
|
+
case "error":
|
|
665
|
+
logger.error?.(message.message, message.data)
|
|
666
|
+
break
|
|
667
|
+
}
|
|
668
|
+
} else if (isWorkerSpanMessage(message)) {
|
|
669
|
+
// Handle span events
|
|
670
|
+
// For span end events, create a span and immediately end it with the timing data
|
|
671
|
+
if (message.event === "end") {
|
|
672
|
+
const logger = getLogger(message.namespace)
|
|
673
|
+
const span = logger.span(undefined, message.props)
|
|
674
|
+
|
|
675
|
+
// Copy span data
|
|
676
|
+
for (const [key, value] of Object.entries(message.spanData)) {
|
|
677
|
+
span.spanData[key] = value
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// End the span (this will output the span timing)
|
|
681
|
+
span.end()
|
|
682
|
+
}
|
|
683
|
+
// Start events are informational only on main thread
|
|
684
|
+
// (the actual timing happens in the worker)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|