@temporalio/interceptors-opentelemetry-v2 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -0
  3. package/lib/client/index.d.ts +26 -0
  4. package/lib/client/index.js +185 -0
  5. package/lib/client/index.js.map +1 -0
  6. package/lib/index.d.ts +13 -0
  7. package/lib/index.js +32 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/instrumentation.d.ts +58 -0
  10. package/lib/instrumentation.js +152 -0
  11. package/lib/instrumentation.js.map +1 -0
  12. package/lib/plugin.d.ts +31 -0
  13. package/lib/plugin.js +66 -0
  14. package/lib/plugin.js.map +1 -0
  15. package/lib/worker/index.d.ts +69 -0
  16. package/lib/worker/index.js +222 -0
  17. package/lib/worker/index.js.map +1 -0
  18. package/lib/workflow/context-manager.d.ts +10 -0
  19. package/lib/workflow/context-manager.js +67 -0
  20. package/lib/workflow/context-manager.js.map +1 -0
  21. package/lib/workflow/definitions.d.ts +127 -0
  22. package/lib/workflow/definitions.js +92 -0
  23. package/lib/workflow/definitions.js.map +1 -0
  24. package/lib/workflow/id-generator.d.ts +8 -0
  25. package/lib/workflow/id-generator.js +30 -0
  26. package/lib/workflow/id-generator.js.map +1 -0
  27. package/lib/workflow/index.d.ts +51 -0
  28. package/lib/workflow/index.js +252 -0
  29. package/lib/workflow/index.js.map +1 -0
  30. package/lib/workflow/performance-polyfill.d.ts +16 -0
  31. package/lib/workflow/performance-polyfill.js +29 -0
  32. package/lib/workflow/performance-polyfill.js.map +1 -0
  33. package/lib/workflow/runtime.d.ts +1 -0
  34. package/lib/workflow/runtime.js +14 -0
  35. package/lib/workflow/runtime.js.map +1 -0
  36. package/lib/workflow/span-exporter.d.ts +9 -0
  37. package/lib/workflow/span-exporter.js +56 -0
  38. package/lib/workflow/span-exporter.js.map +1 -0
  39. package/lib/workflow/workflow-imports-impl.d.ts +7 -0
  40. package/lib/workflow/workflow-imports-impl.js +17 -0
  41. package/lib/workflow/workflow-imports-impl.js.map +1 -0
  42. package/lib/workflow/workflow-imports.d.ts +16 -0
  43. package/lib/workflow/workflow-imports.js +25 -0
  44. package/lib/workflow/workflow-imports.js.map +1 -0
  45. package/lib/workflow-interceptors.d.ts +3 -0
  46. package/lib/workflow-interceptors.js +12 -0
  47. package/lib/workflow-interceptors.js.map +1 -0
  48. package/package.json +78 -0
  49. package/src/client/index.ts +220 -0
  50. package/src/index.ts +14 -0
  51. package/src/instrumentation.ts +159 -0
  52. package/src/plugin.ts +86 -0
  53. package/src/worker/index.ts +264 -0
  54. package/src/workflow/context-manager.ts +53 -0
  55. package/src/workflow/definitions.ts +145 -0
  56. package/src/workflow/id-generator.ts +30 -0
  57. package/src/workflow/index.ts +312 -0
  58. package/src/workflow/performance-polyfill.ts +31 -0
  59. package/src/workflow/runtime.ts +12 -0
  60. package/src/workflow/span-exporter.ts +60 -0
  61. package/src/workflow/workflow-imports-impl.ts +14 -0
  62. package/src/workflow/workflow-imports.ts +39 -0
  63. package/src/workflow-interceptors.ts +14 -0
