loggily 0.3.0 → 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.
Files changed (46) hide show
  1. package/README.md +67 -22
  2. package/package.json +24 -11
  3. package/src/context.ts +26 -11
  4. package/src/core.ts +118 -72
  5. package/src/file-writer.ts +12 -6
  6. package/src/index.browser.ts +9 -1
  7. package/src/index.ts +9 -1
  8. package/src/tracing.ts +11 -3
  9. package/src/worker.ts +119 -132
  10. package/.github/workflows/docs.yml +0 -58
  11. package/.github/workflows/release.yml +0 -31
  12. package/.github/workflows/test.yml +0 -20
  13. package/CHANGELOG.md +0 -45
  14. package/CLAUDE.md +0 -299
  15. package/CONTRIBUTING.md +0 -58
  16. package/benchmarks/overhead.ts +0 -267
  17. package/bun.lock +0 -479
  18. package/docs/api-reference.md +0 -400
  19. package/docs/benchmarks.md +0 -106
  20. package/docs/comparison.md +0 -315
  21. package/docs/conditional-logging-research.md +0 -159
  22. package/docs/guide.md +0 -205
  23. package/docs/migration-from-debug.md +0 -310
  24. package/docs/migration-from-pino.md +0 -178
  25. package/docs/migration-from-winston.md +0 -179
  26. package/docs/site/.vitepress/config.ts +0 -67
  27. package/docs/site/api/configuration.md +0 -94
  28. package/docs/site/api/index.md +0 -61
  29. package/docs/site/api/logger.md +0 -99
  30. package/docs/site/api/worker.md +0 -120
  31. package/docs/site/api/writers.md +0 -69
  32. package/docs/site/guide/getting-started.md +0 -143
  33. package/docs/site/guide/journey.md +0 -203
  34. package/docs/site/guide/migration-from-debug.md +0 -24
  35. package/docs/site/guide/spans.md +0 -139
  36. package/docs/site/guide/why.md +0 -55
  37. package/docs/site/guide/workers.md +0 -113
  38. package/docs/site/guide/zero-overhead.md +0 -87
  39. package/docs/site/index.md +0 -54
  40. package/tests/features.test.ts +0 -552
  41. package/tests/logger.test.ts +0 -944
  42. package/tests/tracing.test.ts +0 -618
  43. package/tests/universal.test.ts +0 -107
  44. package/tests/worker.test.ts +0 -590
  45. package/tsconfig.json +0 -20
  46. package/vitest.config.ts +0 -10
package/src/worker.ts CHANGED
@@ -42,7 +42,9 @@
42
42
 
43
43
  import {
44
44
  createLogger,
45
+ createSpanDataProxy,
45
46
  enableSpans,
47
+ writeSpan,
46
48
  type ConditionalLogger,
47
49
  type LazyMessage,
48
50
  type Logger,
@@ -319,8 +321,19 @@ export function createWorkerLogger(
319
321
  data: data ? { ...props, ...data } : Object.keys(props).length > 0 ? props : undefined,
320
322
  timestamp: Date.now(),
321
323
  })
322
- } catch {
323
- // Worker might be shutting down
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
+ }
324
337
  }
325
338
  }
326
339
 
@@ -348,37 +361,34 @@ export function createWorkerLogger(
348
361
  spanData: {},
349
362
  timestamp: Date.now(),
350
363
  })
351
- } catch {
352
- // Worker might be shutting down
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
+ }
353
377
  }
354
378
 
355
379
  let ended = false
356
380
 
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
- })
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
+ )
382
392
 
383
393
  function endSpan(): void {
384
394
  if (ended) return
@@ -402,8 +412,19 @@ export function createWorkerLogger(
402
412
  spanData: customSpanData,
403
413
  timestamp: Date.now(),
404
414
  })
405
- } catch {
406
- // Worker might be shutting down
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
+ }
407
428
  }
408
429
  }
409
430
 
@@ -476,6 +497,60 @@ export interface WorkerConsoleHandlerOptions {
476
497
  logger?: Logger
477
498
  }
