@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logging/node.ts","../src/logging/node/loki.ts"],"sourcesContent":["import { logs } from \"@opentelemetry/api-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\n\nexport {\n getNodeLokiLoggingRuntime,\n initNodeLokiLogging,\n} from \"./node/loki.js\";\nexport type {\n NodeLokiLoggingOptions,\n NodeLokiLoggingRuntime,\n} from \"./node/loki.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\n\nexport type NodeOpenTelemetryLoggingOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n\n /**\n * Explicit endpoint override. Intended mainly for tests.\n *\n * Production normally uses:\n * OTEL_EXPORTER_OTLP_LOGS_ENDPOINT\n * or:\n * OTEL_EXPORTER_OTLP_ENDPOINT\n */\n endpoint?: string;\n\n /** Optional explicit OTLP headers override. */\n headers?: Record<string, string>;\n\n /** Test-only switch. Production should rely on environment configuration. */\n forceEnable?: boolean;\n};\n\nexport type NodeOpenTelemetryLoggingRuntime = {\n enabled: boolean;\n endpoint: string | null;\n forceFlush: () => Promise<void>;\n shutdown: () => Promise<void>;\n};\n\nlet runtime: NodeOpenTelemetryLoggingRuntime | null = null;\n\nfunction requireNonEmpty(value: string, field: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${field} must be a non-empty string`);\n }\n return normalized;\n}\n\n/**\n * Installs the Node OpenTelemetry LoggerProvider exactly once for this process.\n *\n * IMPORTANT:\n * This module is Node-only. Import and initialize it from the process bootstrap\n * before framework/application modules are loaded. Do not import this module\n * through Vite/React/browser bundles.\n *\n * Application modules should import only:\n *\n * @modainteract/moda-interact-shared/logging\n *\n * Once this provider is installed, createLogger(...) automatically fans safe\n * canonical log records out to stdout and the OpenTelemetry Logs API.\n *\n * Direct Loki delivery, when desired, is independently enabled with:\n *\n * initNodeLokiLogging(...)\n *\n * from this same Node-only package entry.\n */\nexport function initNodeOpenTelemetryLogging(\n options: NodeOpenTelemetryLoggingOptions,\n): NodeOpenTelemetryLoggingRuntime {\n if (runtime) {\n return runtime;\n }\n\n const serviceName = requireNonEmpty(options.serviceName, \"serviceName\");\n const environment = requireNonEmpty(options.environment, \"environment\");\n const serviceNamespace = requireNonEmpty(\n options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE,\n \"serviceNamespace\",\n );\n\n const config = resolveLogsConfig(options);\n\n if (!config.enabled || !config.endpoint) {\n runtime = {\n enabled: false,\n endpoint: null,\n forceFlush: async () => {},\n shutdown: async () => {},\n };\n return runtime;\n }\n\n const exporter = new OTLPLogExporter({\n url: config.endpoint,\n headers: config.headers,\n concurrencyLimit: envInt(\"OTEL_LOG_EXPORT_CONCURRENCY_LIMIT\", 2),\n });\n\n const processor = new BatchLogRecordProcessor({\n exporter,\n maxQueueSize: envInt(\"OTEL_BLRP_MAX_QUEUE_SIZE\", 2048),\n maxExportBatchSize: envInt(\"OTEL_BLRP_MAX_EXPORT_BATCH_SIZE\", 512),\n scheduledDelayMillis: envInt(\"OTEL_BLRP_SCHEDULE_DELAY\", 1_000),\n exportTimeoutMillis: envInt(\"OTEL_BLRP_EXPORT_TIMEOUT\", 30_000),\n });\n\n const provider = new LoggerProvider({\n resource: resourceFromAttributes({\n \"service.namespace\": serviceNamespace,\n \"service.name\": serviceName,\n \"deployment.environment.name\": environment,\n }),\n processors: [processor],\n logRecordLimits: {\n attributeCountLimit: 64,\n attributeValueLengthLimit: 4096,\n },\n });\n\n logs.setGlobalLoggerProvider(provider);\n\n runtime = {\n enabled: true,\n endpoint: config.endpoint,\n forceFlush: async () => {\n try {\n await provider.forceFlush();\n } catch {\n // Telemetry delivery is best-effort.\n }\n },\n shutdown: async () => {\n try {\n await provider.shutdown();\n } catch {\n // Telemetry shutdown is best-effort.\n }\n },\n };\n\n return runtime;\n}\n\nexport function getNodeOpenTelemetryLoggingRuntime():\n | NodeOpenTelemetryLoggingRuntime\n | null {\n return runtime;\n}\n\nexport function resolveLogsEndpoint(\n explicitEndpoint?: string,\n): string | undefined {\n const explicit = nonEmpty(explicitEndpoint);\n if (explicit) {\n return explicit;\n }\n\n const signalEndpoint = envString(\"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT\");\n if (signalEndpoint) {\n return signalEndpoint;\n }\n\n const genericEndpoint = envString(\"OTEL_EXPORTER_OTLP_ENDPOINT\");\n if (!genericEndpoint) {\n return undefined;\n }\n\n const normalized = genericEndpoint.replace(/\\/+$/, \"\");\n if (normalized.endsWith(\"/v1/logs\")) {\n return normalized;\n }\n return `${normalized}/v1/logs`;\n}\n\nexport function parseOtlpHeaders(value: string | undefined):\n | Record<string, string>\n | undefined {\n const raw = nonEmpty(value);\n if (!raw) {\n return undefined;\n }\n\n const headers: Record<string, string> = {};\n\n for (const part of raw.split(\",\")) {\n const separator = part.indexOf(\"=\");\n if (separator <= 0) {\n continue;\n }\n\n const rawKey = part.slice(0, separator).trim();\n const rawValue = part.slice(separator + 1).trim();\n\n if (!rawKey) {\n continue;\n }\n\n headers[safeDecodeURIComponent(rawKey)] =\n safeDecodeURIComponent(rawValue);\n }\n\n return Object.keys(headers).length > 0 ? headers : undefined;\n}\n\nfunction resolveLogsConfig(options: NodeOpenTelemetryLoggingOptions) {\n if (!options.forceEnable && envBool(\"OTEL_SDK_DISABLED\") === true) {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const exporterName = envString(\"OTEL_LOGS_EXPORTER\") ?? \"otlp\";\n\n if (!options.forceEnable && exporterName !== \"otlp\") {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const endpoint = resolveLogsEndpoint(options.endpoint);\n\n if (!endpoint) {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const headers =\n options.headers ??\n parseOtlpHeaders(\n envString(\"OTEL_EXPORTER_OTLP_LOGS_HEADERS\") ??\n envString(\"OTEL_EXPORTER_OTLP_HEADERS\"),\n );\n\n return {\n enabled: true,\n endpoint,\n headers,\n };\n}\n\nfunction envString(name: string): string | undefined {\n return nonEmpty(process.env[name]);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction envBool(name: string): boolean | undefined {\n const value = envString(name);\n if (value === undefined) {\n return undefined;\n }\n return [\"1\", \"true\", \"yes\", \"on\"].includes(value.toLowerCase());\n}\n\nfunction envInt(name: string, fallback: number): number {\n const value = envString(name);\n if (value === undefined) {\n return fallback;\n }\n\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0\n ? Math.floor(parsed)\n : fallback;\n}\n\nfunction safeDecodeURIComponent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n","import {\n createLogger as createWinstonLogger,\n format,\n type Logger as WinstonLogger,\n} from \"winston\";\nimport LokiTransport from \"winston-loki\";\nimport { setLokiLogEmitter } from \"../loki.js\";\nimport type { LogRecord } from \"../types.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\n\nexport type NodeLokiLoggingOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n\n /**\n * Explicit Loki base URL.\n *\n * Production normally supplies LOKI_URL.\n */\n host?: string;\n\n /**\n * Optional Basic Auth override in `username:password` format.\n *\n * Prefer separate LOKI_USERNAME/LOKI_PASSWORD environment variables in\n * deployed services so the composed value never needs to be committed.\n */\n basicAuth?: string;\n\n /**\n * Optional additional static headers.\n *\n * Never include these in log payloads or error messages.\n */\n headers?: Record<string, string>;\n\n batching?: boolean;\n intervalSeconds?: number;\n timeoutMs?: number;\n clearOnError?: boolean;\n\n /**\n * Test-only override. In normal service bootstrap, the presence of a Loki\n * host is sufficient to enable the transport.\n */\n forceEnable?: boolean;\n};\n\nexport type NodeLokiLoggingRuntime = {\n enabled: boolean;\n host: string | null;\n labels: Readonly<Record<string, string>>;\n forceFlush: () => Promise<void>;\n shutdown: () => Promise<void>;\n};\n\nlet runtime: NodeLokiLoggingRuntime | null = null;\n\nfunction requireNonEmpty(value: string, field: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${field} must be a non-empty string`);\n }\n return normalized;\n}\n\n/**\n * Installs the direct Grafana Loki destination exactly once per Node process.\n *\n * IMPORTANT:\n * - This module is Node-only.\n * - It is reached through @modainteract/moda-interact-shared/logging/node.\n * - Do not import it from Vite/browser/application module graphs.\n * - The normal ./logging entry contains no Winston/winston-loki imports.\n */\nexport function initNodeLokiLogging(\n options: NodeLokiLoggingOptions,\n): NodeLokiLoggingRuntime {\n if (runtime) {\n return runtime;\n }\n\n const serviceName = requireNonEmpty(options.serviceName, \"serviceName\");\n const environment = requireNonEmpty(options.environment, \"environment\");\n const serviceNamespace = requireNonEmpty(\n options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE,\n \"serviceNamespace\",\n );\n\n const host = nonEmpty(options.host) ?? envString(\"LOKI_URL\");\n const enabled =\n options.forceEnable === true ||\n (envBool(\"LOKI_ENABLED\") !== false && host !== undefined);\n\n const labels = Object.freeze({\n service_namespace: serviceNamespace,\n service_name: serviceName,\n environment,\n });\n\n if (!enabled || !host) {\n setLokiLogEmitter(null);\n\n runtime = {\n enabled: false,\n host: null,\n labels,\n forceFlush: async () => {},\n shutdown: async () => {},\n };\n\n return runtime;\n }\n\n const basicAuth =\n nonEmpty(options.basicAuth) ??\n buildBasicAuth(\n envString(\"LOKI_USERNAME\"),\n envString(\"LOKI_PASSWORD\"),\n );\n\n const transport = new LokiTransport({\n host,\n json: true,\n batching: options.batching ?? envBool(\"LOKI_BATCHING\") ?? true,\n interval:\n options.intervalSeconds ??\n envInt(\"LOKI_BATCH_INTERVAL_SECONDS\", 5),\n timeout:\n options.timeoutMs ??\n envInt(\"LOKI_TIMEOUT_MS\", 10_000),\n\n // Prefer bounded loss to unbounded memory growth when the logging backend\n // is unavailable. Loki must never become an application dependency.\n clearOnError:\n options.clearOnError ??\n envBool(\"LOKI_CLEAR_ON_ERROR\") ??\n true,\n\n // Current winston-loki guidance recommends keeping timestamp replacement\n // enabled unless there is a specific reason not to.\n replaceTimestamp: true,\n\n // Moda owns flush/shutdown explicitly from the process bootstrap.\n gracefulShutdown: false,\n\n // Critical cardinality rule: only these architecture-approved static\n // labels may be indexed. Never promote Winston meta/data to labels.\n labels,\n useWinstonMetaAsLabels: false,\n\n ...(basicAuth ? { basicAuth } : {}),\n ...(options.headers ? { headers: options.headers } : {}),\n\n // Do not recursively log Loki connection errors through the shared logger.\n // Doing so could create a failure loop while Loki is unavailable.\n onConnectionError: () => {},\n });\n\n const lokiLogger = createWinstonLogger({\n level: \"debug\",\n format: format.json(),\n transports: [transport],\n exitOnError: false,\n });\n\n setLokiLogEmitter((record) => {\n writeCanonicalRecord(lokiLogger, record);\n });\n\n runtime = {\n enabled: true,\n host,\n labels,\n forceFlush: async () => {\n await safeFlush(transport);\n },\n shutdown: async () => {\n setLokiLogEmitter(null);\n await safeFlush(transport);\n await safeCloseTransport(transport);\n\n try {\n lokiLogger.close();\n } catch {\n // best-effort\n }\n },\n };\n\n return runtime;\n}\n\nexport function getNodeLokiLoggingRuntime():\n | NodeLokiLoggingRuntime\n | null {\n return runtime;\n}\n\nfunction writeCanonicalRecord(\n logger: WinstonLogger,\n record: LogRecord,\n): void {\n try {\n // Winston requires a `message`; use the canonical event name.\n //\n // Every other value comes from the already-sanitized immutable Moda\n // LogRecord. Because useWinstonMetaAsLabels=false, these values remain in\n // the JSON log line rather than becoming Loki stream labels.\n logger.log({\n ...record,\n level: record.level,\n message: record.event,\n });\n } catch {\n // Direct Loki emission must never escape to business code.\n }\n}\n\nasync function safeFlush(transport: LokiTransport): Promise<void> {\n try {\n await transport.flush();\n } catch {\n // Logs may be dropped. Business processing must continue.\n }\n}\n\nasync function safeCloseTransport(\n transport: LokiTransport,\n): Promise<void> {\n try {\n const closeable = transport as LokiTransport & {\n close?: () => unknown | Promise<unknown>;\n };\n\n if (typeof closeable.close === \"function\") {\n await Promise.resolve(closeable.close());\n }\n } catch {\n // best-effort shutdown\n }\n}\n\nfunction buildBasicAuth(\n username: string | undefined,\n password: string | undefined,\n): string | undefined {\n if (!username || !password) {\n return undefined;\n }\n\n return `${username}:${password}`;\n}\n\nfunction envString(name: string): string | undefined {\n return nonEmpty(process.env[name]);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n const normalized = value.trim();\n return normalized.length > 0 ? normalized : undefined;\n}\n\nfunction envBool(name: string): boolean | undefined {\n const value = envString(name);\n\n if (value === undefined) {\n return undefined;\n }\n\n const normalized = value.toLowerCase();\n\n if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) {\n return true;\n }\n\n if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) {\n return false;\n }\n\n return undefined;\n}\n\nfunction envInt(name: string, fallback: number): number {\n const value = envString(name);\n\n if (value === undefined) {\n return fallback;\n }\n\n const parsed = Number(value);\n\n return Number.isFinite(parsed) && parsed > 0\n ? Math.floor(parsed)\n : fallback;\n}\n"],"mappings":"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","names":["DEFAULT_SERVICE_NAMESPACE","runtime","requireNonEmpty","envInt","nonEmpty","envString","envBool"]}
@@ -0,0 +1,73 @@
1
+ // src/observability/index.ts
2
+ import {
3
+ context,
4
+ SpanStatusCode,
5
+ trace
6
+ } from "@opentelemetry/api";
7
+ var TRACER_NAME = "@modainteract/moda-interact-shared/observability";
8
+ var MAX_MAPPED_EXCEPTION_FIELD_LENGTH = 1024;
9
+ function getActiveTraceId() {
10
+ return trace.getSpan(context.active())?.spanContext().traceId;
11
+ }
12
+ async function withObservedSpan(name, attributes, work, options = {}) {
13
+ const tracer = trace.getTracer(TRACER_NAME);
14
+ return tracer.startActiveSpan(name, { attributes }, async (span) => {
15
+ try {
16
+ const result = await work();
17
+ span.setStatus({ code: SpanStatusCode.OK });
18
+ return result;
19
+ } catch (error) {
20
+ if (options.mapException) {
21
+ try {
22
+ const mappedException = normalizeMappedException(
23
+ options.mapException(error)
24
+ );
25
+ if (mappedException !== void 0) {
26
+ span.recordException(mappedException);
27
+ }
28
+ } catch {
29
+ }
30
+ } else {
31
+ span.recordException(
32
+ error instanceof Error ? error : new Error(String(error))
33
+ );
34
+ }
35
+ span.setStatus({ code: SpanStatusCode.ERROR });
36
+ throw error;
37
+ } finally {
38
+ span.end();
39
+ }
40
+ });
41
+ }
42
+ function normalizeMappedException(exception) {
43
+ if (typeof exception === "string") {
44
+ return boundedExceptionField(exception);
45
+ }
46
+ if (!exception || typeof exception !== "object") {
47
+ return void 0;
48
+ }
49
+ const code = typeof exception.code === "number" ? exception.code : boundedExceptionField(exception.code);
50
+ const message = boundedExceptionField(exception.message);
51
+ const name = boundedExceptionField(exception.name);
52
+ const stack = boundedExceptionField(exception.stack);
53
+ if (code === void 0 && message === void 0 && name === void 0) {
54
+ return void 0;
55
+ }
56
+ return {
57
+ ...code !== void 0 ? { code } : {},
58
+ ...message !== void 0 ? { message } : {},
59
+ ...name !== void 0 ? { name } : {},
60
+ ...stack !== void 0 ? { stack } : {}
61
+ };
62
+ }
63
+ function boundedExceptionField(value) {
64
+ if (typeof value !== "string") return void 0;
65
+ const bounded = value.trim().slice(0, MAX_MAPPED_EXCEPTION_FIELD_LENGTH);
66
+ return bounded || void 0;
67
+ }
68
+
69
+ export {
70
+ getActiveTraceId,
71
+ withObservedSpan
72
+ };
73
+ //# sourceMappingURL=chunk-A3EK6N46.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/observability/index.ts"],"sourcesContent":["import {\n context,\n SpanStatusCode,\n trace,\n type Attributes,\n type Exception,\n} from \"@opentelemetry/api\";\n\nconst TRACER_NAME = \"@modainteract/moda-interact-shared/observability\";\nconst MAX_MAPPED_EXCEPTION_FIELD_LENGTH = 1_024;\n\nexport type SpanExceptionMapper = (error: unknown) => Exception | undefined;\n\nexport type ObservedSpanOptions = {\n mapException?: SpanExceptionMapper;\n};\n\nexport function getActiveTraceId(): string | undefined {\n return trace.getSpan(context.active())?.spanContext().traceId;\n}\n\nexport async function withObservedSpan<T>(\n name: string,\n attributes: Attributes,\n work: () => Promise<T>,\n options: ObservedSpanOptions = {},\n): Promise<T> {\n const tracer = trace.getTracer(TRACER_NAME);\n\n return tracer.startActiveSpan(name, { attributes }, async (span) => {\n try {\n const result = await work();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (options.mapException) {\n try {\n const mappedException = normalizeMappedException(\n options.mapException(error),\n );\n if (mappedException !== undefined) {\n span.recordException(mappedException);\n }\n } catch {\n // Never fall back to the original value after a mapper is supplied.\n }\n } else {\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n });\n}\n\nfunction normalizeMappedException(\n exception: Exception | undefined,\n): Exception | undefined {\n if (typeof exception === \"string\") {\n return boundedExceptionField(exception);\n }\n if (!exception || typeof exception !== \"object\") {\n return undefined;\n }\n\n const code = typeof exception.code === \"number\"\n ? exception.code\n : boundedExceptionField(exception.code);\n const message = boundedExceptionField(exception.message);\n const name = boundedExceptionField(exception.name);\n const stack = boundedExceptionField(exception.stack);\n\n if (code === undefined && message === undefined && name === undefined) {\n return undefined;\n }\n\n return {\n ...(code !== undefined ? { code } : {}),\n ...(message !== undefined ? { message } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(stack !== undefined ? { stack } : {}),\n } as Exception;\n}\n\nfunction boundedExceptionField(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const bounded = value.trim().slice(0, MAX_MAPPED_EXCEPTION_FIELD_LENGTH);\n return bounded || undefined;\n}"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP,IAAM,cAAc;AACpB,IAAM,oCAAoC;AAQnC,SAAS,mBAAuC;AACrD,SAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC,GAAG,YAAY,EAAE;AACxD;AAEA,eAAsB,iBACpB,MACA,YACA,MACA,UAA+B,CAAC,GACpB;AACZ,QAAM,SAAS,MAAM,UAAU,WAAW;AAE1C,SAAO,OAAO,gBAAgB,MAAM,EAAE,WAAW,GAAG,OAAO,SAAS;AAClE,QAAI;AACF,YAAM,SAAS,MAAM,KAAK;AAC1B,WAAK,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;AAC1C,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,QAAQ,cAAc;AACxB,YAAI;AACF,gBAAM,kBAAkB;AAAA,YACtB,QAAQ,aAAa,KAAK;AAAA,UAC5B;AACA,cAAI,oBAAoB,QAAW;AACjC,iBAAK,gBAAgB,eAAe;AAAA,UACtC;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF,OAAO;AACL,aAAK;AAAA,UACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,QAC1D;AAAA,MACF;AACA,WAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;AAC7C,YAAM;AAAA,IACR,UAAE;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF,CAAC;AACH;AAEA,SAAS,yBACP,WACuB;AACvB,MAAI,OAAO,cAAc,UAAU;AACjC,WAAO,sBAAsB,SAAS;AAAA,EACxC;AACA,MAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,UAAU,SAAS,WACnC,UAAU,OACV,sBAAsB,UAAU,IAAI;AACxC,QAAM,UAAU,sBAAsB,UAAU,OAAO;AACvD,QAAM,OAAO,sBAAsB,UAAU,IAAI;AACjD,QAAM,QAAQ,sBAAsB,UAAU,KAAK;AAEnD,MAAI,SAAS,UAAa,YAAY,UAAa,SAAS,QAAW;AACrE,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3C,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,EACzC;AACF;AAEA,SAAS,sBAAsB,OAAoC;AACjE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK,EAAE,MAAM,GAAG,iCAAiC;AACvE,SAAO,WAAW;AACpB;","names":[]}
@@ -1,367 +1,12 @@
1
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
- }
2
+ getNodeLokiLoggingRuntime,
3
+ getNodeOpenTelemetryLoggingRuntime,
4
+ initNodeLokiLogging,
5
+ initNodeOpenTelemetryLogging,
6
+ parseOtlpHeaders,
7
+ resolveLogsEndpoint
8
+ } from "../chunk-5JNEZEKV.js";
9
+ import "../chunk-XU4XX7SD.js";
365
10
  export {
366
11
  getNodeLokiLoggingRuntime,
367
12
  getNodeOpenTelemetryLoggingRuntime,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/logging/node.ts","../../src/logging/node/loki.ts"],"sourcesContent":["import { logs } from \"@opentelemetry/api-logs\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\n\nexport {\n getNodeLokiLoggingRuntime,\n initNodeLokiLogging,\n} from \"./node/loki.js\";\nexport type {\n NodeLokiLoggingOptions,\n NodeLokiLoggingRuntime,\n} from \"./node/loki.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\n\nexport type NodeOpenTelemetryLoggingOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n\n /**\n * Explicit endpoint override. Intended mainly for tests.\n *\n * Production normally uses:\n * OTEL_EXPORTER_OTLP_LOGS_ENDPOINT\n * or:\n * OTEL_EXPORTER_OTLP_ENDPOINT\n */\n endpoint?: string;\n\n /** Optional explicit OTLP headers override. */\n headers?: Record<string, string>;\n\n /** Test-only switch. Production should rely on environment configuration. */\n forceEnable?: boolean;\n};\n\nexport type NodeOpenTelemetryLoggingRuntime = {\n enabled: boolean;\n endpoint: string | null;\n forceFlush: () => Promise<void>;\n shutdown: () => Promise<void>;\n};\n\nlet runtime: NodeOpenTelemetryLoggingRuntime | null = null;\n\nfunction requireNonEmpty(value: string, field: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${field} must be a non-empty string`);\n }\n return normalized;\n}\n\n/**\n * Installs the Node OpenTelemetry LoggerProvider exactly once for this process.\n *\n * IMPORTANT:\n * This module is Node-only. Import and initialize it from the process bootstrap\n * before framework/application modules are loaded. Do not import this module\n * through Vite/React/browser bundles.\n *\n * Application modules should import only:\n *\n * @modainteract/moda-interact-shared/logging\n *\n * Once this provider is installed, createLogger(...) automatically fans safe\n * canonical log records out to stdout and the OpenTelemetry Logs API.\n *\n * Direct Loki delivery, when desired, is independently enabled with:\n *\n * initNodeLokiLogging(...)\n *\n * from this same Node-only package entry.\n */\nexport function initNodeOpenTelemetryLogging(\n options: NodeOpenTelemetryLoggingOptions,\n): NodeOpenTelemetryLoggingRuntime {\n if (runtime) {\n return runtime;\n }\n\n const serviceName = requireNonEmpty(options.serviceName, \"serviceName\");\n const environment = requireNonEmpty(options.environment, \"environment\");\n const serviceNamespace = requireNonEmpty(\n options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE,\n \"serviceNamespace\",\n );\n\n const config = resolveLogsConfig(options);\n\n if (!config.enabled || !config.endpoint) {\n runtime = {\n enabled: false,\n endpoint: null,\n forceFlush: async () => {},\n shutdown: async () => {},\n };\n return runtime;\n }\n\n const exporter = new OTLPLogExporter({\n url: config.endpoint,\n headers: config.headers,\n concurrencyLimit: envInt(\"OTEL_LOG_EXPORT_CONCURRENCY_LIMIT\", 2),\n });\n\n const processor = new BatchLogRecordProcessor({\n exporter,\n maxQueueSize: envInt(\"OTEL_BLRP_MAX_QUEUE_SIZE\", 2048),\n maxExportBatchSize: envInt(\"OTEL_BLRP_MAX_EXPORT_BATCH_SIZE\", 512),\n scheduledDelayMillis: envInt(\"OTEL_BLRP_SCHEDULE_DELAY\", 1_000),\n exportTimeoutMillis: envInt(\"OTEL_BLRP_EXPORT_TIMEOUT\", 30_000),\n });\n\n const provider = new LoggerProvider({\n resource: resourceFromAttributes({\n \"service.namespace\": serviceNamespace,\n \"service.name\": serviceName,\n \"deployment.environment.name\": environment,\n }),\n processors: [processor],\n logRecordLimits: {\n attributeCountLimit: 64,\n attributeValueLengthLimit: 4096,\n },\n });\n\n logs.setGlobalLoggerProvider(provider);\n\n runtime = {\n enabled: true,\n endpoint: config.endpoint,\n forceFlush: async () => {\n try {\n await provider.forceFlush();\n } catch {\n // Telemetry delivery is best-effort.\n }\n },\n shutdown: async () => {\n try {\n await provider.shutdown();\n } catch {\n // Telemetry shutdown is best-effort.\n }\n },\n };\n\n return runtime;\n}\n\nexport function getNodeOpenTelemetryLoggingRuntime():\n | NodeOpenTelemetryLoggingRuntime\n | null {\n return runtime;\n}\n\nexport function resolveLogsEndpoint(\n explicitEndpoint?: string,\n): string | undefined {\n const explicit = nonEmpty(explicitEndpoint);\n if (explicit) {\n return explicit;\n }\n\n const signalEndpoint = envString(\"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT\");\n if (signalEndpoint) {\n return signalEndpoint;\n }\n\n const genericEndpoint = envString(\"OTEL_EXPORTER_OTLP_ENDPOINT\");\n if (!genericEndpoint) {\n return undefined;\n }\n\n const normalized = genericEndpoint.replace(/\\/+$/, \"\");\n if (normalized.endsWith(\"/v1/logs\")) {\n return normalized;\n }\n return `${normalized}/v1/logs`;\n}\n\nexport function parseOtlpHeaders(value: string | undefined):\n | Record<string, string>\n | undefined {\n const raw = nonEmpty(value);\n if (!raw) {\n return undefined;\n }\n\n const headers: Record<string, string> = {};\n\n for (const part of raw.split(\",\")) {\n const separator = part.indexOf(\"=\");\n if (separator <= 0) {\n continue;\n }\n\n const rawKey = part.slice(0, separator).trim();\n const rawValue = part.slice(separator + 1).trim();\n\n if (!rawKey) {\n continue;\n }\n\n headers[safeDecodeURIComponent(rawKey)] =\n safeDecodeURIComponent(rawValue);\n }\n\n return Object.keys(headers).length > 0 ? headers : undefined;\n}\n\nfunction resolveLogsConfig(options: NodeOpenTelemetryLoggingOptions) {\n if (!options.forceEnable && envBool(\"OTEL_SDK_DISABLED\") === true) {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const exporterName = envString(\"OTEL_LOGS_EXPORTER\") ?? \"otlp\";\n\n if (!options.forceEnable && exporterName !== \"otlp\") {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const endpoint = resolveLogsEndpoint(options.endpoint);\n\n if (!endpoint) {\n return {\n enabled: false,\n endpoint: undefined,\n headers: undefined,\n };\n }\n\n const headers =\n options.headers ??\n parseOtlpHeaders(\n envString(\"OTEL_EXPORTER_OTLP_LOGS_HEADERS\") ??\n envString(\"OTEL_EXPORTER_OTLP_HEADERS\"),\n );\n\n return {\n enabled: true,\n endpoint,\n headers,\n };\n}\n\nfunction envString(name: string): string | undefined {\n return nonEmpty(process.env[name]);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction envBool(name: string): boolean | undefined {\n const value = envString(name);\n if (value === undefined) {\n return undefined;\n }\n return [\"1\", \"true\", \"yes\", \"on\"].includes(value.toLowerCase());\n}\n\nfunction envInt(name: string, fallback: number): number {\n const value = envString(name);\n if (value === undefined) {\n return fallback;\n }\n\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0\n ? Math.floor(parsed)\n : fallback;\n}\n\nfunction safeDecodeURIComponent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n","import {\n createLogger as createWinstonLogger,\n format,\n type Logger as WinstonLogger,\n} from \"winston\";\nimport LokiTransport from \"winston-loki\";\nimport { setLokiLogEmitter } from \"../loki.js\";\nimport type { LogRecord } from \"../types.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\n\nexport type NodeLokiLoggingOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n\n /**\n * Explicit Loki base URL.\n *\n * Production normally supplies LOKI_URL.\n */\n host?: string;\n\n /**\n * Optional Basic Auth override in `username:password` format.\n *\n * Prefer separate LOKI_USERNAME/LOKI_PASSWORD environment variables in\n * deployed services so the composed value never needs to be committed.\n */\n basicAuth?: string;\n\n /**\n * Optional additional static headers.\n *\n * Never include these in log payloads or error messages.\n */\n headers?: Record<string, string>;\n\n batching?: boolean;\n intervalSeconds?: number;\n timeoutMs?: number;\n clearOnError?: boolean;\n\n /**\n * Test-only override. In normal service bootstrap, the presence of a Loki\n * host is sufficient to enable the transport.\n */\n forceEnable?: boolean;\n};\n\nexport type NodeLokiLoggingRuntime = {\n enabled: boolean;\n host: string | null;\n labels: Readonly<Record<string, string>>;\n forceFlush: () => Promise<void>;\n shutdown: () => Promise<void>;\n};\n\nlet runtime: NodeLokiLoggingRuntime | null = null;\n\nfunction requireNonEmpty(value: string, field: string): string {\n const normalized = value.trim();\n if (!normalized) {\n throw new Error(`${field} must be a non-empty string`);\n }\n return normalized;\n}\n\n/**\n * Installs the direct Grafana Loki destination exactly once per Node process.\n *\n * IMPORTANT:\n * - This module is Node-only.\n * - It is reached through @modainteract/moda-interact-shared/logging/node.\n * - Do not import it from Vite/browser/application module graphs.\n * - The normal ./logging entry contains no Winston/winston-loki imports.\n */\nexport function initNodeLokiLogging(\n options: NodeLokiLoggingOptions,\n): NodeLokiLoggingRuntime {\n if (runtime) {\n return runtime;\n }\n\n const serviceName = requireNonEmpty(options.serviceName, \"serviceName\");\n const environment = requireNonEmpty(options.environment, \"environment\");\n const serviceNamespace = requireNonEmpty(\n options.serviceNamespace ?? DEFAULT_SERVICE_NAMESPACE,\n \"serviceNamespace\",\n );\n\n const host = nonEmpty(options.host) ?? envString(\"LOKI_URL\");\n const enabled =\n options.forceEnable === true ||\n (envBool(\"LOKI_ENABLED\") !== false && host !== undefined);\n\n const labels = Object.freeze({\n service_namespace: serviceNamespace,\n service_name: serviceName,\n environment,\n });\n\n if (!enabled || !host) {\n setLokiLogEmitter(null);\n\n runtime = {\n enabled: false,\n host: null,\n labels,\n forceFlush: async () => {},\n shutdown: async () => {},\n };\n\n return runtime;\n }\n\n const basicAuth =\n nonEmpty(options.basicAuth) ??\n buildBasicAuth(\n envString(\"LOKI_USERNAME\"),\n envString(\"LOKI_PASSWORD\"),\n );\n\n const transport = new LokiTransport({\n host,\n json: true,\n batching: options.batching ?? envBool(\"LOKI_BATCHING\") ?? true,\n interval:\n options.intervalSeconds ??\n envInt(\"LOKI_BATCH_INTERVAL_SECONDS\", 5),\n timeout:\n options.timeoutMs ??\n envInt(\"LOKI_TIMEOUT_MS\", 10_000),\n\n // Prefer bounded loss to unbounded memory growth when the logging backend\n // is unavailable. Loki must never become an application dependency.\n clearOnError:\n options.clearOnError ??\n envBool(\"LOKI_CLEAR_ON_ERROR\") ??\n true,\n\n // Current winston-loki guidance recommends keeping timestamp replacement\n // enabled unless there is a specific reason not to.\n replaceTimestamp: true,\n\n // Moda owns flush/shutdown explicitly from the process bootstrap.\n gracefulShutdown: false,\n\n // Critical cardinality rule: only these architecture-approved static\n // labels may be indexed. Never promote Winston meta/data to labels.\n labels,\n useWinstonMetaAsLabels: false,\n\n ...(basicAuth ? { basicAuth } : {}),\n ...(options.headers ? { headers: options.headers } : {}),\n\n // Do not recursively log Loki connection errors through the shared logger.\n // Doing so could create a failure loop while Loki is unavailable.\n onConnectionError: () => {},\n });\n\n const lokiLogger = createWinstonLogger({\n level: \"debug\",\n format: format.json(),\n transports: [transport],\n exitOnError: false,\n });\n\n setLokiLogEmitter((record) => {\n writeCanonicalRecord(lokiLogger, record);\n });\n\n runtime = {\n enabled: true,\n host,\n labels,\n forceFlush: async () => {\n await safeFlush(transport);\n },\n shutdown: async () => {\n setLokiLogEmitter(null);\n await safeFlush(transport);\n await safeCloseTransport(transport);\n\n try {\n lokiLogger.close();\n } catch {\n // best-effort\n }\n },\n };\n\n return runtime;\n}\n\nexport function getNodeLokiLoggingRuntime():\n | NodeLokiLoggingRuntime\n | null {\n return runtime;\n}\n\nfunction writeCanonicalRecord(\n logger: WinstonLogger,\n record: LogRecord,\n): void {\n try {\n // Winston requires a `message`; use the canonical event name.\n //\n // Every other value comes from the already-sanitized immutable Moda\n // LogRecord. Because useWinstonMetaAsLabels=false, these values remain in\n // the JSON log line rather than becoming Loki stream labels.\n logger.log({\n ...record,\n level: record.level,\n message: record.event,\n });\n } catch {\n // Direct Loki emission must never escape to business code.\n }\n}\n\nasync function safeFlush(transport: LokiTransport): Promise<void> {\n try {\n await transport.flush();\n } catch {\n // Logs may be dropped. Business processing must continue.\n }\n}\n\nasync function safeCloseTransport(\n transport: LokiTransport,\n): Promise<void> {\n try {\n const closeable = transport as LokiTransport & {\n close?: () => unknown | Promise<unknown>;\n };\n\n if (typeof closeable.close === \"function\") {\n await Promise.resolve(closeable.close());\n }\n } catch {\n // best-effort shutdown\n }\n}\n\nfunction buildBasicAuth(\n username: string | undefined,\n password: string | undefined,\n): string | undefined {\n if (!username || !password) {\n return undefined;\n }\n\n return `${username}:${password}`;\n}\n\nfunction envString(name: string): string | undefined {\n return nonEmpty(process.env[name]);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n const normalized = value.trim();\n return normalized.length > 0 ? normalized : undefined;\n}\n\nfunction envBool(name: string): boolean | undefined {\n const value = envString(name);\n\n if (value === undefined) {\n return undefined;\n }\n\n const normalized = value.toLowerCase();\n\n if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) {\n return true;\n }\n\n if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) {\n return false;\n }\n\n return undefined;\n}\n\nfunction envInt(name: string, fallback: number): number {\n const value = envString(name);\n\n if (value === undefined) {\n return fallback;\n }\n\n const parsed = Number(value);\n\n return Number.isFinite(parsed) && parsed > 0\n ? Math.floor(parsed)\n : fallback;\n}\n"],"mappings":"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","names":["DEFAULT_SERVICE_NAMESPACE","runtime","requireNonEmpty","envInt","nonEmpty","envString","envBool"]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ import { BullMQOtel } from 'bullmq-otel';
