@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.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/lib/client/index.d.ts +26 -0
- package/lib/client/index.js +185 -0
- package/lib/client/index.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +32 -0
- package/lib/index.js.map +1 -0
- package/lib/instrumentation.d.ts +58 -0
- package/lib/instrumentation.js +152 -0
- package/lib/instrumentation.js.map +1 -0
- package/lib/plugin.d.ts +31 -0
- package/lib/plugin.js +66 -0
- package/lib/plugin.js.map +1 -0
- package/lib/worker/index.d.ts +69 -0
- package/lib/worker/index.js +222 -0
- package/lib/worker/index.js.map +1 -0
- package/lib/workflow/context-manager.d.ts +10 -0
- package/lib/workflow/context-manager.js +67 -0
- package/lib/workflow/context-manager.js.map +1 -0
- package/lib/workflow/definitions.d.ts +127 -0
- package/lib/workflow/definitions.js +92 -0
- package/lib/workflow/definitions.js.map +1 -0
- package/lib/workflow/id-generator.d.ts +8 -0
- package/lib/workflow/id-generator.js +30 -0
- package/lib/workflow/id-generator.js.map +1 -0
- package/lib/workflow/index.d.ts +51 -0
- package/lib/workflow/index.js +252 -0
- package/lib/workflow/index.js.map +1 -0
- package/lib/workflow/performance-polyfill.d.ts +16 -0
- package/lib/workflow/performance-polyfill.js +29 -0
- package/lib/workflow/performance-polyfill.js.map +1 -0
- package/lib/workflow/runtime.d.ts +1 -0
- package/lib/workflow/runtime.js +14 -0
- package/lib/workflow/runtime.js.map +1 -0
- package/lib/workflow/span-exporter.d.ts +9 -0
- package/lib/workflow/span-exporter.js +56 -0
- package/lib/workflow/span-exporter.js.map +1 -0
- package/lib/workflow/workflow-imports-impl.d.ts +7 -0
- package/lib/workflow/workflow-imports-impl.js +17 -0
- package/lib/workflow/workflow-imports-impl.js.map +1 -0
- package/lib/workflow/workflow-imports.d.ts +16 -0
- package/lib/workflow/workflow-imports.js +25 -0
- package/lib/workflow/workflow-imports.js.map +1 -0
- package/lib/workflow-interceptors.d.ts +3 -0
- package/lib/workflow-interceptors.js +12 -0
- package/lib/workflow-interceptors.js.map +1 -0
- package/package.json +78 -0
- package/src/client/index.ts +220 -0
- package/src/index.ts +14 -0
- package/src/instrumentation.ts +159 -0
- package/src/plugin.ts +86 -0
- package/src/worker/index.ts +264 -0
- package/src/workflow/context-manager.ts +53 -0
- package/src/workflow/definitions.ts +145 -0
- package/src/workflow/id-generator.ts +30 -0
- package/src/workflow/index.ts +312 -0
- package/src/workflow/performance-polyfill.ts +31 -0
- package/src/workflow/runtime.ts +12 -0
- package/src/workflow/span-exporter.ts +60 -0
- package/src/workflow/workflow-imports-impl.ts +14 -0
- package/src/workflow/workflow-imports.ts +39 -0
- package/src/workflow-interceptors.ts +14 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api';
|
|
2
|
+
import type {
|
|
3
|
+
Next,
|
|
4
|
+
WorkflowSignalInput,
|
|
5
|
+
WorkflowSignalWithStartInput,
|
|
6
|
+
WorkflowStartInput,
|
|
7
|
+
WorkflowStartOutput,
|
|
8
|
+
WorkflowStartUpdateInput,
|
|
9
|
+
WorkflowStartUpdateOutput,
|
|
10
|
+
WorkflowStartUpdateWithStartInput,
|
|
11
|
+
WorkflowStartUpdateWithStartOutput,
|
|
12
|
+
WorkflowQueryInput,
|
|
13
|
+
WorkflowTerminateInput,
|
|
14
|
+
WorkflowCancelInput,
|
|
15
|
+
WorkflowDescribeInput,
|
|
16
|
+
WorkflowClientInterceptor,
|
|
17
|
+
TerminateWorkflowExecutionResponse,
|
|
18
|
+
RequestCancelWorkflowExecutionResponse,
|
|
19
|
+
DescribeWorkflowExecutionResponse,
|
|
20
|
+
} from '@temporalio/client';
|
|
21
|
+
import {
|
|
22
|
+
instrument,
|
|
23
|
+
headersWithContext,
|
|
24
|
+
RUN_ID_ATTR_KEY,
|
|
25
|
+
WORKFLOW_ID_ATTR_KEY,
|
|
26
|
+
UPDATE_ID_ATTR_KEY,
|
|
27
|
+
TERMINATE_REASON_ATTR_KEY,
|
|
28
|
+
} from '../instrumentation';
|
|
29
|
+
import { SpanName, SPAN_DELIMITER } from '../workflow/definitions';
|
|
30
|
+
|
|
31
|
+
export interface InterceptorOptions {
|
|
32
|
+
readonly tracer?: otel.Tracer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Intercepts calls to start a Workflow.
|
|
37
|
+
*
|
|
38
|
+
* Wraps the operation in an opentelemetry Span and passes it to the Workflow via headers.
|
|
39
|
+
*
|
|
40
|
+
* @experimental This interceptor is experimental and APIs may change without notice.
|
|
41
|
+
*/
|
|
42
|
+
export class OpenTelemetryWorkflowClientInterceptor implements WorkflowClientInterceptor {
|
|
43
|
+
protected readonly tracer: otel.Tracer;
|
|
44
|
+
|
|
45
|
+
constructor(options?: InterceptorOptions) {
|
|
46
|
+
this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-client');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async start(input: WorkflowStartInput, next: Next<WorkflowClientInterceptor, 'start'>): Promise<string> {
|
|
50
|
+
return await instrument({
|
|
51
|
+
tracer: this.tracer,
|
|
52
|
+
spanName: `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`,
|
|
53
|
+
fn: async (span) => {
|
|
54
|
+
const headers = headersWithContext(input.headers);
|
|
55
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId);
|
|
56
|
+
const runId = await next({ ...input, headers });
|
|
57
|
+
span.setAttribute(RUN_ID_ATTR_KEY, runId);
|
|
58
|
+
return runId;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async signal(input: WorkflowSignalInput, next: Next<WorkflowClientInterceptor, 'signal'>): Promise<void> {
|
|
64
|
+
return await instrument({
|
|
65
|
+
tracer: this.tracer,
|
|
66
|
+
spanName: `${SpanName.WORKFLOW_SIGNAL}${SPAN_DELIMITER}${input.signalName}`,
|
|
67
|
+
fn: async (span) => {
|
|
68
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
69
|
+
const headers = headersWithContext(input.headers);
|
|
70
|
+
await next({ ...input, headers });
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async startWithDetails(
|
|
76
|
+
input: WorkflowStartInput,
|
|
77
|
+
next: Next<WorkflowClientInterceptor, 'startWithDetails'>
|
|
78
|
+
): Promise<WorkflowStartOutput> {
|
|
79
|
+
return await instrument({
|
|
80
|
+
tracer: this.tracer,
|
|
81
|
+
spanName: `${SpanName.WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`,
|
|
82
|
+
fn: async (span) => {
|
|
83
|
+
const headers = headersWithContext(input.headers);
|
|
84
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId);
|
|
85
|
+
const output = await next({ ...input, headers });
|
|
86
|
+
span.setAttribute(RUN_ID_ATTR_KEY, output.runId);
|
|
87
|
+
return output;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async startUpdate(
|
|
93
|
+
input: WorkflowStartUpdateInput,
|
|
94
|
+
next: Next<WorkflowClientInterceptor, 'startUpdate'>
|
|
95
|
+
): Promise<WorkflowStartUpdateOutput> {
|
|
96
|
+
return await instrument({
|
|
97
|
+
tracer: this.tracer,
|
|
98
|
+
spanName: `${SpanName.WORKFLOW_START_UPDATE}${SPAN_DELIMITER}${input.updateName}`,
|
|
99
|
+
fn: async (span) => {
|
|
100
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
101
|
+
if (input.options.updateId) {
|
|
102
|
+
span.setAttribute(UPDATE_ID_ATTR_KEY, input.options.updateId);
|
|
103
|
+
}
|
|
104
|
+
const headers = headersWithContext(input.headers);
|
|
105
|
+
const output = await next({ ...input, headers });
|
|
106
|
+
span.setAttribute(RUN_ID_ATTR_KEY, output.workflowRunId);
|
|
107
|
+
return output;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async startUpdateWithStart(
|
|
113
|
+
input: WorkflowStartUpdateWithStartInput,
|
|
114
|
+
next: Next<WorkflowClientInterceptor, 'startUpdateWithStart'>
|
|
115
|
+
): Promise<WorkflowStartUpdateWithStartOutput> {
|
|
116
|
+
return await instrument({
|
|
117
|
+
tracer: this.tracer,
|
|
118
|
+
spanName: `${SpanName.WORKFLOW_UPDATE_WITH_START}${SPAN_DELIMITER}${input.updateName}`,
|
|
119
|
+
fn: async (span) => {
|
|
120
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowStartOptions.workflowId);
|
|
121
|
+
if (input.updateOptions.updateId) {
|
|
122
|
+
span.setAttribute(UPDATE_ID_ATTR_KEY, input.updateOptions.updateId);
|
|
123
|
+
}
|
|
124
|
+
const workflowStartHeaders = headersWithContext(input.workflowStartHeaders);
|
|
125
|
+
const updateHeaders = headersWithContext(input.updateHeaders);
|
|
126
|
+
const output = await next({ ...input, workflowStartHeaders, updateHeaders });
|
|
127
|
+
if (output.workflowExecution.runId) {
|
|
128
|
+
span.setAttribute(RUN_ID_ATTR_KEY, output.workflowExecution.runId);
|
|
129
|
+
}
|
|
130
|
+
return output;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async signalWithStart(
|
|
136
|
+
input: WorkflowSignalWithStartInput,
|
|
137
|
+
next: Next<WorkflowClientInterceptor, 'signalWithStart'>
|
|
138
|
+
): Promise<string> {
|
|
139
|
+
return await instrument({
|
|
140
|
+
tracer: this.tracer,
|
|
141
|
+
spanName: `${SpanName.WORKFLOW_SIGNAL_WITH_START}${SPAN_DELIMITER}${input.workflowType}`,
|
|
142
|
+
fn: async (span) => {
|
|
143
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.options.workflowId);
|
|
144
|
+
const headers = headersWithContext(input.headers);
|
|
145
|
+
const runId = await next({ ...input, headers });
|
|
146
|
+
span.setAttribute(RUN_ID_ATTR_KEY, runId);
|
|
147
|
+
return runId;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async query(input: WorkflowQueryInput, next: Next<WorkflowClientInterceptor, 'query'>): Promise<unknown> {
|
|
153
|
+
return await instrument({
|
|
154
|
+
tracer: this.tracer,
|
|
155
|
+
spanName: `${SpanName.WORKFLOW_QUERY}${SPAN_DELIMITER}${input.queryType}`,
|
|
156
|
+
fn: async (span) => {
|
|
157
|
+
const headers = headersWithContext(input.headers);
|
|
158
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
159
|
+
if (input.workflowExecution.runId) {
|
|
160
|
+
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
|
|
161
|
+
}
|
|
162
|
+
return await next({ ...input, headers });
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async terminate(
|
|
168
|
+
input: WorkflowTerminateInput,
|
|
169
|
+
next: Next<WorkflowClientInterceptor, 'terminate'>
|
|
170
|
+
): Promise<TerminateWorkflowExecutionResponse> {
|
|
171
|
+
return await instrument({
|
|
172
|
+
tracer: this.tracer,
|
|
173
|
+
spanName: SpanName.WORKFLOW_TERMINATE,
|
|
174
|
+
fn: async (span) => {
|
|
175
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
176
|
+
if (input.workflowExecution.runId) {
|
|
177
|
+
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
|
|
178
|
+
}
|
|
179
|
+
if (input.reason) {
|
|
180
|
+
span.setAttribute(TERMINATE_REASON_ATTR_KEY, input.reason);
|
|
181
|
+
}
|
|
182
|
+
return await next(input);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async cancel(
|
|
188
|
+
input: WorkflowCancelInput,
|
|
189
|
+
next: Next<WorkflowClientInterceptor, 'cancel'>
|
|
190
|
+
): Promise<RequestCancelWorkflowExecutionResponse> {
|
|
191
|
+
return await instrument({
|
|
192
|
+
tracer: this.tracer,
|
|
193
|
+
spanName: SpanName.WORKFLOW_CANCEL,
|
|
194
|
+
fn: async (span) => {
|
|
195
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
196
|
+
if (input.workflowExecution.runId) {
|
|
197
|
+
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
|
|
198
|
+
}
|
|
199
|
+
return await next(input);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async describe(
|
|
205
|
+
input: WorkflowDescribeInput,
|
|
206
|
+
next: Next<WorkflowClientInterceptor, 'describe'>
|
|
207
|
+
): Promise<DescribeWorkflowExecutionResponse> {
|
|
208
|
+
return await instrument({
|
|
209
|
+
tracer: this.tracer,
|
|
210
|
+
spanName: SpanName.WORKFLOW_DESCRIBE,
|
|
211
|
+
fn: async (span) => {
|
|
212
|
+
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
|
|
213
|
+
if (input.workflowExecution.runId) {
|
|
214
|
+
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
|
|
215
|
+
}
|
|
216
|
+
return await next(input);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm i @temporalio/interceptors-opentelemetry-v2`
|
|
3
|
+
*
|
|
4
|
+
* Interceptors that add OpenTelemetry tracing.
|
|
5
|
+
*
|
|
6
|
+
* [Documentation](https://docs.temporal.io/typescript/logging#opentelemetry-tracing)
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from './plugin';
|
|
12
|
+
export * from './workflow';
|
|
13
|
+
export * from './worker';
|
|
14
|
+
export { OpenTelemetryWorkflowClientInterceptor } from './client';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opentelemetry instrumentation helper functions
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import * as otel from '@opentelemetry/api';
|
|
6
|
+
import {
|
|
7
|
+
type Headers,
|
|
8
|
+
ApplicationFailure,
|
|
9
|
+
ApplicationFailureCategory,
|
|
10
|
+
defaultPayloadConverter,
|
|
11
|
+
} from '@temporalio/common';
|
|
12
|
+
|
|
13
|
+
/** Default trace header for opentelemetry interceptors */
|
|
14
|
+
export const TRACE_HEADER = '_tracer-data';
|
|
15
|
+
/** As in workflow run id */
|
|
16
|
+
export const RUN_ID_ATTR_KEY = 'run_id';
|
|
17
|
+
/** As in workflow id */
|
|
18
|
+
export const WORKFLOW_ID_ATTR_KEY = 'temporalWorkflowId';
|
|
19
|
+
/** As in activity id */
|
|
20
|
+
export const ACTIVITY_ID_ATTR_KEY = 'temporalActivityId';
|
|
21
|
+
/** As in update id */
|
|
22
|
+
export const UPDATE_ID_ATTR_KEY = 'temporalUpdateId';
|
|
23
|
+
/** As in termination reason */
|
|
24
|
+
export const TERMINATE_REASON_ATTR_KEY = 'temporalTerminateReason';
|
|
25
|
+
/** As in Nexus service */
|
|
26
|
+
export const NEXUS_SERVICE_ATTR_KEY = 'temporalNexusService';
|
|
27
|
+
/** As in Nexus operation */
|
|
28
|
+
export const NEXUS_OPERATION_ATTR_KEY = 'temporalNexusOperation';
|
|
29
|
+
/** As in Nexus endpoint */
|
|
30
|
+
export const NEXUS_ENDPOINT_ATTR_KEY = 'temporalNexusEndpoint';
|
|
31
|
+
|
|
32
|
+
const payloadConverter = defaultPayloadConverter;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* If found, return an otel Context deserialized from the provided headers
|
|
36
|
+
*/
|
|
37
|
+
export function extractContextFromHeaders(headers: Headers): otel.Context | undefined {
|
|
38
|
+
const encodedSpanContext = headers[TRACE_HEADER];
|
|
39
|
+
if (encodedSpanContext === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const textMap: Record<string, string> = payloadConverter.fromPayload(encodedSpanContext);
|
|
43
|
+
return otel.propagation.extract(otel.context.active(), textMap, otel.defaultTextMapGetter);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* If found, return an otel Context deserialized from the provided Nexus headers.
|
|
48
|
+
*
|
|
49
|
+
* Nexus headers are plain `Record<string, string>`, so we extract the context
|
|
50
|
+
* directly from the string map.
|
|
51
|
+
*/
|
|
52
|
+
export function extractContextFromNexusHeaders(headers: Record<string, string>): otel.Context | undefined {
|
|
53
|
+
if (Object.keys(headers).length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return otel.propagation.extract(otel.context.active(), headers, otel.defaultTextMapGetter);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Given headers, return new headers with the current otel context inserted
|
|
61
|
+
*/
|
|
62
|
+
export function headersWithContext(headers: Headers): Headers {
|
|
63
|
+
const carrier = {};
|
|
64
|
+
otel.propagation.inject(otel.context.active(), carrier, otel.defaultTextMapSetter);
|
|
65
|
+
return { ...headers, [TRACE_HEADER]: payloadConverter.toPayload(carrier) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Given Nexus headers, return new headers with the current otel context injected directly
|
|
70
|
+
*/
|
|
71
|
+
export function nexusHeadersWithContext(headers: Record<string, string>): Record<string, string> {
|
|
72
|
+
const carrier: Record<string, string> = {};
|
|
73
|
+
otel.propagation.inject(otel.context.active(), carrier, otel.defaultTextMapSetter);
|
|
74
|
+
return { ...headers, ...carrier };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function wrapWithSpan<T>(
|
|
78
|
+
span: otel.Span,
|
|
79
|
+
fn: (span: otel.Span) => Promise<T>,
|
|
80
|
+
acceptableErrors?: (err: unknown) => boolean
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
try {
|
|
83
|
+
const ret = await fn(span);
|
|
84
|
+
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
85
|
+
return ret;
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
maybeAddErrorToSpan(err, span, acceptableErrors);
|
|
88
|
+
throw err;
|
|
89
|
+
} finally {
|
|
90
|
+
span.end();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wrapWithSpanSync<T>(
|
|
95
|
+
span: otel.Span,
|
|
96
|
+
fn: (span: otel.Span) => T,
|
|
97
|
+
acceptableErrors?: (err: unknown) => boolean
|
|
98
|
+
): T {
|
|
99
|
+
try {
|
|
100
|
+
const ret = fn(span);
|
|
101
|
+
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
102
|
+
return ret;
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
maybeAddErrorToSpan(err, span, acceptableErrors);
|
|
105
|
+
throw err;
|
|
106
|
+
} finally {
|
|
107
|
+
span.end();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function maybeAddErrorToSpan(err: any, span: otel.Span, acceptableErrors?: (err: unknown) => boolean): void {
|
|
112
|
+
const isBenignErr = err instanceof ApplicationFailure && err.category === ApplicationFailureCategory.BENIGN;
|
|
113
|
+
if (acceptableErrors === undefined || !acceptableErrors(err)) {
|
|
114
|
+
const statusCode = isBenignErr ? otel.SpanStatusCode.UNSET : otel.SpanStatusCode.ERROR;
|
|
115
|
+
const message = err != null && typeof err.message === 'string' ? err.message : String(err);
|
|
116
|
+
span.setStatus({ code: statusCode, message });
|
|
117
|
+
span.recordException(err);
|
|
118
|
+
} else {
|
|
119
|
+
span.setStatus({ code: otel.SpanStatusCode.OK });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface InstrumentOptions<T> {
|
|
124
|
+
tracer: otel.Tracer;
|
|
125
|
+
spanName: string;
|
|
126
|
+
fn: (span: otel.Span) => Promise<T>;
|
|
127
|
+
context?: otel.Context;
|
|
128
|
+
acceptableErrors?: (err: unknown) => boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type InstrumentOptionsSync<T> = Omit<InstrumentOptions<T>, 'fn'> & { fn: (span: otel.Span) => T };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Wraps `fn` in a span which ends when function returns or throws
|
|
135
|
+
*/
|
|
136
|
+
export async function instrument<T>({
|
|
137
|
+
tracer,
|
|
138
|
+
spanName,
|
|
139
|
+
fn,
|
|
140
|
+
context,
|
|
141
|
+
acceptableErrors,
|
|
142
|
+
}: InstrumentOptions<T>): Promise<T> {
|
|
143
|
+
if (context) {
|
|
144
|
+
return await tracer.startActiveSpan(
|
|
145
|
+
spanName,
|
|
146
|
+
{},
|
|
147
|
+
context,
|
|
148
|
+
async (span) => await wrapWithSpan(span, fn, acceptableErrors)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return await tracer.startActiveSpan(spanName, async (span) => await wrapWithSpan(span, fn, acceptableErrors));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function instrumentSync<T>({ tracer, spanName, fn, context, acceptableErrors }: InstrumentOptionsSync<T>): T {
|
|
155
|
+
if (context) {
|
|
156
|
+
return tracer.startActiveSpan(spanName, {}, context, (span) => wrapWithSpanSync(span, fn, acceptableErrors));
|
|
157
|
+
}
|
|
158
|
+
return tracer.startActiveSpan(spanName, (span) => wrapWithSpanSync(span, fn, acceptableErrors));
|
|
159
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import type { Resource } from '@opentelemetry/resources';
|
|
3
|
+
import { SimplePlugin } from '@temporalio/plugin';
|
|
4
|
+
import type { InjectedSinks, ReplayWorkerOptions, WorkerOptions } from '@temporalio/worker';
|
|
5
|
+
import type { InterceptorOptions } from './client';
|
|
6
|
+
import { OpenTelemetryWorkflowClientInterceptor } from './client';
|
|
7
|
+
import {
|
|
8
|
+
makeWorkflowExporter,
|
|
9
|
+
OpenTelemetryActivityInboundInterceptor,
|
|
10
|
+
OpenTelemetryActivityOutboundInterceptor,
|
|
11
|
+
OpenTelemetryNexusInboundInterceptor,
|
|
12
|
+
OpenTelemetryNexusOutboundInterceptor,
|
|
13
|
+
} from './worker';
|
|
14
|
+
import type { OpenTelemetrySinks } from './workflow';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options for {@link OpenTelemetryPlugin}.
|
|
18
|
+
*
|
|
19
|
+
* @experimental Plugins is an experimental feature; APIs may change without notice.
|
|
20
|
+
*/
|
|
21
|
+
export interface OpenTelemetryPluginOptions extends InterceptorOptions {
|
|
22
|
+
/** OpenTelemetry resource attributes to attach to exported spans */
|
|
23
|
+
readonly resource: Resource;
|
|
24
|
+
/** Exporter used to send spans to a tracing backend */
|
|
25
|
+
readonly spanProcessor: SpanProcessor;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A plugin that adds OpenTelemetry tracing.
|
|
30
|
+
*
|
|
31
|
+
* Configures Client, Activity, and Workflow interceptors for trace propagation and injects
|
|
32
|
+
* a span exporter sink for Workflow spans.
|
|
33
|
+
*
|
|
34
|
+
* @experimental Plugins is an experimental feature; APIs may change without notice.
|
|
35
|
+
*/
|
|
36
|
+
export class OpenTelemetryPlugin extends SimplePlugin {
|
|
37
|
+
constructor(readonly otelOptions: OpenTelemetryPluginOptions) {
|
|
38
|
+
const workflowInterceptorsPath = require.resolve('./workflow-interceptors');
|
|
39
|
+
const interceptorOptions = otelOptions.tracer ? { tracer: otelOptions.tracer } : {};
|
|
40
|
+
super({
|
|
41
|
+
name: 'OpenTelemetryPlugin',
|
|
42
|
+
clientInterceptors: {
|
|
43
|
+
workflow: [new OpenTelemetryWorkflowClientInterceptor(interceptorOptions)],
|
|
44
|
+
},
|
|
45
|
+
workerInterceptors: {
|
|
46
|
+
client: {
|
|
47
|
+
workflow: [new OpenTelemetryWorkflowClientInterceptor(interceptorOptions)],
|
|
48
|
+
},
|
|
49
|
+
workflowModules: [workflowInterceptorsPath],
|
|
50
|
+
activity: [
|
|
51
|
+
(ctx) => ({
|
|
52
|
+
inbound: new OpenTelemetryActivityInboundInterceptor(ctx, interceptorOptions),
|
|
53
|
+
outbound: new OpenTelemetryActivityOutboundInterceptor(ctx),
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
nexus: [
|
|
57
|
+
(ctx) => ({
|
|
58
|
+
inbound: new OpenTelemetryNexusInboundInterceptor(ctx, interceptorOptions),
|
|
59
|
+
outbound: new OpenTelemetryNexusOutboundInterceptor(ctx),
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
configureWorker(options: WorkerOptions): WorkerOptions {
|
|
67
|
+
return super.configureWorker(this.injectSinks(options));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
configureReplayWorker(options: ReplayWorkerOptions): ReplayWorkerOptions {
|
|
71
|
+
return super.configureReplayWorker(this.injectSinks(options));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private injectSinks<T extends { sinks?: InjectedSinks<any> }>(options: T): T {
|
|
75
|
+
const sinks: InjectedSinks<OpenTelemetrySinks> = {
|
|
76
|
+
exporter: makeWorkflowExporter(this.otelOptions.spanProcessor, this.otelOptions.resource),
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
...options,
|
|
80
|
+
sinks: {
|
|
81
|
+
...options.sinks,
|
|
82
|
+
...sinks,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|