Skip to content

Commit

Permalink
feat: implement W3C Correlation Context propagator
Browse files Browse the repository at this point in the history
Signed-off-by: Ruben Vargas <ruben.vp8510@gmail.com>

Signed-off-by: Ruben Vargas <ruben.vp8510@gmail.com>
  • Loading branch information
rubenvp8510 committed Mar 26, 2020
1 parent e9bc887 commit 61d6eba
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<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 61d6eba

Please sign in to comment.