@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,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
@@ -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,34 @@
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
+ function getActiveTraceId() {
9
+ return trace.getSpan(context.active())?.spanContext().traceId;
10
+ }
11
+ async function withObservedSpan(name, attributes, work) {
12
+ const tracer = trace.getTracer(TRACER_NAME);
13
+ return tracer.startActiveSpan(name, { attributes }, async (span) => {
14
+ try {
15
+ const result = await work();
16
+ span.setStatus({ code: SpanStatusCode.OK });
17
+ return result;
18
+ } catch (error) {
19
+ span.recordException(
20
+ error instanceof Error ? error : new Error(String(error))
21
+ );
22
+ span.setStatus({ code: SpanStatusCode.ERROR });
23
+ throw error;
24
+ } finally {
25
+ span.end();
26
+ }
27
+ });
28
+ }
29
+
30
+ export {
31
+ getActiveTraceId,
32
+ withObservedSpan
33
+ };
34
+ //# sourceMappingURL=chunk-XH6XHKXC.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} from \"@opentelemetry/api\";\n\nconst TRACER_NAME = \"@modainteract/moda-interact-shared/observability\";\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): 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 span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n });\n}"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,IAAM,cAAc;AAEb,SAAS,mBAAuC;AACrD,SAAO,MAAM,QAAQ,QAAQ,OAAO,CAAC,GAAG,YAAY,EAAE;AACxD;AAEA,eAAsB,iBACpB,MACA,YACA,MACY;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,WAAK;AAAA,QACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MAC1D;AACA,WAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;AAC7C,YAAM;AAAA,IACR,UAAE;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,27 @@
1
+ // src/logging/loki.ts
2
+ function globalState() {
3
+ return globalThis;
4
+ }
5
+ function setLokiLogEmitter(emitter) {
6
+ try {
7
+ const state = globalState();
8
+ if (emitter) {
9
+ state.__modaInteractLokiLogEmitterV1 = emitter;
10
+ } else {
11
+ delete state.__modaInteractLokiLogEmitterV1;
12
+ }
13
+ } catch {
14
+ }
15
+ }
16
+ function emitLokiLog(record) {
17
+ try {
18
+ globalState().__modaInteractLokiLogEmitterV1?.(record);
19
+ } catch {
20
+ }
21
+ }
22
+
23
+ export {
24
+ setLokiLogEmitter,
25
+ emitLokiLog
26
+ };
27
+ //# sourceMappingURL=chunk-XU4XX7SD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logging/loki.ts"],"sourcesContent":["import type { LogRecord } from \"./types.js\";\n\nexport type LokiLogEmitter = (record: LogRecord) => void;\n\ntype ModaLoggingGlobal = typeof globalThis & {\n __modaInteractLokiLogEmitterV1?: LokiLogEmitter;\n};\n\nfunction globalState(): ModaLoggingGlobal {\n return globalThis as ModaLoggingGlobal;\n}\n\n/**\n * Installs/removes the Node-owned direct Loki emitter.\n *\n * This bridge deliberately contains no Winston, Loki or Node networking\n * imports so it remains safe inside the normal ./logging entry.\n *\n * globalThis is required because tsup emits ./logging and ./logging/node as\n * independent bundles. A module-local variable would not be shared between\n * those package entry points.\n */\nexport function setLokiLogEmitter(emitter: LokiLogEmitter | null): void {\n try {\n const state = globalState();\n\n if (emitter) {\n state.__modaInteractLokiLogEmitterV1 = emitter;\n } else {\n delete state.__modaInteractLokiLogEmitterV1;\n }\n } catch {\n // Logging configuration must never become a correctness dependency.\n }\n}\n\n/**\n * Sends an already-sanitized canonical Moda log record to the optional Loki\n * emitter installed by ./logging/node.\n */\nexport function emitLokiLog(record: LogRecord): void {\n try {\n globalState().__modaInteractLokiLogEmitterV1?.(record);\n } catch {\n // Direct Loki delivery is always best-effort.\n }\n}\n"],"mappings":";AAQA,SAAS,cAAiC;AACxC,SAAO;AACT;AAYO,SAAS,kBAAkB,SAAsC;AACtE,MAAI;AACF,UAAM,QAAQ,YAAY;AAE1B,QAAI,SAAS;AACX,YAAM,iCAAiC;AAAA,IACzC,OAAO;AACL,aAAO,MAAM;AAAA,IACf;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,YAAY,QAAyB;AACnD,MAAI;AACF,gBAAY,EAAE,iCAAiC,MAAM;AAAA,EACvD,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -1,3 +1,38 @@
1
+ import {
2
+ emitLokiLog
3
+ } from "../chunk-XU4XX7SD.js";
4
+
5
+ // src/logging/otel.ts
6
+ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
7
+ var OTEL_LOGGER_SCOPE_NAME = "@modainteract/moda-interact-shared/logging";
8
+ var SEVERITY_BY_LEVEL = Object.freeze({
9
+ debug: SeverityNumber.DEBUG,
10
+ info: SeverityNumber.INFO,
11
+ warn: SeverityNumber.WARN,
12
+ error: SeverityNumber.ERROR
13
+ });
14
+ function emitOpenTelemetryLog(record) {
15
+ try {
16
+ const logger = logs.getLogger(OTEL_LOGGER_SCOPE_NAME);
17
+ logger.emit({
18
+ eventName: record.event,
19
+ timestamp: new Date(record.timestamp),
20
+ severityNumber: SEVERITY_BY_LEVEL[record.level],
21
+ severityText: record.level.toUpperCase(),
22
+ // Keep the OTLP body identical to the structured stdout record.
23
+ body: JSON.stringify(record),
24
+ attributes: {
25
+ "event.name": record.event,
26
+ "log.level": record.level,
27
+ "service.namespace": record["service.namespace"],
28
+ "service.name": record["service.name"],
29
+ "deployment.environment.name": record["deployment.environment.name"]
30
+ }
31
+ });
32
+ } catch {
33
+ }
34
+ }
35
+
1
36
  // src/logging/redaction.ts
