Skip to content

feat(otel): Add propagator #6095

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

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opentelemetry-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
},
"peerDependencies": {
"@opentelemetry/api": "1.x",
"@opentelemetry/core": "1.x",
"@opentelemetry/sdk-trace-base": "1.x",
"@opentelemetry/semantic-conventions": "1.x"
},
"devDependencies": {
"@opentelemetry/api": "^1.2.0",
"@opentelemetry/core": "^1.7.0",
"@opentelemetry/sdk-trace-base": "^1.7.0",
"@opentelemetry/sdk-trace-node": "^1.7.0"
},
Expand Down
9 changes: 9 additions & 0 deletions packages/opentelemetry-node/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContextKey } from '@opentelemetry/api';

export const SENTRY_TRACE_HEADER = 'sentry-trace';

export const SENTRY_BAGGAGE_HEADER = 'baggage';

export const SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY = createContextKey('SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY');

export const SENTRY_TRACE_PARENT_CONTEXT_KEY = createContextKey('SENTRY_TRACE_PARENT_CONTEXT_KEY');
1 change: 1 addition & 0 deletions packages/opentelemetry-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@sentry/tracing';

export { SentrySpanProcessor } from './spanprocessor';
export { SentryPropogator } from './propogator';
Copy link
Member

@mydea mydea Nov 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be propagator, not propogator? (both in the filename & class name)

Copy link
Member Author

@AbhiPrasad AbhiPrasad Nov 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, fixed this afterwards, see: #6109

I literally spelled it wrong everywhere 😭

94 changes: 94 additions & 0 deletions packages/opentelemetry-node/src/propogator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
Context,
isSpanContextValid,
TextMapGetter,
TextMapPropagator,
TextMapSetter,
trace,
TraceFlags,
} from '@opentelemetry/api';
import { isTracingSuppressed } from '@opentelemetry/core';
import {
baggageHeaderToDynamicSamplingContext,
dynamicSamplingContextToSentryBaggageHeader,
extractTraceparentData,
} from '@sentry/utils';

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

/**
* Injects and extracts `sentry-trace` and `baggage` headers from carriers.
*/
export class SentryPropogator implements TextMapPropagator {
/**
* @inheritDoc
*/
public inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
const spanContext = trace.getSpanContext(context);
if (!spanContext || !isSpanContextValid(spanContext) || isTracingSuppressed(context)) {
return;
}

// TODO: if sentry span use `parentSpanId`.
// Same `isSentryRequest` as is used in `SentrySpanProcessor`.
// const spanId = isSentryRequest(spanContext) ? spanContext.parentSpanId : spanContext.spanId;

const traceparent = `${spanContext.traceId}-${spanContext.spanId}-${
// eslint-disable-next-line no-bitwise
spanContext.traceFlags & TraceFlags.SAMPLED ? 1 : 0
}`;
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);
}
}
}

/**
* @inheritDoc
*/
public extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
let newContext = context;

const maybeSentryTraceHeader: string | string[] | undefined = getter.get(carrier, SENTRY_TRACE_HEADER);
if (maybeSentryTraceHeader) {
const header = Array.isArray(maybeSentryTraceHeader) ? maybeSentryTraceHeader[0] : maybeSentryTraceHeader;
const traceparentData = extractTraceparentData(header);
newContext = newContext.setValue(SENTRY_TRACE_PARENT_CONTEXT_KEY, traceparentData);
if (traceparentData) {
const traceFlags = traceparentData.parentSampled ? TraceFlags.SAMPLED : TraceFlags.NONE;
const spanContext = {
traceId: traceparentData.traceId || '',
spanId: traceparentData.parentSpanId || '',
isRemote: true,
traceFlags,
};
newContext = trace.setSpanContext(newContext, spanContext);
}
}

const maybeBaggageHeader = getter.get(carrier, SENTRY_BAGGAGE_HEADER);
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(maybeBaggageHeader);
newContext = newContext.setValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, dynamicSamplingContext);

return newContext;
}

/**
* @inheritDoc
*/
public fields(): string[] {
return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER];
}
}
46 changes: 31 additions & 15 deletions packages/opentelemetry-node/src/spanprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ import { Context } from '@opentelemetry/api';
import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { getCurrentHub, withScope } from '@sentry/core';
import { Transaction } from '@sentry/tracing';
import { Span as SentrySpan, TransactionContext } from '@sentry/types';
import { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
import { logger } from '@sentry/utils';

import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants';
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
*/
public onStart(otelSpan: OtelSpan, _parentContext: Context): void {
public onStart(otelSpan: OtelSpan, parentContext: Context): void {
const hub = getCurrentHub();
if (!hub) {
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a hub has been setup.');
Expand All @@ -39,7 +42,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,18 +52,17 @@ 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 traceCtx = getTraceData(otelSpan, parentContext);
const transaction = hub.startTransaction({
name: otelSpan.name,
...traceCtx,
// instrumentor: 'otel',
startTimestamp: otelSpan.startTime[0],
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(otelSpan.endTime[0]);
}

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

/**
Expand All @@ -107,13 +109,27 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
}
}

function getTraceData(otelSpan: OtelSpan): Partial<TransactionContext> {
function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
const spanContext = otelSpan.spanContext();
const traceId = spanContext.traceId;
const spanId = spanContext.spanId;

const parentSpanId = otelSpan.parentSpanId;
return { spanId, traceId, parentSpanId };

const traceparentData = parentContext.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined;
const dynamicSamplingContext = parentContext.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY) as
| Partial<DynamicSamplingContext>
| undefined;

return {
spanId,
traceId,
parentSpanId,
metadata: {
// only set dynamic sampling context if sentry-trace header was set
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
source: 'custom',
},
};
}

function finishTransactionWithContextFromOtelData(transaction: Transaction, otelSpan: OtelSpan): void {
Expand Down
Loading