Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 99bd6db

Browse files
authored
Trace Context Propagation: return null instead of empty spancontext (#547)
* Trace Context Propagation: return null instead of empty spancontext * fix review comments
1 parent 8ea8bdc commit 99bd6db

File tree

2 files changed

+63
-91
lines changed

2 files changed

+63
-91
lines changed

packages/opencensus-propagation-tracecontext/src/tracecontext-format.ts

Lines changed: 55 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,46 @@
1616

1717
import {HeaderGetter, HeaderSetter, Propagation, SpanContext} from '@opencensus/core';
1818
import * as crypto from 'crypto';
19-
import {isValidOption, isValidSpanId, isValidTraceId, isValidVersion} from './validators';
19+
import {isValidSpanId, isValidTraceId, isValidVersion} from './validators';
2020

21-
// Header names
21+
/** The traceparent header key. */
2222
export const TRACE_PARENT = 'traceparent';
23+
/** The tracestate header key. */
2324
export const TRACE_STATE = 'tracestate';
25+
/** The default trace options. This defaults to unsampled. */
2426
export const DEFAULT_OPTIONS = 0x0;
27+
/** Regular expression that represents a valid traceparent header. */
28+
const TRACE_PARENT_REGEX = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/;
29+
30+
/**
31+
* Parses a traceparent header value into a SpanContext object, or null if the
32+
* traceparent value is invalid.
33+
*/
34+
function traceParentToSpanContext(traceParent: string): SpanContext|null {
35+
const match = traceParent.match(TRACE_PARENT_REGEX);
36+
if (!match) return null;
37+
const parts = traceParent.split('-');
38+
const [version, traceId, spanId, option] = parts;
39+
// tslint:disable-next-line:ban Needed to parse hexadecimal.
40+
const options = parseInt(option, 16);
41+
42+
if (!isValidVersion(version) || !isValidTraceId(traceId) ||
43+
!isValidSpanId(spanId)) {
44+
return null;
45+
}
46+
return {traceId, spanId, options};
47+
}
48+
49+
/** Converts a headers type to a string. */
50+
function parseHeader(str: string|string[]|undefined): string|undefined {
51+
return Array.isArray(str) ? str[0] : str;
52+
}
2553

2654
/**
2755
* Propagates span context through Trace Context format propagation.
2856
*
2957
* Based on the Trace Context specification:
30-
* https://w3c.github.io/distributed-tracing/report-trace-context.html
58+
* https://www.w3.org/TR/trace-context/
3159
*
3260
* Known Limitations:
3361
* - Multiple `tracestate` headers are merged into a single `tracestate`
@@ -37,65 +65,28 @@ export const DEFAULT_OPTIONS = 0x0;
3765
export class TraceContextFormat implements Propagation {
3866
/**
3967
* Gets the trace context from a request headers. If there is no trace context
40-
* in the headers, or if the parsed `traceId` or `spanId` is invalid, an empty
41-
* context is returned.
68+
* in the headers, or if the parsed `traceId` or `spanId` is invalid, null is
69+
* returned.
4270
* @param getter
4371
*/
4472
extract(getter: HeaderGetter): SpanContext|null {
45-
if (getter) {
46-
// Construct empty span context that we will fill
47-
const spanContext: SpanContext = {
48-
traceId: '',
49-
spanId: '',
50-
options: DEFAULT_OPTIONS,
51-
traceState: undefined
52-
};
53-
54-
let traceState = getter.getHeader(TRACE_STATE);
55-
if (Array.isArray(traceState)) {
56-
// If more than one `tracestate` header is found, we merge them into a
57-
// single header.
58-
traceState = traceState.join(',');
59-
}
60-
spanContext.traceState =
61-
typeof traceState === 'string' ? traceState : undefined;
73+
const traceParentHeader = getter.getHeader(TRACE_PARENT);
74+
if (!traceParentHeader) return null;
75+
const traceParent = parseHeader(traceParentHeader);
76+
if (!traceParent) return null;
6277

63-
// Read headers
64-
let traceParent = getter.getHeader(TRACE_PARENT);
65-
if (Array.isArray(traceParent)) {
66-
traceParent = traceParent[0];
67-
}
78+
const spanContext = traceParentToSpanContext(traceParent);
79+
if (!spanContext) return null;
6880

69-
// Parse TraceParent into version, traceId, spanId, and option flags. All
70-
// parts of the header should be present or it is considered invalid.
71-
const parts = traceParent ? traceParent.split('-') : [];
72-
if (parts.length === 4) {
73-
// Both traceId and spanId must be of valid form for the traceparent
74-
// header to be accepted. If either is not valid we simply return the
75-
// empty spanContext.
76-
const version = parts[0];
77-
const traceId = parts[1];
78-
const spanId = parts[2];
79-
if (!isValidVersion(version) || !isValidTraceId(traceId) ||
80-
!isValidSpanId(spanId)) {
81-
return spanContext;
82-
}
83-
spanContext.traceId = traceId;
84-
spanContext.spanId = spanId;
85-
86-
// Validate options. If the options are invalid we simply reset them to
87-
// default.
88-
let optionsHex = parts[3];
89-
if (!isValidOption(optionsHex)) {
90-
optionsHex = DEFAULT_OPTIONS.toString(16);
91-
}
92-
const options = Number('0x' + optionsHex);
93-
spanContext.options = options;
94-
}
95-
96-
return spanContext;
81+
const traceStateHeader = getter.getHeader(TRACE_STATE);
82+
if (traceStateHeader) {
83+
// If more than one `tracestate` header is found, we merge them into a
84+
// single header.
85+
spanContext.traceState = Array.isArray(traceStateHeader) ?
86+
traceStateHeader.join(',') :
87+
traceStateHeader;
9788
}
98-
return null;
89+
return spanContext;
9990
}
10091

10192
/**
@@ -104,19 +95,14 @@ export class TraceContextFormat implements Propagation {
10495
* @param spanContext
10596
*/
10697
inject(setter: HeaderSetter, spanContext: SpanContext): void {
107-
if (setter && spanContext) {
108-
// Construct traceparent from parts. Make sure the traceId and spanId
109-
// contain the proper number of characters.
110-
const optionsHex = Buffer.from([spanContext.options]).toString('hex');
111-
const traceIdHex =
112-
('00000000000000000000000000000000' + spanContext.traceId).slice(-32);
113-
const spanIdHex = ('0000000000000000' + spanContext.spanId).slice(-16);
114-
const traceParent = `00-${traceIdHex}-${spanIdHex}-${optionsHex}`;
98+
// Construct traceparent from parts. Make sure the traceId and spanId
99+
// contain the proper number of characters.
100+
const traceParent = `00-${spanContext.traceId}-${spanContext.spanId}-0${
101+
(spanContext.options || DEFAULT_OPTIONS).toString(16)}`;
115102

116-
setter.setHeader(TRACE_PARENT, traceParent);
117-
if (spanContext.traceState) {
118-
setter.setHeader(TRACE_STATE, spanContext.traceState);
119-
}
103+
setter.setHeader(TRACE_PARENT, traceParent);
104+
if (spanContext.traceState) {
105+
setter.setHeader(TRACE_STATE, spanContext.traceState);
120106
}
121107
}
122108

@@ -130,8 +116,7 @@ export class TraceContextFormat implements Propagation {
130116
return {
131117
traceId: buff.slice(0, 32),
132118
spanId: buff.slice(32, 48),
133-
options: DEFAULT_OPTIONS,
134-
traceState: undefined
119+
options: DEFAULT_OPTIONS
135120
};
136121
}
137122
}

packages/opencensus-propagation-tracecontext/test/test-tracecontext-format.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,13 @@
1616

1717
import {HeaderGetter, HeaderSetter, SpanContext} from '@opencensus/core';
1818
import * as assert from 'assert';
19-
2019
import {DEFAULT_OPTIONS, TRACE_PARENT, TRACE_STATE, TraceContextFormat} from '../src/';
2120

2221
const traceContextFormat = new TraceContextFormat();
2322

2423
type Headers = Record<string, string|string[]>;
2524

2625
describe('TraceContextPropagation', () => {
27-
let emptySpanContext: SpanContext;
28-
29-
beforeEach(() => {
30-
emptySpanContext = {
31-
traceId: '',
32-
spanId: '',
33-
options: DEFAULT_OPTIONS,
34-
traceState: undefined
35-
};
36-
});
37-
3826
// Generates the appropriate `traceparent` header for the given SpanContext
3927
const traceParentHeaderFromSpanContext =
4028
(spanContext: SpanContext): string => {
@@ -69,7 +57,6 @@ describe('TraceContextPropagation', () => {
6957

7058
it('should gracefully handle multiple traceparent headers', () => {
7159
const firstContext = traceContextFormat.generate();
72-
firstContext.traceState = undefined;
7360

7461
const headers: Headers = {};
7562
headers[TRACE_PARENT] = [
@@ -140,12 +127,8 @@ describe('TraceContextPropagation', () => {
140127
}
141128
};
142129

143-
const shouldReceiveSpanContext = {...emptySpanContext, traceState: ''};
144-
145130
const extractedSpanContext = traceContextFormat.extract(getter);
146-
147-
assert.deepEqual(
148-
extractedSpanContext, shouldReceiveSpanContext, testCase);
131+
assert.deepEqual(extractedSpanContext, null, testCase);
149132
});
150133
});
151134

@@ -206,7 +189,7 @@ describe('TraceContextPropagation', () => {
206189
};
207190

208191
const extractedSpanContext = traceContextFormat.extract(getter);
209-
assert.deepEqual(extractedSpanContext, emptySpanContext);
192+
assert.deepEqual(extractedSpanContext, null);
210193
});
211194
});
212195

@@ -230,8 +213,12 @@ describe('TraceContextPropagation', () => {
230213
});
231214

232215
it('should inject a tracestate header', () => {
233-
const spanContext:
234-
SpanContext = {...emptySpanContext, traceState: 'foo=bar'};
216+
const spanContext: SpanContext = {
217+
traceId: '',
218+
spanId: '',
219+
options: DEFAULT_OPTIONS,
220+
traceState: 'foo=bar'
221+
};
235222
const headers: Headers = {};
236223
const setter: HeaderSetter = {
237224
setHeader(name: string, value: string) {

0 commit comments

Comments
 (0)