@modainteract/moda-interact-shared 0.2.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.
package/README.md CHANGED
@@ -4,8 +4,8 @@ Shared TypeScript contracts and reusable platform primitives used by multiple
4
4
  Moda Interact services.
5
5
 
6
6
  This package is a **library**, not a deployable service. It does not own
7
- transport, persistence, background workers, provider SDK integration, or
8
- application business workflows.
7
+ business transports, persistence, background workers, provider SDK integration,
8
+ or application business workflows.
9
9
 
10
10
  ## Install
11
11
 
@@ -28,10 +28,117 @@ including:
28
28
  - canonical queue/job-name constants where multiple repositories must agree;
29
29
  - deterministic correlation/job identifier helpers;
30
30
  - small pure utilities used by more than one service;
31
- - reusable structured application logging primitives.
31
+ - reusable structured application logging primitives;
32
+ - the reusable base Node OpenTelemetry runtime, including tracing and metrics
33
+ providers, exporters, sampling, HTTP/Undici instrumentation, lifecycle, and
34
+ generic observability helpers.
32
35
 
33
36
  The package deliberately keeps framework and runtime dependencies small.
34
37
 
38
+ ## Node observability runtime
39
+
40
+ The package provides a browser-safe semantic observability entry and a
41
+ Node-only runtime entry:
42
+
43
+ ```ts
44
+ import {
45
+ getActiveTraceId,
46
+ withObservedSpan,
47
+ } from "@modainteract/moda-interact-shared/observability";
48
+
49
+ import {
50
+ initNodeObservability,
51
+ } from "@modainteract/moda-interact-shared/observability/node";
52
+ ```
53
+
54
+ Import `./observability/node` only from a process preload. It installs tracing,
55
+ metrics, HTTP/HTTPS and Undici/fetch instrumentation before framework or worker
56
+ modules load:
57
+
58
+ ```text
59
+ node --import ./observability.mjs <framework-or-worker-entrypoint>
60
+ ```
61
+
62
+ ```js
63
+ // observability.mjs
64
+ import { initNodeObservability } from
65
+ "@modainteract/moda-interact-shared/observability/node";
66
+
67
+ initNodeObservability({
68
+ serviceName: "moda-interact-messaging",
69
+ environment: process.env.DEPLOYMENT_ENVIRONMENT_NAME,
70
+ instrument: { http: true, fetch: true, prisma: false },
71
+ });
72
+ ```
73
+
74
+ Set `prisma: true` only for processes that load Prisma, and keep the preload
75
+ before the first Prisma application import.
76
+
77
+ The runtime uses the canonical `service.namespace`, `service.name`, and
78
+ `deployment.environment.name` resource attributes. It honors
79
+ `OTEL_TRACES_SAMPLER`, ratio arguments for ratio samplers, signal-specific or
80
+ generic OTLP endpoints, and bounded batch/export settings. With no trace or
81
+ metric endpoint those pipelines are no-ops while structured stdout logging
82
+ continues.
83
+
84
+ `forceFlush()` covers traces, metrics, OpenTelemetry Logs, and direct Loki.
85
+ Initialization, export, flush, and shutdown failures are best-effort and do not
86
+ become application startup or business-operation failures.
87
+
88
+ ### BullMQ telemetry
89
+
90
+ Create the shared BullMQ telemetry adapter once per Queue or Worker options
91
+ object and pass it through BullMQ's native `telemetry` option:
92
+
93
+ ```ts
94
+ import { createBullMQTelemetry } from
95
+ "@modainteract/moda-interact-shared/observability/bullmq";
96
+
97
+ const telemetry = createBullMQTelemetry({
98
+ serviceName: "moda-messaging-worker",
99
+ enableMetrics: true,
100
+ });
101
+
102
+ new Worker(queueName, processor, { connection, telemetry });
103
+ ```
104
+
105
+ The adapter uses the global trace and meter providers installed by the Node
106
+ runtime. BullMQ owns propagation metadata in its job options; applications must
107
+ not copy or mutate queue payloads solely to carry trace context. BullMQ metrics
108
+ use queue name, bounded job name, and job state dimensions, never Moda job,
109
+ shop, checkout, conversation, or message IDs.
110
+
111
+ ### GenAI active spans
112
+
113
+ Use the GenAI helpers to model one inbound conversation turn as an independent
114
+ trace with active agent, tool, and automatically instrumented child spans:
115
+
116
+ ```ts
117
+ import {
118
+ observeAgentInvocation,
119
+ observeAgentTool,
120
+ observeConversationTurn,
121
+ } from "@modainteract/moda-interact-shared/observability/genai";
122
+
123
+ await observeConversationTurn("whatsapp", () =>
124
+ observeAgentInvocation({ agentName: "commerce-agent" }, () =>
125
+ observeAgentTool("lookup-products", executeTool),
126
+ ),
127
+ );
128
+ ```
129
+
130
+ The helpers use only the global tracer and meter providers installed by the
131
+ Node runtime. They create no SDK, provider, exporter, or network request, and
132
+ remain lightweight no-ops when providers are absent. Six module-singleton
133
+ instruments record turn, agent, and tool duration and operation outcomes.
134
+ Metric attributes are limited to `outcome=success|error`; turn metrics also use
135
+ the closed `channel=whatsapp|other` vocabulary. Arbitrary agent, provider,
136
+ model, tool, and Moda business identifiers never become metric dimensions.
137
+
138
+ Agent, provider, model, and tool names are trimmed and bounded on spans only.
139
+ Prompt, completion, message, and tool payload bodies are never accepted or
140
+ captured by default.
141
+
35
142
  ## What this package does not own
