Skip to content

Commit 34985ce

Browse files
committed
feat(node-otel): Implement new strict parent handling mode
1 parent 9e39717 commit 34985ce

File tree

3 files changed

+349
-12
lines changed

3 files changed

+349
-12
lines changed

packages/node-experimental/src/sdk/initOtel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function initOtel(): () => void {
2929
const provider = new NodeTracerProvider({
3030
sampler: new AlwaysOnSampler(),
3131
});
32-
provider.addSpanProcessor(new SentrySpanProcessor());
32+
provider.addSpanProcessor(new SentrySpanProcessor({ strictSpanParentHandling: true }));
3333

3434
// We use a custom context manager to keep context in sync with sentry scope
3535
const contextManager = new SentryContextManager();

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,28 @@ import { isSentryRequestSpan } from './utils/isSentryRequest';
1111
import { mapOtelStatus } from './utils/mapOtelStatus';
1212
import { parseSpanDescription } from './utils/parseOtelSpanDescription';
1313

14+
interface SpanProcessorOptions {
15+
/**
16+
* By default, if a span is started and we cannot find a Sentry parent span for it,
17+
* even if the OTEL span has a parent reference, we will still create the Sentry span as a root span.
18+
*
19+
* While this is more tolerant of errors, it means that the generated Spans in Sentry may have an incorrect hierarchy.
20+
*
21+
* When opting into strict span parent handling, we will discard any Spans where we can't find the corresponding parent.
22+
* This also requires that we defer clearing of references to the point where the root span is finished -
23+
* as sometimes these are not fired in correct order, leading to spans being dropped.
24+
*
25+
* Note that enabling this is the more correct option
26+
* and will probably eventually become the default in a future version.
27+
*/
28+
strictSpanParentHandling: boolean;
29+
}
30+
1431
export const SENTRY_SPAN_PROCESSOR_MAP: Map<string, SentrySpan> = new Map<string, SentrySpan>();
1532

16-
// make sure to remove references in maps, to ensure this can be GCed
17-
function clearSpan(otelSpanId: string): void {
18-
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
19-
}
33+
// A map of a sentry span ID to a list of otel span IDs
34+
// When the sentry span is finished, clear all references of the given otel spans
35+
export const SCHEDULE_TO_CLEAR: Map<string, string[]> = new Map<string, string[]>();
2036

2137
/** Get a Sentry span for an otel span ID. */
2238
export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
@@ -28,7 +44,12 @@ export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
2844
* the Sentry SDK.
2945
*/
3046
export class SentrySpanProcessor implements OtelSpanProcessor {
31-
public constructor() {
47+
private _strictSpanParentHandling: boolean;
48+
49+
public constructor({ strictSpanParentHandling }: Partial<SpanProcessorOptions> = {}) {
50+
// Default to false
51+
this._strictSpanParentHandling = !!strictSpanParentHandling;
52+
3253
addTracingExtensions();
3354

3455
addGlobalEventProcessor(event => {
@@ -64,6 +85,13 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
6485
// so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
6586
const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP.get(otelParentSpanId);
6687

88+
if (this._strictSpanParentHandling && !!otelParentSpanId && !sentryParentSpan) {
89+
logger.warn(
90+
`SentrySpanProcessor could not find parent span with OTEL-spanId ${otelParentSpanId}. Dropping the span with OTEL-spanID ${otelSpanId}...`,
91+
);
92+
return;
93+
}
94+
6795
if (sentryParentSpan) {
6896
const sentryChildSpan = sentryParentSpan.startChild({
6997
description: otelSpan.name,
@@ -105,7 +133,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
105133
// leading to an infinite loop.
106134
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
107135
if (isSentryRequestSpan(otelSpan)) {
108-
clearSpan(otelSpanId);
136+
this._clearSpan(otelSpanId);
109137
return;
110138
}
111139

@@ -115,7 +143,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
115143
client && client.emit && client?.emit('otelSpanEnd', otelSpan, mutableOptions);
116144

117145
if (mutableOptions.drop) {
118-
clearSpan(otelSpanId);
146+
this._clearSpan(otelSpanId);
119147
return;
120148
}
121149

@@ -168,7 +196,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
168196

169197
sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime));
170198

171-
clearSpan(otelSpanId);
199+
this._clearSpan(otelSpanId);
172200
}
173201

174202
/**
@@ -188,6 +216,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
188216
}
189217
return Promise.resolve();
190218
}
219+
220+
/**
221+
* Clear all references for a given OTEL span.
222+
*/
223+
private _clearSpan(otelSpanId: string): void {
224+
if (this._strictSpanParentHandling) {
225+
scheduleToClear(otelSpanId);
226+
} else {
227+
clearSpan(otelSpanId);
228+
}
229+
}
191230
}
192231

193232
function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
@@ -263,3 +302,52 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
263302
function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number {
264303
return seconds + nano / 1_000_000_000;
265304
}
305+
306+
function scheduleToClear(otelSpanId: string): void {
307+
const span = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
308+
309+
if (!span) {
310+
// hmm, something is fishy here, but abort...
311+
return;
312+
}
313+
314+
const sentrySpanId = span.spanId;
315+
316+
// This is the root, clear all that have been scheduled
317+
if (spanIsRoot(span) || !span.transaction) {
318+
const toClear = SCHEDULE_TO_CLEAR.get(sentrySpanId) || [];
319+
toClear.push(otelSpanId);
320+
321+
toClear.forEach(otelSpanIdToClear => clearSpan(otelSpanIdToClear));
322+
SCHEDULE_TO_CLEAR.delete(sentrySpanId);
323+
return;
324+
}
325+
326+
// Clear when root span is cleared
327+
const root = span.transaction;
328+
const rootSentrySpanId = root.spanId;
329+
330+
const toClear = SCHEDULE_TO_CLEAR.get(root.spanId);
331+
332+
// If this does not exist, it means we prob. already cleaned it up before
333+
// So we ignore the parent and just clean this span up right now
334+
if (!toClear) {
335+
clearSpan(otelSpanId);
336+
return;
337+
}
338+
339+
toClear.push(otelSpanId);
340+
341+
if (!SCHEDULE_TO_CLEAR.has(rootSentrySpanId)) {
342+
SCHEDULE_TO_CLEAR.set(rootSentrySpanId, toClear);
343+
}
344+
}
345+
346+
function spanIsRoot(span: SentrySpan): span is Transaction {
347+
return span.transaction === span;
348+
}
349+
350+
// make sure to remove references in maps, to ensure this can be GCed
351+
function clearSpan(otelSpanId: string): void {
352+
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
353+
}

0 commit comments

Comments
 (0)