Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e426688

Browse files
authored
fix: Fix and improve opentelemetry interceptors (temporalio#340)
- Make `makeWorkflowExporter` resource param required - Fix Workflow span timestamps - Disable internal SDK tracing by default - Connect child workflow traces to their parent - Connect continueAsNew traces - Some breaking changes were made to the interceptor interfaces - Change trace header name for compatibility with Go and Java tracing implementations
1 parent a0e207d commit e426688

File tree

19 files changed

+635
-487
lines changed

19 files changed

+635
-487
lines changed

package-lock.json

Lines changed: 216 additions & 285 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/client/src/interceptors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export { Next, Headers };
1414
/** Input for WorkflowClientCallsInterceptor.start */
1515
export interface WorkflowStartInput {
1616
/** Name of Workflow to start */
17-
readonly name: string;
17+
readonly workflowType: string;
1818
/** Workflow arguments */
1919
readonly args: unknown[];
2020
readonly headers: Headers;
@@ -30,7 +30,7 @@ export interface WorkflowSignalInput {
3030

3131
/** Input for WorkflowClientCallsInterceptor.signalWithStart */
3232
export interface WorkflowSignalWithStartInput {
33-
readonly workflowName: string;
33+
readonly workflowType: string;
3434
readonly workflowArgs: unknown[];
3535
readonly signalName: string;
3636
readonly signalArgs: unknown[];

packages/client/src/workflow-client.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export class WorkflowClient {
229229
options: compiledOptions,
230230
headers: {},
231231
args,
232-
name: workflowType,
232+
workflowType,
233233
});
234234
return start(...args);
235235
}
@@ -406,14 +406,14 @@ export class WorkflowClient {
406406
*/
407407
protected async _signalWithStartWorkflowHandler(input: WorkflowSignalWithStartInput): Promise<string> {
408408
const { identity, dataConverter } = this.options;
409-
const { options, workflowName, workflowArgs, signalName, signalArgs, headers } = input;
409+
const { options, workflowType, workflowArgs, signalName, signalArgs, headers } = input;
410410
const { runId } = await this.service.signalWithStartWorkflowExecution({
411411
namespace: this.options.namespace,
412412
identity,
413413
requestId: uuid4(),
414414
workflowId: options.workflowId,
415415
workflowIdReusePolicy: options.workflowIdReusePolicy,
416-
workflowType: { name: workflowName },
416+
workflowType: { name: workflowType },
417417
input: { payloads: await dataConverter.toPayloads(...workflowArgs) },
418418
signalName,
419419
signalInput: { payloads: await dataConverter.toPayloads(...signalArgs) },
@@ -443,7 +443,7 @@ export class WorkflowClient {
443443
* Used as the final function of the start interceptor chain
444444
*/
445445
protected async _startWorkflowHandler(input: WorkflowStartInput): Promise<string> {
446-
const { options: opts, name, args, headers } = input;
446+
const { options: opts, workflowType: name, args, headers } = input;
447447
const { identity, dataConverter } = this.options;
448448
const req: StartWorkflowExecutionRequest = {
449449
namespace: this.options.namespace,
@@ -655,7 +655,10 @@ export class WorkflowClient {
655655
/**
656656
* Creates a Workflow handle for new Workflow execution
657657
*/
658-
protected createNewWorkflow<T extends Workflow>(name: string, options?: Partial<WorkflowOptions>): WorkflowHandle<T> {
658+
protected createNewWorkflow<T extends Workflow>(
659+
workflowType: string,
660+
options?: Partial<WorkflowOptions>
661+
): WorkflowHandle<T> {
659662
const mergedOptions = { ...this.options.workflowDefaults, ...options };
660663
assertRequiredWorkflowOptions(mergedOptions);
661664
const compiledOptions = compileWorkflowOptions(addDefaults(mergedOptions));
@@ -671,7 +674,7 @@ export class WorkflowClient {
671674
options: compiledOptions,
672675
headers: {},
673676
args,
674-
name,
677+
workflowType,
675678
});
676679
};
677680

@@ -686,7 +689,7 @@ export class WorkflowClient {
686689
options: compiledOptions,
687690
headers: {},
688691
workflowArgs,
689-
workflowName: name,
692+
workflowType,
690693
signalName: typeof def === 'string' ? def : def.name,
691694
signalArgs,
692695
});

packages/common/src/otel.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { isSpanContextValid, Span, SpanContext } from '@opentelemetry/api';
1+
import * as otel from '@opentelemetry/api';
22
import { defaultDataConverter } from './converter/data-converter';
33
import { Headers } from './interceptors';
44

5-
export const TRACE_HEADER = 'Otel-Trace-Context';
5+
/** Default trace header for opentelemetry interceptors */
6+
export const TRACE_HEADER = '_tracer-data';
67
/** As in workflow run id */
78
export const RUN_ID_ATTR_KEY = 'run_id';
89
/** For a workflow or activity task */
@@ -11,31 +12,42 @@ export const TASK_TOKEN_ATTR_KEY = 'task_token';
1112
export const NUM_JOBS_ATTR_KEY = 'num_jobs';
1213

1314
/**
14-
* If found, return a span context deserialized from the provided headers
15+
* If found, return an otel Context deserialized from the provided headers
1516
*/
16-
export async function extractSpanContextFromHeaders(headers: Headers): Promise<SpanContext | undefined> {
17+
export async function extractContextFromHeaders(headers: Headers): Promise<otel.Context | undefined> {
1718
const encodedSpanContext = headers[TRACE_HEADER];
1819
if (encodedSpanContext === undefined) {
1920
return undefined;
2021
}
21-
const decoded: SpanContext = await defaultDataConverter.fromPayload(encodedSpanContext);
22-
if (isSpanContextValid(decoded)) {
23-
return decoded;
22+
const textMap: Record<string, string> = await defaultDataConverter.fromPayload(encodedSpanContext);
23+
return otel.propagation.extract(otel.context.active(), textMap, otel.defaultTextMapGetter);
24+
}
25+
26+
/**
27+
* If found, return an otel SpanContext deserialized from the provided headers
28+
*/
29+
export async function extractSpanContextFromHeaders(headers: Headers): Promise<otel.SpanContext | undefined> {
30+
const context = await extractContextFromHeaders(headers);
31+
if (context === undefined) {
32+
return undefined;
2433
}
25-
return undefined;
34+
35+
return otel.trace.getSpanContext(context);
2636
}
2737

2838
/**
29-
* Given a span context & headers, return new headers with the span context inserted
39+
* Given headers, return new headers with the current otel context inserted
3040
*/
31-
export async function headersWithSpanContext(context: SpanContext, headers: Headers): Promise<Headers> {
32-
return { ...headers, [TRACE_HEADER]: await defaultDataConverter.toPayload(context) };
41+
export async function headersWithContext(headers: Headers): Promise<Headers> {
42+
const carrier = {};
43+
otel.propagation.inject(otel.context.active(), carrier, otel.defaultTextMapSetter);
44+
return { ...headers, [TRACE_HEADER]: await defaultDataConverter.toPayload(carrier) };
3345
}
3446

3547
/**
3648
* Link a span to an maybe-existing span context
3749
*/
38-
export function linkSpans(fromSpan: Span, toContext?: SpanContext): void {
50+
export function linkSpans(fromSpan: otel.Span, toContext?: otel.SpanContext): void {
3951
if (toContext !== undefined) {
4052
// TODO: I have to go around typescript because otel api 😢
4153
// See https://github.com/open-telemetry/opentelemetry-js-api/issues/124

packages/interceptors-opentelemetry/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
"license": "MIT",
1616
"dependencies": {
1717
"@opentelemetry/api": "^1.0.3",
18-
"@opentelemetry/exporter-jaeger": "^0.25.0",
19-
"@opentelemetry/sdk-node": "^0.25.0",
18+
"@opentelemetry/core": "^1.0.0",
19+
"@opentelemetry/resources": "^1.0.0",
20+
"@opentelemetry/sdk-trace-base": "^1.0.0",
2021
"@temporalio/client": "file:../client",
2122
"@temporalio/common": "file:../common",
2223
"@temporalio/worker": "file:../worker",

packages/interceptors-opentelemetry/src/client/index.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
WorkflowClientCallsInterceptor,
77
WorkflowStartInput,
88
} from '@temporalio/client';
9-
import { SpanName } from '../workflow';
10-
import { headersWithSpanContext, RUN_ID_ATTR_KEY } from '@temporalio/common/lib/otel';
9+
import { SpanName, SPAN_DELIMITER } from '../workflow';
10+
import { instrument } from '../instrumentation';
11+
import { headersWithContext, RUN_ID_ATTR_KEY } from '@temporalio/common/lib/otel';
1112

1213
export interface InterceptorOptions {
1314
readonly tracer?: otel.Tracer;
@@ -25,21 +26,19 @@ export class OpenTelemetryWorkflowClientCallsInterceptor implements WorkflowClie
2526

2627
constructor(options?: InterceptorOptions) {
2728
this.dataConverter = options?.dataConverter ?? defaultDataConverter;
28-
this.tracer = options?.tracer ?? otel.trace.getTracer('client');
29+
this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-client');
2930
}
3031

3132
async start(input: WorkflowStartInput, next: Next<WorkflowClientCallsInterceptor, 'start'>): Promise<string> {
32-
const span = this.tracer.startSpan(SpanName.WORKFLOW_SCHEDULE);
33-
const headers = await headersWithSpanContext(span.spanContext(), input.headers);
34-
try {
35-
const res = await next({ ...input, headers });
36-
span.setAttribute(RUN_ID_ATTR_KEY, res);
37-
return res;
38-
} catch (error: any) {
39-
span.recordException(error);
40-
throw error;
41-
} finally {
42-
span.end();
43-
}
33+
return await instrument(
34+
this.tracer,
35+
`${SpanName.WORKFLOW_START}${SPAN_DELIMITER}${input.workflowType}`,
36+
async (span) => {
37+
const headers = await headersWithContext(input.headers);
38+
const runId = await next({ ...input, headers });
39+
span.setAttribute(RUN_ID_ATTR_KEY, runId);
40+
return runId;
41+
}
42+
);
4443
}
4544
}

packages/interceptors-opentelemetry/src/instrumentation.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,34 @@
33
* @module
44
*/
55
import * as otel from '@opentelemetry/api';
6-
import { errorMessage } from '@temporalio/common';
76

8-
/**
9-
* Wraps `fn` in a span which ends when function returns or throws
10-
*/
11-
export async function instrument<T>(
12-
tracer: otel.Tracer,
13-
name: string,
14-
fn: (span: otel.Span) => Promise<T>
15-
): Promise<T> {
16-
const parentContext = otel.context.active();
17-
const span = tracer.startSpan(name, undefined);
18-
const contextWithSpanSet = otel.trace.setSpan(parentContext, span);
19-
20-
return otel.context.with(contextWithSpanSet, async () => {
21-
try {
22-
const ret = await fn(span);
23-
span.setStatus({ code: otel.SpanStatusCode.OK });
24-
return ret;
25-
} catch (err) {
26-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: errorMessage(err) });
27-
throw err;
28-
} finally {
29-
span.end();
30-
}
31-
});
7+
async function wrapWithSpan<T>(span: otel.Span, fn: (span: otel.Span) => Promise<T>): Promise<T> {
8+
try {
9+
const ret = await fn(span);
10+
span.setStatus({ code: otel.SpanStatusCode.OK });
11+
return ret;
12+
} catch (err: any) {
13+
span.setStatus({ code: otel.SpanStatusCode.ERROR });
14+
span.recordException(err);
15+
throw err;
16+
} finally {
17+
span.end();
18+
}
3219
}
3320

3421
/**
35-
* Instrument `fn` and set parent span context
22+
* Wraps `fn` in a span which ends when function returns or throws
3623
*/
37-
export async function instrumentFromSpanContext<T>(
24+
export async function instrument<T>(
3825
tracer: otel.Tracer,
39-
parent: otel.SpanContext,
4026
name: string,
41-
fn: (span: otel.Span) => Promise<T>
27+
fn: (span: otel.Span) => Promise<T>,
28+
context?: otel.Context
4229
): Promise<T> {
43-
const context = otel.trace.setSpanContext(otel.context.active(), parent);
44-
return otel.context.with(context, async () => {
45-
return instrument(tracer, name, fn);
46-
});
30+
if (context) {
31+
return await otel.context.with(context, async () => {
32+
return await tracer.startActiveSpan(name, async (span) => await wrapWithSpan(span, fn));
33+
});
34+
}
35+
return await tracer.startActiveSpan(name, async (span) => await wrapWithSpan(span, fn));
4736
}

packages/interceptors-opentelemetry/src/worker/index.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as otel from '@opentelemetry/api';
22
import { Resource } from '@opentelemetry/resources';
33
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4+
import { Context as ActivityContext } from '@temporalio/activity';
5+
import { extractContextFromHeaders } from '@temporalio/common/lib/otel';
46
import {
57
ActivityExecuteInput,
68
ActivityInboundCallsInterceptor,
@@ -9,9 +11,8 @@ import {
911
InjectedDependency,
1012
Next,
1113
} from '@temporalio/worker';
12-
import { OpenTelemetryWorkflowExporter, SerializableSpan, SpanName } from '../workflow';
13-
import { instrumentFromSpanContext } from '../instrumentation';
14-
import { extractSpanContextFromHeaders } from '@temporalio/common/lib/otel';
14+
import { OpenTelemetryWorkflowExporter, SerializableSpan, SpanName, SPAN_DELIMITER } from '../workflow';
15+
import { instrument } from '../instrumentation';
1516

1617
export interface InterceptorOptions {
1718
readonly tracer?: otel.Tracer;
@@ -28,17 +29,15 @@ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundC
2829
protected readonly tracer: otel.Tracer;
2930
protected readonly dataConverter: DataConverter;
3031

31-
constructor(options?: InterceptorOptions) {
32+
constructor(protected readonly ctx: ActivityContext, options?: InterceptorOptions) {
3233
this.dataConverter = options?.dataConverter ?? defaultDataConverter;
33-
this.tracer = options?.tracer ?? otel.trace.getTracer('activity');
34+
this.tracer = options?.tracer ?? otel.trace.getTracer('@temporalio/interceptor-activity');
3435
}
3536

3637
async execute(input: ActivityExecuteInput, next: Next<ActivityInboundCallsInterceptor, 'execute'>): Promise<unknown> {
37-
const spanContext = await extractSpanContextFromHeaders(input.headers);
38-
if (spanContext === undefined) {
39-
return await next(input);
40-
}
41-
return await instrumentFromSpanContext(this.tracer, spanContext, SpanName.ACTIVITY_EXECUTE, () => next(input));
38+
const context = await extractContextFromHeaders(input.headers);
39+
const spanName = `${SpanName.ACTIVITY_EXECUTE}${SPAN_DELIMITER}${this.ctx.info.activityType}`;
40+
return await instrument(this.tracer, spanName, () => next(input), context);
4241
}
4342
}
4443

@@ -47,7 +46,7 @@ export class OpenTelemetryActivityInboundInterceptor implements ActivityInboundC
4746
*/
4847
export function makeWorkflowExporter(
4948
exporter: SpanExporter,
50-
resource?: Resource
49+
resource: Resource
5150
): InjectedDependency<OpenTelemetryWorkflowExporter> {
5251
return {
5352
export: {
@@ -67,13 +66,13 @@ export function makeWorkflowExporter(
6766
/**
6867
* Deserialize a serialized span created by the Workflow isolate
6968
*/
70-
function extractReadableSpan(serializable: SerializableSpan, resource?: Resource): ReadableSpan {
69+
function extractReadableSpan(serializable: SerializableSpan, resource: Resource): ReadableSpan {
7170
const { spanContext, ...rest } = serializable;
7271
return {
7372
spanContext() {
7473
return spanContext;
7574
},
76-
resource: resource ?? Resource.EMPTY,
75+
resource,
7776
...rest,
7877
};
7978
}

packages/interceptors-opentelemetry/src/workflow/interfaces.ts renamed to packages/interceptors-opentelemetry/src/workflow/definitions.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,33 @@ export enum SpanName {
3838
/**
3939
* Workflow is scheduled by a client
4040
*/
41-
WORKFLOW_SCHEDULE = 'workflow.schedule',
41+
WORKFLOW_START = 'StartWorkflow',
42+
43+
/**
44+
* Workflow is client calls signalWithStart
45+
*/
46+
WORKFLOW_SIGNAL_WITH_START = 'SignalWithStartWorkflow',
47+
4248
/**
4349
* Workflow run is executing
4450
*/
45-
WORKFLOW_EXECUTE = 'workflow.execute',
51+
WORKFLOW_EXECUTE = 'RunWorkflow',
52+
/**
53+
* Child Workflow is started (by parent Workflow)
54+
*/
55+
CHILD_WORKFLOW_START = 'StartChildWorkflow',
4656
/**
4757
* Activity is scheduled by a Workflow
4858
*/
49-
ACTIVITY_SCHEUDLE = 'activity.schedule',
59+
ACTIVITY_START = 'StartActivity',
5060
/**
5161
* Activity is executing
5262
*/
53-
ACTIVITY_EXECUTE = 'activity.execute',
63+
ACTIVITY_EXECUTE = 'RunActivity',
64+
/**
65+
* Workflow is continuing as new
66+
*/
67+
CONTINUE_AS_NEW = 'ContinueAsNew',
5468
}
69+
70+
export const SPAN_DELIMITER = ':';

0 commit comments

Comments
 (0)