Skip to content

Commit e62c481

Browse files
authored
feat: b3 single header support (#1560)
1 parent 9f72a15 commit e62c481

File tree

12 files changed

+1128
-678
lines changed

12 files changed

+1128
-678
lines changed

packages/opentelemetry-api/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export {
6161
INVALID_SPANID,
6262
INVALID_TRACEID,
6363
INVALID_SPAN_CONTEXT,
64+
isSpanContextValid,
65+
isValidTraceId,
66+
isValidSpanId,
6467
} from './trace/spancontext-utils';
6568

6669
export {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
Context,
19+
GetterFunction,
20+
TextMapPropagator,
21+
SetterFunction,
22+
TraceFlags,
23+
isValidSpanId,
24+
isValidTraceId,
25+
isSpanContextValid,
26+
getParentSpanContext,
27+
setExtractedSpanContext,
28+
} from '@opentelemetry/api';
29+
import { B3_DEBUG_FLAG_KEY } from './b3-common';
30+
31+
/* b3 multi-header keys */
32+
export const X_B3_TRACE_ID = 'x-b3-traceid';
33+
export const X_B3_SPAN_ID = 'x-b3-spanid';
34+
export const X_B3_SAMPLED = 'x-b3-sampled';
35+
export const X_B3_PARENT_SPAN_ID = 'x-b3-parentspanid';
36+
export const X_B3_FLAGS = 'x-b3-flags';
37+
38+
const VALID_SAMPLED_VALUES = new Set([true, 'true', 'True', '1', 1]);
39+
const VALID_UNSAMPLED_VALUES = new Set([false, 'false', 'False', '0', 0]);
40+
41+
function isValidSampledValue(sampled: TraceFlags | undefined): boolean {
42+
return sampled === TraceFlags.SAMPLED || sampled === TraceFlags.NONE;
43+
}
44+
45+
export function parseHeader(header: unknown) {
46+
return Array.isArray(header) ? header[0] : header;
47+
}
48+
49+
function getHeaderValue(carrier: unknown, getter: GetterFunction, key: string) {
50+
const header = getter(carrier, key);
51+
return parseHeader(header);
52+
}
53+
54+
function getTraceId(carrier: unknown, getter: GetterFunction): string {
55+
const traceId = getHeaderValue(carrier, getter, X_B3_TRACE_ID);
56+
if (typeof traceId === 'string') {
57+
return traceId.padStart(32, '0');
58+
}
59+
return '';
60+
}
61+
62+
function getSpanId(carrier: unknown, getter: GetterFunction): string {
63+
const spanId = getHeaderValue(carrier, getter, X_B3_SPAN_ID);
64+
if (typeof spanId === 'string') {
65+
return spanId;
66+
}
67+
return '';
68+
}
69+
70+
function getDebug(
71+
carrier: unknown,
72+
getter: GetterFunction
73+
): string | undefined {
74+
const debug = getHeaderValue(carrier, getter, X_B3_FLAGS);
75+
return debug === '1' ? '1' : undefined;
76+
}
77+
78+
function getTraceFlags(
79+
carrier: unknown,
80+
getter: GetterFunction
81+
): TraceFlags | undefined {
82+
const traceFlags = getHeaderValue(carrier, getter, X_B3_SAMPLED);
83+
const debug = getDebug(carrier, getter);
84+
if (debug === '1' || VALID_SAMPLED_VALUES.has(traceFlags)) {
85+
return TraceFlags.SAMPLED;
86+
}
87+
if (traceFlags === undefined || VALID_UNSAMPLED_VALUES.has(traceFlags)) {
88+
return TraceFlags.NONE;
89+
}
90+
// This indicates to isValidSampledValue that this is not valid
91+
return;
92+
}
93+
94+
/**
95+
* Propagator for the B3 multiple-header HTTP format.
96+
* Based on: https://github.com/openzipkin/b3-propagation
97+
*/
98+
export class B3MultiPropagator implements TextMapPropagator {
99+
inject(context: Context, carrier: unknown, setter: SetterFunction) {
100+
const spanContext = getParentSpanContext(context);
101+
if (!spanContext || !isSpanContextValid(spanContext)) return;
102+
103+
const debug = context.getValue(B3_DEBUG_FLAG_KEY);
104+
setter(carrier, X_B3_TRACE_ID, spanContext.traceId);
105+
setter(carrier, X_B3_SPAN_ID, spanContext.spanId);
106+
// According to the B3 spec, if the debug flag is set,
107+
// the sampled flag shouldn't be propagated as well.
108+
if (debug === '1') {
109+
setter(carrier, X_B3_FLAGS, debug);
110+
} else if (spanContext.traceFlags !== undefined) {
111+
// We set the header only if there is an existing sampling decision.
112+
// Otherwise we will omit it => Absent.
113+
setter(
114+
carrier,
115+
X_B3_SAMPLED,
116+
(TraceFlags.SAMPLED & spanContext.traceFlags) === TraceFlags.SAMPLED
117+
? '1'
118+
: '0'
119+
);
120+
}
121+
}
122+
123+
extract(context: Context, carrier: unknown, getter: GetterFunction): Context {
124+
const traceId = getTraceId(carrier, getter);
125+
const spanId = getSpanId(carrier, getter);
126+
const traceFlags = getTraceFlags(carrier, getter) as TraceFlags;
127+
const debug = getDebug(carrier, getter);
128+
129+
if (
130+
isValidTraceId(traceId) &&
131+
isValidSpanId(spanId) &&
132+
isValidSampledValue(traceFlags)
133+
) {
134+
context = context.setValue(B3_DEBUG_FLAG_KEY, debug);
135+
return setExtractedSpanContext(context, {
136+
traceId,
137+
spanId,
138+
isRemote: true,
139+
traceFlags,
140+
});
141+
}
142+
return context;
143+
}
144+
}

packages/opentelemetry-core/src/context/propagation/B3Propagator.ts

Lines changed: 34 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -19,168 +19,54 @@ import {
1919
GetterFunction,
2020
TextMapPropagator,
2121
SetterFunction,
22-
TraceFlags,
23-
getParentSpanContext,
24-
setExtractedSpanContext,
2522
} from '@opentelemetry/api';
23+
import { B3SinglePropagator, B3_CONTEXT_HEADER } from './B3SinglePropagator';
24+
import { B3MultiPropagator } from './B3MultiPropagator';
2625

27-
import { createContextKey } from '@opentelemetry/context-base';
28-
29-
export const X_B3_TRACE_ID = 'x-b3-traceid';
30-
export const X_B3_SPAN_ID = 'x-b3-spanid';
31-
export const X_B3_SAMPLED = 'x-b3-sampled';
32-
export const X_B3_PARENT_SPAN_ID = 'x-b3-parentspanid';
33-
export const X_B3_FLAGS = 'x-b3-flags';
34-
export const PARENT_SPAN_ID_KEY = createContextKey(
35-
'OpenTelemetry Context Key B3 Parent Span Id'
36-
);
37-
export const DEBUG_FLAG_KEY = createContextKey(
38-
'OpenTelemetry Context Key B3 Debug Flag'
39-
);
40-
const VALID_TRACEID_REGEX = /^([0-9a-f]{16}){1,2}$/i;
41-
const VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i;
42-
const INVALID_ID_REGEX = /^0+$/i;
43-
const VALID_SAMPLED_VALUES = new Set([true, 'true', 'True', '1', 1]);
44-
const VALID_UNSAMPLED_VALUES = new Set([false, 'false', 'False', '0', 0]);
45-
46-
function isValidTraceId(traceId: string): boolean {
47-
return VALID_TRACEID_REGEX.test(traceId) && !INVALID_ID_REGEX.test(traceId);
48-
}
49-
50-
function isValidSpanId(spanId: string): boolean {
51-
return VALID_SPANID_REGEX.test(spanId) && !INVALID_ID_REGEX.test(spanId);
52-
}
53-
54-
function isValidParentSpanID(spanId: string | undefined): boolean {
55-
return spanId === undefined || isValidSpanId(spanId);
56-
}
57-
58-
function isValidSampledValue(sampled: TraceFlags | undefined): boolean {
59-
return sampled === TraceFlags.SAMPLED || sampled === TraceFlags.NONE;
60-
}
61-
62-
function parseHeader(header: unknown) {
63-
return Array.isArray(header) ? header[0] : header;
64-
}
65-
66-
function getHeaderValue(carrier: unknown, getter: GetterFunction, key: string) {
67-
const header = getter(carrier, key);
68-
return parseHeader(header);
69-
}
70-
71-
function getTraceId(carrier: unknown, getter: GetterFunction): string {
72-
const traceId = getHeaderValue(carrier, getter, X_B3_TRACE_ID);
73-
if (typeof traceId === 'string') {
74-
return traceId.padStart(32, '0');
75-
}
76-
return '';
26+
/** Enumeraion of B3 inject encodings */
27+
export enum B3InjectEncoding {
28+
SINGLE_HEADER,
29+
MULTI_HEADER,
7730
}
7831

79-
function getSpanId(carrier: unknown, getter: GetterFunction): string {
80-
const spanId = getHeaderValue(carrier, getter, X_B3_SPAN_ID);
81-
if (typeof spanId === 'string') {
82-
return spanId;
83-
}
84-
return '';
85-
}
86-
87-
function getParentSpanId(
88-
carrier: unknown,
89-
getter: GetterFunction
90-
): string | undefined {
91-
const spanId = getHeaderValue(carrier, getter, X_B3_PARENT_SPAN_ID);
92-
if (typeof spanId === 'string') {
93-
return spanId;
94-
}
95-
return;
96-
}
97-
98-
function getDebug(
99-
carrier: unknown,
100-
getter: GetterFunction
101-
): string | undefined {
102-
const debug = getHeaderValue(carrier, getter, X_B3_FLAGS);
103-
return debug === '1' ? '1' : undefined;
104-
}
105-
106-
function getTraceFlags(
107-
carrier: unknown,
108-
getter: GetterFunction
109-
): TraceFlags | undefined {
110-
const traceFlags = getHeaderValue(carrier, getter, X_B3_SAMPLED);
111-
const debug = getDebug(carrier, getter);
112-
if (debug === '1' || VALID_SAMPLED_VALUES.has(traceFlags)) {
113-
return TraceFlags.SAMPLED;
114-
}
115-
if (traceFlags === undefined || VALID_UNSAMPLED_VALUES.has(traceFlags)) {
116-
return TraceFlags.NONE;
117-
}
118-
// This indicates to isValidSampledValue that this is not valid
119-
return;
32+
/** Configuration for the B3Propagator */
33+
export interface B3PropagatorConfig {
34+
injectEncoding?: B3InjectEncoding;
12035
}
12136

12237
/**
123-
* Propagator for the B3 HTTP header format.
38+
* Propagator that extracts B3 context in both single and multi-header variants,
39+
* with configurable injection format defaulting to B3 single-header. Due to
40+
* the asymmetry in injection and extraction formats this is not suitable to
41+
* be implemented as a composite propagator.
12442
* Based on: https://github.com/openzipkin/b3-propagation
12543
*/
12644
export class B3Propagator implements TextMapPropagator {
127-
inject(context: Context, carrier: unknown, setter: SetterFunction) {
128-
const spanContext = getParentSpanContext(context);
129-
if (!spanContext) return;
130-
const parentSpanId = context.getValue(PARENT_SPAN_ID_KEY) as
131-
| undefined
132-
| string;
133-
if (
134-
isValidTraceId(spanContext.traceId) &&
135-
isValidSpanId(spanContext.spanId) &&
136-
isValidParentSpanID(parentSpanId)
137-
) {
138-
const debug = context.getValue(DEBUG_FLAG_KEY);
139-
setter(carrier, X_B3_TRACE_ID, spanContext.traceId);
140-
setter(carrier, X_B3_SPAN_ID, spanContext.spanId);
141-
if (parentSpanId) {
142-
setter(carrier, X_B3_PARENT_SPAN_ID, parentSpanId);
143-
}
144-
// According to the B3 spec, if the debug flag is set,
145-
// the sampled flag shouldn't be propagated as well.
146-
if (debug === '1') {
147-
setter(carrier, X_B3_FLAGS, debug);
148-
} else if (spanContext.traceFlags !== undefined) {
149-
// We set the header only if there is an existing sampling decision.
150-
// Otherwise we will omit it => Absent.
151-
setter(
152-
carrier,
153-
X_B3_SAMPLED,
154-
(TraceFlags.SAMPLED & spanContext.traceFlags) === TraceFlags.SAMPLED
155-
? '1'
156-
: '0'
157-
);
158-
}
45+
private readonly _b3MultiPropagator: B3MultiPropagator = new B3MultiPropagator();
46+
private readonly _b3SinglePropagator: B3SinglePropagator = new B3SinglePropagator();
47+
private readonly _inject: (
48+
context: Context,
49+
carrier: unknown,
50+
setter: SetterFunction
51+
) => void;
52+
53+
constructor(config: B3PropagatorConfig = {}) {
54+
if (config.injectEncoding === B3InjectEncoding.MULTI_HEADER) {
55+
this._inject = this._b3MultiPropagator.inject;
56+
} else {
57+
this._inject = this._b3SinglePropagator.inject;
15958
}
16059
}
16160

162-
extract(context: Context, carrier: unknown, getter: GetterFunction): Context {
163-
const traceId = getTraceId(carrier, getter);
164-
const spanId = getSpanId(carrier, getter);
165-
const parentSpanId = getParentSpanId(carrier, getter);
166-
const traceFlags = getTraceFlags(carrier, getter) as TraceFlags;
167-
const debug = getDebug(carrier, getter);
61+
inject(context: Context, carrier: unknown, setter: SetterFunction) {
62+
this._inject(context, carrier, setter);
63+
}
16864

169-
if (
170-
isValidTraceId(traceId) &&
171-
isValidSpanId(spanId) &&
172-
isValidParentSpanID(parentSpanId) &&
173-
isValidSampledValue(traceFlags)
174-
) {
175-
context = context.setValue(PARENT_SPAN_ID_KEY, parentSpanId);
176-
context = context.setValue(DEBUG_FLAG_KEY, debug);
177-
return setExtractedSpanContext(context, {
178-
traceId,
179-
spanId,
180-
isRemote: true,
181-
traceFlags,
182-
});
65+
extract(context: Context, carrier: unknown, getter: GetterFunction): Context {
66+
if (getter(carrier, B3_CONTEXT_HEADER)) {
67+
return this._b3SinglePropagator.extract(context, carrier, getter);
68+
} else {
69+
return this._b3MultiPropagator.extract(context, carrier, getter);
18370
}
184-
return context;
18571
}
18672
}

0 commit comments

Comments
 (0)