Skip to content

Commit

Permalink
feat(otel): Add inject functionality to SentryPropagator (#6114)
Browse files Browse the repository at this point in the history
Inject `sentry-trace` and `baggage` headers into context so it's gets propagated on outgoing requests
  • Loading branch information
AbhiPrasad committed Nov 2, 2022
1 parent e00de10 commit f36c268
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 15 deletions.
34 changes: 31 additions & 3 deletions packages/opentelemetry-node/src/propagator.ts
@@ -1,6 +1,17 @@
import { Context, TextMapGetter, TextMapPropagator, TextMapSetter } from '@opentelemetry/api';
import {
Context,
isSpanContextValid,
TextMapGetter,
TextMapPropagator,
TextMapSetter,
trace,
TraceFlags,
} from '@opentelemetry/api';
import { isTracingSuppressed } from '@opentelemetry/core';
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';

import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants';
import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor';

/**
* Injects and extracts `sentry-trace` and `baggage` headers from carriers.
Expand All @@ -9,8 +20,25 @@ export class SentryPropagator implements TextMapPropagator {
/**
* @inheritDoc
*/
public inject(_context: Context, _carrier: unknown, _setter: TextMapSetter): void {
// no-op
public inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
const spanContext = trace.getSpanContext(context);
if (!spanContext || !isSpanContextValid(spanContext) || isTracingSuppressed(context)) {
return;
}

// eslint-disable-next-line no-bitwise
const samplingDecision = spanContext.traceFlags & TraceFlags.SAMPLED ? 1 : 0;
const traceparent = `${spanContext.traceId}-${spanContext.spanId}-${samplingDecision}`;
setter.set(carrier, SENTRY_TRACE_HEADER, traceparent);

const span = SENTRY_SPAN_PROCESSOR_MAP.get(spanContext.spanId);
if (span && span.transaction) {
const dynamicSamplingContext = span.transaction.getDynamicSamplingContext();
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
if (sentryBaggageHeader) {
setter.set(carrier, SENTRY_BAGGAGE_HEADER, sentryBaggageHeader);
}
}
}

/**
Expand Down
18 changes: 10 additions & 8 deletions packages/opentelemetry-node/src/spanprocessor.ts
Expand Up @@ -8,14 +8,16 @@ import { logger } from '@sentry/utils';
import { mapOtelStatus } from './utils/map-otel-status';
import { parseSpanDescription } from './utils/parse-otel-span-description';

export const SENTRY_SPAN_PROCESSOR_MAP: Map<SentrySpan['spanId'], SentrySpan> = new Map<
SentrySpan['spanId'],
SentrySpan
>();

/**
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
* the Sentry SDK.
*/
export class SentrySpanProcessor implements OtelSpanProcessor {
// public only for testing
public readonly _map: Map<SentrySpan['spanId'], SentrySpan> = new Map<SentrySpan['spanId'], SentrySpan>();

/**
* @inheritDoc
*/
Expand All @@ -39,7 +41,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {

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

if (sentryParentSpan) {
const sentryChildSpan = sentryParentSpan.startChild({
Expand All @@ -49,7 +51,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
spanId: otelSpanId,
});

this._map.set(otelSpanId, sentryChildSpan);
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
} else {
const traceCtx = getTraceData(otelSpan);
const transaction = hub.startTransaction({
Expand All @@ -60,7 +62,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
spanId: otelSpanId,
});

this._map.set(otelSpanId, transaction);
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction);
}
}

Expand All @@ -69,7 +71,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
*/
public onEnd(otelSpan: OtelSpan): void {
const otelSpanId = otelSpan.spanContext().spanId;
const sentrySpan = this._map.get(otelSpanId);
const sentrySpan = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);

if (!sentrySpan) {
__DEBUG_BUILD__ &&
Expand All @@ -85,7 +87,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime));
}

this._map.delete(otelSpanId);
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
}

