Skip to content

Commit 452607a

Browse files
committed
ref: Refactor span map handling
1 parent aa5f56b commit 452607a

File tree

8 files changed

+125
-284
lines changed

8 files changed

+125
-284
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({ strictSpanParentHandling: true }));
32+
provider.addSpanProcessor(new SentrySpanProcessor());
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/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: 9 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,46 +10,14 @@ 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-
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-
31-
export const SENTRY_SPAN_PROCESSOR_MAP: Map<string, SentrySpan> = new Map<string, SentrySpan>();
32-
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[]>();
36-
37-
/** Get a Sentry span for an otel span ID. */
38-
export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
39-
return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
40-
}
13+
import { clearSpan, getSentrySpan, setSentrySpan } from './utils/spanMap';
4114

4215
/**
4316
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
4417
* the Sentry SDK.
4518
*/
4619
export class SentrySpanProcessor implements OtelSpanProcessor {
47-
private _strictSpanParentHandling: boolean;
48-
49-
public constructor({ strictSpanParentHandling }: Partial<SpanProcessorOptions> = {}) {
50-
// Default to false
51-
this._strictSpanParentHandling = !!strictSpanParentHandling;
52-
20+
public constructor() {
5321
addTracingExtensions();
5422

5523
addGlobalEventProcessor(event => {
@@ -83,7 +51,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
8351

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

8856
if (sentryParentSpan) {
8957
const sentryChildSpan = sentryParentSpan.startChild({
@@ -93,7 +61,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
9361
spanId: otelSpanId,
9462
});
9563

96-
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
64+
setSentrySpan(otelSpanId, sentryChildSpan);
9765
} else {
9866
const traceCtx = getTraceData(otelSpan, parentContext);
9967
const transaction = getCurrentHub().startTransaction({
@@ -104,11 +72,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
10472
spanId: otelSpanId,
10573
});
10674

107-
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction);
108-
109-
if (this._strictSpanParentHandling) {
110-
SCHEDULE_TO_CLEAR.set(transaction.spanId, []);
111-
}
75+
setSentrySpan(otelSpanId, transaction);
11276
}
11377
}
11478

@@ -122,7 +86,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
12286
if (!sentrySpan) {
12387
__DEBUG_BUILD__ &&
12488
logger.error(`SentrySpanProcessor could not find span with OTEL-spanId ${otelSpanId} to finish.`);
125-
this._clearSpan(otelSpanId);
89+
clearSpan(otelSpanId);
12690
return;
12791
}
12892

@@ -131,7 +95,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
13195
// leading to an infinite loop.
13296
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
13397
if (isSentryRequestSpan(otelSpan)) {
134-
this._clearSpan(otelSpanId);
98+
clearSpan(otelSpanId);
13599
return;
136100
}
137101

@@ -141,7 +105,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
141105
client && client.emit && client?.emit('otelSpanEnd', otelSpan, mutableOptions);
142106

143107
if (mutableOptions.drop) {
144-
this._clearSpan(otelSpanId);
108+
clearSpan(otelSpanId);
145109
return;
146110
}
147111

@@ -194,7 +158,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
194158

195159
sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime));
196160

197-
this._clearSpan(otelSpanId);
161+
clearSpan(otelSpanId);
198162
}
199163

200164
/**
@@ -214,17 +178,6 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
214178
}
215179
return Promise.resolve();
216180
}
217-
218-
/**
219-
* Clear all references for a given OTEL span.
220-
*/
221-
private _clearSpan(otelSpanId: string): void {
222-
if (this._strictSpanParentHandling) {
223-
scheduleToClear(otelSpanId);
224-
} else {
225-
clearSpan(otelSpanId);
226-
}
227-
}
228181
}
229182

230183
function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
@@ -300,50 +253,3 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
300253
function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number {
301254
return seconds + nano / 1_000_000_000;
302255
}
303-
304-
function scheduleToClear(otelSpanId: string): void {
305-
const span = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
306-
307-
if (!span) {
308-
// hmm, something is fishy here, but abort...
309-
// But to be sure we still try to delete the SCHEDULE_TO_CLEAR, to avoid leaks
310-
SCHEDULE_TO_CLEAR.delete(otelSpanId);
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(rootSentrySpanId);
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-
342-
function spanIsRoot(span: SentrySpan): span is Transaction {
343-
return span.transaction === span;
344-
}
345-
346-
// make sure to remove references in maps, to ensure this can be GCed
347-
function clearSpan(otelSpanId: string): void {
348-
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
349-
}

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)