2
37
  var REDACTED = "[REDACTED]";
3
38
  var CIRCULAR = "[Circular]";
@@ -156,10 +191,11 @@ function createLogger(options) {
156
191
  const initialBaseFields = { ...options.baseFields ?? {} };
157
192
  function buildLogger(baseFields) {
158
193
  function emit(level, event, fields = {}) {
194
+ let record;
159
195
  try {
160
196
  const eventName = event.trim() || "unknown";
161
197
  const sanitizedData = sanitizeLogFields({ ...baseFields, ...fields });
162
- const record = Object.freeze({
198
+ record = Object.freeze({
163
199
  timestamp: safeTimestamp(now),
164
200
  level,
165
201
  event: eventName,
@@ -168,9 +204,15 @@ function createLogger(options) {
168
204
  "deployment.environment.name": environment,
169
205
  ...Object.keys(sanitizedData).length > 0 ? { data: sanitizedData } : {}
170
206
  });
207
+ } catch {
208
+ return;
209
+ }
210
+ try {
171
211
  sink(record);
172
212
  } catch {
173
213
  }
214
+ emitOpenTelemetryLog(record);
215
+ emitLokiLog(record);
174
216
  }
175
217
  return Object.freeze({
176
218
  debug: (event, fields) => emit("debug", event, fields),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/logging/redaction.ts","../../src/logging/logger.ts","../../src/logging/types.ts"],"sourcesContent":["export const REDACTED = \"[REDACTED]\";\nexport const CIRCULAR = \"[Circular]\";\nexport const MAX_DEPTH_REACHED = \"[MaxDepth]\";\nexport const TRUNCATED = \"[Truncated]\";\n\nexport const LOG_VALUE_LIMITS = Object.freeze({\n maxDepth: 6,\n maxArrayItems: 50,\n maxObjectKeys: 100,\n maxStringLength: 4096,\n});\n\nconst SENSITIVE_EXACT_KEYS = new Set([\n \"authorization\",\n \"cookie\",\n \"setcookie\",\n \"password\",\n \"passwd\",\n \"apikey\",\n \"privatekey\",\n \"cvv\",\n \"cvc\",\n \"cardnumber\",\n \"creditcard\",\n \"firstname\",\n \"lastname\",\n]);\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\nexport function isSensitiveLogKey(key: string): boolean {\n const normalized = normalizeKey(key);\n\n return (\n SENSITIVE_EXACT_KEYS.has(normalized) ||\n normalized.includes(\"secret\") ||\n normalized.endsWith(\"token\") ||\n normalized.includes(\"email\") ||\n normalized.includes(\"phone\") ||\n normalized.endsWith(\"address\")\n );\n}\n\nfunction sanitizeString(value: string): string {\n const scrubbed = value\n .replace(/\\bBearer\\s+[^\\s,;]+/gi, `Bearer ${REDACTED}`)\n .replace(\n /([?&](?:access_token|refresh_token|verify_token|client_secret|api_key|token|password)=)[^&#\\s]*/gi,\n `$1${REDACTED}`,\n );\n\n if (scrubbed.length <= LOG_VALUE_LIMITS.maxStringLength) {\n return scrubbed;\n }\n\n return `${scrubbed.slice(0, LOG_VALUE_LIMITS.maxStringLength)}…${TRUNCATED}`;\n}\n\nfunction sanitizeError(error: Error): Record<string, unknown> {\n return {\n name: sanitizeString(error.name),\n message: sanitizeString(error.message),\n };\n}\n\nfunction sanitizeValue(\n value: unknown,\n depth: number,\n activeObjects: WeakSet<object>,\n): unknown {\n if (value === null || value === undefined) {\n return value ?? null;\n }\n\n if (typeof value === \"string\") {\n return sanitizeString(value);\n }\n\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : String(value);\n }\n\n if (typeof value === \"boolean\") {\n return value;\n }\n\n if (typeof value === \"bigint\") {\n return value.toString();\n }\n\n if (typeof value === \"symbol\" || typeof value === \"function\") {\n return `[${typeof value}]`;\n }\n\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n }\n\n if (value instanceof Error) {\n return sanitizeError(value);\n }\n\n if (depth >= LOG_VALUE_LIMITS.maxDepth) {\n return MAX_DEPTH_REACHED;\n }\n\n if (typeof value !== \"object\") {\n return String(value);\n }\n\n if (activeObjects.has(value)) {\n return CIRCULAR;\n }\n\n activeObjects.add(value);\n\n try {\n if (Array.isArray(value)) {\n const result = value\n .slice(0, LOG_VALUE_LIMITS.maxArrayItems)\n .map((item) => sanitizeValue(item, depth + 1, activeObjects));\n\n if (value.length > LOG_VALUE_LIMITS.maxArrayItems) {\n result.push(TRUNCATED);\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n const entries = Object.entries(value).slice(0, LOG_VALUE_LIMITS.maxObjectKeys);\n\n for (const [key, item] of entries) {\n result[key] = isSensitiveLogKey(key)\n ? REDACTED\n : sanitizeValue(item, depth + 1, activeObjects);\n }\n\n if (Object.keys(value).length > LOG_VALUE_LIMITS.maxObjectKeys) {\n result.__truncated__ = true;\n }\n\n return result;\n } finally {\n activeObjects.delete(value);\n }\n}\n\nexport function sanitizeLogFields(\n fields: Readonly<Record<string, unknown>>,\n): Record<string, unknown> {\n return sanitizeValue(fields, 0, new WeakSet<object>()) as Record<string, unknown>;\n}\n","import { sanitizeLogFields } from \"./redaction.js\";\nimport type {\n LogFields,\n LogLevel,\n LogRecord,\n LogSink,\n LoggerOptions,\n StructuredLogger,\n} from \"./types.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\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\nfunction defaultSink(record: LogRecord): void {\n const line = JSON.stringify(record);\n\n switch (record.level) {\n case \"debug\":\n console.debug(line);\n break;\n case \"warn\":\n console.warn(line);\n break;\n case \"error\":\n console.error(line);\n break;\n case \"info\":\n default:\n console.info(line);\n break;\n }\n}\n\nfunction safeTimestamp(now: () => Date): string {\n try {\n const value = now();\n if (value instanceof Date && !Number.isNaN(value.getTime())) {\n return value.toISOString();\n }\n } catch {\n // Logging must not become a correctness dependency.\n }\n\n return new Date().toISOString();\n}\n\nexport function createLogger(options: LoggerOptions): StructuredLogger {\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 const sink: LogSink = options.sink ?? defaultSink;\n const now = options.now ?? (() => new Date());\n const initialBaseFields = { ...(options.baseFields ?? {}) };\n\n function buildLogger(baseFields: Record<string, unknown>): StructuredLogger {\n function emit(level: LogLevel, event: string, fields: LogFields = {}): void {\n try {\n const eventName = event.trim() || \"unknown\";\n const sanitizedData = sanitizeLogFields({ ...baseFields, ...fields });\n\n const record: LogRecord = Object.freeze({\n timestamp: safeTimestamp(now),\n level,\n event: eventName,\n \"service.namespace\": serviceNamespace,\n \"service.name\": serviceName,\n \"deployment.environment.name\": environment,\n ...(Object.keys(sanitizedData).length > 0 ? { data: sanitizedData } : {}),\n });\n\n sink(record);\n } catch {\n // Logging is best-effort and must never alter business correctness.\n }\n }\n\n return Object.freeze({\n debug: (event: string, fields?: LogFields) => emit(\"debug\", event, fields),\n info: (event: string, fields?: LogFields) => emit(\"info\", event, fields),\n warn: (event: string, fields?: LogFields) => emit(\"warn\", event, fields),\n error: (event: string, fields?: LogFields) => emit(\"error\", event, fields),\n child: (fields: LogFields) => buildLogger({ ...baseFields, ...fields }),\n });\n }\n\n return buildLogger(initialBaseFields);\n}\n","export const LOG_LEVELS = [\"debug\", \"info\", \"warn\", \"error\"] as const;\n\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nexport type LogFields = Readonly<Record<string, unknown>>;\n\nexport type LogRecord = Readonly<{\n timestamp: string;\n level: LogLevel;\n event: string;\n \"service.namespace\": string;\n \"service.name\": string;\n \"deployment.environment.name\": string;\n data?: Readonly<Record<string, unknown>>;\n}>;\n\nexport type LogSink = (record: LogRecord) => void;\n\nexport type LoggerOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n baseFields?: LogFields;\n sink?: LogSink;\n now?: () => Date;\n};\n\nexport interface StructuredLogger {\n debug(event: string, fields?: LogFields): void;\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n error(event: string, fields?: LogFields): void;\n child(fields: LogFields): StructuredLogger;\n}\n"],"mappings":"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","names":["result"]}
1
+ {"version":3,"sources":["../../src/logging/otel.ts","../../src/logging/redaction.ts","../../src/logging/logger.ts","../../src/logging/types.ts"],"sourcesContent":["import { logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport type { LogLevel, LogRecord } from \"./types.js\";\n\nconst OTEL_LOGGER_SCOPE_NAME = \"@modainteract/moda-interact-shared/logging\";\n\nconst SEVERITY_BY_LEVEL: Readonly<Record<LogLevel, SeverityNumber>> = Object.freeze({\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n});\n\n/**\n * Emits the already-sanitized canonical Moda log record through the global\n * OpenTelemetry Logs API.\n *\n * If no LoggerProvider has been installed, the OpenTelemetry Logs API is a\n * no-op. This is intentional: stdout logging remains available in local/test\n * environments without an OTLP backend.\n *\n * The OpenTelemetry SDK automatically associates emitted records with the\n * active Context when a tracing context manager/provider is present. This is\n * how logs become trace/span-correlated without the shared logger depending on\n * a service-specific tracing SDK.\n */\nexport function emitOpenTelemetryLog(record: LogRecord): void {\n try {\n const logger = logs.getLogger(OTEL_LOGGER_SCOPE_NAME);\n\n logger.emit({\n eventName: record.event,\n timestamp: new Date(record.timestamp),\n severityNumber: SEVERITY_BY_LEVEL[record.level],\n severityText: record.level.toUpperCase(),\n // Keep the OTLP body identical to the structured stdout record.\n body: JSON.stringify(record),\n attributes: {\n \"event.name\": record.event,\n \"log.level\": record.level,\n \"service.namespace\": record[\"service.namespace\"],\n \"service.name\": record[\"service.name\"],\n \"deployment.environment.name\":\n record[\"deployment.environment.name\"],\n },\n });\n } catch {\n // OpenTelemetry is best-effort. It must never change business correctness.\n }\n}\n","export const REDACTED = \"[REDACTED]\";\nexport const CIRCULAR = \"[Circular]\";\nexport const MAX_DEPTH_REACHED = \"[MaxDepth]\";\nexport const TRUNCATED = \"[Truncated]\";\n\nexport const LOG_VALUE_LIMITS = Object.freeze({\n maxDepth: 6,\n maxArrayItems: 50,\n maxObjectKeys: 100,\n maxStringLength: 4096,\n});\n\nconst SENSITIVE_EXACT_KEYS = new Set([\n \"authorization\",\n \"cookie\",\n \"setcookie\",\n \"password\",\n \"passwd\",\n \"apikey\",\n \"privatekey\",\n \"cvv\",\n \"cvc\",\n \"cardnumber\",\n \"creditcard\",\n \"firstname\",\n \"lastname\",\n]);\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\nexport function isSensitiveLogKey(key: string): boolean {\n const normalized = normalizeKey(key);\n\n return (\n SENSITIVE_EXACT_KEYS.has(normalized) ||\n normalized.includes(\"secret\") ||\n normalized.endsWith(\"token\") ||\n normalized.includes(\"email\") ||\n normalized.includes(\"phone\") ||\n normalized.endsWith(\"address\")\n );\n}\n\nfunction sanitizeString(value: string): string {\n const scrubbed = value\n .replace(/\\bBearer\\s+[^\\s,;]+/gi, `Bearer ${REDACTED}`)\n .replace(\n /([?&](?:access_token|refresh_token|verify_token|client_secret|api_key|token|password)=)[^&#\\s]*/gi,\n `$1${REDACTED}`,\n );\n\n if (scrubbed.length <= LOG_VALUE_LIMITS.maxStringLength) {\n return scrubbed;\n }\n\n return `${scrubbed.slice(0, LOG_VALUE_LIMITS.maxStringLength)}…${TRUNCATED}`;\n}\n\nfunction sanitizeError(error: Error): Record<string, unknown> {\n return {\n name: sanitizeString(error.name),\n message: sanitizeString(error.message),\n };\n}\n\nfunction sanitizeValue(\n value: unknown,\n depth: number,\n activeObjects: WeakSet<object>,\n): unknown {\n if (value === null || value === undefined) {\n return value ?? null;\n }\n\n if (typeof value === \"string\") {\n return sanitizeString(value);\n }\n\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : String(value);\n }\n\n if (typeof value === \"boolean\") {\n return value;\n }\n\n if (typeof value === \"bigint\") {\n return value.toString();\n }\n\n if (typeof value === \"symbol\" || typeof value === \"function\") {\n return `[${typeof value}]`;\n }\n\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n }\n\n if (value instanceof Error) {\n return sanitizeError(value);\n }\n\n if (depth >= LOG_VALUE_LIMITS.maxDepth) {\n return MAX_DEPTH_REACHED;\n }\n\n if (typeof value !== \"object\") {\n return String(value);\n }\n\n if (activeObjects.has(value)) {\n return CIRCULAR;\n }\n\n activeObjects.add(value);\n\n try {\n if (Array.isArray(value)) {\n const result = value\n .slice(0, LOG_VALUE_LIMITS.maxArrayItems)\n .map((item) => sanitizeValue(item, depth + 1, activeObjects));\n\n if (value.length > LOG_VALUE_LIMITS.maxArrayItems) {\n result.push(TRUNCATED);\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n const entries = Object.entries(value).slice(0, LOG_VALUE_LIMITS.maxObjectKeys);\n\n for (const [key, item] of entries) {\n result[key] = isSensitiveLogKey(key)\n ? REDACTED\n : sanitizeValue(item, depth + 1, activeObjects);\n }\n\n if (Object.keys(value).length > LOG_VALUE_LIMITS.maxObjectKeys) {\n result.__truncated__ = true;\n }\n\n return result;\n } finally {\n activeObjects.delete(value);\n }\n}\n\nexport function sanitizeLogFields(\n fields: Readonly<Record<string, unknown>>,\n): Record<string, unknown> {\n return sanitizeValue(fields, 0, new WeakSet<object>()) as Record<string, unknown>;\n}\n","import { emitLokiLog } from \"./loki.js\";\nimport { emitOpenTelemetryLog } from \"./otel.js\";\nimport { sanitizeLogFields } from \"./redaction.js\";\nimport type {\n LogFields,\n LogLevel,\n LogRecord,\n LogSink,\n LoggerOptions,\n StructuredLogger,\n} from \"./types.js\";\n\nconst DEFAULT_SERVICE_NAMESPACE = \"moda-interact\";\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\nfunction defaultSink(record: LogRecord): void {\n const line = JSON.stringify(record);\n\n switch (record.level) {\n case \"debug\":\n console.debug(line);\n break;\n case \"warn\":\n console.warn(line);\n break;\n case \"error\":\n console.error(line);\n break;\n case \"info\":\n default:\n console.info(line);\n break;\n }\n}\n\nfunction safeTimestamp(now: () => Date): string {\n try {\n const value = now();\n if (value instanceof Date && !Number.isNaN(value.getTime())) {\n return value.toISOString();\n }\n } catch {\n // Logging must not become a correctness dependency.\n }\n\n return new Date().toISOString();\n}\n\nexport function createLogger(options: LoggerOptions): StructuredLogger {\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 const sink: LogSink = options.sink ?? defaultSink;\n const now = options.now ?? (() => new Date());\n const initialBaseFields = { ...(options.baseFields ?? {}) };\n\n function buildLogger(baseFields: Record<string, unknown>): StructuredLogger {\n function emit(level: LogLevel, event: string, fields: LogFields = {}): void {\n let record: LogRecord;\n\n try {\n const eventName = event.trim() || \"unknown\";\n const sanitizedData = sanitizeLogFields({ ...baseFields, ...fields });\n\n record = Object.freeze({\n timestamp: safeTimestamp(now),\n level,\n event: eventName,\n \"service.namespace\": serviceNamespace,\n \"service.name\": serviceName,\n \"deployment.environment.name\": environment,\n ...(Object.keys(sanitizedData).length > 0\n ? { data: sanitizedData }\n : {}),\n });\n } catch {\n // Sanitization/record construction must never affect business logic.\n return;\n }\n\n // Each destination is deliberately isolated. Failure of one destination\n // must not suppress another destination and must never escape to business\n // code.\n try {\n sink(record);\n } catch {\n // best-effort stdout/custom sink\n }\n\n emitOpenTelemetryLog(record);\n emitLokiLog(record);\n }\n\n return Object.freeze({\n debug: (event: string, fields?: LogFields) => emit(\"debug\", event, fields),\n info: (event: string, fields?: LogFields) => emit(\"info\", event, fields),\n warn: (event: string, fields?: LogFields) => emit(\"warn\", event, fields),\n error: (event: string, fields?: LogFields) => emit(\"error\", event, fields),\n child: (fields: LogFields) => buildLogger({ ...baseFields, ...fields }),\n });\n }\n\n return buildLogger(initialBaseFields);\n}\n","export const LOG_LEVELS = [\"debug\", \"info\", \"warn\", \"error\"] as const;\n\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nexport type LogFields = Readonly<Record<string, unknown>>;\n\nexport type LogRecord = Readonly<{\n timestamp: string;\n level: LogLevel;\n event: string;\n \"service.namespace\": string;\n \"service.name\": string;\n \"deployment.environment.name\": string;\n data?: Readonly<Record<string, unknown>>;\n}>;\n\nexport type LogSink = (record: LogRecord) => void;\n\nexport type LoggerOptions = {\n serviceName: string;\n environment: string;\n serviceNamespace?: string;\n baseFields?: LogFields;\n sink?: LogSink;\n now?: () => Date;\n};\n\nexport interface StructuredLogger {\n debug(event: string, fields?: LogFields): void;\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n error(event: string, fields?: LogFields): void;\n child(fields: LogFields): StructuredLogger;\n}\n"],"mappings":"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","names":["result"]}