@modainteract/moda-interact-shared 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,8 @@ This package does **not** own:
43
43
  - Shopify or Meta SDK integration;
44
44
  - application recovery/business logic;
45
45
  - OpenTelemetry spans or application-specific metrics;
46
- - OTLP transport or observability backends;
46
+ - OpenTelemetry tracing/metrics SDK bootstraps;
47
+ - deployment OTLP endpoint/credential wiring or observability backends;
47
48
  - Render deployment configuration.
48
49
 
49
50
  Those capabilities remain in the repository that owns the corresponding
@@ -302,25 +303,289 @@ traces by default.
302
303
  Logging is best-effort: a logger sink or serialization failure must not alter
303
304
  the success/failure semantics of the business operation being logged.
304
305
 
305
- ## Logging and OpenTelemetry are separate
306
+ ## Logging destinations
306
307
 
307
- The shared logger does **not** automatically create:
308
+ Every call to the shared logger:
308
309
 
309
- - spans;
310
- - counters;
311
- - histograms;
312
- - OTLP exports.
310
+ ```ts
311
+ logger.debug(...)
312
+ logger.info(...)
313
+ logger.warn(...)
314
+ logger.error(...)
315
+ ```
316
+
317
+ emits the same already-sanitized canonical record to up to three independent,
318
+ best-effort destinations:
319
+
320
+ 1. canonical structured JSON to the configured sink (default: console);
321
+ 2. the OpenTelemetry Logs API via the global `LoggerProvider`, if one has been
322
+ installed;
323
+ 3. a direct Grafana Loki transport, if the Node process bootstrap has installed
324
+ the Loki emitter.
325
+
326
+ Each destination is failure-isolated: a failure in one must not suppress
327
+ another and must never affect application/business correctness.
328
+
329
+ The OpenTelemetry API is a no-op when no provider is installed, so stdout
330
+ logging continues to work in local/test environments without any backend.
331
+
332
+ The heavy Node SDK/exporter and the Winston/winston-loki transport are isolated
333
+ in:
334
+
335
+ ```text
336
+ @modainteract/moda-interact-shared/logging/node
337
+ ```
338
+
339
+ and must be imported and initialized only from the Node process bootstrap —
340
+ never through Vite/application/browser bundles.
341
+
342
+ ```text
343
+ application module
344
+ |
345
+ v
346
+ @modainteract/moda-interact-shared/logging
347
+ |
348
+ +--> canonical JSON stdout
349
+ |
350
+ +--> @opentelemetry/api-logs
351
+ | |
352
+ | | global LoggerProvider
353
+ | v
354
+ | ./logging/node
355
+ | |
356
+ | +--> LoggerProvider
357
+ | +--> BatchLogRecordProcessor
358
+ | +--> OTLPLogExporter
359
+ | |
360
+ | v
361
+ | /v1/logs
362
+ |
363
+ +--> lightweight Loki bridge (globalThis emitter)
364
+ |
365
+ v
366
+ ./logging/node
367
+ |
368
+ +--> Winston + winston-loki
369
+ |
370
+ v
371
+ Grafana Loki
372
+ ```
373
+
374
+ Winston and winston-loki exist **only** in the Node-only package graph. The
375
+ normal `./logging` entry contains no Winston, winston-loki or Node networking
376
+ imports.
377
+
378
+ ### Node bootstrap
379
+
380
+ Process bootstrap code installs the optional destinations once:
381
+
382
+ ```ts
383
+ import {
384
+ initNodeLokiLogging,
385
+ initNodeOpenTelemetryLogging,
386
+ } from "@modainteract/moda-interact-shared/logging/node";
387
+
388
+ initNodeLokiLogging({
389
+ serviceName: "moda-interact",
390
+ environment: "production",
391
+ });
392
+
393
+ initNodeOpenTelemetryLogging({
394
+ serviceName: "moda-interact",
395
+ environment: "production",
396
+ });
397
+ ```
398
+
399
+ Each destination is independently configurable, so a deployment may use:
400
+
401
+ ```text
402
+ console only
403
+
404
+ console + Loki
405
+
406
+ console + OTel Logs
407
+
408
+ console + Loki + OTel Logs
409
+ ```
410
+
411
+ Either Node destination may be omitted entirely; without an installed
412
+ provider/emitter the logger remains a safe stdout-only logger.
413
+
414
+ The bootstrap supports the standard OpenTelemetry environment variables:
415
+
416
+ ```text
417
+ OTEL_SDK_DISABLED
418
+
419
+ OTEL_LOGS_EXPORTER
420
+
421
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
422
+ OTEL_EXPORTER_OTLP_ENDPOINT
423
+
424
+ OTEL_EXPORTER_OTLP_LOGS_HEADERS
425
+ OTEL_EXPORTER_OTLP_HEADERS
426
+
427
+ OTEL_BLRP_MAX_QUEUE_SIZE
428
+ OTEL_BLRP_MAX_EXPORT_BATCH_SIZE
429
+ OTEL_BLRP_SCHEDULE_DELAY
430
+ OTEL_BLRP_EXPORT_TIMEOUT
431
+
432
+ OTEL_LOG_EXPORT_CONCURRENCY_LIMIT
433
+ ```
313
434
 
