Skip to content

Commit 475c295

Browse files
authored
fix(node-otel): Refactor OTEL span reference cleanup (#9000)
This PR updates the span reference cleanup to take into account if a span is/may still be referenced somewhere else. Previously, whenever a span finished we removed the reference from the map, to clean up and avoid memory leaks. However, it seems that sometimes spans are ended before a child span is started (at least the hooks may fire in this order). This leads to the potential case where a parent that _should_ exist cannot be found, thus creating a new transaction instead of a span. With this change, we keep more information in our span map, in order to clear sub-spans (=not transactions) only when the root span (=transaction) is finished.
1 parent a7f5911 commit 475c295

File tree

7 files changed

+215
-35
lines changed

7 files changed

+215
-35
lines changed

packages/opentelemetry-node/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getSentrySpan } from './spanprocessor';
1+
import { getSentrySpan } from './utils/spanMap';
22

33
export { SentrySpanProcessor } from './spanprocessor';
44
export { SentryPropagator } from './propagator';

packages/opentelemetry-node/src/propagator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
SENTRY_TRACE_HEADER,
1414
SENTRY_TRACE_PARENT_CONTEXT_KEY,
1515
} from './constants';
16-
import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor';
16+
import { getSentrySpan } from './utils/spanMap';
1717

1818
/**
1919
* Injects and extracts `sentry-trace` and `baggage` headers from carriers.
@@ -30,7 +30,7 @@ export class SentryPropagator extends W3CBaggagePropagator {
3030

3131
let baggage = propagation.getBaggage(context) || propagation.createBaggage({});
3232

33-
const span = SENTRY_SPAN_PROCESSOR_MAP.get(spanContext.spanId);
33+
const span = getSentrySpan(spanContext.spanId);
3434
if (span) {
3535
setter.set(carrier, SENTRY_TRACE_HEADER, span.toTraceparent());
3636

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,7 @@ import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY }
1010
import { isSentryRequestSpan } from './utils/isSentryRequest';
1111
import { mapOtelStatus } from './utils/mapOtelStatus';
1212
import { parseSpanDescription } from './utils/parseOtelSpanDescription';
13-
14-
export const SENTRY_SPAN_PROCESSOR_MAP: Map<string, SentrySpan> = new Map<string, SentrySpan>();
15-
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-
}
20-
21-
/** Get a Sentry span for an otel span ID. */
22-
export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
23-
return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
24-
}
13+
import { clearSpan, getSentrySpan, setSentrySpan } from './utils/spanMap';
2514

