@temporalio/interceptors-opentelemetry 1.13.1 → 1.14.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 (37) hide show
  1. package/{LICENSE.md → LICENSE} +6 -8
  2. package/lib/client/index.d.ts +9 -1
  3. package/lib/client/index.js +122 -3
  4. package/lib/client/index.js.map +1 -1
  5. package/lib/instrumentation.d.ts +21 -3
  6. package/lib/instrumentation.js +52 -12
  7. package/lib/instrumentation.js.map +1 -1
  8. package/lib/worker/index.d.ts +8 -7
  9. package/lib/worker/index.js +29 -4
  10. package/lib/worker/index.js.map +1 -1
  11. package/lib/workflow/context-manager.d.ts +2 -1
  12. package/lib/workflow/context-manager.js +8 -2
  13. package/lib/workflow/context-manager.js.map +1 -1
  14. package/lib/workflow/definitions.d.ts +45 -1
  15. package/lib/workflow/definitions.js +44 -0
  16. package/lib/workflow/definitions.js.map +1 -1
  17. package/lib/workflow/index.d.ts +12 -1
  18. package/lib/workflow/index.js +112 -12
  19. package/lib/workflow/index.js.map +1 -1
  20. package/lib/workflow/runtime.js +3 -2
  21. package/lib/workflow/runtime.js.map +1 -1
  22. package/lib/workflow/span-exporter.d.ts +1 -0
  23. package/lib/workflow/span-exporter.js +5 -25
  24. package/lib/workflow/span-exporter.js.map +1 -1
  25. package/lib/workflow/workflow-module-loader.d.ts +28 -0
  26. package/lib/workflow/workflow-module-loader.js +56 -0
  27. package/lib/workflow/workflow-module-loader.js.map +1 -0
  28. package/package.json +18 -16
  29. package/src/client/index.ts +178 -5
  30. package/src/instrumentation.ts +62 -11
  31. package/src/worker/index.ts +48 -13
  32. package/src/workflow/context-manager.ts +13 -5
  33. package/src/workflow/definitions.ts +54 -0
  34. package/src/workflow/index.ts +146 -12
  35. package/src/workflow/runtime.ts +4 -2
  36. package/src/workflow/span-exporter.ts +7 -3
  37. package/src/workflow/workflow-module-loader.ts +64 -0
@@ -3,27 +3,40 @@
3
3
  import './runtime'; // Patch the Workflow isolate runtime for opentelemetry
4
4
  import * as otel from '@opentelemetry/api';
5
5
  import * as tracing from '@opentelemetry/sdk-trace-base';
