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

Propagate b3 parentspanid and debug flag #1346

Merged
2 changes: 1 addition & 1 deletion packages/opentelemetry-api/src/trace/span_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface SpanContext {
*
* SAMPLED = 0x1 and NONE = 0x0;
*/
traceFlags: TraceFlags;
traceFlags?: TraceFlags;
srjames90 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Tracing-system-specific info to propagate.
*
Expand Down
137 changes: 111 additions & 26 deletions packages/opentelemetry-core/src/context/propagation/B3Propagator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ import { getParentSpanContext, setExtractedSpanContext } from '../context';
export const X_B3_TRACE_ID = 'x-b3-traceid';
export const X_B3_SPAN_ID = 'x-b3-spanid';
export const X_B3_SAMPLED = 'x-b3-sampled';
export const X_B3_PARENT_SPAN_ID = 'x-b3-parentspanid';
export const X_B3_FLAGS = 'x-b3-flags';
export const PARENT_SPAN_ID_KEY = Context.createKey(
'OpenTelemetry Context Key B3 Parent Span Id'
);
export const DEBUG_FLAG_KEY = Context.createKey(
'OpenTelemetry Context Key B3 Debug Flag'
);
const VALID_TRACEID_REGEX = /^([0-9a-f]{16}){1,2}$/i;
const VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i;
const INVALID_ID_REGEX = /^0+$/i;
const VALID_SAMPLED_VALUES = [true, 'true', '1', 1];
srjames90 marked this conversation as resolved.
Show resolved Hide resolved
const VALID_UNSAMPLED_VALUES = [0, '0', 'false', false];

function isValidTraceId(traceId: string): boolean {
return VALID_TRACEID_REGEX.test(traceId) && !INVALID_ID_REGEX.test(traceId);
Expand All @@ -38,25 +48,106 @@ function isValidSpanId(spanId: string): boolean {
return VALID_SPANID_REGEX.test(spanId) && !INVALID_ID_REGEX.test(spanId);
}

function isValidParentSpanID(spanId: string | undefined): boolean {
return spanId === undefined || isValidSpanId(spanId);
}

function isValidSampledValue(sampled: number | undefined): boolean {
return (
sampled === undefined ||
VALID_SAMPLED_VALUES.includes(sampled) ||
VALID_UNSAMPLED_VALUES.includes(sampled)
);
}

function parseHeader(header: unknown) {
return Array.isArray(header) ? header[0] : header;
}

/**
* Propagator for the B3 HTTP header format.
* Based on: https://github.com/openzipkin/b3-propagation
*/
export class B3Propagator implements HttpTextPropagator {
_getHeaderValue(carrier: unknown, getter: GetterFunction, key: string) {
srjames90 marked this conversation as resolved.
Show resolved Hide resolved
const header = getter(carrier, key);
return parseHeader(header);
}

_getTraceId(carrier: unknown, getter: GetterFunction): string {
const traceId = this._getHeaderValue(carrier, getter, X_B3_TRACE_ID);
if (typeof traceId === 'string') {
return traceId.padStart(32, '0');
}
return '';
}

_getSpanId(carrier: unknown, getter: GetterFunction): string {
const spanId = this._getHeaderValue(carrier, getter, X_B3_SPAN_ID);
if (typeof spanId === 'string') {
return spanId;
}
return '';
}

_getParentSpanId(
carrier: unknown,
getter: GetterFunction
): string | undefined {
const spanId = this._getHeaderValue(carrier, getter, X_B3_PARENT_SPAN_ID);
if (typeof spanId === 'string') {
return spanId;
}
return;
}

_getDebug(carrier: unknown, getter: GetterFunction): string | undefined {
const debug = this._getHeaderValue(carrier, getter, X_B3_FLAGS);
return debug === '1' ? '1' : undefined;
}

_getTraceFlags(carrier: unknown, getter: GetterFunction): number | undefined {
const traceFlags = this._getHeaderValue(carrier, getter, X_B3_SAMPLED);
const debug = this._getDebug(carrier, getter);
if (debug === '1') {
return TraceFlags.SAMPLED;
} else if (traceFlags !== undefined) {
if (VALID_SAMPLED_VALUES.includes(traceFlags)) {
return TraceFlags.SAMPLED;
} else if (VALID_UNSAMPLED_VALUES.includes(traceFlags)) {
return TraceFlags.NONE;
} else {
// Invalid traceflag
return 2;
srjames90 marked this conversation as resolved.
Show resolved Hide resolved
}
}
return;
}

inject(context: Context, carrier: unknown, setter: SetterFunction) {
const spanContext = getParentSpanContext(context);
if (!spanContext) return;

const parentSpanId = context.getValue(PARENT_SPAN_ID_KEY) as
| undefined
| string;
if (
isValidTraceId(spanContext.traceId) &&
isValidSpanId(spanContext.spanId)
isValidSpanId(spanContext.spanId) &&
isValidParentSpanID(parentSpanId)
) {
const debug = context.getValue(DEBUG_FLAG_KEY);
setter(carrier, X_B3_TRACE_ID, spanContext.traceId);
setter(carrier, X_B3_SPAN_ID, spanContext.spanId);

// We set the header only if there is an existing sampling decision.
// Otherwise we will omit it => Absent.
if (spanContext.traceFlags !== undefined) {
if (parentSpanId) {
setter(carrier, X_B3_PARENT_SPAN_ID, parentSpanId);
}
// According to the B3 spec, if the debug flag is set,
// the sampled flag shouldn't be propagated as well.
if (debug === '1') {
setter(carrier, X_B3_FLAGS, debug);
} else if (spanContext.traceFlags !== undefined) {
// We set the header only if there is an existing sampling decision.
// Otherwise we will omit it => Absent.
setter(
carrier,
X_B3_SAMPLED,
Expand All @@ -69,31 +160,25 @@ export class B3Propagator implements HttpTextPropagator {
}

extract(context: Context, carrier: unknown, getter: GetterFunction): Context {
const traceIdHeader = getter(carrier, X_B3_TRACE_ID);
const spanIdHeader = getter(carrier, X_B3_SPAN_ID);
const sampledHeader = getter(carrier, X_B3_SAMPLED);

const traceIdHeaderValue = Array.isArray(traceIdHeader)
? traceIdHeader[0]
: traceIdHeader;
const spanId = Array.isArray(spanIdHeader) ? spanIdHeader[0] : spanIdHeader;

const options = Array.isArray(sampledHeader)
? sampledHeader[0]
: sampledHeader;

if (typeof traceIdHeaderValue !== 'string' || typeof spanId !== 'string') {
return context;
}

const traceId = traceIdHeaderValue.padStart(32, '0');
const traceId = this._getTraceId(carrier, getter);
const spanId = this._getSpanId(carrier, getter);
const parentSpanId = this._getParentSpanId(carrier, getter);
const traceFlags = this._getTraceFlags(carrier, getter);
const debug = this._getDebug(carrier, getter);

if (isValidTraceId(traceId) && isValidSpanId(spanId)) {
if (
isValidTraceId(traceId) &&
isValidSpanId(spanId) &&
isValidParentSpanID(parentSpanId) &&
isValidSampledValue(traceFlags)
) {
context = context.setValue(PARENT_SPAN_ID_KEY, parentSpanId);
context = context.setValue(DEBUG_FLAG_KEY, debug);
return setExtractedSpanContext(context, {
traceId,
spanId,
isRemote: true,
traceFlags: isNaN(Number(options)) ? TraceFlags.NONE : Number(options),
traceFlags,
srjames90 marked this conversation as resolved.
Show resolved Hide resolved
});
}
return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class ParentOrElseSampler implements Sampler {
links: Link[]
): SamplingResult {
// Respect the parent sampling decision if there is one
if (parentContext) {
if (parentContext && parentContext.traceFlags !== undefined) {
return {
decision:
(TraceFlags.SAMPLED & parentContext.traceFlags) === TraceFlags.SAMPLED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class ProbabilitySampler implements Sampler {
shouldSample(parentContext?: SpanContext): SamplingResult {
// Respect the parent sampling decision if there is one.
// TODO(#1284): add an option to ignore parent regarding to spec.
if (parentContext) {
if (parentContext && parentContext.traceFlags !== undefined) {
return {
decision:
(TraceFlags.SAMPLED & parentContext.traceFlags) === TraceFlags.SAMPLED
Expand Down
Loading