Skip to content

Commit

Permalink
feat(opentelemetry): Expose sampling helpers
Browse files Browse the repository at this point in the history
When users want to use a custom sampler, they can use these new helpers to still have sentry working nicely with whatever they decide to do in there.

For e.g. trace propagation etc. to work correctly with Sentry, we need to attach some things to trace state etc. These helpers encapsulate this for the user, while still allowing them to decide however they want if the span should be sampled or not.
  • Loading branch information
mydea committed Jun 27, 2024
1 parent cf22ca3 commit 1b3c21a
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 20 deletions.
7 changes: 6 additions & 1 deletion packages/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrat
export { wrapContextManagerClass } from './contextManager';
export { SentryPropagator } from './propagator';
export { SentrySpanProcessor } from './spanProcessor';
export { SentrySampler } from './sampler';
export {
SentrySampler,
sentrySamplerNoDecision,
sentrySamplerNotSampled,
sentrySamplerSampled,
} from './sampler';

export { openTelemetrySetupCheck } from './utils/setupCheck';

Expand Down
79 changes: 60 additions & 19 deletions packages/opentelemetry/src/sampler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Attributes, Context, Span } from '@opentelemetry/api';
import type { Attributes, Context, Span , TraceState as TraceStateInterface} from '@opentelemetry/api';
import { SpanKind } from '@opentelemetry/api';
import { isSpanContextValid, trace } from '@opentelemetry/api';
import { TraceState } from '@opentelemetry/core';
Expand Down Expand Up @@ -40,16 +40,8 @@ export class SentrySampler implements Sampler {
const parentSpan = trace.getSpan(context);
const parentContext = parentSpan?.spanContext();

let traceState = parentContext?.traceState || new TraceState();

// We always keep the URL on the trace state, so we can access it in the propagator
const url = spanAttributes[SEMATTRS_HTTP_URL];
if (url && typeof url === 'string') {
traceState = traceState.set(SENTRY_TRACE_STATE_URL, url);
}

if (!hasTracingEnabled(options)) {
return { decision: SamplingDecision.NOT_RECORD, traceState };
return sentrySamplerNoDecision({ context, spanAttributes });
}

// If we have a http.client span that has no local parent, we never want to sample it
Expand All @@ -59,7 +51,7 @@ export class SentrySampler implements Sampler {
spanAttributes[SEMATTRS_HTTP_METHOD] &&
(!parentSpan || parentContext?.isRemote)
) {
return { decision: SamplingDecision.NOT_RECORD, traceState };
return sentrySamplerNoDecision({ context, spanAttributes });
}

const parentSampled = parentSpan ? getParentSampled(parentSpan, traceId, spanName) : undefined;
Expand All @@ -76,7 +68,7 @@ export class SentrySampler implements Sampler {
mutableSamplingDecision,
);
if (!mutableSamplingDecision.decision) {
return { decision: SamplingDecision.NOT_RECORD, traceState: traceState };
return sentrySamplerNoDecision({ context, spanAttributes });
}

const [sampled, sampleRate] = sampleSpan(options, {
Expand All @@ -96,25 +88,22 @@ export class SentrySampler implements Sampler {
const method = `${spanAttributes[SEMATTRS_HTTP_METHOD]}`.toUpperCase();
if (method === 'OPTIONS' || method === 'HEAD') {
DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`);

return {
decision: SamplingDecision.NOT_RECORD,
...sentrySamplerNotSampled({ context, spanAttributes }),
attributes,
traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
};
}

if (!sampled) {
return {
decision: SamplingDecision.NOT_RECORD,
...sentrySamplerNotSampled({ context, spanAttributes }),
attributes,
traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
};
}

return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
...sentrySamplerSampled({ context, spanAttributes }),
attributes,
traceState,
};
}

Expand Down Expand Up @@ -152,3 +141,55 @@ function getParentSampled(parentSpan: Span, traceId: string, spanName: string):

return undefined;
}

/**
* Returns a SamplingResult that indicates that a span was not sampled, but no definite decision was made yet.
* This indicates to downstream SDKs that they may make their own decision.
*/
export function sentrySamplerNoDecision({
context,
spanAttributes,
}: { context: Context; spanAttributes: SpanAttributes }): SamplingResult {
const traceState = getBaseTraceState(context, spanAttributes);

return { decision: SamplingDecision.NOT_RECORD, traceState };
}

/**
* Returns a SamplingResult that indicates that a span was not sampled.
*/
export function sentrySamplerNotSampled({
context,
spanAttributes,
}: { context: Context; spanAttributes: SpanAttributes }): SamplingResult {
const traceState = getBaseTraceState(context, spanAttributes).set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1');

return { decision: SamplingDecision.NOT_RECORD, traceState };
}

/**
* Returns a SamplingResult that indicates that a span was sampled.
*/
export function sentrySamplerSampled({
context,
spanAttributes,
}: { context: Context; spanAttributes: SpanAttributes }): SamplingResult {
const traceState = getBaseTraceState(context, spanAttributes);

return { decision: SamplingDecision.RECORD_AND_SAMPLED, traceState };
}

function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): TraceStateInterface {
const parentSpan = trace.getSpan(context);
const parentContext = parentSpan?.spanContext();

let traceState = parentContext?.traceState || new TraceState();

// We always keep the URL on the trace state, so we can access it in the propagator
const url = spanAttributes[SEMATTRS_HTTP_URL];
if (url && typeof url === 'string') {
traceState = traceState.set(SENTRY_TRACE_STATE_URL, url);
}

return traceState;
}

0 comments on commit 1b3c21a

Please sign in to comment.