@@ -0,0 +1,312 @@
1
+ // eslint-disable-next-line import/no-unassigned-import
2
+ import './performance-polyfill'; // Zero-import polyfill; must run before OTel modules access `performance`
3
+ // eslint-disable-next-line import/no-unassigned-import
4
+ import './runtime'; // Patch the Workflow isolate runtime for opentelemetry
5
+ import * as otel from '@opentelemetry/api';
6
+ import * as tracing from '@opentelemetry/sdk-trace-base';
7
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
8
+ import type {
9
+ ActivityInput,
10
+ ContinueAsNewInput,
11
+ DisposeInput,
12
+ GetLogAttributesInput,
13
+ GetMetricTagsInput,
14
+ LocalActivityInput,
15
+ Next,
16
+ QueryInput,
17
+ SignalInput,
18
+ SignalWorkflowInput,
19
+ StartChildWorkflowExecutionInput,
20
+ UpdateInput,
21
+ WorkflowExecuteInput,
22
+ WorkflowInboundCallsInterceptor,
23
+ WorkflowInternalsInterceptor,
24
+ WorkflowOutboundCallsInterceptor,
25
+ StartNexusOperationInput,
26
+ StartNexusOperationOutput,
27
+ } from '@temporalio/workflow';
28
+ import {
29
+ instrument,
30
+ instrumentSync,
31
+ extractContextFromHeaders,
32
+ headersWithContext,
33
+ nexusHeadersWithContext,
34
+ UPDATE_ID_ATTR_KEY,
35
+ NEXUS_SERVICE_ATTR_KEY,
36
+ NEXUS_OPERATION_ATTR_KEY,
37
+ NEXUS_ENDPOINT_ATTR_KEY,
38
+ } from '../instrumentation';
39
+ import { ContextManager } from './context-manager';
40
+ import { SpanName, SPAN_DELIMITER } from './definitions';
41
+ import { SpanExporter } from './span-exporter';
42
+ import { DeterministicIdGenerator } from './id-generator';
43
+ import { workflowInfo, ContinueAsNew } from './workflow-imports';
44
+
45
+ export * from './definitions';
46
+
47
+ let tracer: undefined | otel.Tracer = undefined;
48
+ let contextManager: undefined | ContextManager = undefined;
49
+
50
+ function getTracer(): otel.Tracer {
51
+ if (contextManager === undefined) {
52
+ contextManager = new ContextManager();
53
+ }
54
+ if (tracer === undefined) {
55
+ const provider = new tracing.BasicTracerProvider({
56
+ idGenerator: new DeterministicIdGenerator(),
57
+ spanProcessors: [new tracing.SimpleSpanProcessor(new SpanExporter())],
58
+ });
59
+ otel.propagation.setGlobalPropagator(new W3CTraceContextPropagator());
60
+ otel.trace.setGlobalTracerProvider(provider);
61
+ otel.context.setGlobalContextManager(contextManager);
62
+ tracer = provider.getTracer('@temporalio/interceptor-workflow');
63
+ }
64
+ return tracer;
65
+ }
66
+
67
+ /**
68
+ * Intercepts calls to run a Workflow
69
+ *
70
+ * Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is
71
+ * provided in the Workflow input headers.
72
+ *
73
+ * `@temporalio/workflow` must be provided by host package in order to function.
74
+ *
75
+ * @experimental This interceptor is experimental and APIs may change without notice.
76
+ */
77
+ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor {
78
+ protected readonly tracer = getTracer();
79
+
80
+ public async execute(
81
+ input: WorkflowExecuteInput,
82
+ next: Next<WorkflowInboundCallsInterceptor, 'execute'>
83
+ ): Promise<unknown> {
84
+ const context = extractContextFromHeaders(input.headers);
85
+
86
+ return await instrument({
87
+ tracer: this.tracer,
88
+ spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`,
89
+ fn: () => next(input),
90
+ context,
91
+ acceptableErrors: (err) => err instanceof ContinueAsNew,
92
+ });
93
+ }
94
+
95
+ public async handleSignal(
96
+ input: SignalInput,
97
+ next: Next<WorkflowInboundCallsInterceptor, 'handleSignal'>
98
+ ): Promise<void> {
99
+ const context = extractContextFromHeaders(input.headers);
100
+
101
+ return await instrument({
102
+ tracer: this.tracer,
103
+ spanName: `${SpanName.WORKFLOW_HANDLE_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
104
+ fn: () => next(input),
105
+ context,
106
+ });
107
+ }
108
+
109
+ public async handleUpdate(
110
+ input: UpdateInput,
111
+ next: Next<WorkflowInboundCallsInterceptor, 'handleUpdate'>
112
+ ): Promise<unknown> {
113
+ const context = extractContextFromHeaders(input.headers);
114
+
115
+ return await instrument({
116
+ tracer: this.tracer,
117
+ spanName: `${SpanName.WORKFLOW_HANDLE_UPDATE}${SPAN_DELIMITER}${input.name}`,
118
+ fn: (span) => {
119
+ span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId);
120
+ return next(input);
121
+ },
122
+ context,
123
+ });
124
+ }
125
+
126
+ public validateUpdate(input: UpdateInput, next: Next<WorkflowInboundCallsInterceptor, 'validateUpdate'>): void {
127
+ const context = extractContextFromHeaders(input.headers);
128
+ instrumentSync({
129
+ tracer: this.tracer,
130
+ spanName: `${SpanName.WORKFLOW_VALIDATE_UPDATE}${SPAN_DELIMITER}${input.name}`,
131
+ fn: (span) => {
132
+ span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId);
133
+ return next(input);
134
+ },
135
+ context,
136
+ });
137
+ }
138
+
139
+ public async handleQuery(
140
+ input: QueryInput,
141
+ next: Next<WorkflowInboundCallsInterceptor, 'handleQuery'>
142
+ ): Promise<unknown> {
143
+ const context = extractContextFromHeaders(input.headers);
144
+
145
+ return await instrument({
146
+ tracer: this.tracer,
147
+ spanName: `${SpanName.WORKFLOW_HANDLE_QUERY}${SPAN_DELIMITER}${input.queryName}`,
148
+ fn: () => next(input),
149
+ context,
150
+ });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Intercepts outbound calls to schedule an Activity
156
+ *
157
+ * Wraps the operation in an opentelemetry Span and passes it to the Activity via headers.
158
+ *
159
+ * `@temporalio/workflow` must be provided by host package in order to function.
160
+ *
161
+ * @experimental This interceptor is experimental and APIs may change without notice.
162
+ */
163
+ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsInterceptor {
164
+ protected readonly tracer = getTracer();
165
+
166
+ public async scheduleActivity(
167
+ input: ActivityInput,
168
+ next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>
169
+ ): Promise<unknown> {
170
+ return await instrument({
171
+ tracer: this.tracer,
172
+ spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`,
173
+ fn: async () => {
174
+ const headers = headersWithContext(input.headers);
175
+
176
+ return next({
177
+ ...input,
178
+ headers,
179
+ });
180
+ },
181
+ });
182
+ }
183
+
184
+ public async scheduleLocalActivity(
185
+ input: LocalActivityInput,
186
+ next: Next<WorkflowOutboundCallsInterceptor, 'scheduleLocalActivity'>
187
+ ): Promise<unknown> {
188
+ return await instrument({
189
+ tracer: this.tracer,
190
+ spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`,
191
+ fn: async () => {
192
+ const headers = headersWithContext(input.headers);
193
+
194
+ return next({
195
+ ...input,
196
+ headers,
197
+ });
198
+ },
199
+ });
200
+ }
201
+
202
+ public async startNexusOperation(
203
+ input: StartNexusOperationInput,
204
+ next: Next<WorkflowOutboundCallsInterceptor, 'startNexusOperation'>
205
+ ): Promise<StartNexusOperationOutput> {
206
+ return await instrument({
207
+ tracer: this.tracer,
208
+ spanName: `${SpanName.NEXUS_OPERATION_START}${SPAN_DELIMITER}${input.service}/${input.operation}`,
209
+ fn: async (span) => {
210
+ span.setAttribute(NEXUS_SERVICE_ATTR_KEY, input.service);
211
+ span.setAttribute(NEXUS_OPERATION_ATTR_KEY, input.operation);
212
+ span.setAttribute(NEXUS_ENDPOINT_ATTR_KEY, input.endpoint);
213
+ const headers = nexusHeadersWithContext(input.headers);
214
+ return await next({ ...input, headers });
215
+ },
216
+ });
217
+ }
218
+
219
+ public async startChildWorkflowExecution(
220
+ input: StartChildWorkflowExecutionInput,
221
+ next: Next<WorkflowOutboundCallsInterceptor, 'startChildWorkflowExecution'>
222
+ ): Promise<[Promise<string>, Promise<unknown>]> {
223
+ return await instrument({
224
+ tracer: this.tracer,
225
+ spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`,
226
+ fn: async () => {
227
+ const headers = headersWithContext(input.headers);
228
+
229
+ return next({
230
+ ...input,
231
+ headers,
232
+ });
233
+ },
234
+ });
235
+ }
236
+
237
+ public async continueAsNew(
238
+ input: ContinueAsNewInput,
239
+ next: Next<WorkflowOutboundCallsInterceptor, 'continueAsNew'>
240
+ ): Promise<never> {
241
+ return await instrument({
242
+ tracer: this.tracer,
243
+ spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`,
244
+ fn: async () => {
245
+ const headers = headersWithContext(input.headers);
246
+
247
+ return next({
248
+ ...input,
249
+ headers,
250
+ });
251
+ },
252
+ acceptableErrors: (err) => err instanceof ContinueAsNew,
253
+ });
254
+ }
255
+
256
+ public async signalWorkflow(
257
+ input: SignalWorkflowInput,
258
+ next: Next<WorkflowOutboundCallsInterceptor, 'signalWorkflow'>
259
+ ): Promise<void> {
260
+ return await instrument({
261
+ tracer: this.tracer,
262
+ spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
263
+ fn: async () => {
264
+ const headers = headersWithContext(input.headers);
265
+
266
+ return next({
267
+ ...input,
268
+ headers,
269
+ });
270
+ },
271
+ });
272
+ }
273
+
274
+ public getLogAttributes(
275
+ input: GetLogAttributesInput,
276
+ next: Next<WorkflowOutboundCallsInterceptor, 'getLogAttributes'>
277
+ ): Record<string, unknown> {
278
+ const span = otel.trace.getSpan(otel.context.active());
279
+ const spanContext = span?.spanContext();
280
+ if (spanContext && otel.isSpanContextValid(spanContext)) {
281
+ return next({
282
+ trace_id: spanContext.traceId,
283
+ span_id: spanContext.spanId,
284
+ trace_flags: `0${spanContext.traceFlags.toString(16)}`,
285
+ ...input,
286
+ });
287
+ } else {
288
+ return next(input);
289
+ }
290
+ }
291
+
292
+ public getMetricTags(
293
+ input: GetMetricTagsInput,
294
+ next: Next<WorkflowOutboundCallsInterceptor, 'getMetricTags'>
295
+ ): GetMetricTagsInput {
296
+ return next(input);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Intercepts Workflow lifecycle internals.
302
+ *
303
+ * @experimental This interceptor is experimental and APIs may change without notice.
304
+ */
305
+ export class OpenTelemetryInternalsInterceptor implements WorkflowInternalsInterceptor {
306
+ async dispose(input: DisposeInput, next: Next<WorkflowInternalsInterceptor, 'dispose'>): Promise<void> {
307
+ if (contextManager !== undefined) {
308
+ contextManager.disable();
309
+ }
310
+ next(input);
311
+ }
312
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Polyfill performance for the workflow isolate.
3
+ *
4
+ * OTel v2's browser platform accesses `performance` at module scope.
5
+ * This file MUST have zero imports so webpack initializes it before any
6
+ * OTel module that references `performance`.
7
+ *
8
+ * The guard uses two checks:
9
+ * - `__webpack_module_cache__` on globalThis is a positive indicator of the
10
+ * workflow sandbox (set by the SDK's VM creators before the bundle evaluates).
11
+ * - `performance` being undefined confirms polyfilling is needed. The polyfill
12
+ * reads workflow time from the activator so it is safe during preload.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ if ('__webpack_module_cache__' in globalThis && typeof performance === 'undefined') {
18
+ const now = () => ((globalThis as any).__TEMPORAL_ACTIVATOR__?.now as number | undefined) ?? 0;
19
+ Object.assign(globalThis, {
20
+ performance: {
21
+ timeOrigin: now(),
22
+ now() {
23
+ return now() - this.timeOrigin;
24
+ },
25
+ },
26
+ });
27
+ }
28
+
29
+ // Empty export to mark this as a module for ESLint's import/unambiguous rule.
30
+ // This file intentionally has no imports to ensure it initializes before OTel.
31
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sets global variables required for importing opentelemetry in isolate
3
+ * @module
4
+ */
5
+ import { inWorkflowContext } from './workflow-imports';
6
+
7
+ if (inWorkflowContext()) {
8
+ // OTel uses `window` to detect a browser environment
9
+ Object.assign(globalThis, {
10
+ window: globalThis,
11
+ });
12
+ }
@@ -0,0 +1,60 @@
1
+ import type * as tracing from '@opentelemetry/sdk-trace-base';
2
+ import type { ExportResult } from '@opentelemetry/core';
3
+ import { ExportResultCode } from '@opentelemetry/core';
4
+ import type { OpenTelemetrySinks, SerializableSpan, SerializableSpanContext } from './definitions';
5
+ import { proxySinks } from './workflow-imports';
6
+
7
+ export class SpanExporter implements tracing.SpanExporter {
8
+ private exporter?: OpenTelemetrySinks['exporter'];
9
+
10
+ public export(spans: tracing.ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
11
+ if (!this.exporter) {
12
+ this.exporter = proxySinks<OpenTelemetrySinks>().exporter;
13
+ }
14
+ this.exporter.export(spans.map((span) => this.makeSerializable(span)));
15
+ resultCallback({ code: ExportResultCode.SUCCESS });
16
+ }
17
+
18
+ public async shutdown(): Promise<void> {
19
+ // Nothing to shut down
20
+ }
21
+
22
+ public makeSerializable(span: tracing.ReadableSpan): SerializableSpan {
23
+ const { traceState, ...restSpanContext } = span.spanContext();
24
+ // Serialize traceState to a string because TraceState objects lose their
25
+ // prototype methods when crossing the V8 isolate boundary.
26
+ // See: https://github.com/temporalio/sdk-typescript/issues/1738
27
+ const serializableSpanContext: SerializableSpanContext = {
28
+ traceState: traceState?.serialize(),
29
+ ...restSpanContext,
30
+ };
31
+
32
+ let serializableParentSpanContext: SerializableSpanContext | undefined;
33
+ if (span.parentSpanContext) {
34
+ const { traceState: parentTraceState, ...restParentSpanContext } = span.parentSpanContext;
35
+ serializableParentSpanContext = {
36
+ traceState: parentTraceState?.serialize(),
37
+ ...restParentSpanContext,
38
+ };
39
+ }
40
+
41
+ return {
42
+ name: span.name,
43
+ kind: span.kind,
44
+ spanContext: serializableSpanContext,
45
+ parentSpanContext: serializableParentSpanContext,
46
+ startTime: span.startTime,
47
+ endTime: span.endTime,
48
+ status: span.status,
49
+ attributes: span.attributes,
50
+ links: span.links,
51
+ events: span.events,
52
+ duration: span.duration,
53
+ ended: span.ended,
54
+ droppedAttributesCount: span.droppedAttributesCount,
55
+ droppedEventsCount: span.droppedEventsCount,
56
+ droppedLinksCount: span.droppedLinksCount,
57
+ instrumentationScope: span.instrumentationScope,
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Real workflow imports for otel interceptors.
3
+ * This replaces the stub via webpack alias when bundled.
4
+ *
5
+ * @module
6
+ */
7
+ export {
8
+ inWorkflowContext,
9
+ proxySinks,
10
+ workflowInfo,
11
+ AsyncLocalStorage,
12
+ ContinueAsNew,
13
+ getRandomStream,
14
+ } from '@temporalio/workflow';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Workflow imports stub module.
3
+ *
4
+ * This module provides stubs for workflow functionality needed by interceptors.
5
+ * When bundled by the workflow bundler, this is replaced with the real
6
+ * implementation via NormalModuleReplacementPlugin.
7
+ *
8
+ * @module
9
+ */
10
+ import type {
11
+ inWorkflowContext as inWorkflowContextT,
12
+ workflowInfo as workflowInfoT,
13
+ proxySinks as proxySinksT,
14
+ AsyncLocalStorage as AsyncLocalStorageT,
15
+ ContinueAsNew as ContinueAsNewT,
16
+ getRandomStream as getRandomStreamT,
17
+ } from '@temporalio/workflow';
18
+
19
+ import { IllegalStateError } from '@temporalio/common';
20
+
21
+ // always returns false since if using this implementation, we are outside of workflow context
22
+ export const inWorkflowContext: typeof inWorkflowContextT = () => false;
23
+
24
+ // All of the following stubs will throw if used
25
+ export const workflowInfo: typeof workflowInfoT = () => {
26
+ throw new IllegalStateError('Workflow.workflowInfo(...) may only be used from a Workflow Execution.');
27
+ };
28
+
29
+ export const ContinueAsNew = class ContinueAsNew {} as unknown as typeof ContinueAsNewT;
30
+
31
+ export const AsyncLocalStorage = class AsyncLocalStorage {} as unknown as typeof AsyncLocalStorageT;
32
+
33
+ export const proxySinks: typeof proxySinksT = () => {
34
+ throw new IllegalStateError('Proxied sinks functions may only be used from a Workflow Execution.');
35
+ };
36
+
37
+ export const getRandomStream: typeof getRandomStreamT = () => {
38
+ throw new IllegalStateError('Workflow.getRandomStream(...) may only be used from a Workflow Execution.');
39
+ };
@@ -0,0 +1,14 @@
1
+ /** Not a workflow, just interceptors */
2
+
3
+ import type { WorkflowInterceptors } from '@temporalio/workflow';
4
+ import {
5
+ OpenTelemetryInboundInterceptor,
6
+ OpenTelemetryOutboundInterceptor,
7
+ OpenTelemetryInternalsInterceptor,
8
+ } from './workflow';
9
+
10
+ export const interceptors = (): WorkflowInterceptors => ({
11
+ inbound: [new OpenTelemetryInboundInterceptor()],
12
+ outbound: [new OpenTelemetryOutboundInterceptor()],
13
+ internals: [new OpenTelemetryInternalsInterceptor()],
14
+ });