@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.
@@ -0,0 +1,352 @@
1
+ import {
2
+ initNodeLokiLogging,
3
+ initNodeOpenTelemetryLogging
4
+ } from "../chunk-5JNEZEKV.js";
5
+ import "../chunk-XU4XX7SD.js";
6
+
7
+ // src/observability/node.ts
8
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
9
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
10
+ import { PrismaInstrumentation } from "@prisma/instrumentation";
11
+ import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
12
+ import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
13
+ import { resourceFromAttributes } from "@opentelemetry/resources";
14
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
15
+ import { NodeSDK } from "@opentelemetry/sdk-node";
16
+ import {
17
+ AlwaysOffSampler,
18
+ AlwaysOnSampler,
19
+ BatchSpanProcessor,
20
+ ParentBasedSampler,
21
+ TraceIdRatioBasedSampler
22
+ } from "@opentelemetry/sdk-trace-base";
23
+ var DEFAULT_NAMESPACE = "moda-interact";
24
+ var configurationWarnings = /* @__PURE__ */ new Set();
25
+ var runtime;
26
+ function resolveDeploymentEnvironmentName() {
27
+ return nonEmpty(process.env.DEPLOYMENT_ENVIRONMENT_NAME) ?? nonEmpty(process.env.OTEL_DEPLOYMENT_ENVIRONMENT) ?? nonEmpty(process.env.NODE_ENV) ?? "development";
28
+ }
29
+ function initNodeObservability(options) {
30
+ if (runtime) {
31
+ return runtime;
32
+ }
33
+ const serviceName = nonEmpty(options?.serviceName) ?? "unknown-service";
34
+ const environment = nonEmpty(options?.environment) ?? resolveDeploymentEnvironmentName();
35
+ const serviceNamespace = nonEmpty(options?.serviceNamespace) ?? DEFAULT_NAMESPACE;
36
+ if (!nonEmpty(options?.serviceName)) {
37
+ warnConfigurationOnce(
38
+ "service-name",
39
+ "observability disabled: serviceName must be non-empty"
40
+ );
41
+ runtime = disabledRuntime(serviceName, environment);
42
+ return runtime;
43
+ }
44
+ const otelLogs = safeInitLogging(() => initNodeOpenTelemetryLogging({
45
+ serviceName,
46
+ environment,
47
+ serviceNamespace
48
+ }), "OpenTelemetry Logs");
49
+ const loki = safeInitLogging(() => initNodeLokiLogging({
50
+ serviceName,
51
+ environment,
52
+ serviceNamespace
53
+ }), "Loki");
54
+ if (!options.forceEnable && envBool("OTEL_SDK_DISABLED") === true) {
55
+ runtime = disabledRuntime(serviceName, environment, otelLogs, loki);
56
+ return runtime;
57
+ }
58
+ const traceEndpoint = signalEndpoint("TRACES", "traces");
59
+ const metricEndpoint = signalEndpoint("METRICS", "metrics");
60
+ if (!options.forceEnable && !traceEndpoint && !metricEndpoint) {
61
+ runtime = disabledRuntime(serviceName, environment, otelLogs, loki);
62
+ return runtime;
63
+ }
64
+ try {
65
+ const resource = resourceFromAttributes({
66
+ "service.namespace": serviceNamespace,
67
+ "service.name": serviceName,
68
+ "deployment.environment.name": environment
69
+ });
70
+ const profile = options.instrument ?? {};
71
+ const instrumentations = [
72
+ ...profile.http ? [new HttpInstrumentation({
73
+ requestHook: (span, request) => {
74
+ const path = "path" in request ? request.path : request.url;
75
+ setSanitizedUrlAttributes(span, path);
76
+ if ("protocol" in request && "host" in request && typeof request.protocol === "string" && typeof request.host === "string") {
77
+ span.setAttribute(
78
+ "url.full",
79
+ `${request.protocol}//${request.host}${sanitizedPath(path ?? "/")}`
80
+ );
81
+ }
82
+ }
83
+ })] : [],
84
+ ...profile.fetch ? [new UndiciInstrumentation({
85
+ requestHook: (span, request) => {
86
+ const path = sanitizedPath(request.path);
87
+ span.setAttribute("url.path", path);
88
+ span.setAttribute("url.full", `${request.origin}${path}`);
89
+ span.setAttribute("url.query", "");
90
+ }
91
+ })] : [],
92
+ ...profile.prisma ? [new PrismaInstrumentation()] : []
93
+ ];
94
+ const traceProcessor = createTraceProcessor(traceEndpoint);
95
+ const metricReader = createMetricReader(metricEndpoint);
96
+ if (!traceProcessor && !metricReader) {
97
+ runtime = {
98
+ ...disabledRuntime(serviceName, environment, otelLogs, loki),
99
+ error: "observability-exporters-unavailable"
100
+ };
101
+ return runtime;
102
+ }
103
+ const sdk = new NodeSDK({
104
+ resource,
105
+ autoDetectResources: false,
106
+ sampler: resolveSampler(options.traceSampleRatio, environment),
107
+ spanProcessors: traceProcessor ? [traceProcessor] : [],
108
+ ...metricReader ? { metricReader } : {},
109
+ instrumentations
110
+ });
111
+ sdk.start();
112
+ let shutdownPromise;
113
+ runtime = {
114
+ enabled: true,
115
+ serviceName,
116
+ environment,
117
+ forceFlush: async () => {
118
+ await Promise.allSettled([
119
+ traceProcessor?.forceFlush(),
120
+ metricReader?.forceFlush(),
121
+ otelLogs?.forceFlush(),
122
+ loki?.forceFlush()
123
+ ]);
124
+ },
125
+ shutdown: () => {
126
+ shutdownPromise ??= settleAll([
127
+ Promise.resolve(sdk.shutdown()),
128
+ otelLogs?.shutdown(),
129
+ loki?.shutdown()
130
+ ]);
131
+ return shutdownPromise;
132
+ }
133
+ };
134
+ return runtime;
135
+ } catch (error) {
136
+ warnConfigurationOnce(
137
+ "runtime-init",
138
+ "observability disabled: Node runtime initialization failed"
139
+ );
140
+ runtime = {
141
+ ...disabledRuntime(serviceName, environment, otelLogs, loki),
142
+ error: boundedError(error)
143
+ };
144
+ return runtime;
145
+ }
146
+ }
147
+ function getNodeObservabilityRuntime() {
148
+ return runtime;
149
+ }
150
+ function resolveSampler(explicitRatio, environment) {
151
+ const fallbackRatio = environment === "test" ? 1 : 0.1;
152
+ if (explicitRatio !== void 0) {
153
+ if (!Number.isFinite(explicitRatio) || explicitRatio < 0 || explicitRatio > 1) {
154
+ warnConfigurationOnce(
155
+ "explicit-sampler-ratio",
156
+ "invalid traceSampleRatio; using the Moda environment fallback"
157
+ );
158
+ return parentBasedRatio(fallbackRatio);
159
+ }
160
+ return parentBasedRatio(explicitRatio);
161
+ }
162
+ const requested = nonEmpty(process.env.OTEL_TRACES_SAMPLER)?.toLowerCase();
163
+ if (!requested) {
164
+ return parentBasedRatio(fallbackRatio);
165
+ }
166
+ switch (requested) {
167
+ case "always_on":
168
+ return new AlwaysOnSampler();
169
+ case "always_off":
170
+ return new AlwaysOffSampler();
171
+ case "traceidratio":
172
+ return new TraceIdRatioBasedSampler(samplerRatioArg(fallbackRatio));
173
+ case "parentbased_always_on":
174
+ return new ParentBasedSampler({ root: new AlwaysOnSampler() });
175
+ case "parentbased_always_off":
176
+ return new ParentBasedSampler({ root: new AlwaysOffSampler() });
177
+ case "parentbased_traceidratio":
178
+ return parentBasedRatio(samplerRatioArg(fallbackRatio));
179
+ default:
180
+ warnConfigurationOnce(
181
+ "unsupported-sampler",
182
+ "unsupported OTEL_TRACES_SAMPLER; using the Moda environment fallback"
183
+ );
184
+ return parentBasedRatio(fallbackRatio);
185
+ }
186
+ }
187
+ function createTraceProcessor(endpoint) {
188
+ if (!endpoint) {
189
+ return void 0;
190
+ }
191
+ try {
192
+ return new BatchSpanProcessor(
193
+ new OTLPTraceExporter({
194
+ url: endpoint,
195
+ concurrencyLimit: positiveInt("OTEL_EXPORTER_OTLP_TRACES_CONCURRENCY_LIMIT", 2)
196
+ }),
197
+ {
198
+ maxQueueSize: positiveInt("OTEL_BSP_MAX_QUEUE_SIZE", 2048),
199
+ maxExportBatchSize: positiveInt("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", 512),
200
+ scheduledDelayMillis: positiveInt("OTEL_BSP_SCHEDULE_DELAY", 5e3),
201
+ exportTimeoutMillis: positiveInt("OTEL_BSP_EXPORT_TIMEOUT", 3e4)
202
+ }
203
+ );
204
+ } catch {
205
+ warnConfigurationOnce(
206
+ "trace-exporter",
207
+ "trace exporter unavailable; trace export disabled"
208
+ );
209
+ return void 0;
210
+ }
211
+ }
212
+ function createMetricReader(endpoint) {
213
+ if (!endpoint) {
214
+ return void 0;
215
+ }
216
+ try {
217
+ const interval = positiveInt("OTEL_METRIC_EXPORT_INTERVAL", 6e4);
218
+ const timeout = Math.min(
219
+ positiveInt("OTEL_METRIC_EXPORT_TIMEOUT", 3e4),
220
+ interval
221
+ );
222
+ return new PeriodicExportingMetricReader({
223
+ exporter: new OTLPMetricExporter({
224
+ url: endpoint,
225
+ concurrencyLimit: positiveInt("OTEL_EXPORTER_OTLP_METRICS_CONCURRENCY_LIMIT", 2)
226
+ }),
227
+ exportIntervalMillis: interval,
228
+ exportTimeoutMillis: timeout
229
+ });
230
+ } catch {
231
+ warnConfigurationOnce(
232
+ "metric-exporter",
233
+ "metric exporter unavailable; metric export disabled"
234
+ );
235
+ return void 0;
236
+ }
237
+ }
238
+ function samplerRatioArg(fallback) {
239
+ const raw = nonEmpty(process.env.OTEL_TRACES_SAMPLER_ARG);
240
+ if (!raw) {
241
+ return fallback;
242
+ }
243
+ const value = Number(raw);
244
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
245
+ warnConfigurationOnce(
246
+ "sampler-ratio-arg",
247
+ "invalid OTEL_TRACES_SAMPLER_ARG; using the Moda environment fallback"
248
+ );
249
+ return fallback;
250
+ }
251
+ return value;
252
+ }
253
+ function parentBasedRatio(ratio) {
254
+ return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(ratio) });
255
+ }
256
+ function disabledRuntime(serviceName, environment, otelLogs, loki) {
257
+ let shutdownPromise;
258
+ return {
259
+ enabled: false,
260
+ serviceName,
261
+ environment,
262
+ forceFlush: () => settleAll([
263
+ otelLogs?.forceFlush(),
264
+ loki?.forceFlush()
265
+ ]),
266
+ shutdown: () => {
267
+ shutdownPromise ??= settleAll([
268
+ otelLogs?.shutdown(),
269
+ loki?.shutdown()
270
+ ]);
271
+ return shutdownPromise;
272
+ }
273
+ };
274
+ }
275
+ function safeInitLogging(fn, destination) {
276
+ try {
277
+ return fn();
278
+ } catch {
279
+ warnConfigurationOnce(
280
+ `logging-${destination}`,
281
+ `${destination} initialization failed; destination disabled`
282
+ );
283
+ return void 0;
284
+ }
285
+ }
286
+ async function settleAll(operations) {
287
+ await Promise.allSettled(operations);
288
+ }
289
+ function warnConfigurationOnce(key, message) {
290
+ if (configurationWarnings.has(key)) {
291
+ return;
292
+ }
293
+ configurationWarnings.add(key);
294
+ try {
295
+ console.warn(`[moda-observability] ${message.slice(0, 240)}`);
296
+ } catch {
297
+ }
298
+ }
299
+ function boundedError(error) {
300
+ return error instanceof Error ? error.message.slice(0, 300) : "observability-init-failed";
301
+ }
302
+ function setSanitizedUrlAttributes(span, requestPath) {
303
+ if (!requestPath) {
304
+ return;
305
+ }
306
+ span.setAttribute("url.path", sanitizedPath(requestPath));
307
+ span.setAttribute("url.query", "");
308
+ }
309
+ function sanitizedPath(value) {
310
+ return value.split(/[?#]/, 1)[0] || "/";
311
+ }
312
+ function nonEmpty(value) {
313
+ const normalized = value?.trim();
314
+ return normalized ? normalized : void 0;
315
+ }
316
+ function envBool(name) {
317
+ const value = nonEmpty(process.env[name])?.toLowerCase();
318
+ if (["true", "1", "yes", "on"].includes(value ?? "")) {
319
+ return true;
320
+ }
321
+ if (["false", "0", "no", "off"].includes(value ?? "")) {
322
+ return false;
323
+ }
324
+ return void 0;
325
+ }
326
+ function positiveInt(name, fallback) {
327
+ const raw = nonEmpty(process.env[name]);
328
+ if (!raw) {
329
+ return fallback;
330
+ }
331
+ const value = Number(raw);
332
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
333
+ }
334
+ function signalEndpoint(signal, suffix) {
335
+ const exporter = nonEmpty(process.env[`OTEL_${signal}_EXPORTER`]);
336
+ if (exporter && exporter.toLowerCase() !== "otlp") {
337
+ return void 0;
338
+ }
339
+ const exact = nonEmpty(process.env[`OTEL_EXPORTER_OTLP_${signal}_ENDPOINT`]);
340
+ if (exact) {
341
+ return exact;
342
+ }
343
+ const base = nonEmpty(process.env.OTEL_EXPORTER_OTLP_ENDPOINT)?.replace(/\/+$/, "");
344
+ return base ? `${base}/v1/${suffix}` : void 0;
345
+ }
346
+ export {
347
+ getNodeObservabilityRuntime,
348
+ initNodeObservability,
349
+ resolveDeploymentEnvironmentName,
350
+ resolveSampler
351
+ };
352
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/observability/node.ts"],"sourcesContent":["import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { PrismaInstrumentation } from \"@prisma/instrumentation\";\nimport { HttpInstrumentation } from \"@opentelemetry/instrumentation-http\";\nimport { UndiciInstrumentation } from \"@opentelemetry/instrumentation-undici\";\nimport type { Span } from \"@opentelemetry/api\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n AlwaysOffSampler,\n AlwaysOnSampler,\n BatchSpanProcessor,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n type Sampler,\n} from \"@opentelemetry/sdk-trace-base\";\nimport {\n initNodeLokiLogging,\n initNodeOpenTelemetryLogging,\n type NodeLokiLoggingRuntime,\n type NodeOpenTelemetryLoggingRuntime,\n} from \"../logging/node.js\";\n\nconst DEFAULT_NAMESPACE = \"moda-interact\";\nconst configurationWarnings = new Set<string>();\n\nexport type NodeInstrumentProfile = {\n http?: boolean;\n fetch?: boolean;\n prisma?: boolean;\n};\n\nexport type NodeObservabilityOptions = {\n serviceName: string;\n environment?: string;\n serviceNamespace?: string;\n instrument?: NodeInstrumentProfile;\n traceSampleRatio?: number;\n forceEnable?: boolean;\n};\n\nexport type NodeObservabilityRuntime = {\n enabled: boolean;\n serviceName: string;\n environment: string;\n error?: string;\n forceFlush(): Promise<void>;\n shutdown(): Promise<void>;\n};\n\nlet runtime: NodeObservabilityRuntime | undefined;\n\nexport function resolveDeploymentEnvironmentName(): string {\n return nonEmpty(process.env.DEPLOYMENT_ENVIRONMENT_NAME)\n ?? nonEmpty(process.env.OTEL_DEPLOYMENT_ENVIRONMENT)\n ?? nonEmpty(process.env.NODE_ENV)\n ?? \"development\";\n}\n\nexport function initNodeObservability(\n options: NodeObservabilityOptions,\n): NodeObservabilityRuntime {\n if (runtime) {\n return runtime;\n }\n\n const serviceName = nonEmpty(options?.serviceName) ?? \"unknown-service\";\n const environment = nonEmpty(options?.environment)\n ?? resolveDeploymentEnvironmentName();\n const serviceNamespace = nonEmpty(options?.serviceNamespace)\n ?? DEFAULT_NAMESPACE;\n\n if (!nonEmpty(options?.serviceName)) {\n warnConfigurationOnce(\n \"service-name\",\n \"observability disabled: serviceName must be non-empty\",\n );\n runtime = disabledRuntime(serviceName, environment);\n return runtime;\n }\n\n const otelLogs = safeInitLogging(() => initNodeOpenTelemetryLogging({\n serviceName,\n environment,\n serviceNamespace,\n }), \"OpenTelemetry Logs\");\n const loki = safeInitLogging(() => initNodeLokiLogging({\n serviceName,\n environment,\n serviceNamespace,\n }), \"Loki\");\n\n if (!options.forceEnable && envBool(\"OTEL_SDK_DISABLED\") === true) {\n runtime = disabledRuntime(serviceName, environment, otelLogs, loki);\n return runtime;\n }\n\n const traceEndpoint = signalEndpoint(\"TRACES\", \"traces\");\n const metricEndpoint = signalEndpoint(\"METRICS\", \"metrics\");\n\n if (!options.forceEnable && !traceEndpoint && !metricEndpoint) {\n runtime = disabledRuntime(serviceName, environment, otelLogs, loki);\n return runtime;\n }\n\n try {\n const resource = resourceFromAttributes({\n \"service.namespace\": serviceNamespace,\n \"service.name\": serviceName,\n \"deployment.environment.name\": environment,\n });\n const profile = options.instrument ?? {};\n const instrumentations = [\n ...(profile.http ? [new HttpInstrumentation({\n requestHook: (span, request) => {\n const path = \"path\" in request ? request.path : request.url;\n setSanitizedUrlAttributes(span, path);\n if (\n \"protocol\" in request\n && \"host\" in request\n && typeof request.protocol === \"string\"\n && typeof request.host === \"string\"\n ) {\n span.setAttribute(\n \"url.full\",\n `${request.protocol}//${request.host}${sanitizedPath(path ?? \"/\")}`,\n );\n }\n },\n })] : []),\n ...(profile.fetch ? [new UndiciInstrumentation({\n requestHook: (span, request) => {\n const path = sanitizedPath(request.path);\n span.setAttribute(\"url.path\", path);\n span.setAttribute(\"url.full\", `${request.origin}${path}`);\n span.setAttribute(\"url.query\", \"\");\n },\n })] : []),\n ...(profile.prisma ? [new PrismaInstrumentation()] : []),\n ];\n\n const traceProcessor = createTraceProcessor(traceEndpoint);\n const metricReader = createMetricReader(metricEndpoint);\n\n if (!traceProcessor && !metricReader) {\n runtime = {\n ...disabledRuntime(serviceName, environment, otelLogs, loki),\n error: \"observability-exporters-unavailable\",\n };\n return runtime;\n }\n\n const sdk = new NodeSDK({\n resource,\n autoDetectResources: false,\n sampler: resolveSampler(options.traceSampleRatio, environment),\n spanProcessors: traceProcessor ? [traceProcessor] : [],\n ...(metricReader ? { metricReader } : {}),\n instrumentations,\n });\n\n sdk.start();\n\n let shutdownPromise: Promise<void> | undefined;\n runtime = {\n enabled: true,\n serviceName,\n environment,\n forceFlush: async () => {\n await Promise.allSettled([\n traceProcessor?.forceFlush(),\n metricReader?.forceFlush(),\n otelLogs?.forceFlush(),\n loki?.forceFlush(),\n ]);\n },\n shutdown: () => {\n shutdownPromise ??= settleAll([\n Promise.resolve(sdk.shutdown()),\n otelLogs?.shutdown(),\n loki?.shutdown(),\n ]);\n return shutdownPromise;\n },\n };\n return runtime;\n } catch (error) {\n warnConfigurationOnce(\n \"runtime-init\",\n \"observability disabled: Node runtime initialization failed\",\n );\n runtime = {\n ...disabledRuntime(serviceName, environment, otelLogs, loki),\n error: boundedError(error),\n };\n return runtime;\n }\n}\n\nexport function getNodeObservabilityRuntime():\n | NodeObservabilityRuntime\n | undefined {\n return runtime;\n}\n\nexport function resolveSampler(\n explicitRatio: number | undefined,\n environment: string,\n): Sampler {\n const fallbackRatio = environment === \"test\" ? 1 : 0.1;\n\n if (explicitRatio !== undefined) {\n if (!Number.isFinite(explicitRatio) || explicitRatio < 0 || explicitRatio > 1) {\n warnConfigurationOnce(\n \"explicit-sampler-ratio\",\n \"invalid traceSampleRatio; using the Moda environment fallback\",\n );\n return parentBasedRatio(fallbackRatio);\n }\n return parentBasedRatio(explicitRatio);\n }\n\n const requested = nonEmpty(process.env.OTEL_TRACES_SAMPLER)?.toLowerCase();\n if (!requested) {\n return parentBasedRatio(fallbackRatio);\n }\n\n switch (requested) {\n case \"always_on\":\n return new AlwaysOnSampler();\n case \"always_off\":\n return new AlwaysOffSampler();\n case \"traceidratio\":\n return new TraceIdRatioBasedSampler(samplerRatioArg(fallbackRatio));\n case \"parentbased_always_on\":\n return new ParentBasedSampler({ root: new AlwaysOnSampler() });\n case \"parentbased_always_off\":\n return new ParentBasedSampler({ root: new AlwaysOffSampler() });\n case \"parentbased_traceidratio\":\n return parentBasedRatio(samplerRatioArg(fallbackRatio));\n default:\n warnConfigurationOnce(\n \"unsupported-sampler\",\n \"unsupported OTEL_TRACES_SAMPLER; using the Moda environment fallback\",\n );\n return parentBasedRatio(fallbackRatio);\n }\n}\n\nfunction createTraceProcessor(\n endpoint: string | undefined,\n): BatchSpanProcessor | undefined {\n if (!endpoint) {\n return undefined;\n }\n\n try {\n return new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: endpoint,\n concurrencyLimit: positiveInt(\"OTEL_EXPORTER_OTLP_TRACES_CONCURRENCY_LIMIT\", 2),\n }),\n {\n maxQueueSize: positiveInt(\"OTEL_BSP_MAX_QUEUE_SIZE\", 2048),\n maxExportBatchSize: positiveInt(\"OTEL_BSP_MAX_EXPORT_BATCH_SIZE\", 512),\n scheduledDelayMillis: positiveInt(\"OTEL_BSP_SCHEDULE_DELAY\", 5_000),\n exportTimeoutMillis: positiveInt(\"OTEL_BSP_EXPORT_TIMEOUT\", 30_000),\n },\n );\n } catch {\n warnConfigurationOnce(\n \"trace-exporter\",\n \"trace exporter unavailable; trace export disabled\",\n );\n return undefined;\n }\n}\n\nfunction createMetricReader(\n endpoint: string | undefined,\n): PeriodicExportingMetricReader | undefined {\n if (!endpoint) {\n return undefined;\n }\n\n try {\n const interval = positiveInt(\"OTEL_METRIC_EXPORT_INTERVAL\", 60_000);\n const timeout = Math.min(\n positiveInt(\"OTEL_METRIC_EXPORT_TIMEOUT\", 30_000),\n interval,\n );\n\n return new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: endpoint,\n concurrencyLimit: positiveInt(\"OTEL_EXPORTER_OTLP_METRICS_CONCURRENCY_LIMIT\", 2),\n }),\n exportIntervalMillis: interval,\n exportTimeoutMillis: timeout,\n });\n } catch {\n warnConfigurationOnce(\n \"metric-exporter\",\n \"metric exporter unavailable; metric export disabled\",\n );\n return undefined;\n }\n}\n\nfunction samplerRatioArg(fallback: number): number {\n const raw = nonEmpty(process.env.OTEL_TRACES_SAMPLER_ARG);\n if (!raw) {\n return fallback;\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n warnConfigurationOnce(\n \"sampler-ratio-arg\",\n \"invalid OTEL_TRACES_SAMPLER_ARG; using the Moda environment fallback\",\n );\n return fallback;\n }\n return value;\n}\n\nfunction parentBasedRatio(ratio: number): ParentBasedSampler {\n return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(ratio) });\n}\n\nfunction disabledRuntime(\n serviceName: string,\n environment: string,\n otelLogs?: NodeOpenTelemetryLoggingRuntime,\n loki?: NodeLokiLoggingRuntime,\n): NodeObservabilityRuntime {\n let shutdownPromise: Promise<void> | undefined;\n\n return {\n enabled: false,\n serviceName,\n environment,\n forceFlush: () => settleAll([\n otelLogs?.forceFlush(),\n loki?.forceFlush(),\n ]),\n shutdown: () => {\n shutdownPromise ??= settleAll([\n otelLogs?.shutdown(),\n loki?.shutdown(),\n ]);\n return shutdownPromise;\n },\n };\n}\n\nfunction safeInitLogging<T>(fn: () => T, destination: string): T | undefined {\n try {\n return fn();\n } catch {\n warnConfigurationOnce(\n `logging-${destination}`,\n `${destination} initialization failed; destination disabled`,\n );\n return undefined;\n }\n}\n\nasync function settleAll(\n operations: Array<Promise<unknown> | undefined>,\n): Promise<void> {\n await Promise.allSettled(operations);\n}\n\nfunction warnConfigurationOnce(key: string, message: string): void {\n if (configurationWarnings.has(key)) {\n return;\n }\n configurationWarnings.add(key);\n\n try {\n console.warn(`[moda-observability] ${message.slice(0, 240)}`);\n } catch {\n // Configuration diagnostics must not affect application startup.\n }\n}\n\nfunction boundedError(error: unknown): string {\n return error instanceof Error\n ? error.message.slice(0, 300)\n : \"observability-init-failed\";\n}\n\nfunction setSanitizedUrlAttributes(\n span: Span,\n requestPath: string | undefined,\n): void {\n if (!requestPath) {\n return;\n }\n span.setAttribute(\"url.path\", sanitizedPath(requestPath));\n span.setAttribute(\"url.query\", \"\");\n}\n\nfunction sanitizedPath(value: string): string {\n return value.split(/[?#]/, 1)[0] || \"/\";\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n const normalized = value?.trim();\n return normalized ? normalized : undefined;\n}\n\nfunction envBool(name: string): boolean | undefined {\n const value = nonEmpty(process.env[name])?.toLowerCase();\n if ([\"true\", \"1\", \"yes\", \"on\"].includes(value ?? \"\")) {\n return true;\n }\n if ([\"false\", \"0\", \"no\", \"off\"].includes(value ?? \"\")) {\n return false;\n }\n return undefined;\n}\n\nfunction positiveInt(name: string, fallback: number): number {\n const raw = nonEmpty(process.env[name]);\n if (!raw) {\n return fallback;\n }\n const value = Number(raw);\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n}\n\nfunction signalEndpoint(\n signal: \"TRACES\" | \"METRICS\",\n suffix: string,\n): string | undefined {\n const exporter = nonEmpty(process.env[`OTEL_${signal}_EXPORTER`]);\n if (exporter && exporter.toLowerCase() !== \"otlp\") {\n return undefined;\n }\n\n const exact = nonEmpty(process.env[`OTEL_EXPORTER_OTLP_${signal}_ENDPOINT`]);\n if (exact) {\n return exact;\n }\n\n const base = nonEmpty(process.env.OTEL_EXPORTER_OTLP_ENDPOINT)\n ?.replace(/\\/+$/, \"\");\n return base ? `${base}/v1/${suffix}` : undefined;\n}"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modainteract/moda-interact-shared",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,6 +23,26 @@
23
23
  "./logging": {
24
24
  "types": "./dist/logging/index.d.ts",
25
25
  "import": "./dist/logging/index.js"
26
+ },
27
+ "./logging/node": {
28
+ "types": "./dist/logging/node.d.ts",
29
+ "import": "./dist/logging/node.js"
30
+ },
31
+ "./observability": {
32
+ "types": "./dist/observability/index.d.ts",
33
+ "import": "./dist/observability/index.js"
34
+ },
35
+ "./observability/node": {
36
+ "types": "./dist/observability/node.d.ts",
37
+ "import": "./dist/observability/node.js"
38
+ },
39
+ "./observability/bullmq": {
40
+ "types": "./dist/observability/bullmq.d.ts",
41
+ "import": "./dist/observability/bullmq.js"
42
+ },
43
+ "./observability/genai": {
44
+ "types": "./dist/observability/genai.d.ts",
45
+ "import": "./dist/observability/genai.js"
26
46
  }
27
47
  },
28
48
  "files": [
@@ -31,14 +51,33 @@
31
51
  "scripts": {
32
52
  "build": "tsup",
33
53
  "typecheck": "tsc --noEmit",
34
- "test": "tsx --test src/**/*.test.ts",
54
+ "test": "tsx --test \"src/**/*.test.ts\"",
35
55
  "prepack": "npm run build"
36
56
  },
37
57
  "dependencies": {
58
+ "@opentelemetry/api": "^1.9.0",
59
+ "@opentelemetry/api-logs": "0.221.0",
60
+ "@opentelemetry/context-async-hooks": "^2.10.0",
61
+ "@opentelemetry/exporter-logs-otlp-http": "0.221.0",
62
+ "@opentelemetry/exporter-metrics-otlp-http": "0.221.0",
63
+ "@opentelemetry/exporter-trace-otlp-http": "0.221.0",
64
+ "@opentelemetry/instrumentation-http": "0.221.0",
65
+ "@opentelemetry/instrumentation-undici": "0.31.0",
66
+ "@opentelemetry/resources": "^2.10.0",
67
+ "@opentelemetry/sdk-logs": "0.221.0",
68
+ "@opentelemetry/sdk-metrics": "^2.10.0",
69
+ "@opentelemetry/sdk-node": "0.221.0",
70
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
71
+ "@prisma/instrumentation": "^6.19.3",
72
+ "bullmq-otel": "^2.0.1",
73
+ "winston": "3.19.0",
74
+ "winston-loki": "6.1.7",
38
75
  "zod": "^4.0.0"
39
76
  },
40
77
  "devDependencies": {
41
78
  "@types/node": "^26.4.0",
79
+ "bullmq": "^6.3.1",
80
+ "ioredis": "^6.0.0",
42
81
  "tsup": "^8.0.0",
43
82
  "tsx": "^4.23.12",
44
83
  "typescript": "^5.0.0"