Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(otel): Add inject functionality to SentryPropagator #6114

Merged
merged 2 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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