From 78758d3d35fe07eaa0740f81db5eb85e7d81bbd3 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Tue, 3 Mar 2020 15:34:54 -0600 Subject: [PATCH 1/5] feat: implement W3C Correlation Context propagator Signed-off-by: Ruben Vargas Signed-off-by: Ruben Vargas --- .../correlation-context.ts | 37 ++++ .../propagation/HttpCorrelationContext.ts | 108 +++++++++++ .../HttpCorrelationContext.test.ts | 172 ++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 packages/opentelemetry-core/src/correlation-context/correlation-context.ts create mode 100644 packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts create mode 100644 packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts diff --git a/packages/opentelemetry-core/src/correlation-context/correlation-context.ts b/packages/opentelemetry-core/src/correlation-context/correlation-context.ts new file mode 100644 index 0000000000..d2eb75ab29 --- /dev/null +++ b/packages/opentelemetry-core/src/correlation-context/correlation-context.ts @@ -0,0 +1,37 @@ +/*! + * 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' +); + +export function getCorrelationContext( + context: Context +): CorrelationContext | undefined { + return ( + (context.getValue(CORRELATION_CONTEXT) as CorrelationContext) || undefined + ); +} + +export function setCorrelationContext( + context: Context, + correlationContext: CorrelationContext +): Context { + return context.setValue(CORRELATION_CONTEXT, correlationContext); +} diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts new file mode 100644 index 0000000000..447609f3b5 --- /dev/null +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -0,0 +1,108 @@ +/*! + * 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'; + +export const CORRELATION_CONTEXT_HEADER = 'otcorrelationcontext'; +const KEY_PAIR_SEPARATOR = '='; +const PROPERTIES_SEPARATOR = ';'; +const ITEMS_SEPARATOR = ','; + +/* W3C Constrains*/ + +export const MAX_NAME_VALUE_PAIRS = 180; +export const MAX_PER_NAME_VALUE_PAIRS = 4096; +export const MAX_TOTAL_LENGTH = 8192; + +/** + * 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 distContext = getCorrelationContext(context); + if (distContext) { + const all = Object.keys(distContext); + const values = all + .map( + (key: string) => + `${encodeURIComponent(key)}=${encodeURIComponent( + distContext[key].value + )}` + ) + .filter((pair: string) => { + return pair.length <= MAX_PER_NAME_VALUE_PAIRS; + }) + .slice(0, MAX_NAME_VALUE_PAIRS); + const headerValue = values.reduce((hValue: String, current: String) => { + const value = `${hValue}${ + hValue != '' ? ITEMS_SEPARATOR : '' + }${current}`; + return value.length > MAX_TOTAL_LENGTH ? hValue : value; + }, ''); + if (headerValue.length > 0) { + setter(carrier, CORRELATION_CONTEXT_HEADER, headerValue); + } + } + } + + extract(context: Context, carrier: unknown, getter: GetterFunction): Context { + const headerValue: string = getter( + carrier, + CORRELATION_CONTEXT_HEADER + ) as string; + if (!headerValue) return context; + const distributedContext: CorrelationContext = {}; + if (headerValue.length > 0) { + const pairs = headerValue.split(ITEMS_SEPARATOR); + if (pairs.length == 1) return context; + pairs.forEach(entry => { + const valueProps = entry.split(PROPERTIES_SEPARATOR); + if (valueProps.length > 0) { + const keyPairPart = valueProps.shift(); + if (keyPairPart) { + const keyPair = keyPairPart.split(KEY_PAIR_SEPARATOR); + if (keyPair.length > 1) { + const key = decodeURIComponent(keyPair[0].trim()); + let value = decodeURIComponent(keyPair[1].trim()); + if (valueProps.length > 0) { + value = + value + + PROPERTIES_SEPARATOR + + valueProps.join(PROPERTIES_SEPARATOR); + } + distributedContext[key] = { value }; + } + } + } + }); + } + return setCorrelationContext(context, distributedContext); + } +} diff --git a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts new file mode 100644 index 0000000000..d8bd908993 --- /dev/null +++ b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts @@ -0,0 +1,172 @@ +/*! + * Copyright 2019, 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 traceparent 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' }, + }; + + let value = ''; + // Generate long value 2*MAX_PER_NAME_VALUE_PAIRS + for (let i = 0; i < MAX_PER_NAME_VALUE_PAIRS; ++i) { + value += '1a'; + } + 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'; + const extractedCorrelationContext = getCorrelationContext( + httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) + ); + + const expected: CorrelationContext = { + key1: { value: 'd4cda95b' }, + key3: { value: 'c88815a7' }, + }; + assert.deepStrictEqual(extractedCorrelationContext, expected); + }); + }); + + it('returns null 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 = { + 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); + }); + }); +}); From 923dddbacf13148d4db19272ee77973af1bbbbf0 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Fri, 15 May 2020 10:02:24 -0500 Subject: [PATCH 2/5] chore: change correlation context header name according to the spec Signed-off-by: Ruben Vargas --- .../correlation-context/propagation/HttpCorrelationContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 447609f3b5..6bf7c9f211 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -27,7 +27,7 @@ import { setCorrelationContext, } from '../correlation-context'; -export const CORRELATION_CONTEXT_HEADER = 'otcorrelationcontext'; +export const CORRELATION_CONTEXT_HEADER = 'otcorrelations'; const KEY_PAIR_SEPARATOR = '='; const PROPERTIES_SEPARATOR = ';'; const ITEMS_SEPARATOR = ','; From 5d93ffbdef47513b0f68067cecbfaac17a8b619c Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Sun, 17 May 2020 21:03:56 -0500 Subject: [PATCH 3/5] chore: add tests with spaces, fix style Signed-off-by: Ruben Vargas --- .../propagation/HttpCorrelationContext.ts | 50 +++++++++---------- .../HttpCorrelationContext.test.ts | 11 ++-- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 6bf7c9f211..54220266c2 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -21,7 +21,6 @@ import { HttpTextPropagator, SetterFunction, } from '@opentelemetry/api'; - import { getCorrelationContext, setCorrelationContext, @@ -46,29 +45,26 @@ export const MAX_TOTAL_LENGTH = 8192; */ export class HttpCorrelationContext implements HttpTextPropagator { inject(context: Context, carrier: unknown, setter: SetterFunction) { - const distContext = getCorrelationContext(context); - if (distContext) { - const all = Object.keys(distContext); - const values = all - .map( - (key: string) => - `${encodeURIComponent(key)}=${encodeURIComponent( - distContext[key].value - )}` - ) - .filter((pair: string) => { - return pair.length <= MAX_PER_NAME_VALUE_PAIRS; - }) - .slice(0, MAX_NAME_VALUE_PAIRS); - const headerValue = values.reduce((hValue: String, current: String) => { - const value = `${hValue}${ - hValue != '' ? ITEMS_SEPARATOR : '' - }${current}`; - return value.length > MAX_TOTAL_LENGTH ? hValue : value; - }, ''); - if (headerValue.length > 0) { - setter(carrier, CORRELATION_CONTEXT_HEADER, headerValue); - } + const correlationContext = getCorrelationContext(context); + if (!correlationContext) return; + const all = Object.keys(correlationContext); + const values = all + .map( + (key: string) => + `${encodeURIComponent(key)}=${encodeURIComponent( + correlationContext[key].value + )}` + ) + .filter((pair: string) => { + return pair.length <= MAX_PER_NAME_VALUE_PAIRS; + }) + .slice(0, MAX_NAME_VALUE_PAIRS); + const headerValue = values.reduce((hValue: String, current: String) => { + const value = `${hValue}${hValue != '' ? ITEMS_SEPARATOR : ''}${current}`; + return value.length > MAX_TOTAL_LENGTH ? hValue : value; + }, ''); + if (headerValue.length > 0) { + setter(carrier, CORRELATION_CONTEXT_HEADER, headerValue); } } @@ -78,7 +74,7 @@ export class HttpCorrelationContext implements HttpTextPropagator { CORRELATION_CONTEXT_HEADER ) as string; if (!headerValue) return context; - const distributedContext: CorrelationContext = {}; + const correlationContext: CorrelationContext = {}; if (headerValue.length > 0) { const pairs = headerValue.split(ITEMS_SEPARATOR); if (pairs.length == 1) return context; @@ -97,12 +93,12 @@ export class HttpCorrelationContext implements HttpTextPropagator { PROPERTIES_SEPARATOR + valueProps.join(PROPERTIES_SEPARATOR); } - distributedContext[key] = { value }; + correlationContext[key] = { value }; } } } }); } - return setCorrelationContext(context, distributedContext); + return setCorrelationContext(context, correlationContext); } } diff --git a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts index d8bd908993..b734f3ce93 100644 --- a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts +++ b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * 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. @@ -21,12 +21,10 @@ import { } 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, @@ -43,7 +41,7 @@ describe('HttpCorrelationContext', () => { }); describe('.inject()', () => { - it('should set traceparent header', () => { + it('should set correlation context header', () => { const correlationContext: CorrelationContext = { key1: { value: 'd4cda95b652f4a1592b449d5929fda1b' }, key3: { value: 'c88815a7-0fa9-4d95-a1f1-cdccce3c5c2a' }, @@ -116,7 +114,8 @@ describe('HttpCorrelationContext', () => { describe('.extract()', () => { it('should extract context of a sampled span from carrier', () => { - carrier[CORRELATION_CONTEXT_HEADER] = 'key1=d4cda95b,key3=c88815a7'; + carrier[CORRELATION_CONTEXT_HEADER] = + 'key1=d4cda95b,key3=c88815a7, keyn = valn, keym =valm'; const extractedCorrelationContext = getCorrelationContext( httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter) ); @@ -124,6 +123,8 @@ describe('HttpCorrelationContext', () => { const expected: CorrelationContext = { key1: { value: 'd4cda95b' }, key3: { value: 'c88815a7' }, + keyn: { value: 'valn' }, + keym: { value: 'valm' }, }; assert.deepStrictEqual(extractedCorrelationContext, expected); }); From 193b5f9ef68d42733505c4f867e7af498a0cbba7 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Sun, 17 May 2020 22:00:06 -0500 Subject: [PATCH 4/5] chore: remove nested conditions on correlation context extract Signed-off-by: Ruben Vargas --- .../propagation/HttpCorrelationContext.ts | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 54220266c2..917aae39c8 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -75,30 +75,26 @@ export class HttpCorrelationContext implements HttpTextPropagator { ) as string; if (!headerValue) return context; const correlationContext: CorrelationContext = {}; - if (headerValue.length > 0) { - const pairs = headerValue.split(ITEMS_SEPARATOR); - if (pairs.length == 1) return context; - pairs.forEach(entry => { - const valueProps = entry.split(PROPERTIES_SEPARATOR); - if (valueProps.length > 0) { - const keyPairPart = valueProps.shift(); - if (keyPairPart) { - const keyPair = keyPairPart.split(KEY_PAIR_SEPARATOR); - if (keyPair.length > 1) { - const key = decodeURIComponent(keyPair[0].trim()); - let value = decodeURIComponent(keyPair[1].trim()); - if (valueProps.length > 0) { - value = - value + - PROPERTIES_SEPARATOR + - valueProps.join(PROPERTIES_SEPARATOR); - } - correlationContext[key] = { value }; - } - } - } - }); + if (headerValue.length == 0) { + return context; } + const pairs = headerValue.split(ITEMS_SEPARATOR); + if (pairs.length == 1) return context; + pairs.forEach(entry => { + 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); + } + correlationContext[key] = { value }; + }); return setCorrelationContext(context, correlationContext); } } From 4f7adaea2496ff9c20b8d740565f94cc959df3cc Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Mon, 18 May 2020 14:24:48 -0500 Subject: [PATCH 5/5] chore: tsdocs, separate some functions, add export to index.ts Signed-off-by: Ruben Vargas --- .../correlation-context.ts | 8 ++ .../propagation/HttpCorrelationContext.ts | 73 ++++++++++++------- packages/opentelemetry-core/src/index.ts | 2 + .../HttpCorrelationContext.test.ts | 7 +- 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/opentelemetry-core/src/correlation-context/correlation-context.ts b/packages/opentelemetry-core/src/correlation-context/correlation-context.ts index d2eb75ab29..08df15bc0a 100644 --- a/packages/opentelemetry-core/src/correlation-context/correlation-context.ts +++ b/packages/opentelemetry-core/src/correlation-context/correlation-context.ts @@ -21,6 +21,10 @@ 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 { @@ -29,6 +33,10 @@ export function getCorrelationContext( ); } +/** + * @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 diff --git a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts index 917aae39c8..c4134364a3 100644 --- a/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts +++ b/packages/opentelemetry-core/src/correlation-context/propagation/HttpCorrelationContext.ts @@ -26,16 +26,22 @@ import { setCorrelationContext, } from '../correlation-context'; -export const CORRELATION_CONTEXT_HEADER = 'otcorrelations'; const KEY_PAIR_SEPARATOR = '='; const PROPERTIES_SEPARATOR = ';'; const ITEMS_SEPARATOR = ','; -/* W3C Constrains*/ - +// 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. @@ -47,27 +53,33 @@ export class HttpCorrelationContext implements HttpTextPropagator { inject(context: Context, carrier: unknown, setter: SetterFunction) { const correlationContext = getCorrelationContext(context); if (!correlationContext) return; - const all = Object.keys(correlationContext); - const values = all - .map( - (key: string) => - `${encodeURIComponent(key)}=${encodeURIComponent( - correlationContext[key].value - )}` - ) + const keyPairs = this._getKeyPairs(correlationContext) .filter((pair: string) => { return pair.length <= MAX_PER_NAME_VALUE_PAIRS; }) .slice(0, MAX_NAME_VALUE_PAIRS); - const headerValue = values.reduce((hValue: String, current: String) => { - const value = `${hValue}${hValue != '' ? ITEMS_SEPARATOR : ''}${current}`; - return value.length > MAX_TOTAL_LENGTH ? hValue : value; - }, ''); + 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, @@ -81,20 +93,27 @@ export class HttpCorrelationContext implements HttpTextPropagator { const pairs = headerValue.split(ITEMS_SEPARATOR); if (pairs.length == 1) return context; pairs.forEach(entry => { - 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); + const keyPair = this._parsePairKeyValue(entry); + if (keyPair) { + correlationContext[keyPair.key] = { value: keyPair.value }; } - correlationContext[key] = { 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 }; + } } diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 7e561a52bb..f9266cd2ce 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -25,6 +25,8 @@ export * from './context/propagation/B3Propagator'; export * from './context/propagation/composite'; export * from './context/propagation/HttpTraceContext'; export * from './context/propagation/types'; +export * from './correlation-context/correlation-context'; +export * from './correlation-context/propagation/HttpCorrelationContext'; export * from './platform'; export * from './trace/NoRecordingSpan'; export * from './trace/sampler/ProbabilitySampler'; diff --git a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts index b734f3ce93..da903819ac 100644 --- a/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts +++ b/packages/opentelemetry-core/test/correlation-context/HttpCorrelationContext.test.ts @@ -65,11 +65,8 @@ describe('HttpCorrelationContext', () => { key3: { value: 'c88815a7' }, }; - let value = ''; // Generate long value 2*MAX_PER_NAME_VALUE_PAIRS - for (let i = 0; i < MAX_PER_NAME_VALUE_PAIRS; ++i) { - value += '1a'; - } + const value = '1a'.repeat(MAX_PER_NAME_VALUE_PAIRS); correlationContext['longPair'] = { value }; httpTraceContext.inject( @@ -130,7 +127,7 @@ describe('HttpCorrelationContext', () => { }); }); - it('returns null if header is missing', () => { + it('returns undefined if header is missing', () => { assert.deepStrictEqual( getCorrelationContext( httpTraceContext.extract(Context.ROOT_CONTEXT, carrier, defaultGetter)