Skip to content

Commit 727214b

Browse files
committed
feat(node-otel): Implement new strict parent handling mode
1 parent 5a42bd9 commit 727214b

File tree

3 files changed

+350
-12
lines changed

3 files changed

+350
-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: 99 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 "${otelSpan.name}" with OTEL-spanID ${otelSpanId}...`,
91+
);
92+
return;
93+
}
94+
6795
if (sentryParentSpan) {
6896
const sentryChildSpan = sentryParentSpan.startChild({
6997
description: otelSpan.name,
@@ -84,6 +112,10 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
84112
});
85113

86114
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction);
115+
116+
if (this._strictSpanParentHandling) {
117+
SCHEDULE_TO_CLEAR.set(transaction.spanId, []);
118+
}
87119
}
88120
}
89121

@@ -97,6 +129,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
97129
if (!sentrySpan) {
98130
__DEBUG_BUILD__ &&
99131
logger.error(`SentrySpanProcessor could not find span with OTEL-spanId ${otelSpanId} to finish.`);
132+
this._clearSpan(otelSpanId);
100133
return;
101134
}
102135

@@ -105,7 +138,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
105138
// leading to an infinite loop.
106139
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
107140
if (isSentryRequestSpan(otelSpan)) {
108-
clearSpan(otelSpanId);
141+
this._clearSpan(otelSpanId);
109142
return;
110143
}
111144

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

117150
if (mutableOptions.drop) {
118-
clearSpan(otelSpanId);
151+
this._clearSpan(otelSpanId);
119152
return;
120153
}
121154

@@ -168,7 +201,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
168201

169202
sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime));
170203

171-
clearSpan(otelSpanId);
204+
this._clearSpan(otelSpanId);
172205
}
173206

174207
/**
@@ -188,6 +221,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
188221
}
189222
return Promise.resolve();
190223
}
224+
225+
/**
226+
* Clear all references for a given OTEL span.
227+
*/
228+
private _clearSpan(otelSpanId: string): void {
229+
if (this._strictSpanParentHandling) {
230+
scheduleToClear(otelSpanId);
231+
} else {
232+
clearSpan(otelSpanId);
233+
}
234+
}
191235
}
192236

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

0 commit comments

Comments
 (0)