Skip to content

Commit a993786

Browse files
authored
feat(otel): Convert exception otel events to sentry errors (#7165)
1 parent 8a29725 commit a993786

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Context } from '@opentelemetry/api';
22
import { trace } from '@opentelemetry/api';
33
import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
4+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
45
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
56
import { Transaction } from '@sentry/tracing';
67
import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
7-
import { logger } from '@sentry/utils';
8+
import { isString, logger } from '@sentry/utils';
89

910
import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants';
1011
import { isSentryRequestSpan } from './utils/is-sentry-request';
@@ -93,6 +94,12 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
9394
* @inheritDoc
9495
*/
9596
public onEnd(otelSpan: OtelSpan): void {
97+
const hub = getCurrentHub();
98+
if (!hub) {
99+
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onEnd before a hub has been setup.');
100+
return;
101+
}
102+
96103
const otelSpanId = otelSpan.spanContext().spanId;
97104
const sentrySpan = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
98105

@@ -112,6 +119,46 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
112119
return;
113120
}
114121

122+
otelSpan.events.forEach(event => {
123+
if (event.name !== 'exception') {
124+
return;
125+
}
126+
127+
const attributes = event.attributes;
128+
if (!attributes) {
129+
return;
130+
}
131+
132+
const message = attributes[SemanticAttributes.EXCEPTION_MESSAGE];
133+
const syntheticError = new Error(message as string | undefined);
134+
135+
const stack = attributes[SemanticAttributes.EXCEPTION_STACKTRACE];
136+
if (isString(stack)) {
137+
syntheticError.stack = stack;
138+
}
139+
140+
const type = attributes[SemanticAttributes.EXCEPTION_TYPE];
141+
if (isString(type)) {
142+
syntheticError.name = type;
143+
}
144+
145+
hub.captureException(syntheticError, {
146+
captureContext: {
147+
contexts: {
148+
otel: {
149+
attributes: otelSpan.attributes,
150+
resource: otelSpan.resource.attributes,
151+
},
152+
trace: {
153+
trace_id: otelSpan.spanContext().traceId,
154+
span_id: otelSpan.spanContext().spanId,
155+
parent_span_id: otelSpan.parentSpanId,
156+
},
157+
},
158+
},
159+
});
160+
});
161+
115162
if (sentrySpan instanceof Transaction) {
116163
updateTransactionWithOtelData(sentrySpan, otelSpan);
117164
} else {

packages/opentelemetry-node/test/spanprocessor.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,46 @@ describe('SentrySpanProcessor', () => {
750750
trace_id: otelSpan.spanContext().traceId,
751751
});
752752
});
753+
754+
it('generates Sentry errors from opentelemetry span exception events', () => {
755+
let sentryEvent: any;
756+
let otelSpan: any;
757+
758+
client = new NodeClient({
759+
...DEFAULT_NODE_CLIENT_OPTIONS,
760+
beforeSend: event => {
761+
sentryEvent = event;
762+
return null;
763+
},
764+
});
765+
hub = new Hub(client);
766+
makeMain(hub);
767+
768+
const tracer = provider.getTracer('default');
769+
770+
tracer.startActiveSpan('GET /users', parentOtelSpan => {
771+
tracer.startActiveSpan('SELECT * FROM users;', child => {
772+
child.recordException(new Error('this is an otel error!'));
773+
otelSpan = child as OtelSpan;
774+
child.end();
775+
});
776+
777+
parentOtelSpan.end();
778+
});
779+
780+
expect(sentryEvent).toBeDefined();
781+
expect(sentryEvent.exception).toBeDefined();
782+
expect(sentryEvent.exception.values[0]).toEqual({
783+
mechanism: expect.any(Object),
784+
type: 'Error',
785+
value: 'this is an otel error!',
786+
});
787+
expect(sentryEvent.contexts.trace).toEqual({
788+
parent_span_id: otelSpan.parentSpanId,
789+
span_id: otelSpan.spanContext().spanId,
790+
trace_id: otelSpan.spanContext().traceId,
791+
});
792+
});
753793
});
754794

755795
// OTEL expects a custom date format

0 commit comments

Comments
 (0)