36
143
 
37
144
  This package does **not** own:
@@ -42,12 +149,13 @@ This package does **not** own:
42
149
  - Shopify or Meta webhook HTTP handlers;
43
150
  - Shopify or Meta SDK integration;
44
151
  - application recovery/business logic;
45
- - OpenTelemetry spans or application-specific metrics;
46
- - OTLP transport or observability backends;
152
+ - service/domain-specific span names, business attributes, or application
153
+ metrics;
154
+ - deployment OTLP endpoint/credential wiring or observability backends;
47
155
  - Render deployment configuration.
48
156
 
49
- Those capabilities remain in the repository that owns the corresponding
50
- runtime concern.
157
+ Those service-specific semantics and deployment concerns remain in the
158
+ repository that owns the corresponding runtime.
51
159
 
52
160
  ---
53
161
 
@@ -302,25 +410,297 @@ traces by default.
302
410
  Logging is best-effort: a logger sink or serialization failure must not alter
303
411
  the success/failure semantics of the business operation being logged.
304
412
 
305
- ## Logging and OpenTelemetry are separate
413
+ ## Logging destinations
414
+
415
+ Every call to the shared logger:
416
+
417
+ ```ts
418
+ logger.debug(...)
419
+ logger.info(...)
420
+ logger.warn(...)
421
+ logger.error(...)
422
+ ```
306
423
 
307
- The shared logger does **not** automatically create:
424
+ emits the same already-sanitized canonical record to up to three independent,
425
+ best-effort destinations:
308
426
 
309
- - spans;
310
- - counters;
311
- - histograms;
312
- - OTLP exports.
427
+ 1. canonical structured JSON to the configured sink (default: console);
428
+ 2. the OpenTelemetry Logs API via the global `LoggerProvider`, if one has been
429
+ installed;
430
+ 3. a direct Grafana Loki transport, if the Node process bootstrap has installed
431
+ the Loki emitter.
313
432
 
314
- Service-specific OpenTelemetry remains in the owning runtime repository.
433
+ Each destination is failure-isolated: a failure in one must not suppress
434
+ another and must never affect application/business correctness.
435
+
436
+ The OpenTelemetry API is a no-op when no provider is installed, so stdout
437
+ logging continues to work in local/test environments without any backend.
438
+
439
+ The heavy Node SDK/exporter and the Winston/winston-loki transport are isolated
440
+ in:
441
+
442
+ ```text
443
+ @modainteract/moda-interact-shared/logging/node
444
+ ```
315
445
 
