@open-mercato/telemetry 0.10.1-develop.7827.1.5c631908e4 → 0.10.1-develop.7836.1.0ef0f453c6
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +8 -1
- package/README.md +44 -0
- package/dist/init.js +33 -0
- package/dist/init.js.map +2 -2
- package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js +4 -0
- package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js.map +2 -2
- package/dist/provider/otlp-provider.js +24 -2
- package/dist/provider/otlp-provider.js.map +2 -2
- package/dist/runtime-metrics.js +132 -0
- package/dist/runtime-metrics.js.map +7 -0
- package/package.json +2 -2
- package/src/__tests__/env-load-order.test.ts +74 -1
- package/src/__tests__/otlp-integration.test.ts +34 -4
- package/src/__tests__/runtime-metrics.test.ts +239 -0
- package/src/init.ts +33 -0
- package/src/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.ts +6 -0
- package/src/provider/otlp-provider.ts +18 -1
- package/src/runtime-metrics.ts +179 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:telemetry] found
|
|
1
|
+
[build:telemetry] found 31 entry points
|
|
2
2
|
[build:telemetry] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,12 @@ off by default. Spec:
|
|
|
25
25
|
- Emit spans from packages that must not depend on this one via
|
|
26
26
|
`withTelemetrySpan` / `captureTelemetryTrace` from
|
|
27
27
|
`@open-mercato/shared/lib/telemetry/runtime`, never a direct import.
|
|
28
|
-
- Use semantic-convention metric/attribute names when available
|
|
28
|
+
- Use semantic-convention metric/attribute names when available, unless an
|
|
29
|
+
auto-instrumentation the OTLP provider registers already emits that name
|
|
30
|
+
(`PgInstrumentation` owns `db.client.connection.*`); use `om.*` then, since
|
|
31
|
+
two instruments must never share a name.
|
|
32
|
+
- Keep the built-in metric names, units, and fixed labels synchronized with the
|
|
33
|
+
catalog in `packages/telemetry/README.md`.
|
|
29
34
|
- Keep metric labels low-cardinality. Tenant, organization, and user IDs belong
|
|
30
35
|
on span attributes, never metric labels.
|
|
31
36
|
- Apply redaction at the provider boundary as well as at facade call sites.
|
|
@@ -106,6 +111,8 @@ host/queue shared runtime bridge ── absent while off
|
|
|
106
111
|
- `src/provider/*`: noop/console/OTLP providers and global provider registry.
|
|
107
112
|
- `src/init.ts`: explicit-enabled initialization and process-wide bridge
|
|
108
113
|
registration.
|
|
114
|
+
- `src/runtime-metrics.ts`: enabled-only process sampler and the single
|
|
115
|
+
interval that invokes shared periodic metric collectors.
|
|
109
116
|
- `src/nextjs-config.ts`: build-time constants only; no runtime imports.
|
|
110
117
|
- `src/nextjs.ts`: enabled runtime helper.
|
|
111
118
|
- `src/browser/*`: browser RUM. `config.ts` (shared, env-free contract) and
|
package/README.md
CHANGED
|
@@ -285,6 +285,50 @@ implementation, where you can decide what a dropped report means.
|
|
|
285
285
|
`attributes`, so putting it in both `tags` and `extra`, as the snippet above does
|
|
286
286
|
with `tags`, is your choice rather than an accident of the payload.
|
|
287
287
|
|
|
288
|
+
### Built-in runtime metrics
|
|
289
|
+
|
|
290
|
+
An active provider starts one process-wide 10-second sampler. Disabled/noop
|
|
291
|
+
telemetry starts no interval or event-loop monitor. The sampler also invokes
|
|
292
|
+
shared metric collectors, which lets the primary PostgreSQL pool report state
|
|
293
|
+
without making `@open-mercato/shared` depend on this package.
|
|
294
|
+
|
|
295
|
+
| Metric | Kind | Unit | Labels |
|
|
296
|
+
| --- | --- | --- | --- |
|
|
297
|
+
| `om.db.pool.connections` | gauge | `{connection}` | `pool=primary`, `state=idle\|used` |
|
|
298
|
+
| `om.db.pool.pending_requests` | gauge | `{request}` | `pool=primary` |
|
|
299
|
+
| `om.db.pool.max` | gauge | `{connection}` | `pool=primary` |
|
|
300
|
+
| `om.db.pool.wait_time` | histogram | `s` | `pool=primary` |
|
|
301
|
+
| `nodejs.eventloop.utilization` | gauge | `1` | none |
|
|
302
|
+
| `nodejs.eventloop.delay.p50` / `p90` / `p99` | gauge | `s` | none |
|
|
303
|
+
| `process.memory.usage` | gauge | `By` | none |
|
|
304
|
+
| `v8js.memory.heap.used` | gauge | `By` | `v8js.heap.space.name` |
|
|
305
|
+
|
|
306
|
+
Pool acquisition wait is recorded per `pool.connect` call, including failed
|
|
307
|
+
promise and callback acquisitions, and includes opening a new connection when
|
|
308
|
+
the pool grows. Connection-state values are sampled; sum `idle` and `used` for
|
|
309
|
+
the current open-connection total.
|
|
310
|
+
|
|
311
|
+
The pool metrics use the `om.db.pool.*` namespace on purpose. Under an OTLP
|
|
312
|
+
backend `@opentelemetry/instrumentation-pg` already emits the semantic-convention
|
|
313
|
+
`db.client.connection.count` and `db.client.connection.pending_requests` as
|
|
314
|
+
UpDownCounters labelled `db.client.connection.state` /
|
|
315
|
+
`db.client.connection.pool.name`. Reusing those names would export two streams
|
|
316
|
+
per name with conflicting types and label keys. Query `om.db.pool.*` for the
|
|
317
|
+
sampled view, which is also available under the `console` backend.
|
|
318
|
+
|
|
319
|
+
The event-loop delay percentiles come from `monitorEventLoopDelay` with a 20 ms
|
|
320
|
+
resolution, which records the whole timer interval rather than the overshoot.
|
|
321
|
+
An idle process therefore reports about 0.020 s, not 0. Subtract 0.020 s before
|
|
322
|
+
you compare against a real-delay budget: an alert at 0.050 s fires at about
|
|
323
|
+
30 ms of actual blocking.
|
|
324
|
+
|
|
325
|
+
Under an OTLP backend, every histogram recorded in seconds
|
|
326
|
+
(`om.db.pool.wait_time`, `http.server.request.duration`,
|
|
327
|
+
`om.enricher.duration`) is exported with explicit boundaries of 1 ms, 5 ms, 10 ms, 25 ms, 50 ms, 75 ms, 100 ms, 250 ms,
|
|
328
|
+
500 ms, 750 ms, 1 s, 2.5 s, 5 s, 7.5 s and 10 s, so backends can compute
|
|
329
|
+
percentiles. The SDK default is millisecond-scaled and would put every
|
|
330
|
+
realistic latency in its first bucket.
|
|
331
|
+
|
|
288
332
|
### Long-lived jobs: root spans
|
|
289
333
|
|
|
290
334
|
Trace context propagates from the request that triggered a job through the queue
|
package/dist/init.js
CHANGED
|
@@ -15,10 +15,13 @@ import { withSpan } from "./facade/tracer.js";
|
|
|
15
15
|
import { recordHttpDuration } from "./facade/http.js";
|
|
16
16
|
import { histogram } from "./facade/meter.js";
|
|
17
17
|
import { reportError } from "./facade/report-error.js";
|
|
18
|
+
import { startRuntimeMetrics } from "./runtime-metrics.js";
|
|
18
19
|
let initialized = false;
|
|
20
|
+
let initializing;
|
|
19
21
|
let activeProvider;
|
|
20
22
|
let disposeLoggerExtension;
|
|
21
23
|
let disposeRuntime;
|
|
24
|
+
let disposeRuntimeMetrics;
|
|
22
25
|
const logger = createLogger("telemetry");
|
|
23
26
|
const OTLP_DEPENDENCY_UNAVAILABLE = /* @__PURE__ */ Symbol.for("open-mercato.telemetry.otlp-dependency-unavailable");
|
|
24
27
|
class OtlpDependencyUnavailableError extends Error {
|
|
@@ -33,6 +36,15 @@ class OtlpDependencyUnavailableError extends Error {
|
|
|
33
36
|
}
|
|
34
37
|
async function initTelemetry() {
|
|
35
38
|
if (initialized) return;
|
|
39
|
+
if (initializing) return initializing;
|
|
40
|
+
let pending;
|
|
41
|
+
pending = initializeTelemetry().finally(() => {
|
|
42
|
+
if (initializing === pending) initializing = void 0;
|
|
43
|
+
});
|
|
44
|
+
initializing = pending;
|
|
45
|
+
return pending;
|
|
46
|
+
}
|
|
47
|
+
async function initializeTelemetry() {
|
|
36
48
|
resetTelemetryEnvCache();
|
|
37
49
|
const env = readTelemetryEnv();
|
|
38
50
|
const configuredBackend = process.env.TELEMETRY_BACKEND?.trim().toLowerCase() ?? "";
|
|
@@ -50,6 +62,7 @@ async function initTelemetry() {
|
|
|
50
62
|
activeProvider = provider;
|
|
51
63
|
disposeLoggerExtension = registerTelemetryLogger(provider);
|
|
52
64
|
disposeRuntime = registerTelemetryRuntime(createRuntime(provider));
|
|
65
|
+
disposeRuntimeMetrics = startRuntimeMetrics();
|
|
53
66
|
initialized = true;
|
|
54
67
|
logger.info("Telemetry initialized", {
|
|
55
68
|
backend: provider.name,
|
|
@@ -73,11 +86,27 @@ async function loadOtlpProvider(backend) {
|
|
|
73
86
|
return new mod.OtlpProvider({}, backend);
|
|
74
87
|
}
|
|
75
88
|
async function shutdownTelemetry() {
|
|
89
|
+
if (initializing) {
|
|
90
|
+
try {
|
|
91
|
+
await initializing;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
try {
|
|
94
|
+
reportError(err, {
|
|
95
|
+
module: "telemetry",
|
|
96
|
+
code: "telemetry.initialization_failed"
|
|
97
|
+
});
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
logger.warn("Telemetry initialization failed before shutdown", { err });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
76
103
|
const provider = activeProvider;
|
|
77
104
|
activeProvider = void 0;
|
|
78
105
|
initialized = false;
|
|
79
106
|
disposeLoggerExtension?.();
|
|
80
107
|
disposeLoggerExtension = void 0;
|
|
108
|
+
disposeRuntimeMetrics?.();
|
|
109
|
+
disposeRuntimeMetrics = void 0;
|
|
81
110
|
disposeRuntime?.();
|
|
82
111
|
disposeRuntime = void 0;
|
|
83
112
|
clearActiveProvider();
|
|
@@ -89,6 +118,7 @@ function createRuntime(provider) {
|
|
|
89
118
|
captureTraceContext,
|
|
90
119
|
continueTrace: (carrier, name, fn, options) => continueTrace(carrier, name, () => fn(), options),
|
|
91
120
|
withSpan: (name, fn, options) => withSpan(name, fn, options),
|
|
121
|
+
recordMetric: (point) => provider.recordMetric(point),
|
|
92
122
|
recordHistogram: histogram,
|
|
93
123
|
recordHttpDuration,
|
|
94
124
|
reportError,
|
|
@@ -97,9 +127,12 @@ function createRuntime(provider) {
|
|
|
97
127
|
}
|
|
98
128
|
function resetTelemetryInit() {
|
|
99
129
|
initialized = false;
|
|
130
|
+
initializing = void 0;
|
|
100
131
|
activeProvider = void 0;
|
|
101
132
|
disposeLoggerExtension?.();
|
|
102
133
|
disposeLoggerExtension = void 0;
|
|
134
|
+
disposeRuntimeMetrics?.();
|
|
135
|
+
disposeRuntimeMetrics = void 0;
|
|
103
136
|
disposeRuntime?.();
|
|
104
137
|
disposeRuntime = void 0;
|
|
105
138
|
}
|
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 { 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": "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,iBAAiB;AAC1B,SAAS,mBAAmB;
|
|
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'\nimport { startRuntimeMetrics } from './runtime-metrics'\n\nlet initialized = false\nlet initializing: Promise<void> | undefined\nlet activeProvider: TelemetryProvider | undefined\nlet disposeLoggerExtension: (() => void) | undefined\nlet disposeRuntime: (() => void) | undefined\nlet disposeRuntimeMetrics: (() => 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 if (initializing) return initializing\n\n let pending: Promise<void>\n pending = initializeTelemetry().finally(() => {\n if (initializing === pending) initializing = undefined\n })\n initializing = pending\n return pending\n}\n\nasync function initializeTelemetry(): Promise<void> {\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 disposeRuntimeMetrics = startRuntimeMetrics()\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 if (initializing) {\n try {\n await initializing\n } catch (err) {\n try {\n reportError(err, {\n module: 'telemetry',\n code: 'telemetry.initialization_failed',\n })\n } catch {}\n logger.warn('Telemetry initialization failed before shutdown', { err })\n }\n }\n const provider = activeProvider\n activeProvider = undefined\n initialized = false\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntimeMetrics?.()\n disposeRuntimeMetrics = 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 recordMetric: (point) => provider.recordMetric(point),\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 initializing = undefined\n activeProvider = undefined\n disposeLoggerExtension?.()\n disposeLoggerExtension = undefined\n disposeRuntimeMetrics?.()\n disposeRuntimeMetrics = undefined\n disposeRuntime?.()\n disposeRuntime = undefined\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -34,6 +34,10 @@ async function otelApiIsRegistered(page) {
|
|
|
34
34
|
}
|
|
35
35
|
test.describe("TC-TELEMETRY-002: browser RUM runtime", () => {
|
|
36
36
|
test("exports document-load and fetch spans without breaking either fetch path", async ({ page, baseURL }) => {
|
|
37
|
+
test.skip(
|
|
38
|
+
process.env.OM_TEST_BROWSER_TELEMETRY_MODE !== "opt-in",
|
|
39
|
+
"OM_TEST_BROWSER_TELEMETRY_MODE=opt-in is not configured for this lane."
|
|
40
|
+
);
|
|
37
41
|
const origin = new URL(baseURL ?? "http://localhost:3000").origin;
|
|
38
42
|
await page.context().addCookies([{ name: OPT_IN_COOKIE, value: "on", url: origin }]);
|
|
39
43
|
const ingest = recordIngest(page);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, test, type Page, type Request as PlaywrightRequest } from '@playwright/test'\nimport { login } from '@open-mercato/core/helpers/integration/auth'\n\n/**\n * TC-TELEMETRY-002: browser RUM in a real browser.\n *\n * TC-TELEMETRY-001 drives the proxy route with `request.post` \u2014 a useful API contract test that\n * never opens a page. This spec covers what only a browser can: that the SDK actually boots, that\n * the `__omOriginalFetch` patch dance leaves both fetch paths working (a recursion here hangs every\n * API call in the backoffice), that batches reach the proxy, and \u2014 the mirror image \u2014 that a\n * default environment ships none of it.\n *\n * Browser RUM is normally an environment-wide env switch, which a spec cannot flip for one page.\n * The integration environment therefore sets `OM_TEST_BROWSER_TELEMETRY_MODE=opt-in` on top of\n * `OM_TEST_MODE=1`, and only then does the server honour an `om_test_browser_telemetry=on` cookie.\n * Both variables are absent in production. Same escape hatch the auth rate limiter already uses.\n *\n * No collector is configured here, so the proxy answers 204 (accept-and-drop) rather than 202 \u2014\n * which is why the span assertions read the batch the *browser posted* instead of anything a sink\n * received. That is the stronger assertion anyway: it is the browser's output under test.\n */\n\nconst TRACES_PATH = '/api/telemetry/browser-traces'\nconst OPT_IN_COOKIE = 'om_test_browser_telemetry'\n\ntype IngestRecord = { status: number; body: string }\n\n/** Records every proxy call the page makes, with the payload the exporter actually produced. */\nfunction recordIngest(page: Page): IngestRecord[] {\n const records: IngestRecord[] = []\n page.on('response', async (response) => {\n if (!response.url().includes(TRACES_PATH)) return\n const request: PlaywrightRequest = response.request()\n if (request.method() !== 'POST') return\n records.push({ status: response.status(), body: request.postData() ?? '' })\n })\n return records\n}\n\n/**\n * Records the same-origin API GETs the backoffice itself made and that answered 200. Replaying a\n * real one beats naming an endpoint here: the set of routes is not this spec's business, and a\n * path that quietly stops existing would fail the test for the wrong reason.\n */\nfunction recordApiGets(page: Page): string[] {\n const urls: string[] = []\n page.on('response', (response) => {\n const request = response.request()\n if (request.method() !== 'GET') return\n if (response.status() !== 200) return\n try {\n const { pathname } = new URL(response.url())\n if (!pathname.startsWith('/api/') || pathname.startsWith(TRACES_PATH)) return\n urls.push(response.url())\n } catch {\n // A non-absolute or exotic URL is simply not a candidate for replay.\n }\n })\n return urls\n}\n\n/** The web SDK registers itself on this global symbol; its presence is the \"SDK booted\" signal. */\nasync function otelApiIsRegistered(page: Page): Promise<boolean> {\n return page.evaluate(() =>\n Object.getOwnPropertySymbols(globalThis).some((symbol) => symbol.toString().includes('opentelemetry.js.api')),\n )\n}\n\ntest.describe('TC-TELEMETRY-002: browser RUM runtime', () => {\n test('exports document-load and fetch spans without breaking either fetch path', async ({ page, baseURL }) => {\n const origin = new URL(baseURL ?? 'http://localhost:3000').origin\n await page.context().addCookies([{ name: OPT_IN_COOKIE, value: 'on', url: origin }])\n const ingest = recordIngest(page)\n const apiGets = recordApiGets(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await expect.poll(() => otelApiIsRegistered(page), {\n message: 'the web SDK must boot once the server hands the layout a non-null config',\n timeout: 15_000,\n }).toBe(true)\n\n // The patch dance, asserted where it actually runs. `@open-mercato/ui`'s api utils stash the\n // pristine fetch on `__omOriginalFetch` and put a wrapper on `window.fetch`; the SDK has to\n // instrument the stash rather than the wrapper, or direct `apiFetch` callers (DataTable,\n // CrudForm, react-query) produce no spans at all.\n const patchState = await page.evaluate(() => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n return {\n hasStash: typeof w.__omOriginalFetch === 'function',\n // Identical would mean the wrapper was handed back to itself \u2014 an infinite recursion on\n // the first request through `apiFetch`.\n stashIsWrapper: w.__omOriginalFetch === window.fetch,\n }\n })\n expect(patchState.hasStash, '@open-mercato/ui installs the fetch wrapper on every backoffice page').toBe(true)\n expect(patchState.stashIsWrapper, 'instrumenting the wrapper instead of the native fetch recurses forever').toBe(false)\n\n // A DataTable page drives `apiFetch` (which reads the instrumented stash) end to end. Waiting\n // on the API call it makes, rather than on a DOM role, keeps this assertion about the thing\n // under test: that the swap left `apiFetch` able to reach the API at all.\n await expect.poll(() => apiGets.length, {\n message: 'the backoffice must still reach its own API through the patched fetch',\n timeout: 30_000,\n }).toBeGreaterThan(0)\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n\n // Replay one of the page's OWN successful GETs down both paths. A hang here IS the recursion\n // bug, hence the tight budget; a non-200 means the swap broke the wrapper's auth handling.\n const replayUrl = apiGets[apiGets.length - 1]\n const replay = await page.evaluate(async (url: string) => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n const withTimeout = async (call: Promise<Response>) => {\n const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 10_000))\n return (await Promise.race([call, timeout])).status\n }\n return {\n viaWindow: await withTimeout(window.fetch(url)),\n viaStash: await withTimeout(w.__omOriginalFetch!(url)),\n }\n }, replayUrl)\n expect(replay.viaWindow, `window.fetch must still reach ${replayUrl} through the framework wrapper`).toBe(200)\n expect(replay.viaStash, `the instrumented native fetch must still reach ${replayUrl}`).toBe(200)\n\n // Exports are batched on a 3s delay, and a flush is forced when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true })\n document.dispatchEvent(new Event('visibilitychange'))\n })\n await expect.poll(() => ingest.length, {\n message: 'the exporter must post its batches to the same-origin proxy',\n timeout: 20_000,\n }).toBeGreaterThan(0)\n\n // Every answer must be one the OTLP exporter does NOT retry (429/502/503/504 would storm).\n for (const record of ingest) {\n expect([202, 204], `unexpected ingest status ${record.status}`).toContain(record.status)\n }\n\n const spanNames = ingest.flatMap((record) => {\n try {\n const payload = JSON.parse(record.body) as {\n resourceSpans?: Array<{\n resource?: { attributes?: Array<{ key: string; value?: { stringValue?: string } }> }\n scopeSpans?: Array<{ spans?: Array<{ name: string }> }>\n }>\n }\n for (const resourceSpan of payload.resourceSpans ?? []) {\n const serviceName = resourceSpan.resource?.attributes?.find((a) => a.key === 'service.name')\n expect(\n serviceName?.value?.stringValue,\n 'browser spans must carry their own service name so they sort apart from server spans',\n ).toMatch(/-browser$/)\n }\n return (payload.resourceSpans ?? []).flatMap((r) => (r.scopeSpans ?? []).flatMap((s) => s.spans ?? [])).map((s) => s.name)\n } catch {\n return []\n }\n })\n expect(spanNames, 'document-load instrumentation must produce the navigation span').toContain('documentLoad')\n expect(spanNames, 'document-load instrumentation must produce the document fetch span').toContain('documentFetch')\n })\n\n test('ships nothing at all without the opt-in \u2014 the default path must cost a disabled app zero bytes', async ({ page }) => {\n // No cookie: this is the shipped default, and the mirror image of the case above. It is the\n // assertion that protects the \"zero client-bundle cost while off\" promise from regressing.\n const ingest = recordIngest(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n await page.waitForTimeout(5_000)\n\n expect(await otelApiIsRegistered(page), 'the web SDK chunk must never be requested while RUM is off').toBe(false)\n expect(ingest, 'a disabled app must not post a single batch').toHaveLength(0)\n })\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,YAA0D;AAC3E,SAAS,aAAa;AAqBtB,MAAM,cAAc;AACpB,MAAM,gBAAgB;AAKtB,SAAS,aAAa,MAA4B;AAChD,QAAM,UAA0B,CAAC;AACjC,OAAK,GAAG,YAAY,OAAO,aAAa;AACtC,QAAI,CAAC,SAAS,IAAI,EAAE,SAAS,WAAW,EAAG;AAC3C,UAAM,UAA6B,SAAS,QAAQ;AACpD,QAAI,QAAQ,OAAO,MAAM,OAAQ;AACjC,YAAQ,KAAK,EAAE,QAAQ,SAAS,OAAO,GAAG,MAAM,QAAQ,SAAS,KAAK,GAAG,CAAC;AAAA,EAC5E,CAAC;AACD,SAAO;AACT;AAOA,SAAS,cAAc,MAAsB;AAC3C,QAAM,OAAiB,CAAC;AACxB,OAAK,GAAG,YAAY,CAAC,aAAa;AAChC,UAAM,UAAU,SAAS,QAAQ;AACjC,QAAI,QAAQ,OAAO,MAAM,MAAO;AAChC,QAAI,SAAS,OAAO,MAAM,IAAK;AAC/B,QAAI;AACF,YAAM,EAAE,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,CAAC;AAC3C,UAAI,CAAC,SAAS,WAAW,OAAO,KAAK,SAAS,WAAW,WAAW,EAAG;AACvE,WAAK,KAAK,SAAS,IAAI,CAAC;AAAA,IAC1B,QAAQ;AAAA,IAER;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAGA,eAAe,oBAAoB,MAA8B;AAC/D,SAAO,KAAK;AAAA,IAAS,MACnB,OAAO,sBAAsB,UAAU,EAAE,KAAK,CAAC,WAAW,OAAO,SAAS,EAAE,SAAS,sBAAsB,CAAC;AAAA,EAC9G;AACF;AAEA,KAAK,SAAS,yCAAyC,MAAM;AAC3D,OAAK,4EAA4E,OAAO,EAAE,MAAM,QAAQ,MAAM;
|
|
4
|
+
"sourcesContent": ["import { expect, test, type Page, type Request as PlaywrightRequest } from '@playwright/test'\nimport { login } from '@open-mercato/core/helpers/integration/auth'\n\n/**\n * TC-TELEMETRY-002: browser RUM in a real browser.\n *\n * TC-TELEMETRY-001 drives the proxy route with `request.post` \u2014 a useful API contract test that\n * never opens a page. This spec covers what only a browser can: that the SDK actually boots, that\n * the `__omOriginalFetch` patch dance leaves both fetch paths working (a recursion here hangs every\n * API call in the backoffice), that batches reach the proxy, and \u2014 the mirror image \u2014 that a\n * default environment ships none of it.\n *\n * Browser RUM is normally an environment-wide env switch, which a spec cannot flip for one page.\n * The integration environment therefore sets `OM_TEST_BROWSER_TELEMETRY_MODE=opt-in` on top of\n * `OM_TEST_MODE=1`, and only then does the server honour an `om_test_browser_telemetry=on` cookie.\n * Both variables are absent in production. Same escape hatch the auth rate limiter already uses.\n *\n * No collector is configured here, so the proxy answers 204 (accept-and-drop) rather than 202 \u2014\n * which is why the span assertions read the batch the *browser posted* instead of anything a sink\n * received. That is the stronger assertion anyway: it is the browser's output under test.\n */\n\nconst TRACES_PATH = '/api/telemetry/browser-traces'\nconst OPT_IN_COOKIE = 'om_test_browser_telemetry'\n\ntype IngestRecord = { status: number; body: string }\n\n/** Records every proxy call the page makes, with the payload the exporter actually produced. */\nfunction recordIngest(page: Page): IngestRecord[] {\n const records: IngestRecord[] = []\n page.on('response', async (response) => {\n if (!response.url().includes(TRACES_PATH)) return\n const request: PlaywrightRequest = response.request()\n if (request.method() !== 'POST') return\n records.push({ status: response.status(), body: request.postData() ?? '' })\n })\n return records\n}\n\n/**\n * Records the same-origin API GETs the backoffice itself made and that answered 200. Replaying a\n * real one beats naming an endpoint here: the set of routes is not this spec's business, and a\n * path that quietly stops existing would fail the test for the wrong reason.\n */\nfunction recordApiGets(page: Page): string[] {\n const urls: string[] = []\n page.on('response', (response) => {\n const request = response.request()\n if (request.method() !== 'GET') return\n if (response.status() !== 200) return\n try {\n const { pathname } = new URL(response.url())\n if (!pathname.startsWith('/api/') || pathname.startsWith(TRACES_PATH)) return\n urls.push(response.url())\n } catch {\n // A non-absolute or exotic URL is simply not a candidate for replay.\n }\n })\n return urls\n}\n\n/** The web SDK registers itself on this global symbol; its presence is the \"SDK booted\" signal. */\nasync function otelApiIsRegistered(page: Page): Promise<boolean> {\n return page.evaluate(() =>\n Object.getOwnPropertySymbols(globalThis).some((symbol) => symbol.toString().includes('opentelemetry.js.api')),\n )\n}\n\ntest.describe('TC-TELEMETRY-002: browser RUM runtime', () => {\n test('exports document-load and fetch spans without breaking either fetch path', async ({ page, baseURL }) => {\n // The server ignores the opt-in cookie unless the app runs with this variable; lanes that\n // start the app from their own env (the standalone app) do not set it, so there is no SDK to boot.\n test.skip(\n process.env.OM_TEST_BROWSER_TELEMETRY_MODE !== 'opt-in',\n 'OM_TEST_BROWSER_TELEMETRY_MODE=opt-in is not configured for this lane.',\n )\n const origin = new URL(baseURL ?? 'http://localhost:3000').origin\n await page.context().addCookies([{ name: OPT_IN_COOKIE, value: 'on', url: origin }])\n const ingest = recordIngest(page)\n const apiGets = recordApiGets(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await expect.poll(() => otelApiIsRegistered(page), {\n message: 'the web SDK must boot once the server hands the layout a non-null config',\n timeout: 15_000,\n }).toBe(true)\n\n // The patch dance, asserted where it actually runs. `@open-mercato/ui`'s api utils stash the\n // pristine fetch on `__omOriginalFetch` and put a wrapper on `window.fetch`; the SDK has to\n // instrument the stash rather than the wrapper, or direct `apiFetch` callers (DataTable,\n // CrudForm, react-query) produce no spans at all.\n const patchState = await page.evaluate(() => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n return {\n hasStash: typeof w.__omOriginalFetch === 'function',\n // Identical would mean the wrapper was handed back to itself \u2014 an infinite recursion on\n // the first request through `apiFetch`.\n stashIsWrapper: w.__omOriginalFetch === window.fetch,\n }\n })\n expect(patchState.hasStash, '@open-mercato/ui installs the fetch wrapper on every backoffice page').toBe(true)\n expect(patchState.stashIsWrapper, 'instrumenting the wrapper instead of the native fetch recurses forever').toBe(false)\n\n // A DataTable page drives `apiFetch` (which reads the instrumented stash) end to end. Waiting\n // on the API call it makes, rather than on a DOM role, keeps this assertion about the thing\n // under test: that the swap left `apiFetch` able to reach the API at all.\n await expect.poll(() => apiGets.length, {\n message: 'the backoffice must still reach its own API through the patched fetch',\n timeout: 30_000,\n }).toBeGreaterThan(0)\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n\n // Replay one of the page's OWN successful GETs down both paths. A hang here IS the recursion\n // bug, hence the tight budget; a non-200 means the swap broke the wrapper's auth handling.\n const replayUrl = apiGets[apiGets.length - 1]\n const replay = await page.evaluate(async (url: string) => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n const withTimeout = async (call: Promise<Response>) => {\n const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 10_000))\n return (await Promise.race([call, timeout])).status\n }\n return {\n viaWindow: await withTimeout(window.fetch(url)),\n viaStash: await withTimeout(w.__omOriginalFetch!(url)),\n }\n }, replayUrl)\n expect(replay.viaWindow, `window.fetch must still reach ${replayUrl} through the framework wrapper`).toBe(200)\n expect(replay.viaStash, `the instrumented native fetch must still reach ${replayUrl}`).toBe(200)\n\n // Exports are batched on a 3s delay, and a flush is forced when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true })\n document.dispatchEvent(new Event('visibilitychange'))\n })\n await expect.poll(() => ingest.length, {\n message: 'the exporter must post its batches to the same-origin proxy',\n timeout: 20_000,\n }).toBeGreaterThan(0)\n\n // Every answer must be one the OTLP exporter does NOT retry (429/502/503/504 would storm).\n for (const record of ingest) {\n expect([202, 204], `unexpected ingest status ${record.status}`).toContain(record.status)\n }\n\n const spanNames = ingest.flatMap((record) => {\n try {\n const payload = JSON.parse(record.body) as {\n resourceSpans?: Array<{\n resource?: { attributes?: Array<{ key: string; value?: { stringValue?: string } }> }\n scopeSpans?: Array<{ spans?: Array<{ name: string }> }>\n }>\n }\n for (const resourceSpan of payload.resourceSpans ?? []) {\n const serviceName = resourceSpan.resource?.attributes?.find((a) => a.key === 'service.name')\n expect(\n serviceName?.value?.stringValue,\n 'browser spans must carry their own service name so they sort apart from server spans',\n ).toMatch(/-browser$/)\n }\n return (payload.resourceSpans ?? []).flatMap((r) => (r.scopeSpans ?? []).flatMap((s) => s.spans ?? [])).map((s) => s.name)\n } catch {\n return []\n }\n })\n expect(spanNames, 'document-load instrumentation must produce the navigation span').toContain('documentLoad')\n expect(spanNames, 'document-load instrumentation must produce the document fetch span').toContain('documentFetch')\n })\n\n test('ships nothing at all without the opt-in \u2014 the default path must cost a disabled app zero bytes', async ({ page }) => {\n // No cookie: this is the shipped default, and the mirror image of the case above. It is the\n // assertion that protects the \"zero client-bundle cost while off\" promise from regressing.\n const ingest = recordIngest(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n await page.waitForTimeout(5_000)\n\n expect(await otelApiIsRegistered(page), 'the web SDK chunk must never be requested while RUM is off').toBe(false)\n expect(ingest, 'a disabled app must not post a single batch').toHaveLength(0)\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -125,6 +125,27 @@ class OtelSpan {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
const PG_INSTRUMENTATION_OPTIONS = { enhancedDatabaseReporting: false };
|
|
128
|
+
const SECONDS_HISTOGRAM_BUCKETS = [
|
|
129
|
+
1e-3,
|
|
130
|
+
5e-3,
|
|
131
|
+
0.01,
|
|
132
|
+
0.025,
|
|
133
|
+
0.05,
|
|
134
|
+
0.075,
|
|
135
|
+
0.1,
|
|
136
|
+
0.25,
|
|
137
|
+
0.5,
|
|
138
|
+
0.75,
|
|
139
|
+
1,
|
|
140
|
+
2.5,
|
|
141
|
+
5,
|
|
142
|
+
7.5,
|
|
143
|
+
10
|
|
144
|
+
];
|
|
145
|
+
function histogramOptions(unit) {
|
|
146
|
+
if (unit !== "s") return unit ? { unit } : void 0;
|
|
147
|
+
return { unit, advice: { explicitBucketBoundaries: [...SECONDS_HISTOGRAM_BUCKETS] } };
|
|
148
|
+
}
|
|
128
149
|
class OtlpProvider {
|
|
129
150
|
constructor(options = {}, name = "otlp") {
|
|
130
151
|
this.options = options;
|
|
@@ -221,7 +242,7 @@ class OtlpProvider {
|
|
|
221
242
|
if (!instrument) {
|
|
222
243
|
const meter = metrics.getMeter(TRACER_NAME);
|
|
223
244
|
const opts = point.unit ? { unit: point.unit } : void 0;
|
|
224
|
-
instrument = point.kind === "counter" ? meter.createCounter(point.name, opts) : point.kind === "histogram" ? meter.createHistogram(point.name,
|
|
245
|
+
instrument = point.kind === "counter" ? meter.createCounter(point.name, opts) : point.kind === "histogram" ? meter.createHistogram(point.name, histogramOptions(point.unit)) : meter.createGauge(point.name, opts);
|
|
225
246
|
this.instruments.set(key, instrument);
|
|
226
247
|
}
|
|
227
248
|
return instrument;
|
|
@@ -229,6 +250,7 @@ class OtlpProvider {
|
|
|
229
250
|
}
|
|
230
251
|
export {
|
|
231
252
|
OtlpProvider,
|
|
232
|
-
PG_INSTRUMENTATION_OPTIONS
|
|
253
|
+
PG_INSTRUMENTATION_OPTIONS,
|
|
254
|
+
SECONDS_HISTOGRAM_BUCKETS
|
|
233
255
|
};
|
|
234
256
|
//# sourceMappingURL=otlp-provider.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/provider/otlp-provider.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The ONLY file in this package that imports `@opentelemetry/*`. Loaded via\n * dynamic `import()` from `init.ts` (so the heavy SDK never loads when telemetry\n * is off), and the OTEL packages live in `optionalDependencies`.\n *\n * Serves any OTLP backend \u2014 the endpoint + headers come from the standard\n * `OTEL_EXPORTER_OTLP_*` env vars, read by the exporters directly.\n */\nimport {\n trace,\n metrics,\n context,\n isSpanContextValid,\n defaultTextMapGetter,\n defaultTextMapSetter,\n ROOT_CONTEXT,\n SpanStatusCode,\n SpanKind as OtelSpanKind,\n type Link,\n type Span as OtelApiSpan,\n type SpanOptions as OtelSpanOptions,\n type TextMapPropagator,\n type Counter,\n type Histogram,\n type Gauge,\n} from '@opentelemetry/api'\nimport { W3CTraceContextPropagator } from '@opentelemetry/core'\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs'\nimport { NodeSDK } from '@opentelemetry/sdk-node'\nimport type { Instrumentation } from '@opentelemetry/instrumentation'\nimport { resourceFromAttributes } from '@opentelemetry/resources'\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'\nimport {\n BatchSpanProcessor,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-node'\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'\nimport { BatchLogRecordProcessor, type LogRecordProcessor } from '@opentelemetry/sdk-logs'\nimport { PeriodicExportingMetricReader, type IMetricReader } from '@opentelemetry/sdk-metrics'\nimport { PgInstrumentation } from '@opentelemetry/instrumentation-pg'\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'\n\nimport type {\n Attributes,\n AttributeValue,\n LogLevel,\n LogRecord,\n MetricPoint,\n Span,\n SpanKind,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { readTelemetryEnv } from '../env'\nimport { BACKUP_TRACEPARENT_HEADER, BACKUP_TRACESTATE_HEADER } from '../trace-headers'\nimport { redactAttributes, redactPii } from '../facade/redact'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst TRACER_NAME = 'open-mercato'\n\n/**\n * Queue/event trace propagation rides a dedicated W3C propagator on the payload\n * `_trace` carrier \u2014 deliberately NOT the global propagator below.\n */\nconst queuePropagator = new W3CTraceContextPropagator()\n\nconst W3C_TRACEPARENT = 'traceparent'\nconst W3C_TRACESTATE = 'tracestate'\n// Backup headers that mirror the W3C context but a proxy/LB won't rewrite.\nconst BACKUP_TRACEPARENT = BACKUP_TRACEPARENT_HEADER\nconst BACKUP_TRACESTATE = BACKUP_TRACESTATE_HEADER\n\n/**\n * Global propagator: standard W3C plus a backup copy\n * (`x-original-traceparent`), with explicit inbound trust.\n *\n * The problem: a load balancer (e.g. GCP's) reads the inbound `traceparent`,\n * creates its own span, and **rewrites** `traceparent` to point at that span \u2014\n * which our backend never sees. With plain W3C extraction every request becomes a\n * child of that unexported span: no request is a root, and root-span / trace-group\n * views come up empty.\n *\n * The fix (the industry \"backup header\" pattern): on inject we also write\n * `x-original-traceparent`, which the LB leaves untouched. A backup header is\n * still caller-controlled at an HTTP boundary, so its mere\n * presence is not proof that one of our services created it. Extraction ignores\n * both standard and backup headers by default.\n *\n * `TELEMETRY_TRUST_INBOUND_TRACE=true` enables both extraction paths for a\n * deployment behind a trusted upstream. Our dedicated queue/event carrier is\n * unaffected \u2014 it uses `queuePropagator` directly, not this global one.\n */\nconst backupHeaderPropagator: TextMapPropagator = {\n inject(ctx, carrier, setter) {\n // Inject into a plain temp carrier so we can mirror the values regardless of\n // the real carrier's setter shape, then copy both standard + backup headers.\n const tmp: Record<string, string> = {}\n queuePropagator.inject(ctx, tmp, defaultTextMapSetter)\n for (const [key, value] of Object.entries(tmp)) setter.set(carrier, key, value)\n if (tmp[W3C_TRACEPARENT]) setter.set(carrier, BACKUP_TRACEPARENT, tmp[W3C_TRACEPARENT])\n if (tmp[W3C_TRACESTATE]) setter.set(carrier, BACKUP_TRACESTATE, tmp[W3C_TRACESTATE])\n },\n extract(ctx, carrier, getter) {\n // Both the standard and backup headers are caller-controlled at an HTTP\n // boundary. Trust neither unless the deployment explicitly opts in.\n if (!readTelemetryEnv().trustInboundTrace) return ctx\n const first = (value: string | string[] | undefined): string | undefined =>\n Array.isArray(value) ? value[0] : value\n const backupTraceparent = first(getter.get(carrier, BACKUP_TRACEPARENT))\n if (backupTraceparent) {\n const backupCarrier: Record<string, string> = { [W3C_TRACEPARENT]: backupTraceparent }\n const backupTracestate = first(getter.get(carrier, BACKUP_TRACESTATE))\n if (backupTracestate) backupCarrier[W3C_TRACESTATE] = backupTracestate\n return queuePropagator.extract(ctx, backupCarrier, defaultTextMapGetter)\n }\n return queuePropagator.extract(ctx, carrier, getter)\n },\n fields: () => [...queuePropagator.fields(), BACKUP_TRACEPARENT, BACKUP_TRACESTATE],\n}\n\nfunction cleanAttributes(attributes?: Attributes): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attributes) return out\n for (const [key, value] of Object.entries(redactAttributes(attributes))) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nconst SPAN_KIND: Record<SpanKind, OtelSpanKind> = {\n internal: OtelSpanKind.INTERNAL,\n server: OtelSpanKind.SERVER,\n client: OtelSpanKind.CLIENT,\n producer: OtelSpanKind.PRODUCER,\n consumer: OtelSpanKind.CONSUMER,\n}\n\n/**\n * Resolve `links` carriers to span contexts. Extraction runs against\n * `ROOT_CONTEXT`, never the active one \u2014 otherwise an empty carrier would\n * silently link a span to its own ambient parent instead of yielding no link.\n */\nfunction spanLinks(carriers?: TraceCarrier[]): Link[] | undefined {\n if (!carriers?.length) return undefined\n const links: Link[] = []\n for (const carrier of carriers) {\n const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter))\n if (linked && isSpanContextValid(linked)) links.push({ context: linked })\n }\n return links.length ? links : undefined\n}\n\n/**\n * Translate the vendor-neutral options to OTEL's. `root: true` makes OTEL drop\n * the parent from the context it samples against, so the span starts a new trace\n * and the `ParentBasedSampler` falls through to its ratio-based root sampler.\n */\nfunction toOtelSpanOptions(options: SpanOptions): OtelSpanOptions {\n return {\n kind: options.kind ? SPAN_KIND[options.kind] : undefined,\n attributes: cleanAttributes(options.attributes),\n root: options.root,\n links: spanLinks(options.links),\n }\n}\n\nconst SEVERITY: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n}\n\n/** Adapts an OTEL span to the vendor-neutral `Span` interface. */\nclass OtelSpan implements Span {\n constructor(private readonly span: OtelApiSpan) {}\n setAttribute(key: string, value: AttributeValue): void {\n const redacted = cleanAttributes({ [key]: value })[key]\n if (redacted !== undefined) this.span.setAttribute(key, redacted)\n }\n setAttributes(attributes: Attributes): void {\n this.span.setAttributes(cleanAttributes(attributes))\n }\n updateName(name: string): void {\n this.span.updateName(name)\n }\n recordException(error: unknown): void {\n // Redact message + stack before they leave the process (Privacy): the auto\n // record-on-throw path (run-span) and reportError both pass raw errors here.\n this.span.recordException(serializeError(error))\n }\n setStatus(status: 'ok' | 'error', message?: string): void {\n this.span.setStatus({\n code: status === 'error' ? SpanStatusCode.ERROR : SpanStatusCode.OK,\n message: message ? redactPii(message) : undefined,\n })\n }\n end(): void {\n this.span.end()\n }\n}\n\n/**\n * Test seam: override the SDK's exporters/instrumentations so tests can use\n * in-memory exporters (deterministic, no network) instead of the OTLP defaults.\n * Production passes nothing \u2014 the OTLP defaults apply.\n */\nexport type OtlpProviderOptions = {\n spanProcessors?: SpanProcessor[]\n logRecordProcessors?: LogRecordProcessor[]\n metricReaders?: IMetricReader[]\n instrumentations?: Instrumentation[]\n}\n\n/**\n * PII guard (telemetry spec Privacy / R12). The single source of truth for pg\n * instrumentation config: `enhancedDatabaseReporting: false` means bound SQL\n * parameter VALUES are NOT captured \u2014 only the statement shape ($1, $2). User\n * data lives in those params, so this must never be flipped on. Asserted by a\n * regression test.\n */\nexport const PG_INSTRUMENTATION_OPTIONS = { enhancedDatabaseReporting: false } as const\n\nexport class OtlpProvider implements TelemetryProvider {\n /** The configured backend name (signoz | newrelic | otlp) \u2014 all OTLP, vendor differs only by endpoint. */\n readonly name: string\n readonly supports: readonly TelemetrySignal[] = ['traces', 'metrics', 'logs', 'errors']\n\n private sdk: NodeSDK | undefined\n private readonly instruments = new Map<string, Counter | Histogram | Gauge>()\n\n constructor(private readonly options: OtlpProviderOptions = {}, name = 'otlp') {\n this.name = name\n }\n\n async start(): Promise<void> {\n if (this.sdk) return\n const env = readTelemetryEnv()\n\n this.sdk = new NodeSDK({\n resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: env.serviceName }),\n // Global propagation roots requests by default. Both standard and backup\n // inbound headers are honored only when TELEMETRY_TRUST_INBOUND_TRACE is\n // explicitly enabled behind trusted infrastructure.\n textMapPropagator: backupHeaderPropagator,\n // Parent-based so child spans follow the trace's sampling decision; root\n // spans sample at the configured ratio.\n sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(env.samplingRatio) }),\n spanProcessors: this.options.spanProcessors ?? [new BatchSpanProcessor(new OTLPTraceExporter())],\n logRecordProcessors: this.options.logRecordProcessors ?? [\n new BatchLogRecordProcessor({ exporter: new OTLPLogExporter() }),\n ],\n metricReaders: this.options.metricReaders ?? [\n new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n ],\n instrumentations: this.options.instrumentations ?? [\n new PgInstrumentation(PG_INSTRUMENTATION_OPTIONS),\n new UndiciInstrumentation(),\n ],\n })\n this.sdk.start()\n }\n\n async shutdown(): Promise<void> {\n if (!this.sdk) return\n await this.sdk.shutdown()\n this.sdk = undefined\n }\n\n runInSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const tracer = trace.getTracer(TRACER_NAME)\n return tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n )\n }\n\n activeSpan(): Span | undefined {\n const active = trace.getActiveSpan()\n return active ? new OtelSpan(active) : undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n const ctx = trace.getActiveSpan()?.spanContext()\n if (!ctx || !isSpanContextValid(ctx)) return undefined\n return { traceId: ctx.traceId, spanId: ctx.spanId }\n }\n\n inject(carrier: TraceCarrier): void {\n queuePropagator.inject(context.active(), carrier, defaultTextMapSetter)\n }\n\n runInRemoteSpan<T>(carrier: TraceCarrier, name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const parent = queuePropagator.extract(context.active(), carrier, defaultTextMapGetter)\n const tracer = trace.getTracer(TRACER_NAME)\n return context.with(parent, () =>\n tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n ),\n )\n }\n\n emitLog(record: LogRecord): void {\n const attributes: Record<string, AttributeValue> = cleanAttributes(record.attributes)\n if (record.error) {\n attributes['exception.type'] = redactPii(record.error.name)\n attributes['exception.message'] = redactPii(record.error.message)\n if (record.error.stack) attributes['exception.stacktrace'] = redactPii(record.error.stack)\n }\n logs.getLogger(TRACER_NAME).emit({\n severityNumber: SEVERITY[record.level],\n severityText: record.level,\n body: redactPii(record.message),\n attributes,\n })\n }\n\n recordMetric(point: MetricPoint): void {\n const labels = cleanAttributes(point.labels)\n const instrument = this.getInstrument(point)\n if (point.kind === 'counter') (instrument as Counter).add(point.value, labels)\n else (instrument as Histogram | Gauge).record(point.value, labels)\n }\n\n private getInstrument(point: MetricPoint): Counter | Histogram | Gauge {\n const key = `${point.kind}:${point.name}`\n let instrument = this.instruments.get(key)\n if (!instrument) {\n const meter = metrics.getMeter(TRACER_NAME)\n const opts = point.unit ? { unit: point.unit } : undefined\n instrument =\n point.kind === 'counter'\n ? meter.createCounter(point.name, opts)\n : point.kind === 'histogram'\n ? meter.createHistogram(point.name, opts)\n : meter.createGauge(point.name, opts)\n this.instruments.set(key, instrument)\n }\n return instrument\n }\n}\n"],
|
|
5
|
-
"mappings": "AAQA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * The ONLY file in this package that imports `@opentelemetry/*`. Loaded via\n * dynamic `import()` from `init.ts` (so the heavy SDK never loads when telemetry\n * is off), and the OTEL packages live in `optionalDependencies`.\n *\n * Serves any OTLP backend \u2014 the endpoint + headers come from the standard\n * `OTEL_EXPORTER_OTLP_*` env vars, read by the exporters directly.\n */\nimport {\n trace,\n metrics,\n context,\n isSpanContextValid,\n defaultTextMapGetter,\n defaultTextMapSetter,\n ROOT_CONTEXT,\n SpanStatusCode,\n SpanKind as OtelSpanKind,\n type Link,\n type Span as OtelApiSpan,\n type SpanOptions as OtelSpanOptions,\n type TextMapPropagator,\n type Counter,\n type Histogram,\n type Gauge,\n type MetricOptions,\n} from '@opentelemetry/api'\nimport { W3CTraceContextPropagator } from '@opentelemetry/core'\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs'\nimport { NodeSDK } from '@opentelemetry/sdk-node'\nimport type { Instrumentation } from '@opentelemetry/instrumentation'\nimport { resourceFromAttributes } from '@opentelemetry/resources'\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'\nimport {\n BatchSpanProcessor,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-node'\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'\nimport { BatchLogRecordProcessor, type LogRecordProcessor } from '@opentelemetry/sdk-logs'\nimport { PeriodicExportingMetricReader, type IMetricReader } from '@opentelemetry/sdk-metrics'\nimport { PgInstrumentation } from '@opentelemetry/instrumentation-pg'\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'\n\nimport type {\n Attributes,\n AttributeValue,\n LogLevel,\n LogRecord,\n MetricPoint,\n Span,\n SpanKind,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { readTelemetryEnv } from '../env'\nimport { BACKUP_TRACEPARENT_HEADER, BACKUP_TRACESTATE_HEADER } from '../trace-headers'\nimport { redactAttributes, redactPii } from '../facade/redact'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst TRACER_NAME = 'open-mercato'\n\n/**\n * Queue/event trace propagation rides a dedicated W3C propagator on the payload\n * `_trace` carrier \u2014 deliberately NOT the global propagator below.\n */\nconst queuePropagator = new W3CTraceContextPropagator()\n\nconst W3C_TRACEPARENT = 'traceparent'\nconst W3C_TRACESTATE = 'tracestate'\n// Backup headers that mirror the W3C context but a proxy/LB won't rewrite.\nconst BACKUP_TRACEPARENT = BACKUP_TRACEPARENT_HEADER\nconst BACKUP_TRACESTATE = BACKUP_TRACESTATE_HEADER\n\n/**\n * Global propagator: standard W3C plus a backup copy\n * (`x-original-traceparent`), with explicit inbound trust.\n *\n * The problem: a load balancer (e.g. GCP's) reads the inbound `traceparent`,\n * creates its own span, and **rewrites** `traceparent` to point at that span \u2014\n * which our backend never sees. With plain W3C extraction every request becomes a\n * child of that unexported span: no request is a root, and root-span / trace-group\n * views come up empty.\n *\n * The fix (the industry \"backup header\" pattern): on inject we also write\n * `x-original-traceparent`, which the LB leaves untouched. A backup header is\n * still caller-controlled at an HTTP boundary, so its mere\n * presence is not proof that one of our services created it. Extraction ignores\n * both standard and backup headers by default.\n *\n * `TELEMETRY_TRUST_INBOUND_TRACE=true` enables both extraction paths for a\n * deployment behind a trusted upstream. Our dedicated queue/event carrier is\n * unaffected \u2014 it uses `queuePropagator` directly, not this global one.\n */\nconst backupHeaderPropagator: TextMapPropagator = {\n inject(ctx, carrier, setter) {\n // Inject into a plain temp carrier so we can mirror the values regardless of\n // the real carrier's setter shape, then copy both standard + backup headers.\n const tmp: Record<string, string> = {}\n queuePropagator.inject(ctx, tmp, defaultTextMapSetter)\n for (const [key, value] of Object.entries(tmp)) setter.set(carrier, key, value)\n if (tmp[W3C_TRACEPARENT]) setter.set(carrier, BACKUP_TRACEPARENT, tmp[W3C_TRACEPARENT])\n if (tmp[W3C_TRACESTATE]) setter.set(carrier, BACKUP_TRACESTATE, tmp[W3C_TRACESTATE])\n },\n extract(ctx, carrier, getter) {\n // Both the standard and backup headers are caller-controlled at an HTTP\n // boundary. Trust neither unless the deployment explicitly opts in.\n if (!readTelemetryEnv().trustInboundTrace) return ctx\n const first = (value: string | string[] | undefined): string | undefined =>\n Array.isArray(value) ? value[0] : value\n const backupTraceparent = first(getter.get(carrier, BACKUP_TRACEPARENT))\n if (backupTraceparent) {\n const backupCarrier: Record<string, string> = { [W3C_TRACEPARENT]: backupTraceparent }\n const backupTracestate = first(getter.get(carrier, BACKUP_TRACESTATE))\n if (backupTracestate) backupCarrier[W3C_TRACESTATE] = backupTracestate\n return queuePropagator.extract(ctx, backupCarrier, defaultTextMapGetter)\n }\n return queuePropagator.extract(ctx, carrier, getter)\n },\n fields: () => [...queuePropagator.fields(), BACKUP_TRACEPARENT, BACKUP_TRACESTATE],\n}\n\nfunction cleanAttributes(attributes?: Attributes): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attributes) return out\n for (const [key, value] of Object.entries(redactAttributes(attributes))) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nconst SPAN_KIND: Record<SpanKind, OtelSpanKind> = {\n internal: OtelSpanKind.INTERNAL,\n server: OtelSpanKind.SERVER,\n client: OtelSpanKind.CLIENT,\n producer: OtelSpanKind.PRODUCER,\n consumer: OtelSpanKind.CONSUMER,\n}\n\n/**\n * Resolve `links` carriers to span contexts. Extraction runs against\n * `ROOT_CONTEXT`, never the active one \u2014 otherwise an empty carrier would\n * silently link a span to its own ambient parent instead of yielding no link.\n */\nfunction spanLinks(carriers?: TraceCarrier[]): Link[] | undefined {\n if (!carriers?.length) return undefined\n const links: Link[] = []\n for (const carrier of carriers) {\n const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter))\n if (linked && isSpanContextValid(linked)) links.push({ context: linked })\n }\n return links.length ? links : undefined\n}\n\n/**\n * Translate the vendor-neutral options to OTEL's. `root: true` makes OTEL drop\n * the parent from the context it samples against, so the span starts a new trace\n * and the `ParentBasedSampler` falls through to its ratio-based root sampler.\n */\nfunction toOtelSpanOptions(options: SpanOptions): OtelSpanOptions {\n return {\n kind: options.kind ? SPAN_KIND[options.kind] : undefined,\n attributes: cleanAttributes(options.attributes),\n root: options.root,\n links: spanLinks(options.links),\n }\n}\n\nconst SEVERITY: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n}\n\n/** Adapts an OTEL span to the vendor-neutral `Span` interface. */\nclass OtelSpan implements Span {\n constructor(private readonly span: OtelApiSpan) {}\n setAttribute(key: string, value: AttributeValue): void {\n const redacted = cleanAttributes({ [key]: value })[key]\n if (redacted !== undefined) this.span.setAttribute(key, redacted)\n }\n setAttributes(attributes: Attributes): void {\n this.span.setAttributes(cleanAttributes(attributes))\n }\n updateName(name: string): void {\n this.span.updateName(name)\n }\n recordException(error: unknown): void {\n // Redact message + stack before they leave the process (Privacy): the auto\n // record-on-throw path (run-span) and reportError both pass raw errors here.\n this.span.recordException(serializeError(error))\n }\n setStatus(status: 'ok' | 'error', message?: string): void {\n this.span.setStatus({\n code: status === 'error' ? SpanStatusCode.ERROR : SpanStatusCode.OK,\n message: message ? redactPii(message) : undefined,\n })\n }\n end(): void {\n this.span.end()\n }\n}\n\n/**\n * Test seam: override the SDK's exporters/instrumentations so tests can use\n * in-memory exporters (deterministic, no network) instead of the OTLP defaults.\n * Production passes nothing \u2014 the OTLP defaults apply.\n */\nexport type OtlpProviderOptions = {\n spanProcessors?: SpanProcessor[]\n logRecordProcessors?: LogRecordProcessor[]\n metricReaders?: IMetricReader[]\n instrumentations?: Instrumentation[]\n}\n\n/**\n * PII guard (telemetry spec Privacy / R12). The single source of truth for pg\n * instrumentation config: `enhancedDatabaseReporting: false` means bound SQL\n * parameter VALUES are NOT captured \u2014 only the statement shape ($1, $2). User\n * data lives in those params, so this must never be flipped on. Asserted by a\n * regression test.\n */\nexport const PG_INSTRUMENTATION_OPTIONS = { enhancedDatabaseReporting: false } as const\n\n/**\n * Bucket boundaries for every histogram recorded in seconds. Without explicit\n * advice the SDK falls back to millisecond-scaled defaults (0, 5, 10 \u2026 10000),\n * which puts every realistic latency in the first bucket and leaves percentiles\n * uncomputable. A superset of the semconv HTTP-duration advice, plus a 1 ms\n * bucket for sub-millisecond pool acquisitions.\n */\nexport const SECONDS_HISTOGRAM_BUCKETS: readonly number[] = [\n 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,\n]\n\nfunction histogramOptions(unit: string | undefined): MetricOptions | undefined {\n if (unit !== 's') return unit ? { unit } : undefined\n return { unit, advice: { explicitBucketBoundaries: [...SECONDS_HISTOGRAM_BUCKETS] } }\n}\n\nexport class OtlpProvider implements TelemetryProvider {\n /** The configured backend name (signoz | newrelic | otlp) \u2014 all OTLP, vendor differs only by endpoint. */\n readonly name: string\n readonly supports: readonly TelemetrySignal[] = ['traces', 'metrics', 'logs', 'errors']\n\n private sdk: NodeSDK | undefined\n private readonly instruments = new Map<string, Counter | Histogram | Gauge>()\n\n constructor(private readonly options: OtlpProviderOptions = {}, name = 'otlp') {\n this.name = name\n }\n\n async start(): Promise<void> {\n if (this.sdk) return\n const env = readTelemetryEnv()\n\n this.sdk = new NodeSDK({\n resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: env.serviceName }),\n // Global propagation roots requests by default. Both standard and backup\n // inbound headers are honored only when TELEMETRY_TRUST_INBOUND_TRACE is\n // explicitly enabled behind trusted infrastructure.\n textMapPropagator: backupHeaderPropagator,\n // Parent-based so child spans follow the trace's sampling decision; root\n // spans sample at the configured ratio.\n sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(env.samplingRatio) }),\n spanProcessors: this.options.spanProcessors ?? [new BatchSpanProcessor(new OTLPTraceExporter())],\n logRecordProcessors: this.options.logRecordProcessors ?? [\n new BatchLogRecordProcessor({ exporter: new OTLPLogExporter() }),\n ],\n metricReaders: this.options.metricReaders ?? [\n new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n ],\n instrumentations: this.options.instrumentations ?? [\n new PgInstrumentation(PG_INSTRUMENTATION_OPTIONS),\n new UndiciInstrumentation(),\n ],\n })\n this.sdk.start()\n }\n\n async shutdown(): Promise<void> {\n if (!this.sdk) return\n await this.sdk.shutdown()\n this.sdk = undefined\n }\n\n runInSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const tracer = trace.getTracer(TRACER_NAME)\n return tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n )\n }\n\n activeSpan(): Span | undefined {\n const active = trace.getActiveSpan()\n return active ? new OtelSpan(active) : undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n const ctx = trace.getActiveSpan()?.spanContext()\n if (!ctx || !isSpanContextValid(ctx)) return undefined\n return { traceId: ctx.traceId, spanId: ctx.spanId }\n }\n\n inject(carrier: TraceCarrier): void {\n queuePropagator.inject(context.active(), carrier, defaultTextMapSetter)\n }\n\n runInRemoteSpan<T>(carrier: TraceCarrier, name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const parent = queuePropagator.extract(context.active(), carrier, defaultTextMapGetter)\n const tracer = trace.getTracer(TRACER_NAME)\n return context.with(parent, () =>\n tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n ),\n )\n }\n\n emitLog(record: LogRecord): void {\n const attributes: Record<string, AttributeValue> = cleanAttributes(record.attributes)\n if (record.error) {\n attributes['exception.type'] = redactPii(record.error.name)\n attributes['exception.message'] = redactPii(record.error.message)\n if (record.error.stack) attributes['exception.stacktrace'] = redactPii(record.error.stack)\n }\n logs.getLogger(TRACER_NAME).emit({\n severityNumber: SEVERITY[record.level],\n severityText: record.level,\n body: redactPii(record.message),\n attributes,\n })\n }\n\n recordMetric(point: MetricPoint): void {\n const labels = cleanAttributes(point.labels)\n const instrument = this.getInstrument(point)\n if (point.kind === 'counter') (instrument as Counter).add(point.value, labels)\n else (instrument as Histogram | Gauge).record(point.value, labels)\n }\n\n private getInstrument(point: MetricPoint): Counter | Histogram | Gauge {\n const key = `${point.kind}:${point.name}`\n let instrument = this.instruments.get(key)\n if (!instrument) {\n const meter = metrics.getMeter(TRACER_NAME)\n const opts = point.unit ? { unit: point.unit } : undefined\n instrument =\n point.kind === 'counter'\n ? meter.createCounter(point.name, opts)\n : point.kind === 'histogram'\n ? meter.createHistogram(point.name, histogramOptions(point.unit))\n : meter.createGauge(point.name, opts)\n this.instruments.set(key, instrument)\n }\n return instrument\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
|
|
2
|
+
import { getHeapSpaceStatistics } from "node:v8";
|
|
3
|
+
import {
|
|
4
|
+
collectTelemetryMetrics,
|
|
5
|
+
getTelemetryRuntime,
|
|
6
|
+
recordTelemetryMetric
|
|
7
|
+
} from "@open-mercato/shared/lib/telemetry/runtime";
|
|
8
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
9
|
+
const SAMPLE_INTERVAL_MS = 1e4;
|
|
10
|
+
const DELAY_RESOLUTION_MS = 20;
|
|
11
|
+
const NANOSECONDS_PER_SECOND = 1e9;
|
|
12
|
+
const GLOBAL_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/telemetry.runtimeMetrics");
|
|
13
|
+
const logger = createLogger("telemetry").child({ component: "runtime-metrics" });
|
|
14
|
+
const defaultSources = {
|
|
15
|
+
createEventLoopDelayMonitor: () => monitorEventLoopDelay({ resolution: DELAY_RESOLUTION_MS }),
|
|
16
|
+
eventLoopUtilization: (first, second) => performance.eventLoopUtilization(first, second),
|
|
17
|
+
memoryUsage: () => process.memoryUsage(),
|
|
18
|
+
heapSpaceStatistics: () => getHeapSpaceStatistics(),
|
|
19
|
+
setInterval: (handler, intervalMs) => setInterval(handler, intervalMs),
|
|
20
|
+
clearInterval: (interval) => clearInterval(interval)
|
|
21
|
+
};
|
|
22
|
+
function store() {
|
|
23
|
+
const globalStore = globalThis;
|
|
24
|
+
let current = globalStore[GLOBAL_KEY];
|
|
25
|
+
if (!current) {
|
|
26
|
+
current = {};
|
|
27
|
+
globalStore[GLOBAL_KEY] = current;
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
function secondsFromNanoseconds(value) {
|
|
32
|
+
return Number.isFinite(value) ? Math.max(0, value) / NANOSECONDS_PER_SECOND : 0;
|
|
33
|
+
}
|
|
34
|
+
function createRuntimeMetricsSampler(sources = defaultSources) {
|
|
35
|
+
const delay = sources.createEventLoopDelayMonitor();
|
|
36
|
+
let previousUtilization = sources.eventLoopUtilization();
|
|
37
|
+
delay.enable();
|
|
38
|
+
const sample = () => {
|
|
39
|
+
try {
|
|
40
|
+
const currentUtilization = sources.eventLoopUtilization();
|
|
41
|
+
const intervalUtilization = sources.eventLoopUtilization(
|
|
42
|
+
currentUtilization,
|
|
43
|
+
previousUtilization
|
|
44
|
+
);
|
|
45
|
+
previousUtilization = currentUtilization;
|
|
46
|
+
recordTelemetryMetric({
|
|
47
|
+
kind: "gauge",
|
|
48
|
+
name: "nodejs.eventloop.utilization",
|
|
49
|
+
value: intervalUtilization.utilization,
|
|
50
|
+
unit: "1"
|
|
51
|
+
});
|
|
52
|
+
for (const percentile of [50, 90, 99]) {
|
|
53
|
+
recordTelemetryMetric({
|
|
54
|
+
kind: "gauge",
|
|
55
|
+
name: `nodejs.eventloop.delay.p${percentile}`,
|
|
56
|
+
value: secondsFromNanoseconds(delay.percentile(percentile)),
|
|
57
|
+
unit: "s"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
recordTelemetryMetric({
|
|
61
|
+
kind: "gauge",
|
|
62
|
+
name: "process.memory.usage",
|
|
63
|
+
value: sources.memoryUsage().rss,
|
|
64
|
+
unit: "By"
|
|
65
|
+
});
|
|
66
|
+
for (const heapSpace of sources.heapSpaceStatistics()) {
|
|
67
|
+
recordTelemetryMetric({
|
|
68
|
+
kind: "gauge",
|
|
69
|
+
name: "v8js.memory.heap.used",
|
|
70
|
+
value: heapSpace.space_used_size,
|
|
71
|
+
labels: { "v8js.heap.space.name": heapSpace.space_name },
|
|
72
|
+
unit: "By"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
collectTelemetryMetrics((err) => {
|
|
76
|
+
logger.warn("Runtime metric collector failed", { err });
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
try {
|
|
80
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
81
|
+
module: "telemetry",
|
|
82
|
+
code: "telemetry.runtime_sampling_failed"
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
logger.warn("Runtime metric sampling failed", { err });
|
|
87
|
+
} finally {
|
|
88
|
+
delay.reset();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const interval = sources.setInterval(sample, SAMPLE_INTERVAL_MS);
|
|
92
|
+
interval.unref?.();
|
|
93
|
+
let disposed = false;
|
|
94
|
+
return () => {
|
|
95
|
+
if (disposed) return;
|
|
96
|
+
disposed = true;
|
|
97
|
+
sources.clearInterval(interval);
|
|
98
|
+
delay.disable();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function startRuntimeMetrics(sources = defaultSources) {
|
|
102
|
+
const current = store();
|
|
103
|
+
if (!current.active) {
|
|
104
|
+
current.active = {
|
|
105
|
+
references: 0,
|
|
106
|
+
dispose: createRuntimeMetricsSampler(sources)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const active = current.active;
|
|
110
|
+
active.references += 1;
|
|
111
|
+
let released = false;
|
|
112
|
+
return () => {
|
|
113
|
+
if (released) return;
|
|
114
|
+
released = true;
|
|
115
|
+
active.references -= 1;
|
|
116
|
+
if (active.references > 0) return;
|
|
117
|
+
active.dispose();
|
|
118
|
+
const latest = store();
|
|
119
|
+
if (latest.active === active) latest.active = void 0;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function resetRuntimeMetrics() {
|
|
123
|
+
const current = store();
|
|
124
|
+
current.active?.dispose();
|
|
125
|
+
current.active = void 0;
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
createRuntimeMetricsSampler,
|
|
129
|
+
resetRuntimeMetrics,
|
|
130
|
+
startRuntimeMetrics
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=runtime-metrics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/runtime-metrics.ts"],
|
|
4
|
+
"sourcesContent": ["import { monitorEventLoopDelay, performance, type EventLoopUtilization } from 'node:perf_hooks'\nimport { getHeapSpaceStatistics } from 'node:v8'\nimport {\n collectTelemetryMetrics,\n getTelemetryRuntime,\n recordTelemetryMetric,\n} from '@open-mercato/shared/lib/telemetry/runtime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst SAMPLE_INTERVAL_MS = 10_000\nconst DELAY_RESOLUTION_MS = 20\nconst NANOSECONDS_PER_SECOND = 1_000_000_000\nconst GLOBAL_KEY = Symbol.for('@open-mercato/telemetry.runtimeMetrics')\n\nexport type EventLoopDelayMonitor = {\n enable(): void\n disable(): void\n percentile(percentile: number): number\n reset(): void\n}\n\nexport type HeapSpaceSample = {\n space_name: string\n space_used_size: number\n}\n\nexport type RuntimeMetricSources = {\n createEventLoopDelayMonitor(): EventLoopDelayMonitor\n eventLoopUtilization(\n first?: EventLoopUtilization,\n second?: EventLoopUtilization,\n ): EventLoopUtilization\n memoryUsage(): { rss: number }\n heapSpaceStatistics(): HeapSpaceSample[]\n setInterval(handler: () => void, intervalMs: number): ReturnType<typeof setInterval>\n clearInterval(interval: ReturnType<typeof setInterval>): void\n}\n\ntype ActiveRuntimeSampler = {\n references: number\n dispose(): void\n}\n\ntype RuntimeMetricsStore = {\n active?: ActiveRuntimeSampler\n}\n\nconst logger = createLogger('telemetry').child({ component: 'runtime-metrics' })\n\nconst defaultSources: RuntimeMetricSources = {\n createEventLoopDelayMonitor: () => monitorEventLoopDelay({ resolution: DELAY_RESOLUTION_MS }),\n eventLoopUtilization: (first, second) => performance.eventLoopUtilization(first, second),\n memoryUsage: () => process.memoryUsage(),\n heapSpaceStatistics: () => getHeapSpaceStatistics(),\n setInterval: (handler, intervalMs) => setInterval(handler, intervalMs),\n clearInterval: (interval) => clearInterval(interval),\n}\n\nfunction store(): RuntimeMetricsStore {\n const globalStore = globalThis as unknown as Record<symbol, RuntimeMetricsStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\nfunction secondsFromNanoseconds(value: number): number {\n return Number.isFinite(value) ? Math.max(0, value) / NANOSECONDS_PER_SECOND : 0\n}\n\nexport function createRuntimeMetricsSampler(\n sources: RuntimeMetricSources = defaultSources,\n): () => void {\n const delay = sources.createEventLoopDelayMonitor()\n let previousUtilization = sources.eventLoopUtilization()\n delay.enable()\n\n const sample = () => {\n try {\n const currentUtilization = sources.eventLoopUtilization()\n const intervalUtilization = sources.eventLoopUtilization(\n currentUtilization,\n previousUtilization,\n )\n previousUtilization = currentUtilization\n\n recordTelemetryMetric({\n kind: 'gauge',\n name: 'nodejs.eventloop.utilization',\n value: intervalUtilization.utilization,\n unit: '1',\n })\n for (const percentile of [50, 90, 99]) {\n recordTelemetryMetric({\n kind: 'gauge',\n name: `nodejs.eventloop.delay.p${percentile}`,\n value: secondsFromNanoseconds(delay.percentile(percentile)),\n unit: 's',\n })\n }\n\n recordTelemetryMetric({\n kind: 'gauge',\n name: 'process.memory.usage',\n value: sources.memoryUsage().rss,\n unit: 'By',\n })\n for (const heapSpace of sources.heapSpaceStatistics()) {\n recordTelemetryMetric({\n kind: 'gauge',\n name: 'v8js.memory.heap.used',\n value: heapSpace.space_used_size,\n labels: { 'v8js.heap.space.name': heapSpace.space_name },\n unit: 'By',\n })\n }\n\n collectTelemetryMetrics((err) => {\n logger.warn('Runtime metric collector failed', { err })\n })\n } catch (err) {\n try {\n getTelemetryRuntime()?.reportError(err, {\n module: 'telemetry',\n code: 'telemetry.runtime_sampling_failed',\n })\n } catch {}\n logger.warn('Runtime metric sampling failed', { err })\n } finally {\n delay.reset()\n }\n }\n\n const interval = sources.setInterval(sample, SAMPLE_INTERVAL_MS)\n interval.unref?.()\n\n let disposed = false\n return () => {\n if (disposed) return\n disposed = true\n sources.clearInterval(interval)\n delay.disable()\n }\n}\n\nexport function startRuntimeMetrics(\n sources: RuntimeMetricSources = defaultSources,\n): () => void {\n const current = store()\n if (!current.active) {\n current.active = {\n references: 0,\n dispose: createRuntimeMetricsSampler(sources),\n }\n }\n\n const active = current.active\n active.references += 1\n let released = false\n\n return () => {\n if (released) return\n released = true\n active.references -= 1\n if (active.references > 0) return\n active.dispose()\n const latest = store()\n if (latest.active === active) latest.active = undefined\n }\n}\n\n/** Test-only: tear down the process-wide sampler regardless of reference count. */\nexport function resetRuntimeMetrics(): void {\n const current = store()\n current.active?.dispose()\n current.active = undefined\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/telemetry",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7836.1.0ef0f453c6",
|
|
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.10.1-develop.
|
|
41
|
+
"@open-mercato/shared": "0.10.1-develop.7836.1.0ef0f453c6"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": ">=18.0.0"
|