314
- Service-specific OpenTelemetry remains in the owning runtime repository.
435
+ Endpoint rules:
315
436
 
316
- For example:
437
+ ```text
438
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
439
+ exact signal endpoint; do not append /v1/logs
440
+
441
+ OTEL_EXPORTER_OTLP_ENDPOINT
442
+ generic endpoint; append /v1/logs
443
+ ```
444
+
445
+ Moda deliberately does **not** export to the OpenTelemetry default localhost
446
+ endpoint. Without an explicitly configured endpoint the logger remains
447
+ stdout-only.
448
+
449
+ ### Direct Loki transport
450
+
451
+ The direct Loki transport is a lightweight bridge: the normal `./logging` entry
452
+ stores an emitter on `globalThis` and the Node bootstrap installs it. Records
453
+ still pass through the same shared redaction/sanitization used for stdout and
454
+ OpenTelemetry.
455
+
456
+ The Loki environment contract is:
457
+
458
+ ```text
459
+ LOKI_ENABLED
460
+
461
+ LOKI_URL
462
+
463
+ LOKI_USERNAME
464
+ LOKI_PASSWORD
465
+
466
+ LOKI_BATCHING
467
+ LOKI_BATCH_INTERVAL_SECONDS
468
+ LOKI_TIMEOUT_MS
469
+ LOKI_CLEAR_ON_ERROR
470
+ ```
471
+
472
+ Rules:
473
+
474
+ ```text
475
+ no LOKI_URL
476
+ -> direct Loki disabled
477
+
478
+ LOKI_ENABLED=false
479
+ -> direct Loki disabled
480
+
481
+ LOKI_URL present
482
+ -> direct Loki enabled by default
483
+ ```
484
+
485
+ Credentials are Node-bootstrap configuration only. Never log `LOKI_URL`
486
+ credentials, `LOKI_USERNAME`, `LOKI_PASSWORD`, Basic Auth values or connection
487
+ headers.
488
+
489
+ Default transport behavior:
490
+
491
+ ```text
492
+ batching=true
493
+ bounded timeout (LOKI_TIMEOUT_MS)
494
+ clearOnError=true
495
+ replaceTimestamp=true
496
+ gracefulShutdown=false
497
+ ```
498
+
499
+ The owning process bootstrap is responsible for explicit flush/shutdown via the
500
+ returned runtime (`forceFlush`/`shutdown`). If Loki is unavailable, log records
501
+ may be dropped but application processing must continue. Connection errors are
502
+ not recursively written back through the shared logger.
503
+
504
+ ### Loki labels and cardinality
505
+
506
+ The only default Loki labels are the canonical low-cardinality identity:
507
+
508
+ ```text
509
+ service_namespace
510
+ service_name
511
+ environment
512
+ ```
513
+
514
+ Dynamic/high-cardinality identifiers must **not** be promoted to Loki stream
515
+ labels. The implementation explicitly retains
516
+ `useWinstonMetaAsLabels: false`. In particular, do not label by:
517
+
518
+ ```text
519
+ traceId
520
+ spanId
521
+ requestId
522
+ jobId
523
+ deliveryId
524
+ eventId
525
+ recoveryId
526
+ checkoutToken
527
+ cartToken
528
+ shop
529
+ shopDomain
530
+ customerId
531
+ phone
532
+ email
533
+ ```
534
+
535
+ Safe high-cardinality operational identifiers may remain in the sanitized JSON
536
+ log body where justified, but they must never become indexed stream labels.
537
+
538
+ ### Duplicate-delivery warning
539
+
540
+ Do not route the OpenTelemetry Logs destination back into the same Loki
541
+ instance while direct Loki is also enabled, unless duplicate storage is
542
+ intentional:
543
+
544
+ ```text
545
+ direct Loki ON
546
+ +
547
+ OTel Logs ultimately routed to same Loki
548
+ =
549
+ duplicate log storage
550
+ ```
551
+
552
+ The library does not guess backend topology; deployment configuration owns this
553
+ choice.
554
+
555
+ ### Log-signal data safety and correlation
556
+
557
+ - The OpenTelemetry log body is `JSON.stringify` of the same canonical
558
+ sanitized `LogRecord` written to stdout, so the same redaction boundary
559
+ applies to both destinations.
560
+ - Queryable OTel attributes are limited to:
561
+
562
+ ```text
563
+ event.name
564
+ log.level
565
+ service.namespace
566
+ service.name
567
+ deployment.environment.name
568
+ ```
569
+
570
+ - The shared logger never manufactures trace IDs. If an active
571
+ trace/span context exists, the OpenTelemetry Logs SDK associates the emitted
572
+ record with that active context automatically.
573
+ - stdout and OpenTelemetry emission are isolated and best-effort: a failure in
574
+ either must not change application/business correctness.
575
+
576
+ ### Tracing/metrics remain service-specific
577
+
578
+ The shared logger does **not** create spans, counters or histograms.
579
+
580
+ Service-specific OpenTelemetry tracing and metrics remain in the owning runtime
581
+ repository, for example:
317
582
 