316
- For example:
446
+ and must be imported and initialized only from the Node process bootstrap —
447
+ never through Vite/application/browser bundles.
448
+
449
+ ```text
450
+ application module
451
+ |
452
+ v
453
+ @modainteract/moda-interact-shared/logging
454
+ |
455
+ +--> canonical JSON stdout
456
+ |
457
+ +--> @opentelemetry/api-logs
458
+ | |
459
+ | | global LoggerProvider
460
+ | v
461
+ | ./logging/node
462
+ | |
463
+ | +--> LoggerProvider
464
+ | +--> BatchLogRecordProcessor
465
+ | +--> OTLPLogExporter
466
+ | |
467
+ | v
468
+ | /v1/logs
469
+ |
470
+ +--> lightweight Loki bridge (globalThis emitter)
471
+ |
472
+ v
473
+ ./logging/node
474
+ |
475
+ +--> Winston + winston-loki
476
+ |
477
+ v
478
+ Grafana Loki
479
+ ```
480
+
481
+ Winston and winston-loki exist **only** in the Node-only package graph. The
482
+ normal `./logging` entry contains no Winston, winston-loki or Node networking
483
+ imports.
484
+
485
+ ### Node bootstrap
486
+
487
+ Process bootstrap code installs the optional destinations once:
488
+
489
+ ```ts
490
+ import {
491
+ initNodeLokiLogging,
492
+ initNodeOpenTelemetryLogging,
493
+ } from "@modainteract/moda-interact-shared/logging/node";
494
+
495
+ initNodeLokiLogging({
496
+ serviceName: "moda-interact",
497
+ environment: "production",
498
+ });
499
+
500
+ initNodeOpenTelemetryLogging({
501
+ serviceName: "moda-interact",
502
+ environment: "production",
503
+ });
504
+ ```
505
+
506
+ Each destination is independently configurable, so a deployment may use:
507
+
508
+ ```text
509
+ console only
510
+
511
+ console + Loki
512
+
513
+ console + OTel Logs
514
+
515
+ console + Loki + OTel Logs
516
+ ```
517
+
518
+ Either Node destination may be omitted entirely; without an installed
519
+ provider/emitter the logger remains a safe stdout-only logger.
520
+
521
+ The bootstrap supports the standard OpenTelemetry environment variables:
522
+
523
+ ```text
524
+ OTEL_SDK_DISABLED
525
+
526
+ OTEL_LOGS_EXPORTER
527
+
528
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
529
+ OTEL_EXPORTER_OTLP_ENDPOINT
530
+
531
+ OTEL_EXPORTER_OTLP_LOGS_HEADERS
532
+ OTEL_EXPORTER_OTLP_HEADERS
533
+
534
+ OTEL_BLRP_MAX_QUEUE_SIZE
535
+ OTEL_BLRP_MAX_EXPORT_BATCH_SIZE
536
+ OTEL_BLRP_SCHEDULE_DELAY
537
+ OTEL_BLRP_EXPORT_TIMEOUT
538
+
539
+ OTEL_LOG_EXPORT_CONCURRENCY_LIMIT
540
+ ```
541
+
542
+ Endpoint rules:
543
+
544
+ ```text
545
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
546
+ exact signal endpoint; do not append /v1/logs
547
+
548
+ OTEL_EXPORTER_OTLP_ENDPOINT
549
+ generic endpoint; append /v1/logs
550
+ ```
551
+
552
+ Moda deliberately does **not** export to the OpenTelemetry default localhost
553
+ endpoint. Without an explicitly configured endpoint the logger remains
554
+ stdout-only.
555
+
556
+ ### Direct Loki transport
557
+
558
+ The direct Loki transport is a lightweight bridge: the normal `./logging` entry
559
+ stores an emitter on `globalThis` and the Node bootstrap installs it. Records
560
+ still pass through the same shared redaction/sanitization used for stdout and
561
+ OpenTelemetry.
562
+
563
+ The Loki environment contract is:
564
+
565
+ ```text
566
+ LOKI_ENABLED
567
+
568
+ LOKI_URL
569
+
570
+ LOKI_USERNAME
571
+ LOKI_PASSWORD
572
+
573
+ LOKI_BATCHING
574
+ LOKI_BATCH_INTERVAL_SECONDS
575
+ LOKI_TIMEOUT_MS
576
+ LOKI_CLEAR_ON_ERROR
577
+ ```
578
+
579
+ Rules:
580
+
581
+ ```text
582
+ no LOKI_URL
583
+ -> direct Loki disabled
584
+
585
+ LOKI_ENABLED=false
586
+ -> direct Loki disabled
587
+
588
+ LOKI_URL present
589
+ -> direct Loki enabled by default
590
+ ```
591
+
592
+ Credentials are Node-bootstrap configuration only. Never log `LOKI_URL`
593
+ credentials, `LOKI_USERNAME`, `LOKI_PASSWORD`, Basic Auth values or connection
594
+ headers.
595
+
596
+ Default transport behavior:
597
+
598
+ ```text
599
+ batching=true
600
+ bounded timeout (LOKI_TIMEOUT_MS)
601
+ clearOnError=true
602
+ replaceTimestamp=true
603
+ gracefulShutdown=false
604
+ ```
605
+
606
+ The owning process bootstrap is responsible for explicit flush/shutdown via the
607
+ returned runtime (`forceFlush`/`shutdown`). If Loki is unavailable, log records
608
+ may be dropped but application processing must continue. Connection errors are
609
+ not recursively written back through the shared logger.
610
+
611
+ ### Loki labels and cardinality
612
+
613
+ Moda explicitly configures exactly three canonical low-cardinality labels:
614
+
615
+ ```text
616
+ service_namespace
617
+ service_name
618
+ environment
619
+ ```
620
+
621
+ `winston-loki@6.1.7` additionally adds its own low-cardinality `level` label
622
+ to every pushed stream, so a stream carries the three Moda labels plus
623
+ `level`. This is transport-library behaviour and does not change Moda's label
624
+ configuration.
625
+
626
+ Dynamic/high-cardinality identifiers must **not** be promoted to Loki stream
627
+ labels. The implementation explicitly retains
628
+ `useWinstonMetaAsLabels: false`, so winston metadata is never promoted to
629
+ labels. In particular, do not label by:
630
+
631
+ ```text
632
+ traceId
633
+ spanId
634
+ requestId
635
+ jobId
636
+ deliveryId
637
+ eventId
638
+ recoveryId
639
+ checkoutToken
640
+ cartToken
641
+ shop
642
+ shopDomain
643
+ customerId
644
+ phone
645
+ email
646
+ ```
647
+
648
+ Safe high-cardinality operational identifiers may remain in the sanitized JSON
649
+ log body where justified, but they must never become indexed stream labels.
650
+
651
+ ### Duplicate-delivery warning
652
+
653
+ Do not route the OpenTelemetry Logs destination back into the same Loki
654
+ instance while direct Loki is also enabled, unless duplicate storage is
655
+ intentional:
656
+
657
+ ```text
658
+ direct Loki ON
659
+ +
660
+ OTel Logs ultimately routed to same Loki
661
+ =
662
+ duplicate log storage
663
+ ```
664
+
665
+ The library does not guess backend topology; deployment configuration owns this
666
+ choice.
667
+
668
+ ### Log-signal data safety and correlation
669
+
670
+ - The OpenTelemetry log body is `JSON.stringify` of the same canonical
671
+ sanitized `LogRecord` written to stdout, so the same redaction boundary
672
+ applies to both destinations.
673
+ - Queryable OTel attributes are limited to:
674
+
675
+ ```text
676
+ event.name
677
+ log.level
678
+ service.namespace
679
+ service.name
680
+ deployment.environment.name
681
+ ```
682
+
683
+ - The shared logger never manufactures trace IDs. If an active
684
+ trace/span context exists, the OpenTelemetry Logs SDK associates the emitted
685
+ record with that active context automatically.
686
+ - stdout and OpenTelemetry emission are isolated and best-effort: a failure in
687
+ either must not change application/business correctness.
688
+
689
+ ### Tracing/metrics ownership
690
+
691
+ The shared logger does **not** create spans, counters or histograms.
692
+
693
+ The shared observability runtime owns the generic Node SDK, tracing and metrics
694
+ provider mechanics, exporters, sampling, HTTP/Undici instrumentation, generic
695
+ helpers, and lifecycle. Service repositories own their span names, business
696
+ attributes, application-specific metrics, and domain semantics, for example:
317
697
 
