Skip to content

Commit

Permalink
feat(node-otel): Implement new strict parent handling mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Sep 11, 2023
1 parent f63036b commit 3d53051
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function initOtel(): () => void {
const provider = new NodeTracerProvider({
sampler: new AlwaysOnSampler(),
});
provider.addSpanProcessor(new SentrySpanProcessor());
provider.addSpanProcessor(new SentrySpanProcessor({ strictSpanParentHandling: true }));

// We use a custom context manager to keep context in sync with sentry scope
const contextManager = new SentryContextManager();
Expand Down
104 changes: 96 additions & 8 deletions packages/opentelemetry-node/src/spanprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,28 @@ import { isSentryRequestSpan } from './utils/isSentryRequest';
import { mapOtelStatus } from './utils/mapOtelStatus';
import { parseSpanDescription } from './utils/parseOtelSpanDescription';

interface SpanProcessorOptions {
/**
* By default, if a span is started and we cannot find a Sentry parent span for it,
* even if the OTEL span has a parent reference, we will still create the Sentry span as a root span.
*
* While this is more tolerant of errors, it means that the generated Spans in Sentry may have an incorrect hierarchy.
*
* When opting into strict span parent handling, we will discard any Spans where we can't find the corresponding parent.
* This also requires that we defer clearing of references to the point where the root span is finished -
* as sometimes these are not fired in correct order, leading to spans being dropped.
*
* Note that enabling this is the more correct option
* and will probably eventually become the default in a future version.
*/
strictSpanParentHandling: boolean;
}

export const SENTRY_SPAN_PROCESSOR_MAP: Map<string, SentrySpan> = new Map<string, SentrySpan>();

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

/** Get a Sentry span for an otel span ID. */
export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
Expand All @@ -28,7 +44,12 @@ export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
* the Sentry SDK.
*/
export class SentrySpanProcessor implements OtelSpanProcessor {
public constructor() {
private _strictSpanParentHandling: boolean;

public constructor({ strictSpanParentHandling }: Partial<SpanProcessorOptions> = {}) {
// Default to false
this._strictSpanParentHandling = !!strictSpanParentHandling;

addTracingExtensions();

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

if (this._strictSpanParentHandling && !!otelParentSpanId && !sentryParentSpan) {
logger.warn(
`SentrySpanProcessor could not find parent span with OTEL-spanId ${otelParentSpanId}. Dropping the span with OTEL-spanID ${otelSpanId}...`,
);
return;
}

if (sentryParentSpan) {
const sentryChildSpan = sentryParentSpan.startChild({
description: otelSpan.name,
Expand Down Expand Up @@ -105,7 +133,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
// leading to an infinite loop.
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
if (isSentryRequestSpan(otelSpan)) {
clearSpan(otelSpanId);
this._clearSpan(otelSpanId);
return;
}

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

if (mutableOptions.drop) {
clearSpan(otelSpanId);
this._clearSpan(otelSpanId);
return;
}

Expand Down Expand Up @@ -168,7 +196,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {

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

clearSpan(otelSpanId);
this._clearSpan(otelSpanId);
}

/**
Expand All @@ -188,6 +216,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
}
return Promise.resolve();
}

/**
* Clear all references for a given OTEL span.
*/
private _clearSpan(otelSpanId: string): void {
if (this._strictSpanParentHandling) {
scheduleToClear(otelSpanId);
} else {
clearSpan(otelSpanId);
}
}
}

function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
Expand Down Expand Up @@ -263,3 +302,52 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number {
return seconds + nano / 1_000_000_000;
}

function scheduleToClear(otelSpanId: string): void {
const span = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);

if (!span) {
// hmm, something is fishy here, but abort...
return;
}

const sentrySpanId = span.spanId;

// This is the root, clear all that have been scheduled
if (spanIsRoot(span) || !span.transaction) {
const toClear = SCHEDULE_TO_CLEAR.get(sentrySpanId) || [];
toClear.push(otelSpanId);

toClear.forEach(otelSpanIdToClear => clearSpan(otelSpanIdToClear));
SCHEDULE_TO_CLEAR.delete(sentrySpanId);
return;
}

// Clear when root span is cleared
const root = span.transaction;
const rootSentrySpanId = root.spanId;

const toClear = SCHEDULE_TO_CLEAR.get(root.spanId);

// If this does not exist, it means we prob. already cleaned it up before
// So we ignore the parent and just clean this span up right now
if (!toClear) {
clearSpan(otelSpanId);
return;
}

toClear.push(otelSpanId);

if (!SCHEDULE_TO_CLEAR.has(rootSentrySpanId)) {
SCHEDULE_TO_CLEAR.set(rootSentrySpanId, toClear);
}
}

function spanIsRoot(span: SentrySpan): span is Transaction {
return span.transaction === span;
}

// make sure to remove references in maps, to ensure this can be GCed
function clearSpan(otelSpanId: string): void {
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
}
Loading

0 comments on commit 3d53051

Please sign in to comment.