-
Notifications
You must be signed in to change notification settings - Fork 821
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement W3C Correlation Context propagator (#838)
- Loading branch information
1 parent
a268624
commit f978377
Showing
4 changed files
with
336 additions
and
0 deletions.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
packages/opentelemetry-core/src/correlation-context/correlation-context.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/*! | ||
* Copyright 2020, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { CorrelationContext } from '@opentelemetry/api'; | ||
import { Context } from '@opentelemetry/context-base'; | ||
|
||
const CORRELATION_CONTEXT = Context.createKey( | ||
'OpenTelemetry Distributed Contexts Key' | ||
); | ||
|
||
/** | ||
* @param {Context} Context that manage all context values | ||
* @returns {CorrelationContext} Extracted correlation context from the context | ||
*/ | ||
export function getCorrelationContext( | ||
context: Context | ||
): CorrelationContext | undefined { | ||
return ( | ||
(context.getValue(CORRELATION_CONTEXT) as CorrelationContext) || undefined | ||
); | ||
} | ||
|
||
/** | ||
* @param {Context} Context that manage all context values | ||
* @param {CorrelationContext} correlation context that will be set in the actual context | ||
*/ | ||
export function setCorrelationContext( | ||
context: Context, | ||
correlationContext: CorrelationContext | ||
): Context { | ||
return context.setValue(CORRELATION_CONTEXT, correlationContext); | ||
} |
119 changes: 119 additions & 0 deletions
119
packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/*! | ||
* Copyright 2020, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { | ||
Context, | ||
CorrelationContext, | ||
GetterFunction, | ||
HttpTextPropagator, | ||
SetterFunction, | ||
} from '@opentelemetry/api'; | ||
import { | ||
getCorrelationContext, | ||
setCorrelationContext, | ||
} from '../correlation-context'; | ||
|
||
const KEY_PAIR_SEPARATOR = '='; | ||
const PROPERTIES_SEPARATOR = ';'; | ||
const ITEMS_SEPARATOR = ','; | ||
|
||
// Name of the http header used to propagate the correlation context | ||
export const CORRELATION_CONTEXT_HEADER = 'otcorrelations'; | ||
// Maximum number of name-value pairs allowed by w3c spec | ||
export const MAX_NAME_VALUE_PAIRS = 180; | ||
// Maximum number of bytes per a single name-value pair allowed by w3c spec | ||
export const MAX_PER_NAME_VALUE_PAIRS = 4096; | ||
// Maximum total length of all name-value pairs allowed by w3c spec | ||
export const MAX_TOTAL_LENGTH = 8192; | ||
type KeyPair = { | ||
key: string; | ||
value: string; | ||
}; | ||
|
||
/** | ||
* Propagates {@link CorrelationContext} through Context format propagation. | ||
* | ||
* Based on the Correlation Context specification: | ||
* https://w3c.github.io/correlation-context/ | ||
*/ | ||
export class HttpCorrelationContext implements HttpTextPropagator { | ||
inject(context: Context, carrier: unknown, setter: SetterFunction) { | ||
const correlationContext = getCorrelationContext(context); | ||
if (!correlationContext) return; | ||
const keyPairs = this._getKeyPairs(correlationContext) | ||
.filter((pair: string) => { | ||
return pair.length <= MAX_PER_NAME_VALUE_PAIRS; | ||
}) | ||
.slice(0, MAX_NAME_VALUE_PAIRS); | ||
const headerValue = this._serializeKeyPairs(keyPairs); | ||
if (headerValue.length > 0) { | ||
setter(carrier, CORRELATION_CONTEXT_HEADER, headerValue); | ||
} | ||
} | ||
|
||
private _serializeKeyPairs(keyPairs: string[]) { | ||
return keyPairs.reduce((hValue: String, current: String) => { | ||
const value = `${hValue}${hValue != '' ? ITEMS_SEPARATOR : ''}${current}`; | ||
return value.length > MAX_TOTAL_LENGTH ? hValue : value; | ||
}, ''); | ||
} | ||
|
||
private _getKeyPairs(correlationContext: CorrelationContext): string[] { | ||
return Object.keys(correlationContext).map( | ||
(key: string) => | ||
`${encodeURIComponent(key)}=${encodeURIComponent( | ||
correlationContext[key].value | ||
)}` | ||
); | ||
} | ||
|
||
extract(context: Context, carrier: unknown, getter: GetterFunction): Context { | ||
const headerValue: string = getter( | ||
carrier, | ||
CORRELATION_CONTEXT_HEADER | ||
) as string; | ||
if (!headerValue) return context; | ||
const correlationContext: CorrelationContext = {}; | ||
if (headerValue.length == 0) { | ||
return context; | ||
} | ||
const pairs = headerValue.split(ITEMS_SEPARATOR); | ||
if (pairs.length == 1) return context; | ||
pairs.forEach(entry => { | ||
const keyPair = this._parsePairKeyValue(entry); | ||
if (keyPair) { | ||
correlationContext[keyPair.key] = { value: keyPair.value }; | ||
} | ||
}); | ||
return setCorrelationContext(context, correlationContext); | ||
} | ||
|
||
private _parsePairKeyValue(entry: string): KeyPair | undefined { | ||
const valueProps = entry.split(PROPERTIES_SEPARATOR); | ||
if (valueProps.length <= 0) return; | ||
const keyPairPart = valueProps.shift(); | ||
if (!keyPairPart) return; | ||
const keyPair = keyPairPart.split(KEY_PAIR_SEPARATOR); | ||
if (keyPair.length <= 1) return; | ||
const key = decodeURIComponent(keyPair[0].trim()); | ||
let value = decodeURIComponent(keyPair[1].trim()); | ||
if (valueProps.length > 0) { | ||
value = | ||
value + PROPERTIES_SEPARATOR + valueProps.join(PROPERTIES_SEPARATOR); | ||
} | ||
return { key, value }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
/*! | ||
* Copyright 2020, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { | ||
defaultGetter, | ||
defaultSetter, | ||
CorrelationContext, | ||
} from '@opentelemetry/api'; | ||
import { Context } from '@opentelemetry/context-base'; | ||
import * as assert from 'assert'; | ||
import { | ||
getCorrelationContext, | ||
setCorrelationContext, | ||
} from '../../src/correlation-context/correlation-context'; | ||
import { | ||
HttpCorrelationContext, | ||
CORRELATION_CONTEXT_HEADER, | ||
MAX_PER_NAME_VALUE_PAIRS, | ||
} from '../../src/correlation-context/propagation/HttpCorrelationContext'; | ||
|
||
describe('HttpCorrelationContext', () => { | ||
const httpTraceContext = new HttpCorrelationContext(); | ||
|
||
let carrier: { [key: string]: unknown }; | ||
|
||
beforeEach(() => { | ||
carrier = {}; | ||
}); | ||
|
||
describe('.inject()', () => { | ||
it('should set correlation context header', () => { | ||
const correlationContext: CorrelationContext = { | ||
key1: { value: 'd4cda95b652f4a1592b449d5929fda1b' }, | ||
key3: { value: 'c88815a7-0fa9-4d95-a1f1-cdccce3c5c2a' }, | ||
'with/slash': { value: 'with spaces' }, | ||
}; | ||
|
||
httpTraceContext.inject( | ||
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext), | ||
carrier, | ||
defaultSetter | ||
); | ||
assert.deepStrictEqual( | ||
carrier[CORRELATION_CONTEXT_HEADER], | ||
'key1=d4cda95b652f4a1592b449d5929fda1b,key3=c88815a7-0fa9-4d95-a1f1-cdccce3c5c2a,with%2Fslash=with%20spaces' | ||
); | ||
}); | ||
|
||
it('should skip long key-value pairs', () => { | ||
const correlationContext: CorrelationContext = { | ||
key1: { value: 'd4cda95b' }, | ||
key3: { value: 'c88815a7' }, | ||
}; | ||
|
||
// Generate long value 2*MAX_PER_NAME_VALUE_PAIRS | ||
const value = '1a'.repeat(MAX_PER_NAME_VALUE_PAIRS); | ||
correlationContext['longPair'] = { value }; | ||
|
||
httpTraceContext.inject( | ||
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext), | ||
carrier, | ||
defaultSetter | ||
); | ||
assert.deepStrictEqual( | ||
carrier[CORRELATION_CONTEXT_HEADER], | ||
'key1=d4cda95b,key3=c88815a7' | ||
); | ||
}); | ||
|
||
it('should skip all keys that surpassed the max limit of the header', () => { | ||
const correlationContext: CorrelationContext = {}; | ||
|
||
const zeroPad = (num: number, places: number) => | ||
String(num).padStart(places, '0'); | ||
|
||
// key=value with same size , 1024 => 8 keys | ||
for (let i = 0; i < 9; ++i) { | ||
const index = zeroPad(i, 510); | ||
correlationContext[`k${index}`] = { value: `${index}` }; | ||
} | ||
|
||
// Build expected | ||
let expected = ''; | ||
for (let i = 0; i < 8; ++i) { | ||
const index = zeroPad(i, 510); | ||
expected += `k${index}=${index},`; | ||
} | ||
expected = expected.slice(0, -1); | ||
|
||
httpTraceContext.inject( | ||
setCorrelationContext(Context.ROOT_CONTEXT, correlationContext), | ||
carrier, | ||
defaultSetter | ||
); | ||
assert.deepStrictEqual(carrier[CORRELATION_CONTEXT_HEADER], expected); | ||
}); | ||
}); | ||
|
||
describe('.extract()', () => { | ||
it('should extract context of a sampled span from carrier', () => { | ||
carrier[CORRELATION_CONTEXT_HEADER] = | ||
'key1=d4cda95b,key3=c88815a7, keyn = valn, keym =valm'; | ||
const extractedCorrelationContext = getCorrelationContext( | ||
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) | ||
); | ||
|
||
const expected: CorrelationContext = { | ||
key1: { value: 'd4cda95b' }, | ||
key3: { value: 'c88815a7' }, | ||
keyn: { value: 'valn' }, | ||
keym: { value: 'valm' }, | ||
}; | ||
assert.deepStrictEqual(extractedCorrelationContext, expected); | ||
}); | ||
}); | ||
|
||
it('returns undefined if header is missing', () => { | ||
assert.deepStrictEqual( | ||
getCorrelationContext( | ||
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) | ||
), | ||
undefined | ||
); | ||
}); | ||
|
||
it('returns keys with their properties', () => { | ||
carrier[CORRELATION_CONTEXT_HEADER] = | ||
'key1=d4cda95b,key3=c88815a7;prop1=value1'; | ||
const expected: CorrelationContext = { | ||
key1: { value: 'd4cda95b' }, | ||
key3: { value: 'c88815a7;prop1=value1' }, | ||
}; | ||
assert.deepStrictEqual( | ||
getCorrelationContext( | ||
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) | ||
), | ||
expected | ||
); | ||
}); | ||
|
||
it('should gracefully handle an invalid header', () => { | ||
const testCases: Record<string, string> = { | ||
invalidNoKeyValuePair: '289371298nekjh2939299283jbk2b', | ||
invalidDoubleEqual: 'key1==value;key2=value2', | ||
invalidWrongKeyValueFormat: 'key1:value;key2=value2', | ||
invalidDoubleSemicolon: 'key1:value;;key2=value2', | ||
}; | ||
Object.getOwnPropertyNames(testCases).forEach(testCase => { | ||
carrier[CORRELATION_CONTEXT_HEADER] = testCases[testCase]; | ||
|
||
const extractedSpanContext = getCorrelationContext( | ||
httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) | ||
); | ||
assert.deepStrictEqual(extractedSpanContext, undefined, testCase); | ||
}); | ||
}); | ||
}); |