@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,264 @@
1
+ import * as otel from '@opentelemetry/api';
2
+ import { createTraceState } from '@opentelemetry/api';
3
+ import type { Resource } from '@opentelemetry/resources';
4
+ import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
5
+ import type * as nexus from 'nexus-rpc';
6
+ import type { Context as ActivityContext } from '@temporalio/activity';
7
+ import { CompleteAsyncError } from '@temporalio/common';
8
+ import type {
9
+ Next,
10
+ ActivityInboundCallsInterceptor,
11
+ ActivityOutboundCallsInterceptor,
12
+ NexusInboundCallsInterceptor,
13
+ NexusOutboundCallsInterceptor,
14
+ NexusStartOperationInput,
15
+ NexusStartOperationOutput,
16
+ NexusCancelOperationInput,
17
+ InjectedSink,
18
+ GetLogAttributesInput,
19
+ GetMetricTagsInput,
20
+ ActivityExecuteInput,
21
+ } from '@temporalio/worker';
22
+ import {
23
+ instrument,
24
+ extractContextFromHeaders,
25
+ extractContextFromNexusHeaders,
26
+ WORKFLOW_ID_ATTR_KEY,
27
+ RUN_ID_ATTR_KEY,
28
+ ACTIVITY_ID_ATTR_KEY,
29
+ NEXUS_SERVICE_ATTR_KEY,
30
+ NEXUS_OPERATION_ATTR_KEY,
31
+ } from '../instrumentation';
32
+ import {
33
+ type OpenTelemetryWorkflowExporter,
34
+ type SerializableSpan,
35
+ SpanName,
36
+ SPAN_DELIMITER,
37
+ } from '../workflow/definitions';
38
+
39
+ export interface InterceptorOptions {
40
+ readonly tracer?: otel.Tracer;
41
+ }
42
+
43
+ /**
44
+ * Intercepts calls to start an Activity.
45
+ *
46
+ * Wraps the operation in an opentelemetry Span and links it to a parent Span context if one is
47
+ * provided in the Activity input headers.
48
+ *
49
+ * @experimental This interceptor is experimental and APIs may change without notice.
50
+ */
51
+ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundCallsInterceptor {
52
+ protected readonly tracer: otel.Tracer;
53
+
54
+ constructor(
55
+ protected readonly ctx: ActivityContext,
56
+ options?: InterceptorOptions
57
+ ) {
58
+ this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-activity');
59
+ }
60
+
61
+ async execute(input: ActivityExecuteInput, next: Next<ActivityInboundCallsInterceptor, 'execute'>): Promise<unknown> {
62
+ const context = extractContextFromHeaders(input.headers);
63
+ const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`;
64
+ return await instrument({
65
+ tracer: this.tracer,
66
+ spanName,
67
+ fn: (span) => {
68
+ span.setAttribute(ACTIVITY_ID_ATTR_KEY, this.ctx.info.activityId);
69
+ if (this.ctx.info.inWorkflow) {
70
+ span.setAttribute(WORKFLOW_ID_ATTR_KEY, this.ctx.info.workflowExecution!.workflowId);
71
+ span.setAttribute(RUN_ID_ATTR_KEY, this.ctx.info.workflowExecution!.runId);
72
+ }
73
+ return next(input);
74
+ },
75
+ context,
76
+ acceptableErrors: (err) => err instanceof CompleteAsyncError,
77
+ });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Intercepts calls to emit logs and metrics from an Activity.
83
+ *
84
+ * Attach OpenTelemetry context tracing attributes to emitted log messages, if appropriate.
85
+ *
86
+ * @experimental This interceptor is experimental and APIs may change without notice.
87
+ */
88
+ export class OpenTelemetryActivityOutboundInterceptor implements ActivityOutboundCallsInterceptor {
89
+ constructor(protected readonly ctx: ActivityContext) {}
90
+
91
+ public getLogAttributes(
92
+ input: GetLogAttributesInput,
93
+ next: Next<ActivityOutboundCallsInterceptor, 'getLogAttributes'>
94
+ ): Record<string, unknown> {
95
+ const span = otel.trace.getSpan(otel.context.active());
96
+ const spanContext = span?.spanContext();
97
+ if (spanContext && otel.isSpanContextValid(spanContext)) {
98
+ return next({
99
+ trace_id: spanContext.traceId,
100
+ span_id: spanContext.spanId,
101
+ trace_flags: `0${spanContext.traceFlags.toString(16)}`,
102
+ ...input,
103
+ });
104
+ } else {
105
+ return next(input);
106
+ }
107
+ }
108
+
109
+ public getMetricTags(
110
+ input: GetMetricTagsInput,
111
+ next: Next<ActivityOutboundCallsInterceptor, 'getMetricTags'>
112
+ ): GetMetricTagsInput {
113
+ return next(input);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Intercepts inbound Nexus Operation calls on the worker.
119
+ *
120
+ * Wraps `startOperation` and `cancelOperation` in opentelemetry Spans, linking to a parent Span context
121
+ * if one is provided in the Nexus request headers.
122
+ *
123
+ * @experimental This interceptor is experimental and APIs may change without notice.
124
+ */
125
+ export class OpenTelemetryNexusInboundInterceptor implements NexusInboundCallsInterceptor {
126
+ protected readonly tracer: otel.Tracer;
127
+
128
+ constructor(
129
+ protected readonly ctx: nexus.OperationContext,
130
+ options?: InterceptorOptions
131
+ ) {
132
+ this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-nexus');
133
+ }
134
+
135
+ async startOperation(
136
+ input: NexusStartOperationInput,
137
+ next: Next<NexusInboundCallsInterceptor, 'startOperation'>
138
+ ): Promise<NexusStartOperationOutput> {
139
+ const context = extractContextFromNexusHeaders(input.ctx.headers);
140
+ const spanName = `${SpanName.NEXUS_START_OPERATION_EXECUTE}${SPAN_DELIMITER}${input.ctx.service}/${input.ctx.operation}`;
141
+ return await instrument({
142
+ tracer: this.tracer,
143
+ spanName,
144
+ fn: (span) => {
145
+ span.setAttribute(NEXUS_SERVICE_ATTR_KEY, input.ctx.service);
146
+ span.setAttribute(NEXUS_OPERATION_ATTR_KEY, input.ctx.operation);
147
+ return next(input);
148
+ },
149
+ context,
150
+ });
151
+ }
152
+
153
+ async cancelOperation(
154
+ input: NexusCancelOperationInput,
155
+ next: Next<NexusInboundCallsInterceptor, 'cancelOperation'>
156
+ ): Promise<void> {
157
+ const context = extractContextFromNexusHeaders(input.ctx.headers);
158
+ const spanName = `${SpanName.NEXUS_CANCEL_OPERATION_EXECUTE}${SPAN_DELIMITER}${input.ctx.service}/${input.ctx.operation}`;
159
+ return await instrument({
160
+ tracer: this.tracer,
161
+ spanName,
162
+ fn: (span) => {
163
+ span.setAttribute(NEXUS_SERVICE_ATTR_KEY, input.ctx.service);
164
+ span.setAttribute(NEXUS_OPERATION_ATTR_KEY, input.ctx.operation);
165
+ return next(input);
166
+ },
167
+ context,
168
+ });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Intercepts outbound calls from a Nexus Operation handler.
174
+ *
175
+ * Attaches OpenTelemetry context tracing attributes to emitted log messages.
176
+ *
177
+ * @experimental This interceptor is experimental and APIs may change without notice.
178
+ */
179
+ export class OpenTelemetryNexusOutboundInterceptor implements NexusOutboundCallsInterceptor {
180
+ constructor(protected readonly ctx: nexus.OperationContext) {}
181
+
182
+ public getLogAttributes(
183
+ input: GetLogAttributesInput,
184
+ next: Next<NexusOutboundCallsInterceptor, 'getLogAttributes'>
185
+ ): Record<string, unknown> {
186
+ const span = otel.trace.getSpan(otel.context.active());
187
+ const spanContext = span?.spanContext();
188
+ if (spanContext && otel.isSpanContextValid(spanContext)) {
189
+ return next({
190
+ trace_id: spanContext.traceId,
191
+ span_id: spanContext.spanId,
192
+ trace_flags: `0${spanContext.traceFlags.toString(16)}`,
193
+ ...input,
194
+ });
195
+ } else {
196
+ return next(input);
197
+ }
198
+ }
199
+
200
+ public getMetricTags(
201
+ input: GetMetricTagsInput,
202
+ next: Next<NexusOutboundCallsInterceptor, 'getMetricTags'>
203
+ ): GetMetricTagsInput {
204
+ return next(input);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Takes an opentelemetry SpanProcessor and turns it into an injected Workflow span exporter sink.
210
+ */
211
+ export function makeWorkflowExporter(
212
+ processor: SpanProcessor,
213
+ resource: Resource
214
+ ): InjectedSink<OpenTelemetryWorkflowExporter> {
215
+ return {
216
+ export: {
217
+ fn: (info, spanData) => {
218
+ const spans = spanData.map((serialized) => {
219
+ Object.assign(serialized.attributes, info);
220
+ // Spans are copied over from the isolate and are converted to ReadableSpan instances
221
+ return extractReadableSpan(serialized, resource);
222
+ });
223
+
224
+ spans.forEach((span) => processor.onEnd(span));
225
+ },
226
+ },
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Deserialize a serialized span created by the Workflow isolate
232
+ */
233
+ function extractReadableSpan(serializable: SerializableSpan, resource: Resource): ReadableSpan {
234
+ const {
235
+ spanContext: { traceState, ...restSpanContext },
236
+ parentSpanContext: serializedParentSpanContext,
237
+ instrumentationScope,
238
+ ...rest
239
+ } = serializable;
240
+ const spanContext: otel.SpanContext = {
241
+ // Reconstruct the TraceState from the serialized string.
242
+ traceState: traceState ? createTraceState(traceState) : undefined,
243
+ ...restSpanContext,
244
+ };
245
+
246
+ let parentSpanContext: otel.SpanContext | undefined;
247
+ if (serializedParentSpanContext) {
248
+ const { traceState: parentTraceState, ...restParentSpanContext } = serializedParentSpanContext;
249
+ parentSpanContext = {
250
+ traceState: parentTraceState ? createTraceState(parentTraceState) : undefined,
251
+ ...restParentSpanContext,
252
+ };
253
+ }
254
+
255
+ return {
256
+ spanContext() {
257
+ return spanContext;
258
+ },
259
+ parentSpanContext,
260
+ instrumentationScope,
261
+ resource,
262
+ ...rest,
263
+ };
264
+ }
@@ -0,0 +1,53 @@
1
+ import { type AsyncLocalStorage } from 'async_hooks';
2
+ import * as otel from '@opentelemetry/api';
3
+
4
+ export class ContextManager implements otel.ContextManager {
5
+ // The workflow sandbox provides AsyncLocalStorage through globalThis.
6
+ protected storage: AsyncLocalStorage<otel.Context> = new (globalThis as any).AsyncLocalStorage();
7
+
8
+ active(): otel.Context {
9
+ return this.storage.getStore() || otel.ROOT_CONTEXT;
10
+ }
11
+
12
+ bind<T>(context: otel.Context, target: T): T {
13
+ if (typeof target !== 'function') {
14
+ throw new TypeError(`Only function binding is supported, got ${typeof target}`);
15
+ }
16
+
17
+ // Stolen from https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts
18
+ const contextWrapper = (...args: unknown[]) => {
19
+ return this.with(context, () => target.apply(this, args));
20
+ };
21
+ Object.defineProperty(contextWrapper, 'length', {
22
+ enumerable: false,
23
+ configurable: true,
24
+ writable: false,
25
+ value: target.length,
26
+ });
27
+ /**
28
+ * It isn't possible to tell Typescript that contextWrapper is the same as T
29
+ * so we forced to cast as any here.
30
+ */
31
+
32
+ return contextWrapper as any;
33
+ }
34
+
35
+ enable(): this {
36
+ return this;
37
+ }
38
+
39
+ disable(): this {
40
+ this.storage.disable();
41
+ return this;
42
+ }
43
+
44
+ with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
45
+ context: otel.Context,
46
+ fn: F,
47
+ thisArg?: ThisParameterType<F>,
48
+ ...args: A
49
+ ): ReturnType<F> {
50
+ const cb = thisArg == null ? fn : fn.bind(thisArg);
51
+ return this.storage.run(context, cb, ...args);
52
+ }
53
+ }
@@ -0,0 +1,145 @@
1
+ import type * as otel from '@opentelemetry/api';
2
+ import type * as tracing from '@opentelemetry/sdk-trace-base';
3
+ import type { InstrumentationScope } from '@opentelemetry/core';
4
+ import type { Sink, Sinks } from '@temporalio/workflow';
5
+
6
+ /**
7
+ * Serializable version of SpanContext where traceState is converted to a string.
8
+ */
9
+ export type SerializableSpanContext = Omit<otel.SpanContext, 'traceState'> & { traceState?: string };
10
+
11
+ /**
12
+ * Serializable version of the opentelemetry Span for cross isolate copying
13
+ */
14
+ export interface SerializableSpan {
15
+ readonly name: string;
16
+ readonly kind: otel.SpanKind;
17
+ readonly spanContext: SerializableSpanContext;
18
+ readonly parentSpanContext?: SerializableSpanContext;
19
+ readonly startTime: otel.HrTime;
20
+ readonly endTime: otel.HrTime;
21
+ readonly status: otel.SpanStatus;
22
+ readonly attributes: otel.Attributes;
23
+ readonly links: otel.Link[];
24
+ readonly events: tracing.TimedEvent[];
25
+ readonly duration: otel.HrTime;
26
+ readonly ended: boolean;
27
+ readonly droppedAttributesCount: number;
28
+ readonly droppedLinksCount: number;
29
+ readonly droppedEventsCount: number;
30
+ // readonly resource: Resource;
31
+ readonly instrumentationScope: InstrumentationScope;
32
+ }
33
+
34
+ export interface OpenTelemetryWorkflowExporter extends Sink {
35
+ export(span: SerializableSpan[]): void;
36
+ }
37
+
38
+ /**
39
+ * Required external dependencies for Workflow interceptor to export spans
40
+ */
41
+ export interface OpenTelemetrySinks extends Sinks {
42
+ exporter: OpenTelemetryWorkflowExporter;
43
+ }
44
+
45
+ export enum SpanName {
46
+ /**
47
+ * Workflow is scheduled by a client
48
+ */
49
+ WORKFLOW_START = 'StartWorkflow',
50
+
51
+ /**
52
+ * Workflow is signalled
53
+ */
54
+ WORKFLOW_SIGNAL = 'SignalWorkflow',
55
+
56
+ /**
57
+ * Workflow is client calls signalWithStart
58
+ */
59
+ WORKFLOW_SIGNAL_WITH_START = 'SignalWithStartWorkflow',
60
+
61
+ /**
62
+ * Workflow is queried
63
+ */
64
+ WORKFLOW_QUERY = 'QueryWorkflow',
65
+
66
+ /**
67
+ * Workflow update is started by client
68
+ */
69
+ WORKFLOW_START_UPDATE = 'StartWorkflowUpdate',
70
+
71
+ /**
72
+ * Workflow is started with an update
73
+ */
74
+ WORKFLOW_UPDATE_WITH_START = 'UpdateWithStartWorkflow',
75
+
76
+ /**
77
+ * Workflow handles an incoming signal
78
+ */
79
+ WORKFLOW_HANDLE_SIGNAL = 'HandleSignal',
80
+
81
+ /**
82
+ * Workflow handles an incoming query
83
+ */
84
+ WORKFLOW_HANDLE_QUERY = 'HandleQuery',
85
+
86
+ /**
87
+ * Workflow handles an incoming update
88
+ */
89
+ WORKFLOW_HANDLE_UPDATE = 'HandleUpdate',
90
+
91
+ /**
92
+ * Workflow validates an incoming update
93
+ */
94
+ WORKFLOW_VALIDATE_UPDATE = 'ValidateUpdate',
95
+
96
+ /**
97
+ * Workflow is terminated
98
+ */
99
+ WORKFLOW_TERMINATE = 'TerminateWorkflow',
100
+
101
+ /**
102
+ * Workflow is cancelled
103
+ */
104
+ WORKFLOW_CANCEL = 'CancelWorkflow',
105
+
106
+ /**
107
+ * Workflow is described
108
+ */
109
+ WORKFLOW_DESCRIBE = 'DescribeWorkflow',
110
+
111
+ /**
112
+ * Workflow run is executing
113
+ */
114
+ WORKFLOW_EXECUTE = 'RunWorkflow',
115
+ /**
116
+ * Child Workflow is started (by parent Workflow)
117
+ */
118
+ CHILD_WORKFLOW_START = 'StartChildWorkflow',
119
+ /**
120
+ * Activity is scheduled by a Workflow
121
+ */
122
+ ACTIVITY_START = 'StartActivity',
123
+ /**
124
+ * Activity is executing
125
+ */
126
+ ACTIVITY_EXECUTE = 'RunActivity',
127
+ /**
128
+ * Workflow is continuing as new
129
+ */
130
+ CONTINUE_AS_NEW = 'ContinueAsNew',
131
+ /**
132
+ * Nexus operation is started
133
+ */
134
+ NEXUS_OPERATION_START = 'StartNexusOperation',
135
+ /**
136
+ * Nexus operation is executing
137
+ */
138
+ NEXUS_START_OPERATION_EXECUTE = 'RunStartNexusOperation',
139
+ /**
140
+ * Nexus operation cancel is executing
141
+ */
142
+ NEXUS_CANCEL_OPERATION_EXECUTE = 'RunCancelNexusOperation',
143
+ }
144
+
145
+ export const SPAN_DELIMITER = ':';
@@ -0,0 +1,30 @@
1
+ import type { IdGenerator } from '@opentelemetry/sdk-trace-base';
2
+ import { getRandomStream } from './workflow-imports';
3
+
4
+ const OTEL_ID_RANDOM_STREAM = '@temporalio/interceptors-opentelemetry-v2/id-generator';
5
+
6
+ function randomHex(length: number): string {
7
+ const stream = getRandomStream(OTEL_ID_RANDOM_STREAM);
8
+ const chars: string[] = [];
9
+ for (let i = 0; i < length; i++) {
10
+ const nibble = (stream.random() * 16) >>> 0;
11
+ chars.push(nibble.toString(16));
12
+ }
13
+ return chars.join('');
14
+ }
15
+
16
+ /**
17
+ * Generates span and trace IDs from a named workflow random stream.
18
+ */
19
+ export class DeterministicIdGenerator implements IdGenerator {
20
+ generateTraceId(): string {
21
+ const id = randomHex(32);
22
+ // Ensure non-zero per W3C Trace Context spec
23
+ return id === '00000000000000000000000000000000' ? '00000000000000000000000000000001' : id;
24
+ }
25
+
26
+ generateSpanId(): string {
27
+ const id = randomHex(16);
28
+ return id === '0000000000000000' ? '0000000000000001' : id;
29
+ }
30
+ }