Skip to content

Commit

Permalink
feat: implement W3C Correlation Context propagator (#838)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvp8510 committed May 19, 2020
1 parent a268624 commit f978377
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
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);
}
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 };
}
}
2 changes: 2 additions & 0 deletions packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
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);
});
});
});

0 comments on commit f978377

Please sign in to comment.