loggily 0.0.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.github/workflows/docs.yml +58 -0
  2. package/.github/workflows/release.yml +31 -0
  3. package/.github/workflows/test.yml +20 -0
  4. package/CHANGELOG.md +45 -0
  5. package/CLAUDE.md +299 -0
  6. package/CONTRIBUTING.md +58 -0
  7. package/LICENSE +21 -0
  8. package/README.md +102 -3
  9. package/benchmarks/overhead.ts +267 -0
  10. package/bun.lock +479 -0
  11. package/docs/api-reference.md +400 -0
  12. package/docs/benchmarks.md +106 -0
  13. package/docs/comparison.md +315 -0
  14. package/docs/conditional-logging-research.md +159 -0
  15. package/docs/guide.md +205 -0
  16. package/docs/migration-from-debug.md +310 -0
  17. package/docs/migration-from-pino.md +178 -0
  18. package/docs/migration-from-winston.md +179 -0
  19. package/docs/site/.vitepress/config.ts +67 -0
  20. package/docs/site/api/configuration.md +94 -0
  21. package/docs/site/api/index.md +61 -0
  22. package/docs/site/api/logger.md +99 -0
  23. package/docs/site/api/worker.md +120 -0
  24. package/docs/site/api/writers.md +69 -0
  25. package/docs/site/guide/getting-started.md +143 -0
  26. package/docs/site/guide/journey.md +203 -0
  27. package/docs/site/guide/migration-from-debug.md +24 -0
  28. package/docs/site/guide/spans.md +139 -0
  29. package/docs/site/guide/why.md +55 -0
  30. package/docs/site/guide/workers.md +113 -0
  31. package/docs/site/guide/zero-overhead.md +87 -0
  32. package/docs/site/index.md +54 -0
  33. package/package.json +56 -8
  34. package/src/colors.ts +27 -0
  35. package/src/context.ts +155 -0
  36. package/src/core.ts +804 -0
  37. package/src/file-writer.ts +104 -0
  38. package/src/index.browser.ts +64 -0
  39. package/src/index.ts +10 -1
  40. package/src/tracing.ts +142 -0
  41. package/src/worker.ts +687 -0
  42. package/tests/features.test.ts +552 -0
  43. package/tests/logger.test.ts +944 -0
  44. package/tests/tracing.test.ts +618 -0
  45. package/tests/universal.test.ts +107 -0
  46. package/tests/worker.test.ts +590 -0
  47. package/tsconfig.json +20 -0
  48. package/vitest.config.ts +10 -0
package/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
+ }