@open-mercato/telemetry 0.6.8-develop.6993.1.3fc50f3409 → 0.6.8-develop.7000.1.0ae682bad1
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/AGENTS.md +8 -0
- package/README.md +59 -1
- package/dist/init.js +2 -0
- package/dist/init.js.map +2 -2
- package/dist/provider/console-provider.js +3 -0
- package/dist/provider/console-provider.js.map +2 -2
- package/dist/provider/noop-provider.js +2 -0
- package/dist/provider/noop-provider.js.map +2 -2
- package/dist/provider/otlp-provider.js +23 -2
- package/dist/provider/otlp-provider.js.map +2 -2
- package/package.json +2 -2
- package/src/__tests__/otlp-integration.test.ts +43 -0
- package/src/__tests__/otlp-root-sampling.test.ts +77 -0
- package/src/init.ts +2 -0
- package/src/provider/console-provider.ts +4 -1
- package/src/provider/noop-provider.ts +1 -0
- package/src/provider/otlp-provider.ts +39 -8
- package/src/types.ts +34 -0
package/AGENTS.md
CHANGED
|
@@ -17,6 +17,14 @@ off by default. Spec:
|
|
|
17
17
|
provider activates only after an explicit bootstrap registers the exact
|
|
18
18
|
configured name and calls `initTelemetry()`.
|
|
19
19
|
- Name spans `module.entity.action` (lowercase, dot-separated).
|
|
20
|
+
- Root the repeating unit of a long-lived job (batch/page) with
|
|
21
|
+
`withSpan(name, fn, { root: true, links: [runCarrier] })`. Without it the whole
|
|
22
|
+
job inherits one sampling decision from its trigger — a blind spot below ratio
|
|
23
|
+
1.0, an unrenderable trace at 1.0. Always pair `root` with `links` so the chain
|
|
24
|
+
back to the trigger survives.
|
|
25
|
+
- Emit spans from packages that must not depend on this one via
|
|
26
|
+
`withTelemetrySpan` / `captureTelemetryTrace` from
|
|
27
|
+
`@open-mercato/shared/lib/telemetry/runtime`, never a direct import.
|
|
20
28
|
- Use semantic-convention metric/attribute names when available.
|
|
21
29
|
- Keep metric labels low-cardinality. Tenant, organization, and user IDs belong
|
|
22
30
|
on span attributes, never metric labels.
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ bridge, so the package is not loaded on the disabled path.
|
|
|
125
125
|
|
|
126
126
|
| Export | Description |
|
|
127
127
|
| --- | --- |
|
|
128
|
-
| `withSpan(name, fn, opts?)` | Run `fn` in a provider-owned span |
|
|
128
|
+
| `withSpan(name, fn, opts?)` | Run `fn` in a provider-owned span. `opts.root` starts a new trace; `opts.links` attaches causal links (see [Long-lived jobs](#long-lived-jobs-root-spans)) |
|
|
129
129
|
| `currentSpan()` / `setAttributes(attrs)` | Active span access |
|
|
130
130
|
| `counter` / `histogram` / `gauge` | Metric helpers |
|
|
131
131
|
| `reportError(err, ctx?)` | Span exception + shared error log + `om.errors` |
|
|
@@ -138,6 +138,64 @@ bridge, so the package is not loaded on the disabled path.
|
|
|
138
138
|
`@open-mercato/telemetry/nextjs-config` separately exports only
|
|
139
139
|
`telemetryServerExternalPackages` for build configuration.
|
|
140
140
|
|
|
141
|
+
### Long-lived jobs: root spans
|
|
142
|
+
|
|
143
|
+
Trace context propagates from the request that triggered a job through the queue
|
|
144
|
+
into the worker, and `ParentBasedSampler` only decides sampling at a trace's
|
|
145
|
+
**root**. A job that runs for hours therefore inherits one decision made on a
|
|
146
|
+
request from long before it: below `TELEMETRY_SAMPLING_RATIO=1.0` an entire run
|
|
147
|
+
can emit nothing at all, and at `1.0` the run becomes one unrenderable trace.
|
|
148
|
+
|
|
149
|
+
Give the unit of work you actually analyse — a batch, a page — its own trace, and
|
|
150
|
+
link it back so the causal chain survives:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { withSpan, captureTraceContext } from '@open-mercato/telemetry'
|
|
154
|
+
|
|
155
|
+
const runTrace = captureTraceContext() // the triggering job's trace
|
|
156
|
+
|
|
157
|
+
for (const batch of batches) {
|
|
158
|
+
await withSpan('import.batch', async (span) => {
|
|
159
|
+
span.setAttributes({ 'import.batch_index': batch.index })
|
|
160
|
+
await processBatch(batch)
|
|
161
|
+
}, { root: true, links: [runTrace] })
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each batch now samples independently and each trace stays small enough to render.
|
|
166
|
+
Sampling stays probabilistic: at ratio `p` a run of `n` batches still emits
|
|
167
|
+
nothing with probability `(1 - p)^n` — at `p = 0.25` that is 75% for one batch,
|
|
168
|
+
32% for four, 0.3% for twenty. Rooting shrinks the blind spot fast as a run gets
|
|
169
|
+
longer; it does not promise a signal from every run. Only ratio `1.0` does that,
|
|
170
|
+
and rooting is what makes `1.0` renderable.
|
|
171
|
+
|
|
172
|
+
`links` takes W3C **carriers**, so it accepts both `captureTraceContext()` and a
|
|
173
|
+
carrier received from another process; empty or malformed carriers are dropped
|
|
174
|
+
rather than emitted as invalid links.
|
|
175
|
+
|
|
176
|
+
### Emitting spans without depending on this package
|
|
177
|
+
|
|
178
|
+
Packages that must not take a dependency on `@open-mercato/telemetry` use the
|
|
179
|
+
shared runtime bridge instead. With telemetry off, `withTelemetrySpan` is `fn`
|
|
180
|
+
plus one global lookup — no span is allocated and the OTEL SDK is never reached.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import {
|
|
184
|
+
withTelemetrySpan,
|
|
185
|
+
captureTelemetryTrace,
|
|
186
|
+
} from '@open-mercato/shared/lib/telemetry/runtime'
|
|
187
|
+
|
|
188
|
+
const runTrace = captureTelemetryTrace() // undefined when telemetry is off
|
|
189
|
+
|
|
190
|
+
await withTelemetrySpan('data_sync.import.batch', async (span) => {
|
|
191
|
+
span.setAttributes({ 'data_sync.batch_index': index })
|
|
192
|
+
await processBatch()
|
|
193
|
+
}, { root: true, links: runTrace ? [runTrace] : undefined })
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`packages/core/src/modules/data_sync/lib/batch-stream.ts` is the reference
|
|
197
|
+
consumer.
|
|
198
|
+
|
|
141
199
|
## Security and privacy
|
|
142
200
|
|
|
143
201
|
- Explicit off is absolute: custom providers cannot override an unset/noop
|
package/dist/init.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
12
|
import { registerTelemetryLogger } from "./facade/logger-bridge.js";
|
|
13
13
|
import { captureTraceContext, continueTrace } from "./facade/propagation.js";
|
|
14
|
+
import { withSpan } from "./facade/tracer.js";
|
|
14
15
|
import { recordHttpDuration } from "./facade/http.js";
|
|
15
16
|
import { reportError } from "./facade/report-error.js";
|
|
16
17
|
let initialized = false;
|
|
@@ -77,6 +78,7 @@ function createRuntime(provider) {
|
|
|
77
78
|
canUseGlobalTracePropagation: () => isOtlpBackend(provider.name) && readTelemetryEnv().trustInboundTrace,
|
|
78
79
|
captureTraceContext,
|
|
79
80
|
continueTrace: (carrier, name, fn, options) => continueTrace(carrier, name, () => fn(), options),
|
|
81
|
+
withSpan: (name, fn, options) => withSpan(name, fn, options),
|
|
80
82
|
recordHttpDuration,
|
|
81
83
|
reportError,
|
|
82
84
|
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 { 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 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,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;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 { 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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,6 +15,9 @@ class ConsoleSpan {
|
|
|
15
15
|
setAttributes(attributes) {
|
|
16
16
|
Object.assign(this.attributes, attributes);
|
|
17
17
|
}
|
|
18
|
+
updateName(name) {
|
|
19
|
+
this.name = name;
|
|
20
|
+
}
|
|
18
21
|
recordException(error) {
|
|
19
22
|
this.status = "error";
|
|
20
23
|
this.attributes.exception = serializeError(error).message;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/provider/console-provider.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n LogRecord,\n MetricPoint,\n Span,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst logger = createLogger('telemetry')\n\n/**\n * Dev backend: prints span timings and metric points to stdout (via the shared\n * logger). It does NOT handle logs \u2014 the facade's logger already writes those to\n * stdout, so `emitLog` is a no-op here to avoid duplication.\n */\nclass ConsoleSpan implements Span {\n private readonly attributes: Record<string, unknown> = {}\n private status: 'ok' | 'error' = 'ok'\n private readonly startedAt = Date.now()\n\n constructor(private
|
|
5
|
-
"mappings": "AAUA,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAExB,MAAM,SAAS,aAAa,WAAW;AAOvC,MAAM,YAA4B;AAAA,EAKhC,
|
|
4
|
+
"sourcesContent": ["import type {\n LogRecord,\n MetricPoint,\n Span,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst logger = createLogger('telemetry')\n\n/**\n * Dev backend: prints span timings and metric points to stdout (via the shared\n * logger). It does NOT handle logs \u2014 the facade's logger already writes those to\n * stdout, so `emitLog` is a no-op here to avoid duplication.\n */\nclass ConsoleSpan implements Span {\n private readonly attributes: Record<string, unknown> = {}\n private status: 'ok' | 'error' = 'ok'\n private readonly startedAt = Date.now()\n\n constructor(private name: string) {}\n\n setAttribute(key: string, value: string | number | boolean): void {\n this.attributes[key] = value\n }\n setAttributes(attributes: Record<string, string | number | boolean | undefined>): void {\n Object.assign(this.attributes, attributes)\n }\n updateName(name: string): void {\n this.name = name\n }\n recordException(error: unknown): void {\n this.status = 'error'\n this.attributes.exception = serializeError(error).message\n }\n setStatus(status: 'ok' | 'error'): void {\n this.status = status\n }\n end(): void {\n logger[this.status === 'error' ? 'error' : 'debug'](`Span ${this.name}`, {\n span: this.name,\n durationMs: Date.now() - this.startedAt,\n status: this.status,\n })\n }\n}\n\nexport class ConsoleProvider implements TelemetryProvider {\n readonly name = 'console'\n readonly supports: readonly TelemetrySignal[] = ['traces', 'metrics']\n\n async start(): Promise<void> {}\n async shutdown(): Promise<void> {}\n\n runInSpan<T>(name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return runSpan(new ConsoleSpan(name), fn)\n }\n\n activeSpan(): Span | undefined {\n return undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n // ConsoleSpan carries no real trace ids (dev-only timing print).\n return undefined\n }\n\n inject(_carrier: TraceCarrier): void {}\n\n runInRemoteSpan<T>(_carrier: TraceCarrier, name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return runSpan(new ConsoleSpan(name), fn)\n }\n\n emitLog(_record: LogRecord): void {}\n\n recordMetric(point: MetricPoint): void {\n logger.debug(`Metric ${point.name}`, {\n metric: point.name,\n kind: point.kind,\n value: point.value,\n ...point.labels,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAExB,MAAM,SAAS,aAAa,WAAW;AAOvC,MAAM,YAA4B;AAAA,EAKhC,YAAoB,MAAc;AAAd;AAJpB,SAAiB,aAAsC,CAAC;AACxD,SAAQ,SAAyB;AACjC,SAAiB,YAAY,KAAK,IAAI;AAAA,EAEH;AAAA,EAEnC,aAAa,KAAa,OAAwC;AAChE,SAAK,WAAW,GAAG,IAAI;AAAA,EACzB;AAAA,EACA,cAAc,YAAyE;AACrF,WAAO,OAAO,KAAK,YAAY,UAAU;AAAA,EAC3C;AAAA,EACA,WAAW,MAAoB;AAC7B,SAAK,OAAO;AAAA,EACd;AAAA,EACA,gBAAgB,OAAsB;AACpC,SAAK,SAAS;AACd,SAAK,WAAW,YAAY,eAAe,KAAK,EAAE;AAAA,EACpD;AAAA,EACA,UAAU,QAA8B;AACtC,SAAK,SAAS;AAAA,EAChB;AAAA,EACA,MAAY;AACV,WAAO,KAAK,WAAW,UAAU,UAAU,OAAO,EAAE,QAAQ,KAAK,IAAI,IAAI;AAAA,MACvE,MAAM,KAAK;AAAA,MACX,YAAY,KAAK,IAAI,IAAI,KAAK;AAAA,MAC9B,QAAQ,KAAK;AAAA,IACf,CAAC;AAAA,EACH;AACF;AAEO,MAAM,gBAA6C;AAAA,EAAnD;AACL,SAAS,OAAO;AAChB,SAAS,WAAuC,CAAC,UAAU,SAAS;AAAA;AAAA,EAEpE,MAAM,QAAuB;AAAA,EAAC;AAAA,EAC9B,MAAM,WAA0B;AAAA,EAAC;AAAA,EAEjC,UAAa,MAAc,UAAuB,IAA0B;AAC1E,WAAO,QAAQ,IAAI,YAAY,IAAI,GAAG,EAAE;AAAA,EAC1C;AAAA,EAEA,aAA+B;AAC7B,WAAO;AAAA,EACT;AAAA,EAEA,qBAA+C;AAE7C,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,UAA8B;AAAA,EAAC;AAAA,EAEtC,gBAAmB,UAAwB,MAAc,UAAuB,IAA0B;AACxG,WAAO,QAAQ,IAAI,YAAY,IAAI,GAAG,EAAE;AAAA,EAC1C;AAAA,EAEA,QAAQ,SAA0B;AAAA,EAAC;AAAA,EAEnC,aAAa,OAA0B;AACrC,WAAO,MAAM,UAAU,MAAM,IAAI,IAAI;AAAA,MACnC,QAAQ,MAAM;AAAA,MACd,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,MACb,GAAG,MAAM;AAAA,IACX,CAAC;AAAA,EACH;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/provider/noop-provider.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n LogRecord,\n MetricPoint,\n Span,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\n\n/** A span that does nothing \u2014 used when telemetry is off. */\nexport const NOOP_SPAN: Span = {\n setAttribute() {},\n setAttributes() {},\n recordException() {},\n setStatus() {},\n end() {},\n}\n\n/**\n * The default backend: a hard no-op. `runInSpan` still runs `fn` (so app logic\n * is unaffected); everything else is a cheap return. The heavy OTEL SDK is\n * never imported on this path.\n */\nexport class NoopProvider implements TelemetryProvider {\n readonly name = 'noop'\n readonly supports: readonly TelemetrySignal[] = []\n\n async start(): Promise<void> {}\n async shutdown(): Promise<void> {}\n\n runInSpan<T>(_name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return fn(NOOP_SPAN)\n }\n\n activeSpan(): Span | undefined {\n return undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n return undefined\n }\n\n inject(_carrier: TraceCarrier): void {}\n\n runInRemoteSpan<T>(_carrier: TraceCarrier, _name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return fn(NOOP_SPAN)\n }\n\n emitLog(_record: LogRecord): void {}\n recordMetric(_point: MetricPoint): void {}\n}\n"],
|
|
5
|
-
"mappings": "AAYO,MAAM,YAAkB;AAAA,EAC7B,eAAe;AAAA,EAAC;AAAA,EAChB,gBAAgB;AAAA,EAAC;AAAA,EACjB,kBAAkB;AAAA,EAAC;AAAA,EACnB,YAAY;AAAA,EAAC;AAAA,EACb,MAAM;AAAA,EAAC;AACT;AAOO,MAAM,aAA0C;AAAA,EAAhD;AACL,SAAS,OAAO;AAChB,SAAS,WAAuC,CAAC;AAAA;AAAA,EAEjD,MAAM,QAAuB;AAAA,EAAC;AAAA,EAC9B,MAAM,WAA0B;AAAA,EAAC;AAAA,EAEjC,UAAa,OAAe,UAAuB,IAA0B;AAC3E,WAAO,GAAG,SAAS;AAAA,EACrB;AAAA,EAEA,aAA+B;AAC7B,WAAO;AAAA,EACT;AAAA,EAEA,qBAA+C;AAC7C,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,UAA8B;AAAA,EAAC;AAAA,EAEtC,gBAAmB,UAAwB,OAAe,UAAuB,IAA0B;AACzG,WAAO,GAAG,SAAS;AAAA,EACrB;AAAA,EAEA,QAAQ,SAA0B;AAAA,EAAC;AAAA,EACnC,aAAa,QAA2B;AAAA,EAAC;AAC3C;",
|
|
4
|
+
"sourcesContent": ["import type {\n LogRecord,\n MetricPoint,\n Span,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\n\n/** A span that does nothing \u2014 used when telemetry is off. */\nexport const NOOP_SPAN: Span = {\n setAttribute() {},\n setAttributes() {},\n updateName() {},\n recordException() {},\n setStatus() {},\n end() {},\n}\n\n/**\n * The default backend: a hard no-op. `runInSpan` still runs `fn` (so app logic\n * is unaffected); everything else is a cheap return. The heavy OTEL SDK is\n * never imported on this path.\n */\nexport class NoopProvider implements TelemetryProvider {\n readonly name = 'noop'\n readonly supports: readonly TelemetrySignal[] = []\n\n async start(): Promise<void> {}\n async shutdown(): Promise<void> {}\n\n runInSpan<T>(_name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return fn(NOOP_SPAN)\n }\n\n activeSpan(): Span | undefined {\n return undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n return undefined\n }\n\n inject(_carrier: TraceCarrier): void {}\n\n runInRemoteSpan<T>(_carrier: TraceCarrier, _name: string, _options: SpanOptions, fn: (span: Span) => T): T {\n return fn(NOOP_SPAN)\n }\n\n emitLog(_record: LogRecord): void {}\n recordMetric(_point: MetricPoint): void {}\n}\n"],
|
|
5
|
+
"mappings": "AAYO,MAAM,YAAkB;AAAA,EAC7B,eAAe;AAAA,EAAC;AAAA,EAChB,gBAAgB;AAAA,EAAC;AAAA,EACjB,aAAa;AAAA,EAAC;AAAA,EACd,kBAAkB;AAAA,EAAC;AAAA,EACnB,YAAY;AAAA,EAAC;AAAA,EACb,MAAM;AAAA,EAAC;AACT;AAOO,MAAM,aAA0C;AAAA,EAAhD;AACL,SAAS,OAAO;AAChB,SAAS,WAAuC,CAAC;AAAA;AAAA,EAEjD,MAAM,QAAuB;AAAA,EAAC;AAAA,EAC9B,MAAM,WAA0B;AAAA,EAAC;AAAA,EAEjC,UAAa,OAAe,UAAuB,IAA0B;AAC3E,WAAO,GAAG,SAAS;AAAA,EACrB;AAAA,EAEA,aAA+B;AAC7B,WAAO;AAAA,EACT;AAAA,EAEA,qBAA+C;AAC7C,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,UAA8B;AAAA,EAAC;AAAA,EAEtC,gBAAmB,UAAwB,OAAe,UAAuB,IAA0B;AACzG,WAAO,GAAG,SAAS;AAAA,EACrB;AAAA,EAEA,QAAQ,SAA0B;AAAA,EAAC;AAAA,EACnC,aAAa,QAA2B;AAAA,EAAC;AAC3C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isSpanContextValid,
|
|
6
6
|
defaultTextMapGetter,
|
|
7
7
|
defaultTextMapSetter,
|
|
8
|
+
ROOT_CONTEXT,
|
|
8
9
|
SpanStatusCode,
|
|
9
10
|
SpanKind as OtelSpanKind
|
|
10
11
|
} from "@opentelemetry/api";
|
|
@@ -72,6 +73,23 @@ const SPAN_KIND = {
|
|
|
72
73
|
producer: OtelSpanKind.PRODUCER,
|
|
73
74
|
consumer: OtelSpanKind.CONSUMER
|
|
74
75
|
};
|
|
76
|
+
function spanLinks(carriers) {
|
|
77
|
+
if (!carriers?.length) return void 0;
|
|
78
|
+
const links = [];
|
|
79
|
+
for (const carrier of carriers) {
|
|
80
|
+
const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter));
|
|
81
|
+
if (linked && isSpanContextValid(linked)) links.push({ context: linked });
|
|
82
|
+
}
|
|
83
|
+
return links.length ? links : void 0;
|
|
84
|
+
}
|
|
85
|
+
function toOtelSpanOptions(options) {
|
|
86
|
+
return {
|
|
87
|
+
kind: options.kind ? SPAN_KIND[options.kind] : void 0,
|
|
88
|
+
attributes: cleanAttributes(options.attributes),
|
|
89
|
+
root: options.root,
|
|
90
|
+
links: spanLinks(options.links)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
75
93
|
const SEVERITY = {
|
|
76
94
|
debug: SeverityNumber.DEBUG,
|
|
77
95
|
info: SeverityNumber.INFO,
|
|
@@ -89,6 +107,9 @@ class OtelSpan {
|
|
|
89
107
|
setAttributes(attributes) {
|
|
90
108
|
this.span.setAttributes(cleanAttributes(attributes));
|
|
91
109
|
}
|
|
110
|
+
updateName(name) {
|
|
111
|
+
this.span.updateName(name);
|
|
112
|
+
}
|
|
92
113
|
recordException(error) {
|
|
93
114
|
this.span.recordException(serializeError(error));
|
|
94
115
|
}
|
|
@@ -145,7 +166,7 @@ class OtlpProvider {
|
|
|
145
166
|
const tracer = trace.getTracer(TRACER_NAME);
|
|
146
167
|
return tracer.startActiveSpan(
|
|
147
168
|
name,
|
|
148
|
-
|
|
169
|
+
toOtelSpanOptions(options),
|
|
149
170
|
(otelSpan) => runSpan(new OtelSpan(otelSpan), fn)
|
|
150
171
|
);
|
|
151
172
|
}
|
|
@@ -168,7 +189,7 @@ class OtlpProvider {
|
|
|
168
189
|
parent,
|
|
169
190
|
() => tracer.startActiveSpan(
|
|
170
191
|
name,
|
|
171
|
-
|
|
192
|
+
toOtelSpanOptions(options),
|
|
172
193
|
(otelSpan) => runSpan(new OtelSpan(otelSpan), fn)
|
|
173
194
|
)
|
|
174
195
|
);
|
|
@@ -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 SpanStatusCode,\n SpanKind as OtelSpanKind,\n type Span as OtelApiSpan,\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 { 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 = 'x-original-traceparent'\nconst BACKUP_TRACESTATE = 'x-original-tracestate'\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\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 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(\n name,\n { kind: options.kind ? SPAN_KIND[options.kind] : undefined, attributes: cleanAttributes(options.attributes) },\n (otelSpan) => 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(\n name,\n { kind: options.kind ? SPAN_KIND[options.kind] : undefined, attributes: cleanAttributes(options.attributes) },\n (otelSpan) => 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,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} 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 { 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 = 'x-original-traceparent'\nconst BACKUP_TRACESTATE = 'x-original-tracestate'\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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/telemetry",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7000.1.0ae682bad1",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
32
|
+
"@open-mercato/shared": "0.6.8-develop.7000.1.0ae682bad1"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
35
|
"@opentelemetry/api": "^1.9.1",
|
|
@@ -102,6 +102,49 @@ describe('OtlpProvider (in-memory exporters)', () => {
|
|
|
102
102
|
expect(parentSpanId(child as ReadableSpan)).toBe(parent?.spanContext().spanId)
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
it('root: true starts a NEW trace instead of nesting under the active one', () => {
|
|
106
|
+
withSpan('long-job', () => withSpan('batch', () => undefined, { root: true }))
|
|
107
|
+
const spans = spanExporter.getFinishedSpans()
|
|
108
|
+
const job = spans.find((s) => s.name === 'long-job')
|
|
109
|
+
const batch = spans.find((s) => s.name === 'batch')
|
|
110
|
+
expect(job && batch).toBeTruthy()
|
|
111
|
+
expect(batch?.spanContext().traceId).not.toBe(job?.spanContext().traceId)
|
|
112
|
+
expect(parentSpanId(batch as ReadableSpan)).toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('links a rooted span back to the trace that caused it', () => {
|
|
116
|
+
let carrier: Record<string, string> = {}
|
|
117
|
+
withSpan('run', () => {
|
|
118
|
+
carrier = captureTraceContext()
|
|
119
|
+
})
|
|
120
|
+
const run = spanExporter.getFinishedSpans().find((s) => s.name === 'run')
|
|
121
|
+
|
|
122
|
+
withSpan('linked-batch', () => undefined, { root: true, links: [carrier] })
|
|
123
|
+
const batch = spanExporter.getFinishedSpans().find((s) => s.name === 'linked-batch')
|
|
124
|
+
|
|
125
|
+
expect(batch?.spanContext().traceId).not.toBe(run?.spanContext().traceId)
|
|
126
|
+
expect(batch?.links).toHaveLength(1)
|
|
127
|
+
expect(batch?.links[0]?.context.traceId).toBe(run?.spanContext().traceId)
|
|
128
|
+
expect(batch?.links[0]?.context.spanId).toBe(run?.spanContext().spanId)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('drops empty and malformed link carriers instead of emitting invalid links', () => {
|
|
132
|
+
withSpan('unlinked', () => undefined, {
|
|
133
|
+
root: true,
|
|
134
|
+
links: [{}, { traceparent: 'not-a-traceparent' }],
|
|
135
|
+
})
|
|
136
|
+
const span = spanExporter.getFinishedSpans().find((s) => s.name === 'unlinked')
|
|
137
|
+
expect(span?.links).toHaveLength(0)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('does not link a rooted span to its ambient parent when the carrier is empty', () => {
|
|
141
|
+
// Extraction must run against ROOT_CONTEXT: an empty carrier means "no
|
|
142
|
+
// link", never "link to whatever happens to be active here".
|
|
143
|
+
withSpan('ambient', () => withSpan('rooted', () => undefined, { root: true, links: [{}] }))
|
|
144
|
+
const rooted = spanExporter.getFinishedSpans().find((s) => s.name === 'rooted')
|
|
145
|
+
expect(rooted?.links).toHaveLength(0)
|
|
146
|
+
})
|
|
147
|
+
|
|
105
148
|
it('inject() writes a valid W3C traceparent inside an active span', () => {
|
|
106
149
|
let carrier: Record<string, string> = {}
|
|
107
150
|
withSpan('producer', () => {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the bug `root` exists to fix.
|
|
3
|
+
*
|
|
4
|
+
* `ParentBasedSampler` only rolls the dice at a trace's ROOT; every descendant
|
|
5
|
+
* inherits that one decision. A sync/backfill job inherits the trace of the
|
|
6
|
+
* request that triggered it, so below ratio 1.0 an entire multi-day run is a
|
|
7
|
+
* coin flip taken hours earlier — lose it and the run emits nothing at all, for
|
|
8
|
+
* its whole life, with no way to intervene from outside.
|
|
9
|
+
*
|
|
10
|
+
* Ratio 0 makes that deterministic here: a span nested under a sampled parent is
|
|
11
|
+
* still recorded (inheritance), while a `root: true` span takes a FRESH decision
|
|
12
|
+
* and is dropped. A run of rooted batches therefore never rides on one flip.
|
|
13
|
+
*
|
|
14
|
+
* Its own file because the sampling ratio is read once at `provider.start()` and
|
|
15
|
+
* NodeSDK installs process-global tracer state.
|
|
16
|
+
*/
|
|
17
|
+
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
18
|
+
|
|
19
|
+
import { OtlpProvider } from '../provider/otlp-provider'
|
|
20
|
+
import { setActiveProvider, resetActiveProvider } from '../provider/registry'
|
|
21
|
+
import { resetTelemetryEnvCache } from '../env'
|
|
22
|
+
import { withSpan, continueTrace } from '../index'
|
|
23
|
+
|
|
24
|
+
const spanExporter = new InMemorySpanExporter()
|
|
25
|
+
let provider: OtlpProvider
|
|
26
|
+
|
|
27
|
+
/** A remote parent already marked sampled (trace-flags `01`). */
|
|
28
|
+
const SAMPLED_REMOTE_CARRIER = {
|
|
29
|
+
traceparent: `00-${'1'.repeat(32)}-${'2'.repeat(16)}-01`,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The span processor hands off to the exporter off the microtask queue. */
|
|
33
|
+
const settleExports = () => new Promise((resolve) => setTimeout(resolve, 20))
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
process.env.TELEMETRY_BACKEND = 'otlp'
|
|
37
|
+
process.env.TELEMETRY_SAMPLING_RATIO = '0'
|
|
38
|
+
resetTelemetryEnvCache()
|
|
39
|
+
provider = new OtlpProvider({
|
|
40
|
+
spanProcessors: [new SimpleSpanProcessor(spanExporter)],
|
|
41
|
+
instrumentations: [],
|
|
42
|
+
})
|
|
43
|
+
await provider.start()
|
|
44
|
+
setActiveProvider(provider)
|
|
45
|
+
// The first span after NodeSDK.start() can be dropped while the global tracer
|
|
46
|
+
// provider finishes installing — warm up, settle a tick, then reset.
|
|
47
|
+
withSpan('warmup', () => undefined)
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
49
|
+
spanExporter.reset()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await provider.shutdown()
|
|
54
|
+
resetActiveProvider()
|
|
55
|
+
delete process.env.TELEMETRY_BACKEND
|
|
56
|
+
delete process.env.TELEMETRY_SAMPLING_RATIO
|
|
57
|
+
resetTelemetryEnvCache()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('root spans take their own sampling decision', () => {
|
|
61
|
+
it('drops a rooted span at ratio 0 while a nested span still inherits the parent decision', async () => {
|
|
62
|
+
continueTrace(SAMPLED_REMOTE_CARRIER, 'queue.job', () => {
|
|
63
|
+
withSpan('nested.batch', () => undefined)
|
|
64
|
+
withSpan('rooted.batch', () => undefined, { root: true })
|
|
65
|
+
})
|
|
66
|
+
await settleExports()
|
|
67
|
+
|
|
68
|
+
const names = spanExporter.getFinishedSpans().map((span) => span.name)
|
|
69
|
+
// Inherited from the sampled remote parent — the status quo for anything
|
|
70
|
+
// that nests under the triggering request.
|
|
71
|
+
expect(names).toContain('queue.job')
|
|
72
|
+
expect(names).toContain('nested.batch')
|
|
73
|
+
// Re-sampled from scratch against the ratio, so the trigger's decision no
|
|
74
|
+
// longer governs it.
|
|
75
|
+
expect(names).not.toContain('rooted.batch')
|
|
76
|
+
})
|
|
77
|
+
})
|
package/src/init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
14
|
import { registerTelemetryLogger } from './facade/logger-bridge'
|
|
15
15
|
import { captureTraceContext, continueTrace } from './facade/propagation'
|
|
16
|
+
import { withSpan } from './facade/tracer'
|
|
16
17
|
import { recordHttpDuration } from './facade/http'
|
|
17
18
|
import { reportError } from './facade/report-error'
|
|
18
19
|
|
|
@@ -117,6 +118,7 @@ function createRuntime(provider: TelemetryProvider): TelemetryRuntime {
|
|
|
117
118
|
captureTraceContext,
|
|
118
119
|
continueTrace: (carrier, name, fn, options) =>
|
|
119
120
|
continueTrace(carrier, name, () => fn(), options),
|
|
121
|
+
withSpan: (name, fn, options) => withSpan(name, fn, options),
|
|
120
122
|
recordHttpDuration,
|
|
121
123
|
reportError,
|
|
122
124
|
shutdown: shutdownTelemetry,
|
|
@@ -24,7 +24,7 @@ class ConsoleSpan implements Span {
|
|
|
24
24
|
private status: 'ok' | 'error' = 'ok'
|
|
25
25
|
private readonly startedAt = Date.now()
|
|
26
26
|
|
|
27
|
-
constructor(private
|
|
27
|
+
constructor(private name: string) {}
|
|
28
28
|
|
|
29
29
|
setAttribute(key: string, value: string | number | boolean): void {
|
|
30
30
|
this.attributes[key] = value
|
|
@@ -32,6 +32,9 @@ class ConsoleSpan implements Span {
|
|
|
32
32
|
setAttributes(attributes: Record<string, string | number | boolean | undefined>): void {
|
|
33
33
|
Object.assign(this.attributes, attributes)
|
|
34
34
|
}
|
|
35
|
+
updateName(name: string): void {
|
|
36
|
+
this.name = name
|
|
37
|
+
}
|
|
35
38
|
recordException(error: unknown): void {
|
|
36
39
|
this.status = 'error'
|
|
37
40
|
this.attributes.exception = serializeError(error).message
|
|
@@ -13,9 +13,12 @@ import {
|
|
|
13
13
|
isSpanContextValid,
|
|
14
14
|
defaultTextMapGetter,
|
|
15
15
|
defaultTextMapSetter,
|
|
16
|
+
ROOT_CONTEXT,
|
|
16
17
|
SpanStatusCode,
|
|
17
18
|
SpanKind as OtelSpanKind,
|
|
19
|
+
type Link,
|
|
18
20
|
type Span as OtelApiSpan,
|
|
21
|
+
type SpanOptions as OtelSpanOptions,
|
|
19
22
|
type TextMapPropagator,
|
|
20
23
|
type Counter,
|
|
21
24
|
type Histogram,
|
|
@@ -139,6 +142,35 @@ const SPAN_KIND: Record<SpanKind, OtelSpanKind> = {
|
|
|
139
142
|
consumer: OtelSpanKind.CONSUMER,
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Resolve `links` carriers to span contexts. Extraction runs against
|
|
147
|
+
* `ROOT_CONTEXT`, never the active one — otherwise an empty carrier would
|
|
148
|
+
* silently link a span to its own ambient parent instead of yielding no link.
|
|
149
|
+
*/
|
|
150
|
+
function spanLinks(carriers?: TraceCarrier[]): Link[] | undefined {
|
|
151
|
+
if (!carriers?.length) return undefined
|
|
152
|
+
const links: Link[] = []
|
|
153
|
+
for (const carrier of carriers) {
|
|
154
|
+
const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter))
|
|
155
|
+
if (linked && isSpanContextValid(linked)) links.push({ context: linked })
|
|
156
|
+
}
|
|
157
|
+
return links.length ? links : undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Translate the vendor-neutral options to OTEL's. `root: true` makes OTEL drop
|
|
162
|
+
* the parent from the context it samples against, so the span starts a new trace
|
|
163
|
+
* and the `ParentBasedSampler` falls through to its ratio-based root sampler.
|
|
164
|
+
*/
|
|
165
|
+
function toOtelSpanOptions(options: SpanOptions): OtelSpanOptions {
|
|
166
|
+
return {
|
|
167
|
+
kind: options.kind ? SPAN_KIND[options.kind] : undefined,
|
|
168
|
+
attributes: cleanAttributes(options.attributes),
|
|
169
|
+
root: options.root,
|
|
170
|
+
links: spanLinks(options.links),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
const SEVERITY: Record<LogLevel, SeverityNumber> = {
|
|
143
175
|
debug: SeverityNumber.DEBUG,
|
|
144
176
|
info: SeverityNumber.INFO,
|
|
@@ -156,6 +188,9 @@ class OtelSpan implements Span {
|
|
|
156
188
|
setAttributes(attributes: Attributes): void {
|
|
157
189
|
this.span.setAttributes(cleanAttributes(attributes))
|
|
158
190
|
}
|
|
191
|
+
updateName(name: string): void {
|
|
192
|
+
this.span.updateName(name)
|
|
193
|
+
}
|
|
159
194
|
recordException(error: unknown): void {
|
|
160
195
|
// Redact message + stack before they leave the process (Privacy): the auto
|
|
161
196
|
// record-on-throw path (run-span) and reportError both pass raw errors here.
|
|
@@ -241,10 +276,8 @@ export class OtlpProvider implements TelemetryProvider {
|
|
|
241
276
|
|
|
242
277
|
runInSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T {
|
|
243
278
|
const tracer = trace.getTracer(TRACER_NAME)
|
|
244
|
-
return tracer.startActiveSpan(
|
|
245
|
-
|
|
246
|
-
{ kind: options.kind ? SPAN_KIND[options.kind] : undefined, attributes: cleanAttributes(options.attributes) },
|
|
247
|
-
(otelSpan) => runSpan(new OtelSpan(otelSpan), fn),
|
|
279
|
+
return tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>
|
|
280
|
+
runSpan(new OtelSpan(otelSpan), fn),
|
|
248
281
|
)
|
|
249
282
|
}
|
|
250
283
|
|
|
@@ -267,10 +300,8 @@ export class OtlpProvider implements TelemetryProvider {
|
|
|
267
300
|
const parent = queuePropagator.extract(context.active(), carrier, defaultTextMapGetter)
|
|
268
301
|
const tracer = trace.getTracer(TRACER_NAME)
|
|
269
302
|
return context.with(parent, () =>
|
|
270
|
-
tracer.startActiveSpan(
|
|
271
|
-
|
|
272
|
-
{ kind: options.kind ? SPAN_KIND[options.kind] : undefined, attributes: cleanAttributes(options.attributes) },
|
|
273
|
-
(otelSpan) => runSpan(new OtelSpan(otelSpan), fn),
|
|
303
|
+
tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>
|
|
304
|
+
runSpan(new OtelSpan(otelSpan), fn),
|
|
274
305
|
),
|
|
275
306
|
)
|
|
276
307
|
}
|
package/src/types.ts
CHANGED
|
@@ -43,6 +43,29 @@ export type SpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer
|
|
|
43
43
|
export type SpanOptions = {
|
|
44
44
|
kind?: SpanKind
|
|
45
45
|
attributes?: Attributes
|
|
46
|
+
/**
|
|
47
|
+
* Start a NEW trace instead of nesting under the active context, so the
|
|
48
|
+
* sampler takes a fresh decision for this span.
|
|
49
|
+
*
|
|
50
|
+
* Without it a long-lived job (a multi-day import, a reindex) is one trace
|
|
51
|
+
* under one `ParentBased` decision taken on whatever request triggered it:
|
|
52
|
+
* below ratio 1.0 a whole run can emit nothing, and at ratio 1.0 it becomes an
|
|
53
|
+
* unrenderable million-span trace. Rooting the unit of work the analysis
|
|
54
|
+
* actually reasons about — a batch, a page — bounds both. Pair with `links` so
|
|
55
|
+
* the causal chain back to the trigger survives.
|
|
56
|
+
*
|
|
57
|
+
* Meaningful for `withSpan` only. Passing it through `continueTrace` would
|
|
58
|
+
* discard the very parent the carrier supplies, defeating that call's purpose;
|
|
59
|
+
* link to the carrier instead of continuing it if that is what you want.
|
|
60
|
+
*/
|
|
61
|
+
root?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Causal links to other traces, as W3C carriers — `captureTraceContext()` for
|
|
64
|
+
* the currently active trace, or the `_trace` carrier off a queue/event
|
|
65
|
+
* payload for a remote one. Empty or malformed carriers are dropped rather
|
|
66
|
+
* than emitted as invalid links.
|
|
67
|
+
*/
|
|
68
|
+
links?: TraceCarrier[]
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
/**
|
|
@@ -52,6 +75,17 @@ export type SpanOptions = {
|
|
|
52
75
|
export interface Span {
|
|
53
76
|
setAttribute(key: string, value: AttributeValue): void
|
|
54
77
|
setAttributes(attributes: Attributes): void
|
|
78
|
+
/**
|
|
79
|
+
* Rename an in-flight span, for work whose identity is only known once it has
|
|
80
|
+
* run — a read that turns out to have drained its stream rather than produced
|
|
81
|
+
* a batch. Keeping such a span under the batch name would over-count batches
|
|
82
|
+
* and add an unlabelled latency sample to every dashboard built on that name.
|
|
83
|
+
*
|
|
84
|
+
* Optional so a third-party `TelemetryProvider` predating it still satisfies
|
|
85
|
+
* the interface. Call it as `span.updateName?.(…)` and treat "not renamed" as
|
|
86
|
+
* a tolerable degradation, never a correctness requirement.
|
|
87
|
+
*/
|
|
88
|
+
updateName?(name: string): void
|
|
55
89
|
recordException(error: unknown): void
|
|
56
90
|
setStatus(status: 'ok' | 'error', message?: string): void
|
|
57
91
|
end(): void
|