478
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
+
479
554
  /**
480
555
  * Create a handler for worker console messages.
481
556
  *
@@ -519,42 +594,8 @@ export function createWorkerConsoleHandler(
519
594
 
520
595
  return (message: WorkerConsoleMessage) => {
521
596
  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
- }
597
+ const { message: msg, data } = formatConsoleArgs(message.args)
598
+ dispatchToLogger(logger, message.level, msg, data)
558
599
  }
559
600
  }
560
601
 
@@ -608,77 +649,23 @@ export function createWorkerLogHandler(options: WorkerLogHandlerOptions = {}): (
608
649
 
609
650
  return (message: WorkerMessage) => {
610
651
  if (isWorkerConsoleMessage(message)) {
611
- // Handle console messages
612
652
  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
- }
653
+ const { message: msg, data } = formatConsoleArgs(message.args)
654
+ dispatchToLogger(logger, message.level, msg, data)
646
655
  } else if (isWorkerLogMessage(message)) {
647
- // Handle structured log messages
648
656
  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
- }
657
+ dispatchToLogger(logger, message.level, message.message, message.data)
668
658
  } else if (isWorkerSpanMessage(message)) {
669
659
  // Handle span events
670
- // For span end events, create a span and immediately end it with the timing data
660
+ // For span end events, output the span with original worker timing data
671
661
  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()
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
+ })
682
669
  }
683
670
  // Start events are informational only on main thread
684
671
  // (the actual timing happens in the worker)
@@ -1,58 +0,0 @@
1
- name: Deploy Documentation
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- paths:
7
- - "docs/**"
8
- - "src/**"
9
- - ".github/workflows/docs.yml"
10
- workflow_dispatch:
11
-
12
- permissions:
13
- contents: read
14
- pages: write
15
- id-token: write
16
-
17
- concurrency:
18
- group: pages
19
- cancel-in-progress: false
20
-
21
- jobs:
22
- build:
23
- runs-on: ubuntu-latest
24
- steps:
25
- - name: Checkout
26
- uses: actions/checkout@v4
27
- with:
28
- fetch-depth: 0
29
-
30
- - name: Setup Bun
31
- uses: oven-sh/setup-bun@v2
32
- with:
33
- bun-version: latest
34
-
35
- - name: Setup Pages
36
- uses: actions/configure-pages@v4
37
-
38
- - name: Install dependencies
39
- run: bun install
40
-
41
- - name: Build docs
42
- run: bun run docs:build
43
-
44
- - name: Upload artifact
45
- uses: actions/upload-pages-artifact@v3
46
- with:
47
- path: docs/site/.vitepress/dist
48
-
49
- deploy:
50
- environment:
51
- name: github-pages
52
- url: ${{ steps.deployment.outputs.page_url }}
53
- needs: build
54
- runs-on: ubuntu-latest
55
- steps:
56
- - name: Deploy to GitHub Pages
57
- id: deployment
58
- uses: actions/deploy-pages@v4
@@ -1,31 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
-
8
- permissions:
9
- contents: write
10
-
11
- jobs:
12
- release:
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v4
16
- - uses: oven-sh/setup-bun@v2
17
- with:
18
- bun-version: latest
19
- - run: bun install
20
- - run: bun test
21
- - uses: actions/setup-node@v4
22
- with:
23
- node-version: '22'
24
- registry-url: 'https://registry.npmjs.org'
25
- - run: npm publish --access public
26
- env:
27
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28
- - name: Create GitHub Release
29
- uses: softprops/action-gh-release@v2
30
- with:
31
- generate_release_notes: true
@@ -1,20 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
- workflow_dispatch:
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: oven-sh/setup-bun@v2
16
- with:
17
- bun-version: latest
18
- - run: bun install
19
- - run: bun run typecheck
20
- - run: bun test
package/CHANGELOG.md DELETED
@@ -1,45 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to loggily will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.2.0] - 2026-03-04
9
-
10
- ### Added
11
-
12
- - **Lazy messages** -- Pass `() => string` functions that are only called when the level is enabled
13
- - **Child context loggers** -- `log.child({ requestId: "abc" })` creates a logger with structured context fields in every message
14
- - **LOG_FORMAT env var** -- `LOG_FORMAT=json` explicitly enables structured JSON output
15
- - `setLogFormat()` / `getLogFormat()` -- Programmatic log format control
16
- - `setDebugFilter()` / `getDebugFilter()` -- Programmatic namespace filtering (like `DEBUG` env var)
17
- - **File writer** -- `createFileWriter(path, opts?)` for buffered file output with auto-flush
18
- - **Writer system** -- `addWriter(fn)` to subscribe to all formatted log output
19
- - `setOutputMode()` / `getOutputMode()` -- Control output destination (`console`, `stderr`, `writers-only`)
20
- - `setSuppressConsole()` -- Suppress console output while writers still receive
21
- - Comprehensive test suite (153 tests)
22
-
23
- ### Changed
24
-
25
- - `createLogger()` now returns a `ConditionalLogger` directly (no separate function needed)
26
- - Improved documentation with full API reference and comparison guides
27
-
28
- ## [0.1.0] - 2026-01-15
29
-
30
- ### Added
31
-
32
- - Initial release
33
- - `createLogger(name, props?)` -- Create structured logger
34
- - Logger methods: `trace`, `debug`, `info`, `warn`, `error`
35
- - Child loggers with `.logger(namespace, props?)`
36
- - Span timing with `.span(namespace, props?)` and `using` keyword support
37
- - `SpanData` with id, traceId, parentId, startTime, endTime, duration
38
- - Custom span attributes via `span.spanData.key = value`
39
- - Configuration via environment variables: `LOG_LEVEL`, `TRACE`, `TRACE_FORMAT`
40
- - Programmatic configuration: `setLogLevel`, `getLogLevel`, `enableSpans`, `disableSpans`, `spansAreEnabled`
41
- - `setTraceFilter()` / `getTraceFilter()` -- Namespace-based span output control
42
- - Dual output format: pretty console (dev) and JSON (production)
43
- - Worker thread support: `createWorkerLogger`, `createWorkerLogHandler`, `forwardConsole`
44
- - Span collection for testing: `startCollecting`, `stopCollecting`, `getCollectedSpans`, `clearCollectedSpans`
45
- - `resetIds()` for deterministic tests