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 00000000000..d2eb75ab297 --- /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 00000000000..447609f3b52 --- /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 00000000000..be07e3481b3 --- /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 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); + }); + }); +});