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