2615
/**
2716
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
@@ -62,7 +51,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
6251

6352
// Otel supports having multiple non-nested spans at the same time
6453
// so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
65-
const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP.get(otelParentSpanId);
54+
const sentryParentSpan = otelParentSpanId && getSentrySpan(otelParentSpanId);
6655

6756
if (sentryParentSpan) {
6857
const sentryChildSpan = sentryParentSpan.startChild({
@@ -72,7 +61,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
7261
spanId: otelSpanId,
7362
});
7463

75-
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
64+
setSentrySpan(otelSpanId, sentryChildSpan);
7665
} else {
7766
const traceCtx = getTraceData(otelSpan, parentContext);
7867
const transaction = getCurrentHub().startTransaction({
@@ -83,7 +72,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
8372
spanId: otelSpanId,
8473
});
8574

86-
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction);
75+
setSentrySpan(otelSpanId, transaction);
8776
}
8877
}
8978

@@ -97,6 +86,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
9786
if (!sentrySpan) {
9887
__DEBUG_BUILD__ &&
9988
logger.error(`SentrySpanProcessor could not find span with OTEL-spanId ${otelSpanId} to finish.`);
89+
clearSpan(otelSpanId);
10090
return;
10191
}
10292

packages/opentelemetry-node/src/utils/spanData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Transaction } from '@sentry/core';
33
import type { Context, SpanOrigin } from '@sentry/types';
44

5-
import { getSentrySpan } from '../spanprocessor';
5+
import { getSentrySpan } from './spanMap';
66

77
type SentryTags = Record<string, string>;
88
type SentryData = Record<string, unknown>;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Span as SentrySpan } from '@sentry/types';
2+
3+
interface SpanMapEntry {
4+
sentrySpan: SentrySpan;
5+
ref: SpanRefType;
6+
// These are not direct children, but all spans under the tree of a root span.
7+
subSpans: string[];
8+
}
9+
10+
const SPAN_REF_ROOT = Symbol('root');
11+
const SPAN_REF_CHILD = Symbol('child');
12+
const SPAN_REF_CHILD_ENDED = Symbol('child_ended');
13+
type SpanRefType = typeof SPAN_REF_ROOT | typeof SPAN_REF_CHILD | typeof SPAN_REF_CHILD_ENDED;
14+
15+
/** Exported only for tests. */
16+
export const SPAN_MAP = new Map<string, SpanMapEntry>();
17+
18+
/**
19+
* Get a Sentry span for a given span ID.
20+
*/
21+
export function getSentrySpan(spanId: string): SentrySpan | undefined {
22+
const entry = SPAN_MAP.get(spanId);
23+
return entry ? entry.sentrySpan : undefined;
24+
}
25+
26+
/**
27+
* Set a Sentry span for a given span ID.
28+
* This is necessary so we can lookup parent spans later.
29+
* We also keep a list of children for root spans only, in order to be able to clean them up together.
30+
*/
31+
export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void {
32+
let ref: SpanRefType = SPAN_REF_ROOT;
33+
34+
const rootSpanId = sentrySpan.transaction?.spanId;
35+
36+
if (rootSpanId && rootSpanId !== spanId) {
37+
const root = SPAN_MAP.get(rootSpanId);
38+
if (root) {
39+
root.subSpans.push(spanId);
40+
ref = SPAN_REF_CHILD;
41+
}
42+
}
43+
44+
SPAN_MAP.set(spanId, {
45+
sentrySpan,
46+
ref,
47+
subSpans: [],
48+
});
49+
}
50+
51+
/**
52+
* Clear references of the given span ID.
53+
*/
54+
export function clearSpan(spanId: string): void {
55+
const entry = SPAN_MAP.get(spanId);
56+
if (!entry) {
57+
return;
58+
}
59+
60+
const { ref, subSpans } = entry;
61+
62+
// If this is a child, mark it as ended.
63+
if (ref === SPAN_REF_CHILD) {
64+
entry.ref = SPAN_REF_CHILD_ENDED;
65+
return;
66+
}
67+
68+
// If this is a root span, clear all (ended) children
69+
if (ref === SPAN_REF_ROOT) {
70+
for (const childId of subSpans) {
71+
const child = SPAN_MAP.get(childId);
72+
if (!child) {
73+
continue;
74+
}
75+
76+
if (child.ref === SPAN_REF_CHILD_ENDED) {
77+
// if the child has already ended, just clear it
78+
SPAN_MAP.delete(childId);
79+
} else if (child.ref === SPAN_REF_CHILD) {
80+
// If the child has not ended yet, mark it as a root span so it is cleared when it ends.
81+
child.ref = SPAN_REF_ROOT;
82+
}
83+
}
84+
85+
SPAN_MAP.delete(spanId);
86+
return;
87+
}
88+
89+
// Generally, `clearSpan` should never be called for ref === SPAN_REF_CHILD_ENDED
90+
// But if it does, just clear the span
91+
SPAN_MAP.delete(spanId);
92+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
SENTRY_TRACE_PARENT_CONTEXT_KEY,
1818
} from '../src/constants';
1919
import { SentryPropagator } from '../src/propagator';
20-
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';
20+
import { setSentrySpan, SPAN_MAP } from '../src/utils/spanMap';
2121

2222
beforeAll(() => {
2323
addTracingExtensions();
@@ -51,7 +51,7 @@ describe('SentryPropagator', () => {
5151
makeMain(hub);
5252

5353
afterEach(() => {
54-
SENTRY_SPAN_PROCESSOR_MAP.clear();
54+
SPAN_MAP.clear();
5555
});
5656

5757
enum PerfType {
@@ -61,12 +61,12 @@ describe('SentryPropagator', () => {
6161

6262
function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) {
6363
const transaction = new Transaction(transactionContext, hub);
64-
SENTRY_SPAN_PROCESSOR_MAP.set(transaction.spanId, transaction);
64+
setSentrySpan(transaction.spanId, transaction);
6565
if (type === PerfType.Span) {
6666
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6767
const { spanId, ...ctx } = transactionContext;
6868
const span = transaction.startChild({ ...ctx, description: transaction.name });
69-
SENTRY_SPAN_PROCESSOR_MAP.set(span.spanId, span);
69+
setSentrySpan(span.spanId, span);
7070
}
7171
}
7272

0 commit comments

Comments
 (0)