Skip to content

Commit 61d6eba

Browse files
committed
feat: implement W3C Correlation Context propagator
Signed-off-by: Ruben Vargas <[email protected]> Signed-off-by: Ruben Vargas <[email protected]>
1 parent e9bc887 commit 61d6eba

File tree

3 files changed

+317
-0
lines changed

3 files changed

+317
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*!
2+
* Copyright 2020, 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 { CorrelationContext } from '@opentelemetry/api';
18+
import { Context } from '@opentelemetry/context-base';
19+
20+
const CORRELATION_CONTEXT = Context.createKey(
21+
'OpenTelemetry Distributed Contexts Key'
22+
);
23+
24+
export function getCorrelationContext(
25+
context: Context
26+
): CorrelationContext | undefined {
27+
return (
28+
(context.getValue(CORRELATION_CONTEXT) as CorrelationContext) || undefined
29+
);
30+
}
31+
32+
export function setCorrelationContext(
33+
context: Context,
34+
correlationContext: CorrelationContext
35+
): Context {
36+
return context.setValue(CORRELATION_CONTEXT, correlationContext);
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*!
2+
* Copyright 2020, 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+
CorrelationContext,
20+
GetterFunction,
21+
HttpTextPropagator,
22+
SetterFunction,
23+
} from '@opentelemetry/api';
24+
25+
import {
26+
getCorrelationContext,
27+
setCorrelationContext,
28+
} from '../correlation-context';
29+
30+
export const CORRELATION_CONTEXT_HEADER = 'otcorrelationcontext';
31+
const KEY_PAIR_SEPARATOR = '=';
32+
const PROPERTIES_SEPARATOR = ';';
33+
const ITEMS_SEPARATOR = ',';
34+
35+
/* W3C Constrains*/
36+
37+
export const MAX_NAME_VALUE_PAIRS = 180;
38+
export const MAX_PER_NAME_VALUE_PAIRS = 4096;
39+
export const MAX_TOTAL_LENGTH = 8192;
40+
41+
/**
42+
* Propagates {@link CorrelationContext} through Context format propagation.
43+
*
44+
* Based on the Correlation Context specification:
45+
* https://w3c.github.io/correlation-context/
46+
*/
47+
export class HttpCorrelationContext implements HttpTextPropagator {
48+
inject(context: Context, carrier: unknown, setter: SetterFunction) {
49+
const distContext = getCorrelationContext(context);
50+
if (distContext) {
51+
const all = Object.keys(distContext);
52+
const values = all
53+
.map(
54+
(key: string) =>
55+
`${encodeURIComponent(key)}=${encodeURIComponent(
56+
distContext[key].value
57+
)}`
58+
)
59+
.filter((pair: string) => {
60+
return pair.length <= MAX_PER_NAME_VALUE_PAIRS;
61+
})
62+
.slice(0, MAX_NAME_VALUE_PAIRS);
63+
const headerValue = values.reduce((hValue: String, current: String) => {
64+
const value = `${hValue}${
65+
hValue != '' ? ITEMS_SEPARATOR : ''
66+
}${current}`;
67+
return value.length > MAX_TOTAL_LENGTH ? hValue : value;
68+
}, '');
69+
if (headerValue.length > 0) {
70+
setter(carrier, CORRELATION_CONTEXT_HEADER, headerValue);
71+
}
72+
}
73+
}
74+
75+
extract(context: Context, carrier: unknown, getter: GetterFunction): Context {
76+
const headerValue: string = getter(
77+
carrier,
78+
CORRELATION_CONTEXT_HEADER
79+
) as string;
80+
if (!headerValue) return context;
81+
const distributedContext: CorrelationContext = {};
82+
if (headerValue.length > 0) {
83+
const pairs = headerValue.split(ITEMS_SEPARATOR);
84+
if (pairs.length == 1) return context;
85+
pairs.forEach(entry => {
86+
const valueProps = entry.split(PROPERTIES_SEPARATOR);
87+
if (valueProps.length > 0) {
88+
const keyPairPart = valueProps.shift();
89+
if (keyPairPart) {
90+
const keyPair = keyPairPart.split(KEY_PAIR_SEPARATOR);
91+
if (keyPair.length > 1) {
92+
const key = decodeURIComponent(keyPair[0].trim());
93+
let value = decodeURIComponent(keyPair[1].trim());
94+
if (valueProps.length > 0) {
95+
value =
96+
value +
97+
PROPERTIES_SEPARATOR +
98+
valueProps.join(PROPERTIES_SEPARATOR);
99+
}
100+
distributedContext[key] = { value };
101+
}
102+
}
103+
}
104+
});
105+
}
106+
return setCorrelationContext(context, distributedContext);
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*!
2+
* Copyright 2019, 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+
defaultGetter,
19+
defaultSetter,
20+
CorrelationContext,
21+
} from '@opentelemetry/api';
22+
import { Context } from '@opentelemetry/context-base';
23+
import * as assert from 'assert';
24+
25+
import {
26+
getCorrelationContext,
27+
setCorrelationContext,
28+
} from '../../src/correlation-context/correlation-context';
29+
30+
import {
31+
HttpCorrelationContext,
32+
CORRELATION_CONTEXT_HEADER,
33+
MAX_PER_NAME_VALUE_PAIRS,
34+
} from '../../src/correlation-context/propagation/HttpCorrelationContext';
35+
36+
describe('HttpCorrelationContext', () => {
37+
const httpTraceContext = new HttpCorrelationContext();
38+
39+
let carrier: { [key: string]: unknown };
40+
41+
beforeEach(() => {
42+
carrier = {};
43+
});
44+
45+
describe('.inject()', () => {
46+
it('should set traceparent header', () => {
47+
const correlationContext: CorrelationContext = {
48+
key1: { value: 'd4cda95b652f4a1592b449d5929fda1b' },
49+
key3: { value: 'c88815a7-0fa9-4d95-a1f1-cdccce3c5c2a' },
50+
'with/slash': { value: 'with spaces' },
51+
};
52+
53+
httpTraceContext.inject(
54+
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext),
55+
carrier,
56+
defaultSetter
57+
);
58+
assert.deepStrictEqual(
59+
carrier[CORRELATION_CONTEXT_HEADER],
60+
'key1=d4cda95b652f4a1592b449d5929fda1b,key3=c88815a7-0fa9-4d95-a1f1-cdccce3c5c2a,with%2Fslash=with%20spaces'
61+
);
62+
});
63+
64+
it('should skip long key-value pairs', () => {
65+
const correlationContext: CorrelationContext = {
66+
key1: { value: 'd4cda95b' },
67+
key3: { value: 'c88815a7' },
68+
};
69+
70+
let value = '';
71+
// Generate long value 2*MAX_PER_NAME_VALUE_PAIRS
72+
for (let i = 0; i < MAX_PER_NAME_VALUE_PAIRS; ++i) {
73+
value += '1a';
74+
}
75+
correlationContext['longPair'] = { value };
76+
77+
httpTraceContext.inject(
78+
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext),
79+
carrier,
80+
defaultSetter
81+
);
82+
assert.deepStrictEqual(
83+
carrier[CORRELATION_CONTEXT_HEADER],
84+
'key1=d4cda95b,key3=c88815a7'
85+
);
86+
});
87+
88+
it('should skip all keys that surpassed the max limit of the header', () => {
89+
const correlationContext: CorrelationContext = {};
90+
91+
const zeroPad = (num: number, places: number) =>
92+
String(num).padStart(places, '0');
93+
94+
// key=value with same size , 1024 => 8 keys
95+
for (let i = 0; i < 9; ++i) {
96+
const index = zeroPad(i, 510);
97+
correlationContext[`k${index}`] = { value: `${index}` };
98+
}
99+
100+
// Build expected
101+
let expected = '';
102+
for (let i = 0; i < 8; ++i) {
103+
const index = zeroPad(i, 510);
104+
expected += `k${index}=${index},`;
105+
}
106+
expected = expected.slice(0, -1);
107+
108+
httpTraceContext.inject(
109+
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext),
110+
carrier,
111+
defaultSetter
112+
);
113+
assert.deepStrictEqual(carrier[CORRELATION_CONTEXT_HEADER], expected);
114+
});
115+
});
116+
117+
describe('.extract()', () => {
118+
it('should extract context of a sampled span from carrier', () => {
119+
carrier[CORRELATION_CONTEXT_HEADER] = 'key1=d4cda95b,key3=c88815a7';
120+
const extractedCorrelationContext = getCorrelationContext(
121+
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter)
122+
);
123+
124+
const expected: CorrelationContext = {
125+
key1: { value: 'd4cda95b' },
126+
key3: { value: 'c88815a7' },
127+
};
128+
assert.deepStrictEqual(extractedCorrelationContext, expected);
129+
});
130+
});
131+
132+
it('returns null if header is missing', () => {
133+
assert.deepStrictEqual(
134+
getCorrelationContext(
135+
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter)
136+
),
137+
undefined
138+
);
139+
});
140+
141+
it('returns properties', () => {
142+
carrier[CORRELATION_CONTEXT_HEADER] =
143+
'key1=d4cda95b,key3=c88815a7;prop1=value1';
144+
const expected: CorrelationContext = {
145+
key1: { value: 'd4cda95b' },
146+
key3: { value: 'c88815a7;prop1=value1' },
147+
};
148+
assert.deepStrictEqual(
149+
getCorrelationContext(
150+
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter)
151+
),
152+
expected
153+
);
154+
});
155+
156+
it('should gracefully handle an invalid header', () => {
157+
const testCases: Record<string, string> = {
158+
invalidNoKeyValuePair: '289371298nekjh2939299283jbk2b',
159+
invalidDoubleEqual: 'key1==value;key2=value2',
160+
invalidWrongKeyValueFormat: 'key1:value;key2=value2',
161+
invalidDoubleSemicolon: 'key1:value;;key2=value2',
162+
};
163+
Object.getOwnPropertyNames(testCases).forEach(testCase => {
164+
carrier[CORRELATION_CONTEXT_HEADER] = testCases[testCase];
165+
166+
const extractedSpanContext = getCorrelationContext(
167+
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter)
168+
);
169+
assert.deepStrictEqual(extractedSpanContext, undefined, testCase);
170+
});
171+
});
172+
});

0 commit comments

Comments
 (0)