@open-mercato/telemetry 0.8.1-develop.7297.1.67605e74f9 → 0.8.1-develop.7441.1.b42958b98c

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
@@ -170,7 +170,13 @@ export async function register(): Promise<void> {
170
170
  if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {
171
171
  const { registerTelemetryForNextjs } =
172
172
  await import('@open-mercato/telemetry/nextjs')
173
- await registerTelemetryForNextjs()
173
+ try {
174
+ await registerTelemetryForNextjs()
175
+ } catch (err) {
176
+ const nodeProcess = process
177
+ nodeProcess.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
178
+ nodeProcess.exit(1)
179
+ }
174
180
  }
175
181
  }
176
182
  ```
package/dist/init.js CHANGED
@@ -13,12 +13,24 @@ import { registerTelemetryLogger } from "./facade/logger-bridge.js";
13
13
  import { captureTraceContext, continueTrace } from "./facade/propagation.js";
14
14
  import { withSpan } from "./facade/tracer.js";
15
15
  import { recordHttpDuration } from "./facade/http.js";
16
+ import { histogram } from "./facade/meter.js";
16
17
  import { reportError } from "./facade/report-error.js";
17
18
  let initialized = false;
18
19
  let activeProvider;
19
20
  let disposeLoggerExtension;
20
21
  let disposeRuntime;
21
22
  const logger = createLogger("telemetry");
23
+ const OTLP_DEPENDENCY_UNAVAILABLE = /* @__PURE__ */ Symbol.for("open-mercato.telemetry.otlp-dependency-unavailable");
24
+ class OtlpDependencyUnavailableError extends Error {
25
+ constructor(backend, cause) {
26
+ super(
27
+ `[internal] OTLP telemetry backend "${backend}" cannot start because its OpenTelemetry runtime dependencies are unavailable. Install optional dependencies, set TELEMETRY_BACKEND=console, or unset TELEMETRY_BACKEND to disable telemetry.`,
28
+ { cause }
29
+ );
30
+ this.name = "OtlpDependencyUnavailableError";
31
+ Object.defineProperty(this, OTLP_DEPENDENCY_UNAVAILABLE, { value: true });
32
+ }
33
+ }
22
34
  async function initTelemetry() {
23
35
  if (initialized) return;
24
36
  resetTelemetryEnvCache();
@@ -52,15 +64,13 @@ async function resolveProvider(backend) {
52
64
  return void 0;
53
65
  }
54
66
  async function loadOtlpProvider(backend) {
67
+ let mod;
55
68
  try {
56
- const mod = await import("./provider/otlp-provider.js");
57
- return new mod.OtlpProvider({}, backend);
58
- } catch (err) {
59
- logger.warn("OTLP provider unavailable; falling back to console", {
60
- reason: err instanceof Error ? err.message : String(err)
61
- });
62
- return new ConsoleProvider();
69
+ mod = await import("./provider/otlp-provider.js");
70
+ } catch (error) {
71
+ throw new OtlpDependencyUnavailableError(backend, error);
63
72
  }
73
+ return new mod.OtlpProvider({}, backend);
64
74
  }
65
75
  async function shutdownTelemetry() {
66
76
  const provider = activeProvider;
@@ -79,6 +89,7 @@ function createRuntime(provider) {
79
89
  captureTraceContext,
80
90
  continueTrace: (carrier, name, fn, options) => continueTrace(carrier, name, () => fn(), options),
81
91
  withSpan: (name, fn, options) => withSpan(name, fn, options),
92
+ recordHistogram: histogram,
82
93
  recordHttpDuration,
83
94
  reportError,
84
95
  shutdown: shutdownTelemetry
package/dist/init.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/init.ts"],
4
- "sourcesContent": ["import type { TelemetryBackendName, TelemetryProvider } from './types'\nimport { isOtlpBackend, readTelemetryEnv, resetTelemetryEnvCache } from './env'\nimport { ConsoleProvider } from './provider/console-provider'\nimport {\n getRegisteredProvider,\n clearActiveProvider,\n setActiveProvider,\n} from './provider/registry'\nimport {\n registerTelemetryRuntime,\n type TelemetryRuntime,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { registerTelemetryLogger } from './facade/logger-bridge'\nimport { captureTraceContext, continueTrace } from './facade/propagation'\nimport { withSpan } from './facade/tracer'\nimport { recordHttpDuration } from './facade/http'\nimport { reportError } from './facade/report-error'\n\nlet initialized = false\nlet activeProvider: TelemetryProvider | undefined\nlet disposeLoggerExtension: (() => void) | undefined\nlet disposeRuntime: (() => void) | undefined\n\nconst logger = createLogger('telemetry')\n\n/**\n * One-shot bootstrap, invoked from `apps/mercato/instrumentation.ts` (web) and,\n * for cross-boundary propagation, from the queue/worker bootstrap. Resolves the\n * backend from `TELEMETRY_BACKEND`, starts it, and installs it as the active\n * provider. Idempotent and safe to call when telemetry is off (defaults to\n * no-op).\n */\nexport async function initTelemetry(): Promise<void> {\n if (initialized) return\n\n // Drop any env snapshot memoized BEFORE the host loaded its `.env` (the CLI\n // binary imports this package before dotenv runs) so init resolves the backend\n // from the actual, fully-loaded environment.\n resetTelemetryEnvCache()\n const env = readTelemetryEnv()\n const configuredBackend = process.env.TELEMETRY_BACKEND?.trim().toLowerCase() ?? ''\n const registeredProvider = configuredBackend && configuredBackend !== 'noop'\n ? getRegisteredProvider(configuredBackend)\n : undefined\n // Explicit off is absolute: do not resolve providers, register hooks, or\n // install process-wide bridges when the backend is unset/noop/unregistered.\n if (!env.enabled && !registeredProvider) return\n\n const provider = registeredProvider ?? await resolveProvider(env.backend)\n if (!provider) {\n logger.warn('Telemetry backend is unsupported; telemetry remains disabled', {\n backend: configuredBackend || env.backend,\n })\n return\n }\n\n // Only flag initialized AFTER start() succeeds \u2014 a throw here must leave\n // telemetry re-initializable, not stuck flagged-on with an unstarted provider.\n await provider.start()\n setActiveProvider(provider)\n activeProvider = provider\n disposeLoggerExtension = registerTelemetryLogger(provider)\n disposeRuntime = registerTelemetryRuntime(createRuntime(provider))\n initialized = true\n\n logger.info('Telemetry initialized', {\n backend: provider.name,\n samplingRatio: env.samplingRatio,\n })\n}\n\nasync function resolveProvider(backend: TelemetryBackendName): Promise<TelemetryProvider | undefined> {\n const registered = getRegisteredProvider(backend)\n if (registered) return registered\n\n if (backend === 'console') return new ConsoleProvider()\n // signoz | newrelic | otlp \u2014 same OTLP provider, vendor differs only by endpoint.\n if (isOtlpBackend(backend)) return await loadOtlpProvider(backend)\n return undefined\n}\n\n/**\n * Dynamically load the OTLP provider so `@opentelemetry/*` (optionalDependencies)\n * is imported only when an OTLP backend is selected. Falls back to console if\n * the OTEL packages are absent rather than crashing the app.\n */\nasync function loadOtlpProvider(backend: TelemetryBackendName): Promise<TelemetryProvider> {\n try {\n const mod = await import('./provider/otlp-provider')\n return new mod.OtlpProvider({}, backend)\n } catch (err) {\n logger.warn('OTLP provider unavailable; falling back to console', {\n reason: err instanceof Error ? err.message : String(err),\n })\n return new ConsoleProvider()\n }\n}\n\n/** Flush + tear down the active backend (shutdown hook / `after()`). */\nexport async function shutdownTelemetry(): Promise<void> {\n const provider = activeProvider\n activeProvider = undefined\n initialized = false\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntime?.()\n disposeRuntime = undefined\n clearActiveProvider()\n if (provider) await provider.shutdown()\n}\n\nfunction createRuntime(provider: TelemetryProvider): TelemetryRuntime {\n return {\n canUseGlobalTracePropagation: () =>\n isOtlpBackend(provider.name as TelemetryBackendName)\n && readTelemetryEnv().trustInboundTrace,\n captureTraceContext,\n continueTrace: (carrier, name, fn, options) =>\n continueTrace(carrier, name, () => fn(), options),\n withSpan: (name, fn, options) => withSpan(name, fn, options),\n recordHttpDuration,\n reportError,\n shutdown: shutdownTelemetry,\n }\n}\n\n/** Test-only: allow re-init in jest. */\nexport function resetTelemetryInit(): void {\n initialized = false\n activeProvider = undefined\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntime?.()\n disposeRuntime = undefined\n}\n"],
5
- "mappings": "AACA,SAAS,eAAe,kBAAkB,8BAA8B;AACxE,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,+BAA+B;AACxC,SAAS,qBAAqB,qBAAqB;AACnD,SAAS,gBAAgB;AACzB,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAE5B,IAAI,cAAc;AAClB,IAAI;AACJ,IAAI;AACJ,IAAI;AAEJ,MAAM,SAAS,aAAa,WAAW;AASvC,eAAsB,gBAA+B;AACnD,MAAI,YAAa;AAKjB,yBAAuB;AACvB,QAAM,MAAM,iBAAiB;AAC7B,QAAM,oBAAoB,QAAQ,IAAI,mBAAmB,KAAK,EAAE,YAAY,KAAK;AACjF,QAAM,qBAAqB,qBAAqB,sBAAsB,SAClE,sBAAsB,iBAAiB,IACvC;AAGJ,MAAI,CAAC,IAAI,WAAW,CAAC,mBAAoB;AAEzC,QAAM,WAAW,sBAAsB,MAAM,gBAAgB,IAAI,OAAO;AACxE,MAAI,CAAC,UAAU;AACb,WAAO,KAAK,gEAAgE;AAAA,MAC1E,SAAS,qBAAqB,IAAI;AAAA,IACpC,CAAC;AACD;AAAA,EACF;AAIA,QAAM,SAAS,MAAM;AACrB,oBAAkB,QAAQ;AAC1B,mBAAiB;AACjB,2BAAyB,wBAAwB,QAAQ;AACzD,mBAAiB,yBAAyB,cAAc,QAAQ,CAAC;AACjE,gBAAc;AAEd,SAAO,KAAK,yBAAyB;AAAA,IACnC,SAAS,SAAS;AAAA,IAClB,eAAe,IAAI;AAAA,EACrB,CAAC;AACH;AAEA,eAAe,gBAAgB,SAAuE;AACpG,QAAM,aAAa,sBAAsB,OAAO;AAChD,MAAI,WAAY,QAAO;AAEvB,MAAI,YAAY,UAAW,QAAO,IAAI,gBAAgB;AAEtD,MAAI,cAAc,OAAO,EAAG,QAAO,MAAM,iBAAiB,OAAO;AACjE,SAAO;AACT;AAOA,eAAe,iBAAiB,SAA2D;AACzF,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,0BAA0B;AACnD,WAAO,IAAI,IAAI,aAAa,CAAC,GAAG,OAAO;AAAA,EACzC,SAAS,KAAK;AACZ,WAAO,KAAK,sDAAsD;AAAA,MAChE,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACzD,CAAC;AACD,WAAO,IAAI,gBAAgB;AAAA,EAC7B;AACF;AAGA,eAAsB,oBAAmC;AACvD,QAAM,WAAW;AACjB,mBAAiB;AACjB,gBAAc;AACd,2BAAyB;AACzB,2BAAyB;AACzB,mBAAiB;AACjB,mBAAiB;AACjB,sBAAoB;AACpB,MAAI,SAAU,OAAM,SAAS,SAAS;AACxC;AAEA,SAAS,cAAc,UAA+C;AACpE,SAAO;AAAA,IACL,8BAA8B,MAC5B,cAAc,SAAS,IAA4B,KAChD,iBAAiB,EAAE;AAAA,IACxB;AAAA,IACA,eAAe,CAAC,SAAS,MAAM,IAAI,YACjC,cAAc,SAAS,MAAM,MAAM,GAAG,GAAG,OAAO;AAAA,IAClD,UAAU,CAAC,MAAM,IAAI,YAAY,SAAS,MAAM,IAAI,OAAO;AAAA,IAC3D;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAGO,SAAS,qBAA2B;AACzC,gBAAc;AACd,mBAAiB;AACjB,2BAAyB;AACzB,2BAAyB;AACzB,mBAAiB;AACjB,mBAAiB;AACnB;",
4
+ "sourcesContent": ["import type { TelemetryBackendName, TelemetryProvider } from './types'\nimport { isOtlpBackend, readTelemetryEnv, resetTelemetryEnvCache } from './env'\nimport { ConsoleProvider } from './provider/console-provider'\nimport {\n getRegisteredProvider,\n clearActiveProvider,\n setActiveProvider,\n} from './provider/registry'\nimport {\n registerTelemetryRuntime,\n type TelemetryRuntime,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { registerTelemetryLogger } from './facade/logger-bridge'\nimport { captureTraceContext, continueTrace } from './facade/propagation'\nimport { withSpan } from './facade/tracer'\nimport { recordHttpDuration } from './facade/http'\nimport { histogram } from './facade/meter'\nimport { reportError } from './facade/report-error'\n\nlet initialized = false\nlet activeProvider: TelemetryProvider | undefined\nlet disposeLoggerExtension: (() => void) | undefined\nlet disposeRuntime: (() => void) | undefined\n\nconst logger = createLogger('telemetry')\nconst OTLP_DEPENDENCY_UNAVAILABLE = Symbol.for('open-mercato.telemetry.otlp-dependency-unavailable')\n\nclass OtlpDependencyUnavailableError extends Error {\n constructor(backend: TelemetryBackendName, cause: unknown) {\n super(\n `[internal] OTLP telemetry backend \"${backend}\" cannot start because its OpenTelemetry runtime dependencies are unavailable. Install optional dependencies, set TELEMETRY_BACKEND=console, or unset TELEMETRY_BACKEND to disable telemetry.`,\n { cause },\n )\n this.name = 'OtlpDependencyUnavailableError'\n Object.defineProperty(this, OTLP_DEPENDENCY_UNAVAILABLE, { value: true })\n }\n}\n\n/**\n * One-shot bootstrap, invoked from `apps/mercato/instrumentation.ts` (web) and,\n * for cross-boundary propagation, from the queue/worker bootstrap. Resolves the\n * backend from `TELEMETRY_BACKEND`, starts it, and installs it as the active\n * provider. Idempotent and safe to call when telemetry is off (defaults to\n * no-op).\n */\nexport async function initTelemetry(): Promise<void> {\n if (initialized) return\n\n // Drop any env snapshot memoized BEFORE the host loaded its `.env` (the CLI\n // binary imports this package before dotenv runs) so init resolves the backend\n // from the actual, fully-loaded environment.\n resetTelemetryEnvCache()\n const env = readTelemetryEnv()\n const configuredBackend = process.env.TELEMETRY_BACKEND?.trim().toLowerCase() ?? ''\n const registeredProvider = configuredBackend && configuredBackend !== 'noop'\n ? getRegisteredProvider(configuredBackend)\n : undefined\n // Explicit off is absolute: do not resolve providers, register hooks, or\n // install process-wide bridges when the backend is unset/noop/unregistered.\n if (!env.enabled && !registeredProvider) return\n\n const provider = registeredProvider ?? await resolveProvider(env.backend)\n if (!provider) {\n logger.warn('Telemetry backend is unsupported; telemetry remains disabled', {\n backend: configuredBackend || env.backend,\n })\n return\n }\n\n // Only flag initialized AFTER start() succeeds \u2014 a throw here must leave\n // telemetry re-initializable, not stuck flagged-on with an unstarted provider.\n await provider.start()\n setActiveProvider(provider)\n activeProvider = provider\n disposeLoggerExtension = registerTelemetryLogger(provider)\n disposeRuntime = registerTelemetryRuntime(createRuntime(provider))\n initialized = true\n\n logger.info('Telemetry initialized', {\n backend: provider.name,\n samplingRatio: env.samplingRatio,\n })\n}\n\nasync function resolveProvider(backend: TelemetryBackendName): Promise<TelemetryProvider | undefined> {\n const registered = getRegisteredProvider(backend)\n if (registered) return registered\n\n if (backend === 'console') return new ConsoleProvider()\n // signoz | newrelic | otlp \u2014 same OTLP provider, vendor differs only by endpoint.\n if (isOtlpBackend(backend)) return await loadOtlpProvider(backend)\n return undefined\n}\n\n/**\n * Dynamically load the OTLP provider so `@opentelemetry/*` (optionalDependencies)\n * is imported only when an OTLP backend is selected. An explicit OTLP selection\n * must fail at startup when those packages are unavailable instead of silently\n * switching telemetry semantics.\n */\nasync function loadOtlpProvider(backend: TelemetryBackendName): Promise<TelemetryProvider> {\n let mod: typeof import('./provider/otlp-provider')\n try {\n mod = await import('./provider/otlp-provider')\n } catch (error) {\n throw new OtlpDependencyUnavailableError(backend, error)\n }\n return new mod.OtlpProvider({}, backend)\n}\n\n/** Flush + tear down the active backend (shutdown hook / `after()`). */\nexport async function shutdownTelemetry(): Promise<void> {\n const provider = activeProvider\n activeProvider = undefined\n initialized = false\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntime?.()\n disposeRuntime = undefined\n clearActiveProvider()\n if (provider) await provider.shutdown()\n}\n\nfunction createRuntime(provider: TelemetryProvider): TelemetryRuntime {\n return {\n canUseGlobalTracePropagation: () =>\n isOtlpBackend(provider.name as TelemetryBackendName)\n && readTelemetryEnv().trustInboundTrace,\n captureTraceContext,\n continueTrace: (carrier, name, fn, options) =>\n continueTrace(carrier, name, () => fn(), options),\n withSpan: (name, fn, options) => withSpan(name, fn, options),\n recordHistogram: histogram,\n recordHttpDuration,\n reportError,\n shutdown: shutdownTelemetry,\n }\n}\n\n/** Test-only: allow re-init in jest. */\nexport function resetTelemetryInit(): void {\n initialized = false\n activeProvider = undefined\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntime?.()\n disposeRuntime = undefined\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/nextjs.js CHANGED
@@ -3,6 +3,10 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
3
3
  import { telemetryServerExternalPackages } from "./nextjs-config.js";
4
4
  import { recordHttpDuration } from "./facade/http.js";
5
5
  const logger = createLogger("telemetry");
6
+ const OTLP_DEPENDENCY_UNAVAILABLE = /* @__PURE__ */ Symbol.for("open-mercato.telemetry.otlp-dependency-unavailable");
7
+ function isOtlpDependencyUnavailableError(error) {
8
+ return error instanceof Error && Reflect.get(error, OTLP_DEPENDENCY_UNAVAILABLE) === true;
9
+ }
6
10
  async function registerTelemetryForNextjs() {
7
11
  if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs") return;
8
12
  if (!isTelemetryBackendEnabled()) return;
@@ -10,6 +14,7 @@ async function registerTelemetryForNextjs() {
10
14
  try {
11
15
  await initTelemetry();
12
16
  } catch (error) {
17
+ if (isOtlpDependencyUnavailableError(error)) throw error;
13
18
  logger.warn("Init from Next.js instrumentation failed", { err: error });
14
19
  return;
15
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/nextjs.ts"],
4
- "sourcesContent": ["/**\n * Next.js wiring helpers for `@open-mercato/telemetry`.\n *\n * Runtime helpers for an enabled Next.js telemetry integration. Build-time\n * config belongs in `@open-mercato/telemetry/nextjs-config`, which has no\n * runtime imports.\n */\nimport { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nexport { telemetryServerExternalPackages } from './nextjs-config'\nexport { recordHttpDuration } from './facade/http'\n\nconst logger = createLogger('telemetry')\n\n/**\n * One-line telemetry bootstrap for a Next.js `instrumentation.ts`. Initializes\n * the active backend (no-op unless `TELEMETRY_BACKEND` is set), degrades to no\n * telemetry on init failure (never bubbles a rejection out of Next's\n * `register()`), and registers a best-effort flush on `SIGTERM`/`SIGINT`. Skips\n * the edge runtime \u2014 the OTEL NodeSDK is Node-only \u2014 so callers may import it\n * unconditionally, though gating the dynamic import on\n * `NEXT_RUNTIME === 'nodejs'` also keeps the SDK off the edge bundle.\n */\nexport async function registerTelemetryForNextjs(): Promise<void> {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== 'nodejs') return\n if (!isTelemetryBackendEnabled()) return\n const { initTelemetry, shutdownTelemetry } = await import('./init')\n try {\n await initTelemetry()\n } catch (error) {\n logger.warn('Init from Next.js instrumentation failed', { err: error })\n return\n }\n // A signal listener suppresses Node's default termination, so after the\n // best-effort flush the handler re-raises the signal. `once` has already\n // removed this listener by then, so the re-raise falls through to the default\n // terminate (or to another component's own handler, e.g. Next's).\n const flush = (signal: NodeJS.Signals) => {\n void shutdownTelemetry()\n .catch(() => {})\n .finally(() => {\n process.kill(process.pid, signal)\n })\n }\n process.once('SIGTERM', flush)\n process.once('SIGINT', flush)\n}\n"],
5
- "mappings": "AAOA,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAC7B,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;AAEnC,MAAM,SAAS,aAAa,WAAW;AAWvC,eAAsB,6BAA4C;AAChE,MAAI,QAAQ,IAAI,gBAAgB,QAAQ,IAAI,iBAAiB,SAAU;AACvE,MAAI,CAAC,0BAA0B,EAAG;AAClC,QAAM,EAAE,eAAe,kBAAkB,IAAI,MAAM,OAAO,QAAQ;AAClE,MAAI;AACF,UAAM,cAAc;AAAA,EACtB,SAAS,OAAO;AACd,WAAO,KAAK,4CAA4C,EAAE,KAAK,MAAM,CAAC;AACtE;AAAA,EACF;AAKA,QAAM,QAAQ,CAAC,WAA2B;AACxC,SAAK,kBAAkB,EACpB,MAAM,MAAM;AAAA,IAAC,CAAC,EACd,QAAQ,MAAM;AACb,cAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,IAClC,CAAC;AAAA,EACL;AACA,UAAQ,KAAK,WAAW,KAAK;AAC7B,UAAQ,KAAK,UAAU,KAAK;AAC9B;",
4
+ "sourcesContent": ["/**\n * Next.js wiring helpers for `@open-mercato/telemetry`.\n *\n * Runtime helpers for an enabled Next.js telemetry integration. Build-time\n * config belongs in `@open-mercato/telemetry/nextjs-config`, which has no\n * runtime imports.\n */\nimport { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nexport { telemetryServerExternalPackages } from './nextjs-config'\nexport { recordHttpDuration } from './facade/http'\n\nconst logger = createLogger('telemetry')\nconst OTLP_DEPENDENCY_UNAVAILABLE = Symbol.for('open-mercato.telemetry.otlp-dependency-unavailable')\n\nfunction isOtlpDependencyUnavailableError(error: unknown): boolean {\n return error instanceof Error && Reflect.get(error, OTLP_DEPENDENCY_UNAVAILABLE) === true\n}\n\n/**\n * One-line telemetry bootstrap for a Next.js `instrumentation.ts`. Initializes\n * the active backend (no-op unless `TELEMETRY_BACKEND` is set), propagates an\n * explicit OTLP missing-dependency failure, degrades to no telemetry for other\n * initialization failures, and registers a best-effort flush on\n * `SIGTERM`/`SIGINT`. Skips the edge runtime \u2014 the OTEL NodeSDK is Node-only \u2014\n * so callers may import it unconditionally, though gating the dynamic import on\n * `NEXT_RUNTIME === 'nodejs'` also keeps the SDK off the edge bundle.\n */\nexport async function registerTelemetryForNextjs(): Promise<void> {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== 'nodejs') return\n if (!isTelemetryBackendEnabled()) return\n const { initTelemetry, shutdownTelemetry } = await import('./init')\n try {\n await initTelemetry()\n } catch (error) {\n if (isOtlpDependencyUnavailableError(error)) throw error\n logger.warn('Init from Next.js instrumentation failed', { err: error })\n return\n }\n // A signal listener suppresses Node's default termination, so after the\n // best-effort flush the handler re-raises the signal. `once` has already\n // removed this listener by then, so the re-raise falls through to the default\n // terminate (or to another component's own handler, e.g. Next's).\n const flush = (signal: NodeJS.Signals) => {\n void shutdownTelemetry()\n .catch(() => {})\n .finally(() => {\n process.kill(process.pid, signal)\n })\n }\n process.once('SIGTERM', flush)\n process.once('SIGINT', flush)\n}\n"],
5
+ "mappings": "AAOA,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAC7B,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;AAEnC,MAAM,SAAS,aAAa,WAAW;AACvC,MAAM,8BAA8B,uBAAO,IAAI,oDAAoD;AAEnG,SAAS,iCAAiC,OAAyB;AACjE,SAAO,iBAAiB,SAAS,QAAQ,IAAI,OAAO,2BAA2B,MAAM;AACvF;AAWA,eAAsB,6BAA4C;AAChE,MAAI,QAAQ,IAAI,gBAAgB,QAAQ,IAAI,iBAAiB,SAAU;AACvE,MAAI,CAAC,0BAA0B,EAAG;AAClC,QAAM,EAAE,eAAe,kBAAkB,IAAI,MAAM,OAAO,QAAQ;AAClE,MAAI;AACF,UAAM,cAAc;AAAA,EACtB,SAAS,OAAO;AACd,QAAI,iCAAiC,KAAK,EAAG,OAAM;AACnD,WAAO,KAAK,4CAA4C,EAAE,KAAK,MAAM,CAAC;AACtE;AAAA,EACF;AAKA,QAAM,QAAQ,CAAC,WAA2B;AACxC,SAAK,kBAAkB,EACpB,MAAM,MAAM;AAAA,IAAC,CAAC,EACd,QAAQ,MAAM;AACb,cAAQ,KAAK,QAAQ,KAAK,MAAM;AAAA,IAClC,CAAC;AAAA,EACL;AACA,UAAQ,KAAK,WAAW,KAAK;AAC7B,UAAQ,KAAK,UAAU,KAAK;AAC9B;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/telemetry",
3
- "version": "0.8.1-develop.7297.1.67605e74f9",
3
+ "version": "0.8.1-develop.7441.1.b42958b98c",
4
4
  "license": "MIT",
5
5
  "description": "Vendor-neutral spans, metrics, and error reporting backed by a pluggable OTLP provider. Off by default; the telemetry runtime is loaded only when explicitly enabled.",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@open-mercato/shared": "0.8.1-develop.7297.1.67605e74f9"
41
+ "@open-mercato/shared": "0.8.1-develop.7441.1.b42958b98c"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18.0.0"
@@ -25,7 +25,7 @@ function provider(name = 'noop'): TelemetryProvider {
25
25
  inject: () => {},
26
26
  runInRemoteSpan: (_carrier, _name, _options, fn) => fn(NOOP_SPAN),
27
27
  emitLog: () => {},
28
- recordMetric: () => {},
28
+ recordMetric: jest.fn(),
29
29
  }
30
30
  }
31
31
 
@@ -77,4 +77,26 @@ describe('telemetry explicit opt-in boundary', () => {
77
77
  expect(getLoggerExtension()).toBeDefined()
78
78
  expect(getTelemetryRuntime()).toBeDefined()
79
79
  })
80
+
81
+ it('wires shared runtime histograms to the active provider', async () => {
82
+ const customConsole = provider('console')
83
+ registerProvider(customConsole)
84
+ process.env.TELEMETRY_BACKEND = 'console'
85
+
86
+ await initTelemetry()
87
+ getTelemetryRuntime()?.recordHistogram?.(
88
+ 'om.enricher.duration',
89
+ 0.25,
90
+ { 'enricher.id': 'customers.people' },
91
+ 's',
92
+ )
93
+
94
+ expect(customConsole.recordMetric).toHaveBeenCalledWith({
95
+ kind: 'histogram',
96
+ name: 'om.enricher.duration',
97
+ value: 0.25,
98
+ labels: { 'enricher.id': 'customers.people' },
99
+ unit: 's',
100
+ })
101
+ })
80
102
  })
@@ -0,0 +1,208 @@
1
+ import type {
2
+ Attributes,
3
+ LogRecord,
4
+ MetricPoint,
5
+ Span,
6
+ SpanOptions,
7
+ TelemetryProvider,
8
+ TraceCarrier,
9
+ TraceContext,
10
+ } from '../types'
11
+
12
+ function createSpan(): Span {
13
+ return {
14
+ setAttribute(_key: string, _value: string | number | boolean): void {},
15
+ setAttributes(_attributes: Attributes): void {},
16
+ recordException(_error: unknown): void {},
17
+ setStatus(_status: 'ok' | 'error', _message?: string): void {},
18
+ end(): void {},
19
+ }
20
+ }
21
+
22
+ function createProvider(name = 'otlp'): TelemetryProvider {
23
+ const span = createSpan()
24
+ return {
25
+ name,
26
+ supports: ['traces', 'metrics', 'logs', 'errors'],
27
+ start: jest.fn(async () => {}),
28
+ shutdown: jest.fn(async () => {}),
29
+ runInSpan<T>(_name: string, _options: SpanOptions, operation: (activeSpan: Span) => T): T {
30
+ return operation(span)
31
+ },
32
+ activeSpan(): Span | undefined {
33
+ return span
34
+ },
35
+ activeTraceContext(): TraceContext | undefined {
36
+ return undefined
37
+ },
38
+ inject(_carrier: TraceCarrier): void {},
39
+ runInRemoteSpan<T>(
40
+ _carrier: TraceCarrier,
41
+ _name: string,
42
+ _options: SpanOptions,
43
+ operation: (activeSpan: Span) => T,
44
+ ): T {
45
+ return operation(span)
46
+ },
47
+ emitLog(_record: LogRecord): void {},
48
+ recordMetric(_point: MetricPoint): void {},
49
+ }
50
+ }
51
+
52
+ describe('OTLP dependency bootstrap', () => {
53
+ const originalBackend = process.env.TELEMETRY_BACKEND
54
+ let shutdownTelemetry: (() => Promise<void>) | undefined
55
+
56
+ beforeEach(() => {
57
+ jest.resetModules()
58
+ jest.restoreAllMocks()
59
+ shutdownTelemetry = undefined
60
+ delete process.env.TELEMETRY_BACKEND
61
+ })
62
+
63
+ afterEach(async () => {
64
+ await shutdownTelemetry?.()
65
+ if (originalBackend === undefined) delete process.env.TELEMETRY_BACKEND
66
+ else process.env.TELEMETRY_BACKEND = originalBackend
67
+ jest.dontMock('../provider/console-provider')
68
+ jest.dontMock('../provider/otlp-provider')
69
+ jest.dontMock('../init')
70
+ jest.resetModules()
71
+ jest.restoreAllMocks()
72
+ })
73
+
74
+ it.each(['otlp', 'signoz', 'newrelic'])('fails clearly when %s dependencies cannot load', async (backend) => {
75
+ const importCause = new Error('simulated missing package at /private/runtime with token=secret-value')
76
+ const consoleProvider = jest.fn()
77
+ jest.doMock('../provider/console-provider', () => ({ ConsoleProvider: consoleProvider }))
78
+ jest.doMock('../provider/otlp-provider', () => {
79
+ throw importCause
80
+ })
81
+ const telemetry = await import('../init')
82
+ shutdownTelemetry = telemetry.shutdownTelemetry
83
+ process.env.TELEMETRY_BACKEND = backend
84
+
85
+ const failure: unknown = await telemetry.initTelemetry().catch((error: unknown) => error)
86
+
87
+ expect(failure).toBeInstanceOf(Error)
88
+ expect((failure as Error).message).toContain(`OTLP telemetry backend "${backend}" cannot start`)
89
+ expect((failure as Error).message).toContain('Install optional dependencies')
90
+ expect((failure as Error).message).not.toContain('/private/runtime')
91
+ expect((failure as Error).message).not.toContain('secret-value')
92
+ expect((failure as Error).cause).toBe(importCause)
93
+ expect(consoleProvider).not.toHaveBeenCalled()
94
+ const [{ getActiveProvider }, { getLoggerExtension }, { getTelemetryRuntime }] = await Promise.all([
95
+ import('../provider/registry'),
96
+ import('@open-mercato/shared/lib/logger'),
97
+ import('@open-mercato/shared/lib/telemetry/runtime'),
98
+ ])
99
+ expect(getActiveProvider().name).toBe('noop')
100
+ expect(getLoggerExtension()).toBeUndefined()
101
+ expect(getTelemetryRuntime()).toBeUndefined()
102
+ })
103
+
104
+ it('starts the dynamically loaded provider when OTLP dependencies are available', async () => {
105
+ const provider = createProvider()
106
+ const OtlpProvider = jest.fn(() => provider)
107
+ jest.doMock('../provider/otlp-provider', () => ({ OtlpProvider }))
108
+ const telemetry = await import('../init')
109
+ shutdownTelemetry = telemetry.shutdownTelemetry
110
+ process.env.TELEMETRY_BACKEND = 'otlp'
111
+
112
+ await telemetry.initTelemetry()
113
+
114
+ expect(OtlpProvider).toHaveBeenCalledWith({}, 'otlp')
115
+ expect(provider.start).toHaveBeenCalledTimes(1)
116
+ })
117
+
118
+ it.each([undefined, 'noop'])('does not resolve OTLP dependencies when telemetry is %s', async (backend) => {
119
+ const loadOtlpModule = jest.fn(() => ({ OtlpProvider: jest.fn() }))
120
+ jest.doMock('../provider/otlp-provider', loadOtlpModule)
121
+ const telemetry = await import('../init')
122
+ shutdownTelemetry = telemetry.shutdownTelemetry
123
+ if (backend === undefined) delete process.env.TELEMETRY_BACKEND
124
+ else process.env.TELEMETRY_BACKEND = backend
125
+
126
+ await telemetry.initTelemetry()
127
+
128
+ expect(loadOtlpModule).not.toHaveBeenCalled()
129
+ })
130
+
131
+ it('can retry initialization after a dependency load failure', async () => {
132
+ const provider = createProvider()
133
+ const importCause = new Error('simulated first-load failure')
134
+ let loadAttempts = 0
135
+ jest.doMock('../provider/otlp-provider', () => {
136
+ loadAttempts += 1
137
+ if (loadAttempts === 1) throw importCause
138
+ return { OtlpProvider: jest.fn(() => provider) }
139
+ })
140
+ const telemetry = await import('../init')
141
+ shutdownTelemetry = telemetry.shutdownTelemetry
142
+ process.env.TELEMETRY_BACKEND = 'otlp'
143
+
144
+ await expect(telemetry.initTelemetry()).rejects.toHaveProperty('cause', importCause)
145
+ await telemetry.initTelemetry()
146
+
147
+ expect(loadAttempts).toBe(2)
148
+ expect(provider.start).toHaveBeenCalledTimes(1)
149
+ })
150
+
151
+ it('propagates a dependency load failure through the standard Next.js bootstrap', async () => {
152
+ const importCause = new Error('simulated missing package')
153
+ jest.doMock('../provider/otlp-provider', () => {
154
+ throw importCause
155
+ })
156
+ const telemetry = await import('../init')
157
+ shutdownTelemetry = telemetry.shutdownTelemetry
158
+ const { registerTelemetryForNextjs } = await import('../nextjs')
159
+ process.env.TELEMETRY_BACKEND = 'otlp'
160
+
161
+ await expect(registerTelemetryForNextjs()).rejects.toMatchObject({
162
+ name: 'OtlpDependencyUnavailableError',
163
+ cause: importCause,
164
+ })
165
+ })
166
+
167
+ it('keeps unrelated provider initialization failures best effort in Next.js', async () => {
168
+ const startupFailure = new Error('collector configuration rejected')
169
+ const initTelemetry = jest.fn(async () => {
170
+ throw startupFailure
171
+ })
172
+ jest.doMock('../init', () => ({
173
+ initTelemetry,
174
+ shutdownTelemetry: jest.fn(async () => {}),
175
+ }))
176
+ const { registerTelemetryForNextjs } = await import('../nextjs')
177
+ process.env.TELEMETRY_BACKEND = 'otlp'
178
+
179
+ await expect(registerTelemetryForNextjs()).resolves.toBeUndefined()
180
+ expect(initTelemetry).toHaveBeenCalledTimes(1)
181
+ })
182
+
183
+ it('preserves a provider startup failure and can retry it without misclassifying dependencies', async () => {
184
+ const provider = createProvider()
185
+ const startupFailure = new Error('collector configuration rejected')
186
+ jest.mocked(provider.start)
187
+ .mockRejectedValueOnce(startupFailure)
188
+ .mockResolvedValueOnce(undefined)
189
+ jest.doMock('../provider/otlp-provider', () => ({
190
+ OtlpProvider: jest.fn(() => provider),
191
+ }))
192
+ const telemetry = await import('../init')
193
+ shutdownTelemetry = telemetry.shutdownTelemetry
194
+ process.env.TELEMETRY_BACKEND = 'otlp'
195
+
196
+ await expect(telemetry.initTelemetry()).rejects.toBe(startupFailure)
197
+ const [{ getActiveProvider }, { getTelemetryRuntime }] = await Promise.all([
198
+ import('../provider/registry'),
199
+ import('@open-mercato/shared/lib/telemetry/runtime'),
200
+ ])
201
+ expect(getActiveProvider().name).toBe('noop')
202
+ expect(getTelemetryRuntime()).toBeUndefined()
203
+
204
+ await telemetry.initTelemetry()
205
+
206
+ expect(provider.start).toHaveBeenCalledTimes(2)
207
+ })
208
+ })
package/src/init.ts CHANGED
@@ -15,6 +15,7 @@ import { registerTelemetryLogger } from './facade/logger-bridge'
15
15
  import { captureTraceContext, continueTrace } from './facade/propagation'
16
16
  import { withSpan } from './facade/tracer'
17
17
  import { recordHttpDuration } from './facade/http'
18
+ import { histogram } from './facade/meter'
18
19
  import { reportError } from './facade/report-error'
19
20
 
20
21
  let initialized = false
@@ -23,6 +24,18 @@ let disposeLoggerExtension: (() => void) | undefined
23
24
  let disposeRuntime: (() => void) | undefined
24
25
 
25
26
  const logger = createLogger('telemetry')
27
+ const OTLP_DEPENDENCY_UNAVAILABLE = Symbol.for('open-mercato.telemetry.otlp-dependency-unavailable')
28
+
29
+ class OtlpDependencyUnavailableError extends Error {
30
+ constructor(backend: TelemetryBackendName, cause: unknown) {
31
+ super(
32
+ `[internal] OTLP telemetry backend "${backend}" cannot start because its OpenTelemetry runtime dependencies are unavailable. Install optional dependencies, set TELEMETRY_BACKEND=console, or unset TELEMETRY_BACKEND to disable telemetry.`,
33
+ { cause },
34
+ )
35
+ this.name = 'OtlpDependencyUnavailableError'
36
+ Object.defineProperty(this, OTLP_DEPENDENCY_UNAVAILABLE, { value: true })
37
+ }
38
+ }
26
39
 
27
40
  /**
28
41
  * One-shot bootstrap, invoked from `apps/mercato/instrumentation.ts` (web) and,
@@ -82,19 +95,18 @@ async function resolveProvider(backend: TelemetryBackendName): Promise<Telemetry
82
95
 
83
96
  /**
84
97
  * Dynamically load the OTLP provider so `@opentelemetry/*` (optionalDependencies)
85
- * is imported only when an OTLP backend is selected. Falls back to console if
86
- * the OTEL packages are absent rather than crashing the app.
98
+ * is imported only when an OTLP backend is selected. An explicit OTLP selection
99
+ * must fail at startup when those packages are unavailable instead of silently
100
+ * switching telemetry semantics.
87
101
  */
88
102
  async function loadOtlpProvider(backend: TelemetryBackendName): Promise<TelemetryProvider> {
103
+ let mod: typeof import('./provider/otlp-provider')
89
104
  try {
90
- const mod = await import('./provider/otlp-provider')
91
- return new mod.OtlpProvider({}, backend)
92
- } catch (err) {
93
- logger.warn('OTLP provider unavailable; falling back to console', {
94
- reason: err instanceof Error ? err.message : String(err),
95
- })
96
- return new ConsoleProvider()
105
+ mod = await import('./provider/otlp-provider')
106
+ } catch (error) {
107
+ throw new OtlpDependencyUnavailableError(backend, error)
97
108
  }
109
+ return new mod.OtlpProvider({}, backend)
98
110
  }
99
111
 
100
112
  /** Flush + tear down the active backend (shutdown hook / `after()`). */
@@ -119,6 +131,7 @@ function createRuntime(provider: TelemetryProvider): TelemetryRuntime {
119
131
  continueTrace: (carrier, name, fn, options) =>
120
132
  continueTrace(carrier, name, () => fn(), options),
121
133
  withSpan: (name, fn, options) => withSpan(name, fn, options),
134
+ recordHistogram: histogram,
122
135
  recordHttpDuration,
123
136
  reportError,
124
137
  shutdown: shutdownTelemetry,
package/src/nextjs.ts CHANGED
@@ -11,14 +11,19 @@ export { telemetryServerExternalPackages } from './nextjs-config'
11
11
  export { recordHttpDuration } from './facade/http'
12
12
 
13
13
  const logger = createLogger('telemetry')
14
+ const OTLP_DEPENDENCY_UNAVAILABLE = Symbol.for('open-mercato.telemetry.otlp-dependency-unavailable')
15
+
16
+ function isOtlpDependencyUnavailableError(error: unknown): boolean {
17
+ return error instanceof Error && Reflect.get(error, OTLP_DEPENDENCY_UNAVAILABLE) === true
18
+ }
14
19
 
15
20
  /**
16
21
  * One-line telemetry bootstrap for a Next.js `instrumentation.ts`. Initializes
17
- * the active backend (no-op unless `TELEMETRY_BACKEND` is set), degrades to no
18
- * telemetry on init failure (never bubbles a rejection out of Next's
19
- * `register()`), and registers a best-effort flush on `SIGTERM`/`SIGINT`. Skips
20
- * the edge runtime — the OTEL NodeSDK is Node-only — so callers may import it
21
- * unconditionally, though gating the dynamic import on
22
+ * the active backend (no-op unless `TELEMETRY_BACKEND` is set), propagates an
23
+ * explicit OTLP missing-dependency failure, degrades to no telemetry for other
24
+ * initialization failures, and registers a best-effort flush on
25
+ * `SIGTERM`/`SIGINT`. Skips the edge runtime — the OTEL NodeSDK is Node-only —
26
+ * so callers may import it unconditionally, though gating the dynamic import on
22
27
  * `NEXT_RUNTIME === 'nodejs'` also keeps the SDK off the edge bundle.
23
28
  */
24
29
  export async function registerTelemetryForNextjs(): Promise<void> {
@@ -28,6 +33,7 @@ export async function registerTelemetryForNextjs(): Promise<void> {
28
33
  try {
29
34
  await initTelemetry()
30
35
  } catch (error) {
36
+ if (isOtlpDependencyUnavailableError(error)) throw error
31
37
  logger.warn('Init from Next.js instrumentation failed', { err: error })
32
38
  return
33
39
  }