6
- import {
6
+ import type {
7
7
  ActivityInput,
8
- ContinueAsNew,
9
8
  ContinueAsNewInput,
10
9
  DisposeInput,
11
10
  GetLogAttributesInput,
11
+ GetMetricTagsInput,
12
12
  LocalActivityInput,
13
13
  Next,
14
+ QueryInput,
14
15
  SignalInput,
15
16
  SignalWorkflowInput,
16
17
  StartChildWorkflowExecutionInput,
18
+ UpdateInput,
17
19
  WorkflowExecuteInput,
18
20
  WorkflowInboundCallsInterceptor,
19
- workflowInfo,
20
21
  WorkflowInternalsInterceptor,
21
22
  WorkflowOutboundCallsInterceptor,
23
+ StartNexusOperationInput,
24
+ StartNexusOperationOutput,
22
25
  } from '@temporalio/workflow';
23
- import { instrument, extractContextFromHeaders, headersWithContext } from '../instrumentation';
26
+ import {
27
+ instrument,
28
+ instrumentSync,
29
+ extractContextFromHeaders,
30
+ headersWithContext,
31
+ UPDATE_ID_ATTR_KEY,
32
+ NEXUS_SERVICE_ATTR_KEY,
33
+ NEXUS_OPERATION_ATTR_KEY,
34
+ NEXUS_ENDPOINT_ATTR_KEY,
35
+ } from '../instrumentation';
24
36
  import { ContextManager } from './context-manager';
25
37
  import { SpanName, SPAN_DELIMITER } from './definitions';
26
38
  import { SpanExporter } from './span-exporter';
39
+ import { ensureWorkflowModuleLoaded, getWorkflowModule, hasSdkFlag } from './workflow-module-loader';
27
40
 
28
41
  export * from './definitions';
29
42
 
@@ -48,15 +61,25 @@ function getTracer(): otel.Tracer {
48
61
  *
49
62
  * Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is
50
63
  * provided in the Workflow input headers.
64
+ *
65
+ * `@temporalio/workflow` must be provided by host package in order to function.
51
66
  */
52
67
  export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInterceptor {
53
68
  protected readonly tracer = getTracer();
54
69
 
70
+ public constructor() {
71
+ ensureWorkflowModuleLoaded();
72
+ }
73
+
55
74
  public async execute(
56
75
  input: WorkflowExecuteInput,
57
76
  next: Next<WorkflowInboundCallsInterceptor, 'execute'>
58
77
  ): Promise<unknown> {
59
- const context = await extractContextFromHeaders(input.headers);
78
+ const { workflowInfo, ContinueAsNew } = getWorkflowModule();
79
+
80
+ const context = extractContextFromHeaders(input.headers);
81
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
82
+
60
83
  return await instrument({
61
84
  tracer: this.tracer,
62
85
  spanName: `${SpanName.WORKFLOW_EXECUTE}${SPAN_DELIMITER}${workflowInfo().workflowType}`,
@@ -70,10 +93,65 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte
70
93
  input: SignalInput,
71
94
  next: Next<WorkflowInboundCallsInterceptor, 'handleSignal'>
72
95
  ): Promise<void> {
73
- const context = await extractContextFromHeaders(input.headers);
96
+ // Tracing of inbound signals was added in v1.11.5.
97
+ if (!hasSdkFlag('OpenTelemetryInterceptorsTracesInboundSignals')) return next(input);
98
+
99
+ const context = extractContextFromHeaders(input.headers);
100
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
101
+
74
102
  return await instrument({
75
103
  tracer: this.tracer,
76
- spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
104
+ spanName: `${SpanName.WORKFLOW_HANDLE_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
105
+ fn: () => next(input),
106
+ context,
107
+ });
108
+ }
109
+
110
+ public async handleUpdate(
111
+ input: UpdateInput,
112
+ next: Next<WorkflowInboundCallsInterceptor, 'handleUpdate'>
113
+ ): Promise<unknown> {
114
+ if (!hasSdkFlag('OpenTelemetryInterceptorsInstrumentsAllMethods')) return next(input);
115
+
116
+ const context = extractContextFromHeaders(input.headers);
117
+
118
+ return await instrument({
119
+ tracer: this.tracer,
120
+ spanName: `${SpanName.WORKFLOW_HANDLE_UPDATE}${SPAN_DELIMITER}${input.name}`,
121
+ fn: (span) => {
122
+ span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId);
123
+ return next(input);
124
+ },
125
+ context,
126
+ });
127
+ }
128
+
129
+ public validateUpdate(input: UpdateInput, next: Next<WorkflowInboundCallsInterceptor, 'validateUpdate'>): void {
130
+ if (!hasSdkFlag('OpenTelemetryInterceptorsInstrumentsAllMethods')) return next(input);
131
+
132
+ const context = extractContextFromHeaders(input.headers);
133
+ instrumentSync({
134
+ tracer: this.tracer,
135
+ spanName: `${SpanName.WORKFLOW_VALIDATE_UPDATE}${SPAN_DELIMITER}${input.name}`,
136
+ fn: (span) => {
137
+ span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateId);
138
+ return next(input);
139
+ },
140
+ context,
141
+ });
142
+ }
143
+
144
+ public async handleQuery(
145
+ input: QueryInput,
146
+ next: Next<WorkflowInboundCallsInterceptor, 'handleQuery'>
147
+ ): Promise<unknown> {
148
+ if (!hasSdkFlag('OpenTelemetryInterceptorsInstrumentsAllMethods')) return next(input);
149
+
150
+ const context = extractContextFromHeaders(input.headers);
151
+
152
+ return await instrument({
153
+ tracer: this.tracer,
154
+ spanName: `${SpanName.WORKFLOW_HANDLE_QUERY}${SPAN_DELIMITER}${input.queryName}`,
77
155
  fn: () => next(input),
78
156
  context,
79
157
  });
@@ -84,10 +162,16 @@ export class OpenTelemetryInboundInterceptor implements WorkflowInboundCallsInte
84
162
  * Intercepts outbound calls to schedule an Activity
85
163
  *
86
164
  * Wraps the operation in an opentelemetry Span and passes it to the Activity via headers.
165
+ *
166
+ * `@temporalio/workflow` must be provided by host package in order to function.
87
167
  */
88
168
  export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsInterceptor {
89
169
  protected readonly tracer = getTracer();
90
170
 
171
+ public constructor() {
172
+ ensureWorkflowModuleLoaded();
173
+ }
174
+
91
175
  public async scheduleActivity(
92
176
  input: ActivityInput,
93
177
  next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>
@@ -96,7 +180,9 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
96
180
  tracer: this.tracer,
97
181
  spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`,
98
182
  fn: async () => {
99
- const headers = await headersWithContext(input.headers);
183
+ const headers = headersWithContext(input.headers);
184
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
185
+
100
186
  return next({
101
187
  ...input,
102
188
  headers,
@@ -109,11 +195,16 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
109
195
  input: LocalActivityInput,
110
196
  next: Next<WorkflowOutboundCallsInterceptor, 'scheduleLocalActivity'>
111
197
  ): Promise<unknown> {
198
+ // Tracing of local activities was added in v1.11.6.
199
+ if (!hasSdkFlag('OpenTelemetryInterceptorsTracesLocalActivities')) return next(input);
200
+
112
201
  return await instrument({
113
202
  tracer: this.tracer,
114
203
  spanName: `${SpanName.ACTIVITY_START}${SPAN_DELIMITER}${input.activityType}`,
115
204
  fn: async () => {
116
- const headers = await headersWithContext(input.headers);
205
+ const headers = headersWithContext(input.headers);
206
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
207
+
117
208
  return next({
118
209
  ...input,
119
210
  headers,
@@ -122,6 +213,24 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
122
213
  });
123
214
  }
124
215
 
216
+ public async startNexusOperation(
217
+ input: StartNexusOperationInput,
218
+ next: Next<WorkflowOutboundCallsInterceptor, 'startNexusOperation'>
219
+ ): Promise<StartNexusOperationOutput> {
220
+ if (!hasSdkFlag('OpenTelemetryInterceptorsInstrumentsAllMethods')) return next(input);
221
+
222
+ return await instrument({
223
+ tracer: this.tracer,
224
+ spanName: `${SpanName.NEXUS_OPERATION_START}${SPAN_DELIMITER}${input.service}${SPAN_DELIMITER}${input.operation}`,
225
+ fn: async (span) => {
226
+ span.setAttribute(NEXUS_SERVICE_ATTR_KEY, input.service);
227
+ span.setAttribute(NEXUS_OPERATION_ATTR_KEY, input.operation);
228
+ span.setAttribute(NEXUS_ENDPOINT_ATTR_KEY, input.endpoint);
229
+ return await next(input);
230
+ },
231
+ });
232
+ }
233
+
125
234
  public async startChildWorkflowExecution(
126
235
  input: StartChildWorkflowExecutionInput,
127
236
  next: Next<WorkflowOutboundCallsInterceptor, 'startChildWorkflowExecution'>
@@ -130,7 +239,9 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
130
239
  tracer: this.tracer,
131
240
  spanName: `${SpanName.CHILD_WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`,
132
241
  fn: async () => {
133
- const headers = await headersWithContext(input.headers);
242
+ const headers = headersWithContext(input.headers);
243
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
244
+
134
245
  return next({
135
246
  ...input,
136
247
  headers,
@@ -143,11 +254,14 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
143
254
  input: ContinueAsNewInput,
144
255
  next: Next<WorkflowOutboundCallsInterceptor, 'continueAsNew'>
145
256
  ): Promise<never> {
257
+ const { ContinueAsNew } = getWorkflowModule();
146
258
  return await instrument({
147
259
  tracer: this.tracer,
148
260
  spanName: `${SpanName.CONTINUE_AS_NEW}${SPAN_DELIMITER}${input.options.workflowType}`,
149
261
  fn: async () => {
150
- const headers = await headersWithContext(input.headers);
262
+ const headers = headersWithContext(input.headers);
263
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
264
+
151
265
  return next({
152
266
  ...input,
153
267
  headers,
@@ -165,7 +279,9 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
165
279
  tracer: this.tracer,
166
280
  spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
167
281
  fn: async () => {
168
- const headers = await headersWithContext(input.headers);
282
+ const headers = headersWithContext(input.headers);
283
+ if (!hasSdkFlag('OpenTelemetryInterceporsAvoidsExtraYields')) await Promise.resolve();
284
+
169
285
  return next({
170
286
  ...input,
171
287
  headers,
@@ -191,6 +307,24 @@ export class OpenTelemetryOutboundInterceptor implements WorkflowOutboundCallsIn
191
307
  return next(input);
192
308
  }
193
309
  }
310
+
311
+ public getMetricTags(
312
+ input: GetMetricTagsInput,
313
+ next: Next<WorkflowOutboundCallsInterceptor, 'getMetricTags'>
314
+ ): GetMetricTagsInput {
315
+ const span = otel.trace.getSpan(otel.context.active());
316
+ const spanContext = span?.spanContext();
317
+ if (spanContext && otel.isSpanContextValid(spanContext)) {
318
+ return next({
319
+ trace_id: spanContext.traceId,
320
+ span_id: spanContext.spanId,
321
+ trace_flags: `0${spanContext.traceFlags.toString(16)}`,
322
+ ...input,
323
+ });
324
+ } else {
325
+ return next(input);
326
+ }
327
+ }
194
328
  }
195
329
 
196
330
  export class OpenTelemetryInternalsInterceptor implements WorkflowInternalsInterceptor {
@@ -2,9 +2,11 @@
2
2
  * Sets global variables required for importing opentelemetry in isolate
3
3
  * @module
4
4
  */
5
- import { inWorkflowContext } from '@temporalio/workflow';
5
+ import { getWorkflowModuleIfAvailable } from './workflow-module-loader';
6
6
 
7
- if (inWorkflowContext()) {
7
+ const inWorkflowContext = getWorkflowModuleIfAvailable()?.inWorkflowContext;
8
+
9
+ if (inWorkflowContext?.()) {
8
10
  // Required by opentelemetry (pretend to be a browser)
9
11
  Object.assign(globalThis, {
10
12
  performance: {
@@ -1,13 +1,17 @@
1
1
  import * as tracing from '@opentelemetry/sdk-trace-base';
2
2
  import { ExportResult, ExportResultCode } from '@opentelemetry/core';
3
- import * as wf from '@temporalio/workflow';
4
3
  import { OpenTelemetrySinks, SerializableSpan } from './definitions';
4
+ import { ensureWorkflowModuleLoaded, getWorkflowModuleIfAvailable } from './workflow-module-loader';
5
5
 
6
- const { exporter } = wf.proxySinks<OpenTelemetrySinks>();
6
+ const exporter = getWorkflowModuleIfAvailable()?.proxySinks<OpenTelemetrySinks>()?.exporter;
7
7
 
8
8
  export class SpanExporter implements tracing.SpanExporter {
9
+ public constructor() {
10
+ ensureWorkflowModuleLoaded();
11
+ }
12
+
9
13
  public export(spans: tracing.ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
10
- exporter.export(spans.map((span) => this.makeSerializable(span)));
14
+ exporter!.export(spans.map((span) => this.makeSerializable(span)));
11
15
  resultCallback({ code: ExportResultCode.SUCCESS });
12
16
  }
13
17
 
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Utilities for working with a possibly missing `@temporalio/workflow` peer dependency
3
+ * @module
4
+ */
5
+ import type * as WorkflowModule from '@temporalio/workflow';
6
+ import type { SdkFlags as SdkFlagsT } from '@temporalio/workflow/lib/flags';
7
+
8
+ type SdkFlags = typeof SdkFlagsT;
9
+
10
+ // @temporalio/workflow is an optional peer dependency.
11
+ // It can be missing as long as the user isn't attempting to construct a workflow interceptor.
12
+ // If we start shipping ES modules alongside CJS, we will have to reconsider
13
+ // this dynamic import as `import` is async for ES modules.
14
+ let workflowModule: typeof WorkflowModule | undefined;
15
+ let workflowModuleLoadError: any | undefined;
16
+
17
+ try {
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ workflowModule = require('@temporalio/workflow');
20
+ } catch (err) {
21
+ // Capture the module not found error to rethrow if the module is required
22
+ workflowModuleLoadError = err;
23
+ }
24
+
25
+ /**
26
+ * Returns `@temporalio/workflow` module if present.
27
+ * Throws if the module failed to load
28
+ */
29
+ export function getWorkflowModule(): typeof WorkflowModule {
30
+ if (workflowModuleLoadError) {
31
+ throw workflowModuleLoadError;
32
+ }
33
+ return workflowModule!;
34
+ }
35
+
36
+ /**
37
+ * Returns if an SDK flag was set
38
+ *
39
+ * Expects to be called only after `ensureWorkflowModuleLoaded`.
40
+ * Will throw if `@temporalio/workflow` is not available
41
+ */
42
+ export function hasSdkFlag(flag: keyof SdkFlags): boolean {
43
+ const { SdkFlags } = require('@temporalio/workflow/lib/flags') as typeof import('@temporalio/workflow/lib/flags'); // eslint-disable-line @typescript-eslint/no-require-imports
44
+ const { getActivator } =
45
+ require('@temporalio/workflow/lib/global-attributes') as typeof import('@temporalio/workflow/lib/global-attributes'); // eslint-disable-line @typescript-eslint/no-require-imports
46
+
47
+ return getActivator().hasFlag(SdkFlags[flag]);
48
+ }
49
+
50
+ /**
51
+ * Checks if the workflow module loaded successfully and throws if not.
52
+ */
53
+ export function ensureWorkflowModuleLoaded(): void {
54
+ if (workflowModuleLoadError) {
55
+ throw workflowModuleLoadError;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Returns the workflow module if available, or undefined if it failed to load.
61
+ */
62
+ export function getWorkflowModuleIfAvailable(): typeof WorkflowModule | undefined {
63
+ return workflowModule;
64
+ }