From 7bba4d2333797f982f7815aee107574819702c4a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 2 Nov 2022 12:42:10 +0100 Subject: [PATCH] feat(otel): Add inject functionality to SentryPropagator --- packages/opentelemetry-node/src/propagator.ts | 34 ++- .../opentelemetry-node/src/spanprocessor.ts | 18 +- .../test/propagator.test.ts | 197 ++++++++++++++++++ .../test/spanprocessor.test.ts | 4 +- 4 files changed, 240 insertions(+), 13 deletions(-) diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index 897cc8819e33..ab85a2f08d23 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/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. @@ -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); + } + } } /** diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 4bddcc43e1cc..10cd0d957eea 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -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 = 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 = new Map(); - /** * @inheritDoc */ @@ -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({ @@ -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({ @@ -60,7 +62,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { spanId: otelSpanId, }); - this._map.set(otelSpanId, transaction); + SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction); } } @@ -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__ && @@ -85,7 +87,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime)); } - this._map.delete(otelSpanId); + SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId); } /** diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 62311a586b6c..11f403abc600 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/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(); + let carrier: { [key: string]: unknown }; + + beforeEach(() => { + carrier = {}; + }); it('returns fields set', () => { expect(propogator.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); + propogator.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)); + propogator.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); + propogator.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)); + propogator.inject(context, carrier, defaultTextMapSetter); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); + }); + }); + }); + }); }); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 570d9d36bb27..1dddf135e1dd 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -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 @@ -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) {