@open-mercato/shared 0.6.8-develop.6998.1.d2bc46c56c → 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/dist/lib/telemetry/runtime.js +14 -1
- package/dist/lib/telemetry/runtime.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/telemetry/__tests__/runtime.test.ts +211 -0
- package/src/lib/telemetry/runtime.ts +61 -0
- package/src/modules/events/types.ts +4 -0
|
@@ -26,10 +26,23 @@ function getTelemetryRuntime() {
|
|
|
26
26
|
function resetTelemetryRuntime() {
|
|
27
27
|
store().active = void 0;
|
|
28
28
|
}
|
|
29
|
+
const NOOP_SPAN = { setAttributes() {
|
|
30
|
+
} };
|
|
31
|
+
function withTelemetrySpan(name, fn, options) {
|
|
32
|
+
const runtime = getTelemetryRuntime();
|
|
33
|
+
if (!runtime?.withSpan) return fn(NOOP_SPAN);
|
|
34
|
+
return runtime.withSpan(name, fn, options);
|
|
35
|
+
}
|
|
36
|
+
function captureTelemetryTrace() {
|
|
37
|
+
const carrier = getTelemetryRuntime()?.captureTraceContext();
|
|
38
|
+
return carrier && Object.keys(carrier).length > 0 ? carrier : void 0;
|
|
39
|
+
}
|
|
29
40
|
export {
|
|
41
|
+
captureTelemetryTrace,
|
|
30
42
|
getTelemetryRuntime,
|
|
31
43
|
isTelemetryBackendEnabled,
|
|
32
44
|
registerTelemetryRuntime,
|
|
33
|
-
resetTelemetryRuntime
|
|
45
|
+
resetTelemetryRuntime,
|
|
46
|
+
withTelemetrySpan
|
|
34
47
|
};
|
|
35
48
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/telemetry/runtime.ts"],
|
|
4
|
-
"sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>\n\nexport type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'\n\n/** The subset of the telemetry package's `Span` that bridge consumers need. */\nexport type TelemetrySpan = {\n setAttributes(attributes: TelemetrySpanAttributes): void\n /**\n * Rename an in-flight span whose identity is only known once it has run.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 call it\n * as `span.updateName?.(\u2026)`.\n */\n updateName?(name: string): void\n}\n\nexport type TelemetrySpanOptions = {\n kind?: TelemetrySpanKind\n attributes?: TelemetrySpanAttributes\n /** Start a new trace so the sampler decides for this span alone. */\n root?: boolean\n /** Causal links to other traces, as W3C carriers. */\n links?: TelemetryTraceCarrier[]\n}\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n /**\n * Optional so an older bootstrap that predates span support still satisfies\n * the contract; consumers go through `withTelemetrySpan` and degrade to\n * running `fn` untraced.\n */\n withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n\nconst NOOP_SPAN: TelemetrySpan = { setAttributes() {} }\n\n/**\n * Run `fn` inside a span, from a package that must not depend on\n * `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global\n * lookup \u2014 no span object is allocated and the OTEL SDK is never reached.\n *\n * Pass `root: true` for the unit of work a long-lived job should be sampled and\n * rendered by (a batch, a page) so the job is not one trace under one sampling\n * decision, and `links` to keep the causal chain back to what triggered it.\n */\nexport function withTelemetrySpan<T>(\n name: string,\n fn: (span: TelemetrySpan) => T,\n options?: TelemetrySpanOptions,\n): T {\n const runtime = getTelemetryRuntime()\n if (!runtime?.withSpan) return fn(NOOP_SPAN)\n return runtime.withSpan(name, fn, options)\n}\n\n/**\n * The active trace as a carrier, for use as a `links` entry. `undefined` when\n * telemetry is off or nothing is active, which `withTelemetrySpan` treats as\n * \"no link\" rather than an invalid one.\n */\nexport function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {\n const carrier = getTelemetryRuntime()?.captureTraceContext()\n return carrier && Object.keys(carrier).length > 0 ? carrier : undefined\n}\n"],
|
|
5
|
+
"mappings": "AAwDA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;AAEA,MAAM,YAA2B,EAAE,gBAAgB;AAAC,EAAE;AAW/C,SAAS,kBACd,MACA,IACA,SACG;AACH,QAAM,UAAU,oBAAoB;AACpC,MAAI,CAAC,SAAS,SAAU,QAAO,GAAG,SAAS;AAC3C,SAAO,QAAQ,SAAS,MAAM,IAAI,OAAO;AAC3C;AAOO,SAAS,wBAA2D;AACzE,QAAM,UAAU,oBAAoB,GAAG,oBAAoB;AAC3D,SAAO,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AAChE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7000.1.0ae682bad1';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7000.1.0ae682bad1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.8-develop.
|
|
108
|
+
"@open-mercato/cache": "0.6.8-develop.7000.1.0ae682bad1",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureTelemetryTrace,
|
|
3
|
+
getTelemetryRuntime,
|
|
4
|
+
isTelemetryBackendEnabled,
|
|
5
|
+
registerTelemetryRuntime,
|
|
6
|
+
resetTelemetryRuntime,
|
|
7
|
+
withTelemetrySpan,
|
|
8
|
+
type TelemetryRuntime,
|
|
9
|
+
type TelemetrySpan,
|
|
10
|
+
type TelemetrySpanOptions,
|
|
11
|
+
type TelemetryTraceCarrier,
|
|
12
|
+
} from '../runtime'
|
|
13
|
+
|
|
14
|
+
type SpanCall = { name: string; options?: TelemetrySpanOptions }
|
|
15
|
+
|
|
16
|
+
function createRuntime(overrides: Partial<TelemetryRuntime> = {}): TelemetryRuntime {
|
|
17
|
+
return {
|
|
18
|
+
canUseGlobalTracePropagation: () => true,
|
|
19
|
+
captureTraceContext: () => ({}),
|
|
20
|
+
continueTrace: (_carrier, _name, fn) => fn(),
|
|
21
|
+
recordHttpDuration: () => {},
|
|
22
|
+
reportError: () => {},
|
|
23
|
+
shutdown: async () => {},
|
|
24
|
+
...overrides,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createTracingRuntime(): { runtime: TelemetryRuntime; calls: SpanCall[]; span: TelemetrySpan } {
|
|
29
|
+
const calls: SpanCall[] = []
|
|
30
|
+
const span: TelemetrySpan = { setAttributes: jest.fn(), updateName: jest.fn() }
|
|
31
|
+
const runtime = createRuntime({
|
|
32
|
+
withSpan: (name, fn, options) => {
|
|
33
|
+
calls.push({ name, options })
|
|
34
|
+
return fn(span)
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
return { runtime, calls, span }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('isTelemetryBackendEnabled', () => {
|
|
41
|
+
const originalBackend = process.env.TELEMETRY_BACKEND
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
if (originalBackend === undefined) delete process.env.TELEMETRY_BACKEND
|
|
45
|
+
else process.env.TELEMETRY_BACKEND = originalBackend
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('recognizes every supported backend', () => {
|
|
49
|
+
for (const backend of ['console', 'signoz', 'newrelic', 'otlp']) {
|
|
50
|
+
expect(isTelemetryBackendEnabled(backend)).toBe(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('normalizes surrounding whitespace and casing', () => {
|
|
55
|
+
expect(isTelemetryBackendEnabled(' CoNsOle ')).toBe(true)
|
|
56
|
+
expect(isTelemetryBackendEnabled('\tOTLP\n')).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects unknown, blank, and missing values', () => {
|
|
60
|
+
expect(isTelemetryBackendEnabled('off')).toBe(false)
|
|
61
|
+
expect(isTelemetryBackendEnabled('consoles')).toBe(false)
|
|
62
|
+
expect(isTelemetryBackendEnabled('')).toBe(false)
|
|
63
|
+
expect(isTelemetryBackendEnabled(' ')).toBe(false)
|
|
64
|
+
|
|
65
|
+
delete process.env.TELEMETRY_BACKEND
|
|
66
|
+
expect(isTelemetryBackendEnabled()).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('falls back to TELEMETRY_BACKEND only when no value is passed', () => {
|
|
70
|
+
process.env.TELEMETRY_BACKEND = 'signoz'
|
|
71
|
+
expect(isTelemetryBackendEnabled()).toBe(true)
|
|
72
|
+
expect(isTelemetryBackendEnabled('off')).toBe(false)
|
|
73
|
+
|
|
74
|
+
process.env.TELEMETRY_BACKEND = 'off'
|
|
75
|
+
expect(isTelemetryBackendEnabled()).toBe(false)
|
|
76
|
+
expect(isTelemetryBackendEnabled('console')).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('telemetry runtime registry', () => {
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
resetTelemetryRuntime()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('exposes no runtime until one is registered', () => {
|
|
86
|
+
expect(getTelemetryRuntime()).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('exposes the registered runtime across separate lookups', () => {
|
|
90
|
+
const runtime = createRuntime()
|
|
91
|
+
registerTelemetryRuntime(runtime)
|
|
92
|
+
|
|
93
|
+
expect(getTelemetryRuntime()).toBe(runtime)
|
|
94
|
+
expect(getTelemetryRuntime()).toBe(runtime)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('clears the runtime when its own disposer runs', () => {
|
|
98
|
+
const dispose = registerTelemetryRuntime(createRuntime())
|
|
99
|
+
dispose()
|
|
100
|
+
|
|
101
|
+
expect(getTelemetryRuntime()).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('leaves a newer runtime in place when a superseded disposer runs', () => {
|
|
105
|
+
const first = createRuntime()
|
|
106
|
+
const second = createRuntime()
|
|
107
|
+
const disposeFirst = registerTelemetryRuntime(first)
|
|
108
|
+
registerTelemetryRuntime(second)
|
|
109
|
+
|
|
110
|
+
disposeFirst()
|
|
111
|
+
|
|
112
|
+
expect(getTelemetryRuntime()).toBe(second)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('clears the runtime on reset', () => {
|
|
116
|
+
registerTelemetryRuntime(createRuntime())
|
|
117
|
+
resetTelemetryRuntime()
|
|
118
|
+
|
|
119
|
+
expect(getTelemetryRuntime()).toBeUndefined()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('withTelemetrySpan', () => {
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
resetTelemetryRuntime()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('runs the callback untraced with a usable noop span when telemetry is off', () => {
|
|
129
|
+
const seen: TelemetrySpan[] = []
|
|
130
|
+
const result = withTelemetrySpan('job.batch', (span) => {
|
|
131
|
+
seen.push(span)
|
|
132
|
+
span.setAttributes({ 'om.tenant_id': 'tenant-1', count: 2, ok: true })
|
|
133
|
+
span.updateName?.('job.batch.renamed')
|
|
134
|
+
return 'done'
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(result).toBe('done')
|
|
138
|
+
expect(seen).toHaveLength(1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('runs the callback untraced when the registered runtime predates span support', () => {
|
|
142
|
+
registerTelemetryRuntime(createRuntime())
|
|
143
|
+
|
|
144
|
+
const result = withTelemetrySpan('job.batch', (span) => {
|
|
145
|
+
span.setAttributes({ ok: true })
|
|
146
|
+
return 41 + 1
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(result).toBe(42)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('delegates to the runtime and forwards the name, options, and span', () => {
|
|
153
|
+
const { runtime, calls, span } = createTracingRuntime()
|
|
154
|
+
registerTelemetryRuntime(runtime)
|
|
155
|
+
|
|
156
|
+
const options: TelemetrySpanOptions = {
|
|
157
|
+
kind: 'consumer',
|
|
158
|
+
root: true,
|
|
159
|
+
links: [{ traceparent: '00-aaaa-bbbb-01' }],
|
|
160
|
+
attributes: { 'data_sync.batch_index': 3 },
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = withTelemetrySpan(
|
|
164
|
+
'data_sync.import.batch',
|
|
165
|
+
(received) => {
|
|
166
|
+
expect(received).toBe(span)
|
|
167
|
+
received.setAttributes({ 'data_sync.records': 10 })
|
|
168
|
+
return 'traced'
|
|
169
|
+
},
|
|
170
|
+
options,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(result).toBe('traced')
|
|
174
|
+
expect(calls).toEqual([{ name: 'data_sync.import.batch', options }])
|
|
175
|
+
expect(span.setAttributes).toHaveBeenCalledWith({ 'data_sync.records': 10 })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('propagates a callback failure from the traced path', () => {
|
|
179
|
+
const { runtime } = createTracingRuntime()
|
|
180
|
+
registerTelemetryRuntime(runtime)
|
|
181
|
+
|
|
182
|
+
expect(() =>
|
|
183
|
+
withTelemetrySpan('data_sync.import.batch', () => {
|
|
184
|
+
throw new Error('[internal] batch failed')
|
|
185
|
+
}),
|
|
186
|
+
).toThrow('[internal] batch failed')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('captureTelemetryTrace', () => {
|
|
191
|
+
afterEach(() => {
|
|
192
|
+
resetTelemetryRuntime()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('returns undefined when telemetry is off', () => {
|
|
196
|
+
expect(captureTelemetryTrace()).toBeUndefined()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('returns undefined when the active runtime captures an empty carrier', () => {
|
|
200
|
+
registerTelemetryRuntime(createRuntime({ captureTraceContext: () => ({}) }))
|
|
201
|
+
|
|
202
|
+
expect(captureTelemetryTrace()).toBeUndefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('returns the captured carrier when a trace is active', () => {
|
|
206
|
+
const carrier: TelemetryTraceCarrier = { traceparent: '00-aaaa-bbbb-01' }
|
|
207
|
+
registerTelemetryRuntime(createRuntime({ captureTraceContext: () => carrier }))
|
|
208
|
+
|
|
209
|
+
expect(captureTelemetryTrace()).toEqual(carrier)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
export type TelemetryTraceCarrier = Record<string, string>
|
|
2
2
|
|
|
3
|
+
export type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>
|
|
4
|
+
|
|
5
|
+
export type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'
|
|
6
|
+
|
|
7
|
+
/** The subset of the telemetry package's `Span` that bridge consumers need. */
|
|
8
|
+
export type TelemetrySpan = {
|
|
9
|
+
setAttributes(attributes: TelemetrySpanAttributes): void
|
|
10
|
+
/**
|
|
11
|
+
* Rename an in-flight span whose identity is only known once it has run.
|
|
12
|
+
* Optional so a bootstrap predating it still satisfies the contract — call it
|
|
13
|
+
* as `span.updateName?.(…)`.
|
|
14
|
+
*/
|
|
15
|
+
updateName?(name: string): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TelemetrySpanOptions = {
|
|
19
|
+
kind?: TelemetrySpanKind
|
|
20
|
+
attributes?: TelemetrySpanAttributes
|
|
21
|
+
/** Start a new trace so the sampler decides for this span alone. */
|
|
22
|
+
root?: boolean
|
|
23
|
+
/** Causal links to other traces, as W3C carriers. */
|
|
24
|
+
links?: TelemetryTraceCarrier[]
|
|
25
|
+
}
|
|
26
|
+
|
|
3
27
|
export type TelemetryRuntime = {
|
|
4
28
|
/**
|
|
5
29
|
* True only when the active SDK may safely use the process-global W3C
|
|
@@ -13,6 +37,12 @@ export type TelemetryRuntime = {
|
|
|
13
37
|
fn: () => T,
|
|
14
38
|
options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },
|
|
15
39
|
): T
|
|
40
|
+
/**
|
|
41
|
+
* Optional so an older bootstrap that predates span support still satisfies
|
|
42
|
+
* the contract; consumers go through `withTelemetrySpan` and degrade to
|
|
43
|
+
* running `fn` untraced.
|
|
44
|
+
*/
|
|
45
|
+
withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T
|
|
16
46
|
recordHttpDuration(method: string, route: string, status: number, startedAt: number): void
|
|
17
47
|
reportError(
|
|
18
48
|
error: unknown,
|
|
@@ -70,3 +100,34 @@ export function getTelemetryRuntime(): TelemetryRuntime | undefined {
|
|
|
70
100
|
export function resetTelemetryRuntime(): void {
|
|
71
101
|
store().active = undefined
|
|
72
102
|
}
|
|
103
|
+
|
|
104
|
+
const NOOP_SPAN: TelemetrySpan = { setAttributes() {} }
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run `fn` inside a span, from a package that must not depend on
|
|
108
|
+
* `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global
|
|
109
|
+
* lookup — no span object is allocated and the OTEL SDK is never reached.
|
|
110
|
+
*
|
|
111
|
+
* Pass `root: true` for the unit of work a long-lived job should be sampled and
|
|
112
|
+
* rendered by (a batch, a page) so the job is not one trace under one sampling
|
|
113
|
+
* decision, and `links` to keep the causal chain back to what triggered it.
|
|
114
|
+
*/
|
|
115
|
+
export function withTelemetrySpan<T>(
|
|
116
|
+
name: string,
|
|
117
|
+
fn: (span: TelemetrySpan) => T,
|
|
118
|
+
options?: TelemetrySpanOptions,
|
|
119
|
+
): T {
|
|
120
|
+
const runtime = getTelemetryRuntime()
|
|
121
|
+
if (!runtime?.withSpan) return fn(NOOP_SPAN)
|
|
122
|
+
return runtime.withSpan(name, fn, options)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The active trace as a carrier, for use as a `links` entry. `undefined` when
|
|
127
|
+
* telemetry is off or nothing is active, which `withTelemetrySpan` treats as
|
|
128
|
+
* "no link" rather than an invalid one.
|
|
129
|
+
*/
|
|
130
|
+
export function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {
|
|
131
|
+
const carrier = getTelemetryRuntime()?.captureTraceContext()
|
|
132
|
+
return carrier && Object.keys(carrier).length > 0 ? carrier : undefined
|
|
133
|
+
}
|
|
@@ -62,6 +62,10 @@ export interface EventPayload {
|
|
|
62
62
|
export interface EmitOptions {
|
|
63
63
|
/** If true, the event will be persisted to a queue for async processing */
|
|
64
64
|
persistent?: boolean
|
|
65
|
+
/** Trusted tenant scope forwarded to subscribers separately from the payload */
|
|
66
|
+
tenantId?: string | null
|
|
67
|
+
/** Trusted organization scope forwarded to subscribers separately from the payload */
|
|
68
|
+
organizationId?: string | null
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
// =============================================================================
|