@modainteract/moda-interact-shared 0.3.0 → 0.5.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 +183 -16
- package/dist/chunk-5JNEZEKV.js +374 -0
- package/dist/chunk-5JNEZEKV.js.map +1 -0
- package/dist/chunk-A3EK6N46.js +73 -0
- package/dist/chunk-A3EK6N46.js.map +1 -0
- package/dist/logging/node.js +8 -363
- package/dist/logging/node.js.map +1 -1
- package/dist/observability/bullmq.d.ts +10 -0
- package/dist/observability/bullmq.js +14 -0
- package/dist/observability/bullmq.js.map +1 -0
- package/dist/observability/genai.d.ts +17 -0
- package/dist/observability/genai.js +121 -0
- package/dist/observability/genai.js.map +1 -0
- package/dist/observability/index.d.ts +10 -0
- package/dist/observability/index.js +9 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/node.d.ts +29 -0
- package/dist/observability/node.js +352 -0
- package/dist/observability/node.js.map +1 -0
- package/package.json +29 -1
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
|
-
|
|
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,152 @@ 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
|
+
Pass `recordMetrics: false` to activate a helper's span without recording its
|
|
131
|
+
GenAI metrics. The option applies only to that helper invocation, so pass it to
|
|
132
|
+
each nested helper whose metrics should be suppressed:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const spansOnly = { recordMetrics: false } as const;
|
|
136
|
+
|
|
137
|
+
await observeConversationTurn("whatsapp", () =>
|
|
138
|
+
observeAgentInvocation({ agentName: "commerce-agent" }, () =>
|
|
139
|
+
observeAgentTool("lookup-products", executeTool, spansOnly),
|
|
140
|
+
spansOnly,
|
|
141
|
+
),
|
|
142
|
+
spansOnly,
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
An optional `mapException` callback can replace the application failure with a
|
|
147
|
+
bounded telemetry-safe representation for `span.recordException`. The original
|
|
148
|
+
thrown value is still rethrown unchanged. If the mapper throws or returns no
|
|
149
|
+
usable fields, the exception event is omitted and the span remains `ERROR`;
|
|
150
|
+
the original value is never recorded as fallback. Apply the mapper to every
|
|
151
|
+
nested helper that may observe the same rethrown failure:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const safeObservation = {
|
|
155
|
+
recordMetrics: false,
|
|
156
|
+
mapException: () => ({
|
|
157
|
+
name: "ProviderError",
|
|
158
|
+
message: "Provider operation failed",
|
|
159
|
+
}),
|
|
160
|
+
} as const;
|
|
161
|
+
|
|
162
|
+
await observeAgentTool("lookup-products", executeTool, safeObservation);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The helpers use only the global tracer and meter providers installed by the
|
|
166
|
+
Node runtime. They create no SDK, provider, exporter, or network request, and
|
|
167
|
+
remain lightweight no-ops when providers are absent. Six module-singleton
|
|
168
|
+
instruments record turn, agent, and tool duration and operation outcomes.
|
|
169
|
+
Metric attributes are limited to `outcome=success|error`; turn metrics also use
|
|
170
|
+
the closed `channel=whatsapp|other` vocabulary. Arbitrary agent, provider,
|
|
171
|
+
model, tool, and Moda business identifiers never become metric dimensions.
|
|
172
|
+
|
|
173
|
+
Agent, provider, model, and tool names are trimmed and bounded on spans only.
|
|
174
|
+
Prompt, completion, message, and tool payload bodies are never accepted or
|
|
175
|
+
captured by default.
|
|
176
|
+
|
|
35
177
|
## What this package does not own
|
|
36
178
|
|
|
37
179
|
This package does **not** own:
|
|
@@ -42,13 +184,13 @@ This package does **not** own:
|
|
|
42
184
|
- Shopify or Meta webhook HTTP handlers;
|
|
43
185
|
- Shopify or Meta SDK integration;
|
|
44
186
|
- application recovery/business logic;
|
|
45
|
-
-
|
|
46
|
-
|
|
187
|
+
- service/domain-specific span names, business attributes, or application
|
|
188
|
+
metrics;
|
|
47
189
|
- deployment OTLP endpoint/credential wiring or observability backends;
|
|
48
190
|
- Render deployment configuration.
|
|
49
191
|
|
|
50
|
-
Those
|
|
51
|
-
runtime
|
|
192
|
+
Those service-specific semantics and deployment concerns remain in the
|
|
193
|
+
repository that owns the corresponding runtime.
|
|
52
194
|
|
|
53
195
|
---
|
|
54
196
|
|
|
@@ -503,7 +645,7 @@ not recursively written back through the shared logger.
|
|
|
503
645
|
|
|
504
646
|
### Loki labels and cardinality
|
|
505
647
|
|
|
506
|
-
|
|
648
|
+
Moda explicitly configures exactly three canonical low-cardinality labels:
|
|
507
649
|
|
|
508
650
|
```text
|
|
509
651
|
service_namespace
|
|
@@ -511,9 +653,15 @@ service_name
|
|
|
511
653
|
environment
|
|
512
654
|
```
|
|
513
655
|
|
|
656
|
+
`winston-loki@6.1.7` additionally adds its own low-cardinality `level` label
|
|
657
|
+
to every pushed stream, so a stream carries the three Moda labels plus
|
|
658
|
+
`level`. This is transport-library behaviour and does not change Moda's label
|
|
659
|
+
configuration.
|
|
660
|
+
|
|
514
661
|
Dynamic/high-cardinality identifiers must **not** be promoted to Loki stream
|
|
515
662
|
labels. The implementation explicitly retains
|
|
516
|
-
`useWinstonMetaAsLabels: false
|
|
663
|
+
`useWinstonMetaAsLabels: false`, so winston metadata is never promoted to
|
|
664
|
+
labels. In particular, do not label by:
|
|
517
665
|
|
|
518
666
|
```text
|
|
519
667
|
traceId
|
|
@@ -573,12 +721,14 @@ choice.
|
|
|
573
721
|
- stdout and OpenTelemetry emission are isolated and best-effort: a failure in
|
|
574
722
|
either must not change application/business correctness.
|
|
575
723
|
|
|
576
|
-
### Tracing/metrics
|
|
724
|
+
### Tracing/metrics ownership
|
|
577
725
|
|
|
578
726
|
The shared logger does **not** create spans, counters or histograms.
|
|
579
727
|
|
|
580
|
-
|
|
581
|
-
|
|
728
|
+
The shared observability runtime owns the generic Node SDK, tracing and metrics
|
|
729
|
+
provider mechanics, exporters, sampling, HTTP/Undici instrumentation, generic
|
|
730
|
+
helpers, and lifecycle. Service repositories own their span names, business
|
|
731
|
+
attributes, application-specific metrics, and domain semantics, for example:
|
|
582
732
|
|
|
583
733
|
```text
|
|
584
734
|
Shopify operation
|
|
@@ -615,6 +765,19 @@ The public package exposes independent entry points.
|
|
|
615
765
|
Node-only logging bootstrap
|
|
616
766
|
(OpenTelemetry LoggerProvider, batching, OTLP HTTP exporter;
|
|
617
767
|
Winston + winston-loki direct transport)
|
|
768
|
+
|
|
769
|
+
@modainteract/moda-interact-shared/observability
|
|
770
|
+
browser-safe generic observability helpers
|
|
771
|
+
|
|
772
|
+
@modainteract/moda-interact-shared/observability/node
|
|
773
|
+
reusable Node OpenTelemetry SDK, tracing/metrics providers, exporters,
|
|
774
|
+
sampling, HTTP/Undici instrumentation, and lifecycle
|
|
775
|
+
|
|
776
|
+
@modainteract/moda-interact-shared/observability/bullmq
|
|
777
|
+
Node-only BullMQ native telemetry adapter
|
|
778
|
+
|
|
779
|
+
@modainteract/moda-interact-shared/observability/genai
|
|
780
|
+
Node/runtime GenAI conversation-turn, agent, and tool active-span helpers
|
|
618
781
|
```
|
|
619
782
|
|
|
620
783
|
Consumers should prefer the narrowest appropriate subpath.
|
|
@@ -631,27 +794,31 @@ Examples of repository ownership:
|
|
|
631
794
|
- normalizes/validates Shopify events;
|
|
632
795
|
- publishes the appropriate queue jobs;
|
|
633
796
|
- uses the shared structured logger for generic log mechanics;
|
|
634
|
-
-
|
|
797
|
+
- uses the shared observability runtime for generic OpenTelemetry mechanics;
|
|
798
|
+
- owns Shopify-specific observability semantics and application metrics.
|
|
635
799
|
|
|
636
800
|
**moda-interact-background**
|
|
637
801
|
|
|
638
802
|
- parses shared cross-service contracts before acting;
|
|
639
803
|
- runs BullMQ workers and recovery workflows;
|
|
640
804
|
- uses the shared logger for generic structured logging;
|
|
641
|
-
-
|
|
805
|
+
- uses the shared observability runtime for generic OpenTelemetry mechanics;
|
|
806
|
+
- owns worker/recovery observability semantics and application metrics.
|
|
642
807
|
|
|
643
808
|
**moda-interact-messaging**
|
|
644
809
|
|
|
645
810
|
- owns Meta/WhatsApp ingress;
|
|
646
811
|
- uses the shared logger for generic structured logging;
|
|
647
|
-
-
|
|
812
|
+
- uses the shared observability runtime for generic OpenTelemetry mechanics;
|
|
813
|
+
- owns Meta/WhatsApp-specific observability semantics and application metrics.
|
|
648
814
|
|
|
649
815
|
**moda-interact-admin**
|
|
650
816
|
|
|
651
817
|
- owns the internal admin application;
|
|
652
818
|
- may use the same shared logger where server-side operational logging is
|
|
653
819
|
required;
|
|
654
|
-
-
|
|
820
|
+
- uses the shared observability runtime for generic OpenTelemetry mechanics;
|
|
821
|
+
- owns admin-specific observability semantics and application metrics.
|
|
655
822
|
|
|
656
823
|
**moda-interact-database**
|
|
657
824
|
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setLokiLogEmitter
|
|
3
|
+
} from "./chunk-XU4XX7SD.js";
|
|
4
|
+
|
|
5
|
+
// src/logging/node.ts
|
|
6
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
7
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
8
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
9
|
+
import {
|
|
10
|
+
BatchLogRecordProcessor,
|
|
11
|
+
LoggerProvider
|
|
12
|
+
} from "@opentelemetry/sdk-logs";
|
|
13
|
+
|
|
14
|
+
// src/logging/node/loki.ts
|
|
15
|
+
import {
|
|
16
|
+
createLogger as createWinstonLogger,
|
|
17
|
+
format
|
|
18
|
+
} from "winston";
|
|
19
|
+
import LokiTransport from "winston-loki";
|
|
20
|
+
var DEFAULT_SERVICE_NAMESPACE = "moda-interact";
|
|
21
|
+
var runtime = null;
|
|
22
|
+
function requireNonEmpty(value, field) {
|
|
23
|
+
const normalized = value.trim();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
function initNodeLokiLogging(options) {
|
|
30
|
+
if (runtime) {
|
|
31
|
+
return runtime;
|
|
32
|
+
}
|
|
33
|
+
const serviceName = requireNonEmpty(options.serviceName, "serviceName");
|
|
34
|
+
const environment = requireNonEmpty(options.environment, "environment");
|
|
35
|
+
const serviceNamespace = requireNonEmpty(
|
|
36
|
+
options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE,
|
|
37
|
+
"serviceNamespace"
|
|
38
|
+
);
|
|
39
|
+
const host = nonEmpty(options.host) ?? envString("LOKI_URL");
|
|
40
|
+
const enabled = options.forceEnable === true || envBool("LOKI_ENABLED") !== false && host !== void 0;
|
|
41
|
+
const labels = Object.freeze({
|
|
42
|
+
service_namespace: serviceNamespace,
|
|
43
|
+
service_name: serviceName,
|
|
44
|
+
environment
|
|
45
|
+
});
|
|
46
|
+
if (!enabled || !host) {
|
|
47
|
+
setLokiLogEmitter(null);
|
|
48
|
+
runtime = {
|
|
49
|
+
enabled: false,
|
|
50
|
+
host: null,
|
|
51
|
+
labels,
|
|
52
|
+
forceFlush: async () => {
|
|
53
|
+
},
|
|
54
|
+
shutdown: async () => {
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return runtime;
|
|
58
|
+
}
|
|
59
|
+
const basicAuth = nonEmpty(options.basicAuth) ?? buildBasicAuth(
|
|
60
|
+
envString("LOKI_USERNAME"),
|
|
61
|
+
envString("LOKI_PASSWORD")
|
|
62
|
+
);
|
|
63
|
+
const transport = new LokiTransport({
|
|
64
|
+
host,
|
|
65
|
+
json: true,
|
|
66
|
+
batching: options.batching ?? envBool("LOKI_BATCHING") ?? true,
|
|
67
|
+
interval: options.intervalSeconds ?? envInt("LOKI_BATCH_INTERVAL_SECONDS", 5),
|
|
68
|
+
timeout: options.timeoutMs ?? envInt("LOKI_TIMEOUT_MS", 1e4),
|
|
69
|
+
// Prefer bounded loss to unbounded memory growth when the logging backend
|
|
70
|
+
// is unavailable. Loki must never become an application dependency.
|
|
71
|
+
clearOnError: options.clearOnError ?? envBool("LOKI_CLEAR_ON_ERROR") ?? true,
|
|
72
|
+
// Current winston-loki guidance recommends keeping timestamp replacement
|
|
73
|
+
// enabled unless there is a specific reason not to.
|
|
74
|
+
replaceTimestamp: true,
|
|
75
|
+
// Moda owns flush/shutdown explicitly from the process bootstrap.
|
|
76
|
+
gracefulShutdown: false,
|
|
77
|
+
// Critical cardinality rule: only these architecture-approved static
|
|
78
|
+
// labels may be indexed. Never promote Winston meta/data to labels.
|
|
79
|
+
labels,
|
|
80
|
+
useWinstonMetaAsLabels: false,
|
|
81
|
+
...basicAuth ? { basicAuth } : {},
|
|
82
|
+
...options.headers ? { headers: options.headers } : {},
|
|
83
|
+
// Do not recursively log Loki connection errors through the shared logger.
|
|
84
|
+
// Doing so could create a failure loop while Loki is unavailable.
|
|
85
|
+
onConnectionError: () => {
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const lokiLogger = createWinstonLogger({
|
|
89
|
+
level: "debug",
|
|
90
|
+
format: format.json(),
|
|
91
|
+
transports: [transport],
|
|
92
|
+
exitOnError: false
|
|
93
|
+
});
|
|
94
|
+
setLokiLogEmitter((record) => {
|
|
95
|
+
writeCanonicalRecord(lokiLogger, record);
|
|
96
|
+
});
|
|
97
|
+
runtime = {
|
|
98
|
+
enabled: true,
|
|
99
|
+
host,
|
|
100
|
+
labels,
|
|
101
|
+
forceFlush: async () => {
|
|
102
|
+
await safeFlush(transport);
|
|
103
|
+
},
|
|
104
|
+
shutdown: async () => {
|
|
105
|
+
setLokiLogEmitter(null);
|
|
106
|
+
await safeFlush(transport);
|
|
107
|
+
await safeCloseTransport(transport);
|
|
108
|
+
try {
|
|
109
|
+
lokiLogger.close();
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return runtime;
|
|
115
|
+
}
|
|
116
|
+
function getNodeLokiLoggingRuntime() {
|
|
117
|
+
return runtime;
|
|
118
|
+
}
|
|
119
|
+
function writeCanonicalRecord(logger, record) {
|
|
120
|
+
try {
|
|
121
|
+
logger.log({
|
|
122
|
+
...record,
|
|
123
|
+
level: record.level,
|
|
124
|
+
message: record.event
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function safeFlush(transport) {
|
|
130
|
+
try {
|
|
131
|
+
await transport.flush();
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function safeCloseTransport(transport) {
|
|
136
|
+
try {
|
|
137
|
+
const closeable = transport;
|
|
138
|
+
if (typeof closeable.close === "function") {
|
|
139
|
+
await Promise.resolve(closeable.close());
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildBasicAuth(username, password) {
|
|
145
|
+
if (!username || !password) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
return `${username}:${password}`;
|
|
149
|
+
}
|
|
150
|
+
function envString(name) {
|
|
151
|
+
return nonEmpty(process.env[name]);
|
|
152
|
+
}
|
|
153
|
+
function nonEmpty(value) {
|
|
154
|
+
if (value === void 0) {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
const normalized = value.trim();
|
|
158
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
159
|
+
}
|
|
160
|
+
function envBool(name) {
|
|
161
|
+
const value = envString(name);
|
|
162
|
+
if (value === void 0) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
const normalized = value.toLowerCase();
|
|
166
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
function envInt(name, fallback) {
|
|
175
|
+
const value = envString(name);
|
|
176
|
+
if (value === void 0) {
|
|
177
|
+
return fallback;
|
|
178
|
+
}
|
|
179
|
+
const parsed = Number(value);
|
|
180
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/logging/node.ts
|
|
184
|
+
var DEFAULT_SERVICE_NAMESPACE2 = "moda-interact";
|
|
185
|
+
var runtime2 = null;
|
|
186
|
+
function requireNonEmpty2(value, field) {
|
|
187
|
+
const normalized = value.trim();
|
|
188
|
+
if (!normalized) {
|
|
189
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
190
|
+
}
|
|
191
|
+
return normalized;
|
|
192
|
+
}
|
|
193
|
+
function initNodeOpenTelemetryLogging(options) {
|
|
194
|
+
if (runtime2) {
|
|
195
|
+
return runtime2;
|
|
196
|
+
}
|
|
197
|
+
const serviceName = requireNonEmpty2(options.serviceName, "serviceName");
|
|
198
|
+
const environment = requireNonEmpty2(options.environment, "environment");
|
|
199
|
+
const serviceNamespace = requireNonEmpty2(
|
|
200
|
+
options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE2,
|
|
201
|
+
"serviceNamespace"
|
|
202
|
+
);
|
|
203
|
+
const config = resolveLogsConfig(options);
|
|
204
|
+
if (!config.enabled || !config.endpoint) {
|
|
205
|
+
runtime2 = {
|
|
206
|
+
enabled: false,
|
|
207
|
+
endpoint: null,
|
|
208
|
+
forceFlush: async () => {
|
|
209
|
+
},
|
|
210
|
+
shutdown: async () => {
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return runtime2;
|
|
214
|
+
}
|
|
215
|
+
const exporter = new OTLPLogExporter({
|
|
216
|
+
url: config.endpoint,
|
|
217
|
+
headers: config.headers,
|
|
218
|
+
concurrencyLimit: envInt2("OTEL_LOG_EXPORT_CONCURRENCY_LIMIT", 2)
|
|
219
|
+
});
|
|
220
|
+
const processor = new BatchLogRecordProcessor({
|
|
221
|
+
exporter,
|
|
222
|
+
maxQueueSize: envInt2("OTEL_BLRP_MAX_QUEUE_SIZE", 2048),
|
|
223
|
+
maxExportBatchSize: envInt2("OTEL_BLRP_MAX_EXPORT_BATCH_SIZE", 512),
|
|
224
|
+
scheduledDelayMillis: envInt2("OTEL_BLRP_SCHEDULE_DELAY", 1e3),
|
|
225
|
+
exportTimeoutMillis: envInt2("OTEL_BLRP_EXPORT_TIMEOUT", 3e4)
|
|
226
|
+
});
|
|
227
|
+
const provider = new LoggerProvider({
|
|
228
|
+
resource: resourceFromAttributes({
|
|
229
|
+
"service.namespace": serviceNamespace,
|
|
230
|
+
"service.name": serviceName,
|
|
231
|
+
"deployment.environment.name": environment
|
|
232
|
+
}),
|
|
233
|
+
processors: [processor],
|
|
234
|
+
logRecordLimits: {
|
|
235
|
+
attributeCountLimit: 64,
|
|
236
|
+
attributeValueLengthLimit: 4096
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
logs.setGlobalLoggerProvider(provider);
|
|
240
|
+
runtime2 = {
|
|
241
|
+
enabled: true,
|
|
242
|
+
endpoint: config.endpoint,
|
|
243
|
+
forceFlush: async () => {
|
|
244
|
+
try {
|
|
245
|
+
await provider.forceFlush();
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
shutdown: async () => {
|
|
250
|
+
try {
|
|
251
|
+
await provider.shutdown();
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return runtime2;
|
|
257
|
+
}
|
|
258
|
+
function getNodeOpenTelemetryLoggingRuntime() {
|
|
259
|
+
return runtime2;
|
|
260
|
+
}
|
|
261
|
+
function resolveLogsEndpoint(explicitEndpoint) {
|
|
262
|
+
const explicit = nonEmpty2(explicitEndpoint);
|
|
263
|
+
if (explicit) {
|
|
264
|
+
return explicit;
|
|
265
|
+
}
|
|
266
|
+
const signalEndpoint = envString2("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
|
|
267
|
+
if (signalEndpoint) {
|
|
268
|
+
return signalEndpoint;
|
|
269
|
+
}
|
|
270
|
+
const genericEndpoint = envString2("OTEL_EXPORTER_OTLP_ENDPOINT");
|
|
271
|
+
if (!genericEndpoint) {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
const normalized = genericEndpoint.replace(/\/+$/, "");
|
|
275
|
+
if (normalized.endsWith("/v1/logs")) {
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
return `${normalized}/v1/logs`;
|
|
279
|
+
}
|
|
280
|
+
function parseOtlpHeaders(value) {
|
|
281
|
+
const raw = nonEmpty2(value);
|
|
282
|
+
if (!raw) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
const headers = {};
|
|
286
|
+
for (const part of raw.split(",")) {
|
|
287
|
+
const separator = part.indexOf("=");
|
|
288
|
+
if (separator <= 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const rawKey = part.slice(0, separator).trim();
|
|
292
|
+
const rawValue = part.slice(separator + 1).trim();
|
|
293
|
+
if (!rawKey) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
headers[safeDecodeURIComponent(rawKey)] = safeDecodeURIComponent(rawValue);
|
|
297
|
+
}
|
|
298
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
299
|
+
}
|
|
300
|
+
function resolveLogsConfig(options) {
|
|
301
|
+
if (!options.forceEnable && envBool2("OTEL_SDK_DISABLED") === true) {
|
|
302
|
+
return {
|
|
303
|
+
enabled: false,
|
|
304
|
+
endpoint: void 0,
|
|
305
|
+
headers: void 0
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const exporterName = envString2("OTEL_LOGS_EXPORTER") ?? "otlp";
|
|
309
|
+
if (!options.forceEnable && exporterName !== "otlp") {
|
|
310
|
+
return {
|
|
311
|
+
enabled: false,
|
|
312
|
+
endpoint: void 0,
|
|
313
|
+
headers: void 0
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const endpoint = resolveLogsEndpoint(options.endpoint);
|
|
317
|
+
if (!endpoint) {
|
|
318
|
+
return {
|
|
319
|
+
enabled: false,
|
|
320
|
+
endpoint: void 0,
|
|
321
|
+
headers: void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const headers = options.headers ?? parseOtlpHeaders(
|
|
325
|
+
envString2("OTEL_EXPORTER_OTLP_LOGS_HEADERS") ?? envString2("OTEL_EXPORTER_OTLP_HEADERS")
|
|
326
|
+
);
|
|
327
|
+
return {
|
|
328
|
+
enabled: true,
|
|
329
|
+
endpoint,
|
|
330
|
+
headers
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function envString2(name) {
|
|
334
|
+
return nonEmpty2(process.env[name]);
|
|
335
|
+
}
|
|
336
|
+
function nonEmpty2(value) {
|
|
337
|
+
if (value === void 0) {
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
const trimmed = value.trim();
|
|
341
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
342
|
+
}
|
|
343
|
+
function envBool2(name) {
|
|
344
|
+
const value = envString2(name);
|
|
345
|
+
if (value === void 0) {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
349
|
+
}
|
|
350
|
+
function envInt2(name, fallback) {
|
|
351
|
+
const value = envString2(name);
|
|
352
|
+
if (value === void 0) {
|
|
353
|
+
return fallback;
|
|
354
|
+
}
|
|
355
|
+
const parsed = Number(value);
|
|
356
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
357
|
+
}
|
|
358
|
+
function safeDecodeURIComponent(value) {
|
|
359
|
+
try {
|
|
360
|
+
return decodeURIComponent(value);
|
|
361
|
+
} catch {
|
|
362
|
+
return value;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export {
|
|
367
|
+
initNodeLokiLogging,
|
|
368
|
+
getNodeLokiLoggingRuntime,
|
|
369
|
+
initNodeOpenTelemetryLogging,
|
|
370
|
+
getNodeOpenTelemetryLoggingRuntime,
|
|
371
|
+
resolveLogsEndpoint,
|
|
372
|
+
parseOtlpHeaders
|
|
373
|
+
};
|
|
374
|
+
//# sourceMappingURL=chunk-5JNEZEKV.js.map
|