/**
Expand Down
201 changes: 199 additions & 2 deletions packages/opentelemetry-node/test/propagator.test.ts
@@ -1,10 +1,207 @@
import { defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api';
import { suppressTracing } from '@opentelemetry/core';
import { Hub, makeMain } from '@sentry/core';
import { addExtensionMethods, Transaction } from '@sentry/tracing';
import { TransactionContext } from '@sentry/types';

import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants';
import { SentryPropagator } from '../src/propagator';
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';

beforeAll(() => {
addExtensionMethods();
});

describe('SentryPropagator', () => {
const propogator = new SentryPropagator();
const propagator = new SentryPropagator();
let carrier: { [key: string]: unknown };

beforeEach(() => {
carrier = {};
});

it('returns fields set', () => {
expect(propogator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]);
expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]);
});

describe('inject', () => {
describe('sentry-trace', () => {
it.each([
[
'should set sentry-trace header when sampled',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
},
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
],
[
'should set sentry-trace header when not sampled',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
},
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0',
],
[
'should NOT set sentry-trace header when traceId is empty',
{
traceId: '',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
},
undefined,
],
[
'should NOT set sentry-trace header when spanId is empty',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '',
traceFlags: TraceFlags.NONE,
},
undefined,
],
])('%s', (_name, spanContext, expected) => {
const context = trace.setSpanContext(ROOT_CONTEXT, spanContext);
propagator.inject(context, carrier, defaultTextMapSetter);
expect(carrier[SENTRY_TRACE_HEADER]).toBe(expected);
});

it('should NOT set sentry-trace header if instrumentation is supressed', () => {
const spanContext = {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
};
const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext));
propagator.inject(context, carrier, defaultTextMapSetter);
expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined);
});
});

describe('baggage', () => {
const client = {
getOptions: () => ({
environment: 'production',
release: '1.0.0',
}),
getDsn: () => ({
publicKey: 'abc',
}),
};
// @ts-ignore Use mock client for unit tests
const hub: Hub = new Hub(client);
makeMain(hub);

afterEach(() => {
SENTRY_SPAN_PROCESSOR_MAP.clear();
});

enum PerfType {
Transaction = 'transaction',
Span = 'span',
}

function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) {
const transaction = new Transaction(transactionContext, hub);
SENTRY_SPAN_PROCESSOR_MAP.set(transaction.spanId, transaction);
if (type === PerfType.Span) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { spanId, ...ctx } = transactionContext;
const span = transaction.startChild({ ...ctx, description: transaction.name });
SENTRY_SPAN_PROCESSOR_MAP.set(span.spanId, span);
}
}

describe.each([PerfType.Transaction, PerfType.Span])('with active %s', type => {
it.each([
[
'should set baggage header when sampled',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
},
{
name: 'sampled-transaction',
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
sampled: true,
},
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
],
[
'should NOT set baggage header when not sampled',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
},
{
name: 'not-sampled-transaction',
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
sampled: false,
},
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=not-sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
],
[
'should NOT set baggage header when traceId is empty',
{
traceId: '',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
},
{
name: 'empty-traceId-transaction',
traceId: '',
spanId: '6e0c63257de34c92',
sampled: true,
},
undefined,
],
[
'should NOT set baggage header when spanId is empty',
{
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '',
traceFlags: TraceFlags.SAMPLED,
},
{
name: 'empty-spanId-transaction',
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '',
sampled: true,
},
undefined,
],
])('%s', (_name, spanContext, transactionContext, expected) => {
createTransactionAndMaybeSpan(type, transactionContext);
const context = trace.setSpanContext(ROOT_CONTEXT, spanContext);
propagator.inject(context, carrier, defaultTextMapSetter);
expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(expected);
});

it('should NOT set sentry-trace header if instrumentation is supressed', () => {
const spanContext = {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.SAMPLED,
};
const transactionContext = {
name: 'sampled-transaction',
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
sampled: true,
};
createTransactionAndMaybeSpan(type, transactionContext);
const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext));
propagator.inject(context, carrier, defaultTextMapSetter);
expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined);
});
});
});
});
});
4 changes: 2 additions & 2 deletions packages/opentelemetry-node/test/spanprocessor.test.ts
Expand Up @@ -8,7 +8,7 @@ import { Hub, makeMain } from '@sentry/core';
import { addExtensionMethods, Span as SentrySpan, SpanStatusType, Transaction } from '@sentry/tracing';
import { Contexts, Scope } from '@sentry/types';

import { SentrySpanProcessor } from '../src/spanprocessor';
import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor';

// Integration Test of SentrySpanProcessor

Expand Down Expand Up @@ -41,7 +41,7 @@ describe('SentrySpanProcessor', () => {
});

function getSpanForOtelSpan(otelSpan: OtelSpan | OpenTelemetry.Span) {
return spanProcessor._map.get(otelSpan.spanContext().spanId);
return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpan.spanContext().spanId);
}

function getContext(transaction: Transaction) {
Expand Down

0 comments on commit f36c268

Please sign in to comment.