2
+
3
+ type BullMQTelemetryOptions = {
4
+ serviceName: string;
5
+ version?: string;
6
+ enableMetrics?: boolean;
7
+ };
8
+ declare function createBullMQTelemetry(options: BullMQTelemetryOptions): BullMQOtel;
9
+
10
+ export { type BullMQTelemetryOptions, createBullMQTelemetry };
@@ -0,0 +1,14 @@
1
+ // src/observability/bullmq.ts
2
+ import { BullMQOtel } from "bullmq-otel";
3
+ function createBullMQTelemetry(options) {
4
+ return new BullMQOtel({
5
+ tracerName: options.serviceName,
6
+ meterName: options.serviceName,
7
+ version: options.version ?? "unknown",
8
+ enableMetrics: options.enableMetrics ?? true
9
+ });
10
+ }
11
+ export {
12
+ createBullMQTelemetry
13
+ };
14
+ //# sourceMappingURL=bullmq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/observability/bullmq.ts"],"sourcesContent":["import { BullMQOtel } from \"bullmq-otel\";\n\nexport type BullMQTelemetryOptions = {\n serviceName: string;\n version?: string;\n enableMetrics?: boolean;\n};\n\nexport function createBullMQTelemetry(\n options: BullMQTelemetryOptions,\n): BullMQOtel {\n return new BullMQOtel({\n tracerName: options.serviceName,\n meterName: options.serviceName,\n version: options.version ?? \"unknown\",\n enableMetrics: options.enableMetrics ?? true,\n });\n}"],"mappings":";AAAA,SAAS,kBAAkB;AAQpB,SAAS,sBACd,SACY;AACZ,SAAO,IAAI,WAAW;AAAA,IACpB,YAAY,QAAQ;AAAA,IACpB,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ,WAAW;AAAA,IAC5B,eAAe,QAAQ,iBAAiB;AAAA,EAC1C,CAAC;AACH;","names":[]}
@@ -0,0 +1,17 @@
1
+ import { ObservedSpanOptions } from './index.js';
2
+ export { SpanExceptionMapper } from './index.js';
3
+ import '@opentelemetry/api';
4
+
5
+ type AgentObservation = {
6
+ agentName: string;
7
+ provider?: string;
8
+ model?: string;
9
+ };
10
+ type GenAIObservationOptions = ObservedSpanOptions & {
11
+ recordMetrics?: boolean;
12
+ };
13
+ declare function observeConversationTurn<T>(channel: "whatsapp" | "other", work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
14
+ declare function observeAgentInvocation<T>(observation: AgentObservation, work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
15
+ declare function observeAgentTool<T>(toolName: string, work: () => Promise<T>, options?: GenAIObservationOptions): Promise<T>;
16
+
17
+ export { type AgentObservation, type GenAIObservationOptions, ObservedSpanOptions, observeAgentInvocation, observeAgentTool, observeConversationTurn };