Skip to content

Commit d106a84

Browse files
authored
Don't set error status on otel spans for benign exceptions (#1786)
1 parent 770ef97 commit d106a84

File tree

5 files changed

+66
-4
lines changed

5 files changed

+66
-4
lines changed

packages/interceptors-opentelemetry/src/instrumentation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @module
44
*/
55
import * as otel from '@opentelemetry/api';
6-
import { Headers, defaultPayloadConverter } from '@temporalio/common';
6+
import { ApplicationFailure, ApplicationFailureCategory, Headers, defaultPayloadConverter } from '@temporalio/common';
77

88
/** Default trace header for opentelemetry interceptors */
99
export const TRACE_HEADER = '_tracer-data';
@@ -43,8 +43,10 @@ async function wrapWithSpan<T>(
4343
span.setStatus({ code: otel.SpanStatusCode.OK });
4444
return ret;
4545
} catch (err: any) {
46+
const isBenignErr = err instanceof ApplicationFailure && err.category === ApplicationFailureCategory.BENIGN;
4647
if (acceptableErrors === undefined || !acceptableErrors(err)) {
47-
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
48+
const statusCode = isBenignErr ? otel.SpanStatusCode.UNSET : otel.SpanStatusCode.ERROR;
49+
span.setStatus({ code: statusCode, message: (err as Error).message ?? String(err) });
4850
span.recordException(err);
4951
} else {
5052
span.setStatus({ code: otel.SpanStatusCode.OK });

packages/test/src/activities/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
22
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3-
import { Context } from '@temporalio/activity';
4-
import { ApplicationFailure } from '@temporalio/common';
3+
import { activityInfo, Context } from '@temporalio/activity';
4+
import { ApplicationFailure, ApplicationFailureCategory } from '@temporalio/common';
55
import { ProtoActivityInput, ProtoActivityResult } from '../../protos/root';
66
import { cancellableFetch as cancellableFetchInner } from './cancellable-fetch';
77
import { fakeProgress as fakeProgressInner } from './fake-progress';
@@ -93,3 +93,12 @@ export async function progressiveSleep(): Promise<void> {
9393
export async function protoActivity(args: ProtoActivityInput): Promise<ProtoActivityResult> {
9494
return ProtoActivityResult.create({ sentence: `${args.name} is ${args.age} years old.` });
9595
}
96+
97+
export async function throwMaybeBenign(): Promise<void> {
98+
if (activityInfo().attempt === 1) {
99+
throw ApplicationFailure.create({ message: 'not benign' });
100+
}
101+
if (activityInfo().attempt === 2) {
102+
throw ApplicationFailure.create({ message: 'benign', category: ApplicationFailureCategory.BENIGN });
103+
}
104+
}

packages/test/src/test-otel.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,43 @@ if (RUN_INTEGRATION_TESTS) {
470470
const exceptionEvents = span.events.filter((event) => event.name === 'exception');
471471
t.is(exceptionEvents.length, 1);
472472
});
473+
474+
test('Otel workflow omits ApplicationError with BENIGN category', async (t) => {
475+
const memoryExporter = new InMemorySpanExporter();
476+
const provider = new BasicTracerProvider();
477+
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
478+
provider.register();
479+
const tracer = provider.getTracer('test-error-tracer');
480+
481+
const worker = await Worker.create({
482+
workflowsPath: require.resolve('./workflows'),
483+
activities,
484+
taskQueue: 'test-otel-benign-err',
485+
interceptors: {
486+
activity: [
487+
(ctx) => {
488+
return { inbound: new OpenTelemetryActivityInboundInterceptor(ctx, { tracer }) };
489+
},
490+
],
491+
},
492+
});
493+
494+
const client = new WorkflowClient();
495+
496+
await worker.runUntil(
497+
client.execute(workflows.throwMaybeBenignErr, {
498+
taskQueue: 'test-otel-benign-err',
499+
workflowId: uuid4(),
500+
retry: { maximumAttempts: 3 },
501+
})
502+
);
503+
504+
const spans = memoryExporter.getFinishedSpans();
505+
t.is(spans.length, 3);
506+
t.is(spans[0].status.code, SpanStatusCode.ERROR);
507+
t.is(spans[0].status.message, 'not benign');
508+
t.is(spans[1].status.code, SpanStatusCode.UNSET);
509+
t.is(spans[1].status.message, 'benign');
510+
t.is(spans[2].status.code, SpanStatusCode.OK);
511+
});
473512
}

packages/test/src/workflows/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export * from './swc';
7979
export * from './tasks-and-microtasks';
8080
export * from './text-encoder-decoder';
8181
export * from './throw-async';
82+
export * from './throw-maybe-benign';
8283
export * from './trailing-timer';
8384
export * from './try-to-continue-after-completion';
8485
export * from './two-strings';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as workflow from '@temporalio/workflow';
2+
import * as activities from '../activities';
3+
4+
const { throwMaybeBenign } = workflow.proxyActivities<typeof activities>({
5+
startToCloseTimeout: '5s',
6+
retry: { maximumAttempts: 3, backoffCoefficient: 1, initialInterval: 500 },
7+
});
8+
9+
export async function throwMaybeBenignErr(): Promise<void> {
10+
await throwMaybeBenign();
11+
}

0 commit comments

Comments
 (0)