318
583
  ```text
319
584
  Shopify operation
320
585
  |
321
- +--> shared structured logger
586
+ +--> shared structured logger (stdout + OTel Logs)
322
587
  |
323
- +--> Shopify-specific OpenTelemetry
588
+ +--> Shopify-specific OpenTelemetry (spans/metrics)
324
589
  ```
325
590
 
326
591
  This separation is intentional. Fields that are useful in diagnostic logs can
@@ -344,6 +609,12 @@ The public package exposes independent entry points.
344
609
 
345
610
  @modainteract/moda-interact-shared/logging
346
611
  reusable structured logging API
612
+ (stdout + OTel Logs + direct Loki fan-out)
613
+
614
+ @modainteract/moda-interact-shared/logging/node
615
+ Node-only logging bootstrap
616
+ (OpenTelemetry LoggerProvider, batching, OTLP HTTP exporter;
617
+ Winston + winston-loki direct transport)
347
618
  ```
348
619
 
349
620
  Consumers should prefer the narrowest appropriate subpath.
@@ -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"]}
@@ -0,0 +1,103 @@
1
+ type NodeLokiLoggingOptions = {
2
+ serviceName: string;
3
+ environment: string;
4
+ serviceNamespace?: string;
5
+ /**
6
+ * Explicit Loki base URL.
7
+ *
8
+ * Production normally supplies LOKI_URL.
9
+ */
10
+ host?: string;
11
+ /**
12
+ * Optional Basic Auth override in `username:password` format.
13
+ *
14
+ * Prefer separate LOKI_USERNAME/LOKI_PASSWORD environment variables in
15
+ * deployed services so the composed value never needs to be committed.
16
+ */
17
+ basicAuth?: string;
18
+ /**
19
+ * Optional additional static headers.
20
+ *
21
+ * Never include these in log payloads or error messages.
22
+ */
23
+ headers?: Record<string, string>;
24
+ batching?: boolean;
25
+ intervalSeconds?: number;
26
+ timeoutMs?: number;
27
+ clearOnError?: boolean;
28
+ /**
29
+ * Test-only override. In normal service bootstrap, the presence of a Loki
30
+ * host is sufficient to enable the transport.
31
+ */
32
+ forceEnable?: boolean;
33
+ };
34
+ type NodeLokiLoggingRuntime = {
35
+ enabled: boolean;
36
+ host: string | null;
37
+ labels: Readonly<Record<string, string>>;
38
+ forceFlush: () => Promise<void>;
39
+ shutdown: () => Promise<void>;
40
+ };
41
+ /**
42
+ * Installs the direct Grafana Loki destination exactly once per Node process.
43
+ *
44
+ * IMPORTANT:
45
+ * - This module is Node-only.
46
+ * - It is reached through @modainteract/moda-interact-shared/logging/node.
47
+ * - Do not import it from Vite/browser/application module graphs.
48
+ * - The normal ./logging entry contains no Winston/winston-loki imports.
49
+ */
50
+ declare function initNodeLokiLogging(options: NodeLokiLoggingOptions): NodeLokiLoggingRuntime;
51
+ declare function getNodeLokiLoggingRuntime(): NodeLokiLoggingRuntime | null;
52
+
53
+ type NodeOpenTelemetryLoggingOptions = {
54
+ serviceName: string;
55
+ environment: string;
56
+ serviceNamespace?: string;
57
+ /**
58
+ * Explicit endpoint override. Intended mainly for tests.
59
+ *
60
+ * Production normally uses:
61
+ * OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
62
+ * or:
63
+ * OTEL_EXPORTER_OTLP_ENDPOINT
64
+ */
65
+ endpoint?: string;
66
+ /** Optional explicit OTLP headers override. */
67
+ headers?: Record<string, string>;
68
+ /** Test-only switch. Production should rely on environment configuration. */
69
+ forceEnable?: boolean;
70
+ };
71
+ type NodeOpenTelemetryLoggingRuntime = {
72
+ enabled: boolean;
73
+ endpoint: string | null;
74
+ forceFlush: () => Promise<void>;
75
+ shutdown: () => Promise<void>;
76
+ };
77
+ /**
78
+ * Installs the Node OpenTelemetry LoggerProvider exactly once for this process.
79
+ *
80
+ * IMPORTANT:
81
+ * This module is Node-only. Import and initialize it from the process bootstrap
82
+ * before framework/application modules are loaded. Do not import this module
83
+ * through Vite/React/browser bundles.
84
+ *
85
+ * Application modules should import only:
86
+ *
87
+ * @modainteract/moda-interact-shared/logging
88
+ *
89
+ * Once this provider is installed, createLogger(...) automatically fans safe
90
+ * canonical log records out to stdout and the OpenTelemetry Logs API.
91
+ *
92
+ * Direct Loki delivery, when desired, is independently enabled with:
93
+ *
94
+ * initNodeLokiLogging(...)
95
+ *
96
+ * from this same Node-only package entry.
97
+ */
98
+ declare function initNodeOpenTelemetryLogging(options: NodeOpenTelemetryLoggingOptions): NodeOpenTelemetryLoggingRuntime;
99
+ declare function getNodeOpenTelemetryLoggingRuntime(): NodeOpenTelemetryLoggingRuntime | null;
100
+ declare function resolveLogsEndpoint(explicitEndpoint?: string): string | undefined;
101
+ declare function parseOtlpHeaders(value: string | undefined): Record<string, string> | undefined;
102
+
103
+ export { type NodeLokiLoggingOptions, type NodeLokiLoggingRuntime, type NodeOpenTelemetryLoggingOptions, type NodeOpenTelemetryLoggingRuntime, getNodeLokiLoggingRuntime, getNodeOpenTelemetryLoggingRuntime, initNodeLokiLogging, initNodeOpenTelemetryLogging, parseOtlpHeaders, resolveLogsEndpoint };
@@ -0,0 +1,373 @@
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
+ export {
366
+ getNodeLokiLoggingRuntime,
367
+ getNodeOpenTelemetryLoggingRuntime,
368
+ initNodeLokiLogging,
369
+ initNodeOpenTelemetryLogging,
370
+ parseOtlpHeaders,
371
+ resolveLogsEndpoint
372
+ };
373
+ //# sourceMappingURL=node.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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modainteract/moda-interact-shared",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,6 +23,10 @@
23
23
  "./logging": {
24
24
  "types": "./dist/logging/index.d.ts",
25
25
  "import": "./dist/logging/index.js"
26
+ },
27
+ "./logging/node": {
28
+ "types": "./dist/logging/node.d.ts",
29
+ "import": "./dist/logging/node.js"
26
30
  }
27
31
  },
28
32
  "files": [
@@ -31,10 +35,17 @@
31
35
  "scripts": {
32
36
  "build": "tsup",
33
37
  "typecheck": "tsc --noEmit",
34
- "test": "tsx --test src/**/*.test.ts",
38
+ "test": "tsx --test \"src/**/*.test.ts\"",
35
39
  "prepack": "npm run build"
36
40
  },
37
41
  "dependencies": {
42
+ "@opentelemetry/api": "^1.9.0",
43
+ "@opentelemetry/api-logs": "0.221.0",
44
+ "@opentelemetry/exporter-logs-otlp-http": "0.221.0",
45
+ "@opentelemetry/resources": "^2.10.0",
46
+ "@opentelemetry/sdk-logs": "0.221.0",
47
+ "winston": "3.19.0",
48
+ "winston-loki": "6.1.7",
38
49
  "zod": "^4.0.0"
39
50
  },
40
51
  "devDependencies": {