318
698
  ```text
319
699
  Shopify operation
320
700
  |
321
- +--> shared structured logger
701
+ +--> shared structured logger (stdout + OTel Logs)
322
702
  |
323
- +--> Shopify-specific OpenTelemetry
703
+ +--> Shopify-specific OpenTelemetry (spans/metrics)
324
704
  ```
325
705
 
326
706
  This separation is intentional. Fields that are useful in diagnostic logs can
@@ -344,6 +724,25 @@ The public package exposes independent entry points.
344
724
 
345
725
  @modainteract/moda-interact-shared/logging
346
726
  reusable structured logging API
727
+ (stdout + OTel Logs + direct Loki fan-out)
728
+
729
+ @modainteract/moda-interact-shared/logging/node
730
+ Node-only logging bootstrap
731
+ (OpenTelemetry LoggerProvider, batching, OTLP HTTP exporter;
732
+ Winston + winston-loki direct transport)
733
+
734
+ @modainteract/moda-interact-shared/observability
735
+ browser-safe generic observability helpers
736
+
737
+ @modainteract/moda-interact-shared/observability/node
738
+ reusable Node OpenTelemetry SDK, tracing/metrics providers, exporters,
739
+ sampling, HTTP/Undici instrumentation, and lifecycle
740
+
741
+ @modainteract/moda-interact-shared/observability/bullmq
742
+ Node-only BullMQ native telemetry adapter
743
+
744
+ @modainteract/moda-interact-shared/observability/genai
745
+ Node/runtime GenAI conversation-turn, agent, and tool active-span helpers
347
746
  ```
348
747
 
349
748
  Consumers should prefer the narrowest appropriate subpath.
@@ -360,27 +759,31 @@ Examples of repository ownership:
360
759
  - normalizes/validates Shopify events;
361
760
  - publishes the appropriate queue jobs;
362
761
  - uses the shared structured logger for generic log mechanics;
363
- - owns Shopify-specific observability semantics and OpenTelemetry.
762
+ - uses the shared observability runtime for generic OpenTelemetry mechanics;
763
+ - owns Shopify-specific observability semantics and application metrics.
364
764
 
365
765
  **moda-interact-background**
366
766
 
367
767
  - parses shared cross-service contracts before acting;
368
768
  - runs BullMQ workers and recovery workflows;
369
769
  - uses the shared logger for generic structured logging;
370
- - owns worker/recovery OpenTelemetry.
770
+ - uses the shared observability runtime for generic OpenTelemetry mechanics;
771
+ - owns worker/recovery observability semantics and application metrics.
371
772
 
372
773
  **moda-interact-messaging**
373
774
 
374
775
  - owns Meta/WhatsApp ingress;
375
776
  - uses the shared logger for generic structured logging;
376
- - owns Meta/WhatsApp-specific observability and OpenTelemetry.
777
+ - uses the shared observability runtime for generic OpenTelemetry mechanics;
778
+ - owns Meta/WhatsApp-specific observability semantics and application metrics.
377
779
 
378
780
  **moda-interact-admin**
379
781
 
380
782
  - owns the internal admin application;
381
783
  - may use the same shared logger where server-side operational logging is
382
784
  required;
383
- - owns admin-specific application telemetry.
785
+ - uses the shared observability runtime for generic OpenTelemetry mechanics;
786
+ - owns admin-specific observability semantics and application metrics.
384
787
 
385
788
  **moda-interact-database**
386
789