Skip to content
This repository has been archived by the owner on Nov 10, 2022. It is now read-only.

Add TraceState implementation to API #147

Merged
merged 8 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export * from './trace/SpanOptions';
export * from './trace/status';
export * from './trace/trace_flags';
export * from './trace/trace_state';
export { createTraceState } from './trace/internal/utils';
export * from './trace/tracer_provider';
export * from './trace/tracer';

Expand Down
110 changes: 110 additions & 0 deletions src/trace/internal/tracestate-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
dyladan marked this conversation as resolved.
Show resolved Hide resolved
* Copyright The 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 { TraceState } from '../trace_state';
import { validateKey, validateValue } from './validators';

const MAX_TRACE_STATE_ITEMS = 32;
const MAX_TRACE_STATE_LEN = 512;
const LIST_MEMBERS_SEPARATOR = ',';
const LIST_MEMBER_KEY_VALUE_SPLITTER = '=';

/**
* TraceState must be a class and not a simple object type because of the spec
* requirement (https://www.w3.org/TR/trace-context/#tracestate-field).
*
* Here is the list of allowed mutations:
* - New key-value pair should be added into the beginning of the list
* - The value of any key can be updated. Modified keys MUST be moved to the
* beginning of the list.
*/
export class TraceStateImpl implements TraceState {
private _internalState: Map<string, string> = new Map();

constructor(rawTraceState?: string) {
if (rawTraceState) this._parse(rawTraceState);
}

set(key: string, value: string): TraceStateImpl {
// TODO: Benchmark the different approaches(map vs list) and
// use the faster one.
const traceState = this._clone();
if (traceState._internalState.has(key)) {
traceState._internalState.delete(key);
}
traceState._internalState.set(key, value);
return traceState;
}

unset(key: string): TraceStateImpl {
const traceState = this._clone();
traceState._internalState.delete(key);
return traceState;
}

get(key: string): string | undefined {
return this._internalState.get(key);
}

serialize(): string {
return this._keys()
.reduce((agg: string[], key) => {
agg.push(key + LIST_MEMBER_KEY_VALUE_SPLITTER + this.get(key));
return agg;
}, [])
.join(LIST_MEMBERS_SEPARATOR);
}

private _parse(rawTraceState: string) {
if (rawTraceState.length > MAX_TRACE_STATE_LEN) return;
this._internalState = rawTraceState
.split(LIST_MEMBERS_SEPARATOR)
.reverse() // Store in reverse so new keys (.set(...)) will be placed at the beginning
.reduce((agg: Map<string, string>, part: string) => {
const listMember = part.trim(); // Optional Whitespace (OWS) handling
const i = listMember.indexOf(LIST_MEMBER_KEY_VALUE_SPLITTER);
if (i !== -1) {
const key = listMember.slice(0, i);
const value = listMember.slice(i + 1, part.length);
if (validateKey(key) && validateValue(value)) {
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
agg.set(key, value);
} else {
// TODO: Consider to add warning log
}
}
return agg;
}, new Map());

// Because of the reverse() requirement, trunc must be done after map is created
if (this._internalState.size > MAX_TRACE_STATE_ITEMS) {
this._internalState = new Map(
Array.from(this._internalState.entries())
.reverse() // Use reverse same as original tracestate parse chain
.slice(0, MAX_TRACE_STATE_ITEMS)
);
}
}

private _keys(): string[] {
return Array.from(this._internalState.keys()).reverse();
}

private _clone(): TraceStateImpl {
const traceState = new TraceStateImpl();
traceState._internalState = new Map(this._internalState);
return traceState;
}
}
22 changes: 22 additions & 0 deletions src/trace/internal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The 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 { TraceStateImpl } from './tracestate-impl';


export function createTraceState(rawTraceState?: string): TraceStateImpl {
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
return new TraceStateImpl(rawTraceState);
}
45 changes: 45 additions & 0 deletions src/trace/internal/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright The 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.
*/

const VALID_KEY_CHAR_RANGE = '[_0-9a-z-*/]';
const VALID_KEY = `[a-z]${VALID_KEY_CHAR_RANGE}{0,255}`;
const VALID_VENDOR_KEY = `[a-z0-9]${VALID_KEY_CHAR_RANGE}{0,240}@[a-z]${VALID_KEY_CHAR_RANGE}{0,13}`;
const VALID_KEY_REGEX = new RegExp(`^(?:${VALID_KEY}|${VALID_VENDOR_KEY})$`);
const VALID_VALUE_BASE_REGEX = /^[ -~]{0,255}[!-~]$/;
const INVALID_VALUE_COMMA_EQUAL_REGEX = /,|=/;

/**
* Key is opaque string up to 256 characters printable. It MUST begin with a
* lowercase letter, and can only contain lowercase letters a-z, digits 0-9,
* underscores _, dashes -, asterisks *, and forward slashes /.
* For multi-tenant vendor scenarios, an at sign (@) can be used to prefix the
* vendor name. Vendors SHOULD set the tenant ID at the beginning of the key.
* see https://www.w3.org/TR/trace-context/#key
*/
export function validateKey(key: string): boolean {
return VALID_KEY_REGEX.test(key);
}

/**
* Value is opaque string up to 256 characters printable ASCII RFC0020
* characters (i.e., the range 0x20 to 0x7E) except comma , and =.
*/
export function validateValue(value: string): boolean {
return (
VALID_VALUE_BASE_REGEX.test(value) &&
!INVALID_VALUE_COMMA_EQUAL_REGEX.test(value)
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
);
}
92 changes: 92 additions & 0 deletions test/trace/tracestate-validators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright The 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 * as assert from 'assert';
import { validateKey, validateValue } from '../../src/trace/internal/validators';

describe('validators', () => {
describe('validateKey', () => {
const validKeysTestCases = [
'abcdefghijklmnopqrstuvwxyz0123456789-_*/',
'baz-',
'baz_',
'baz*',
'baz*bar',
'baz/',
'tracestate',
'fw529a3039@dt',
'6cab5bb-29a@dt',
];
validKeysTestCases.forEach(testCase =>
it(`returns true when key contains valid chars ${testCase}`, () => {
assert.ok(validateKey(testCase), `${testCase} should be valid`);
})
);

const invalidKeysTestCases = [
'1_key',
'kEy_1',
'k'.repeat(257),
'key,',
'TrAcEsTaTE',
'TRACESTATE',
'',
'6num',
];
invalidKeysTestCases.forEach(testCase =>
it(`returns true when key contains invalid chars ${testCase}`, () => {
assert.ok(!validateKey(testCase), `${testCase} should be invalid`);
})
);
});

describe('validateValue', () => {
const validValuesTestCases = [
'first second',
'baz*',
'baz$',
'baz@',
'first-second',
'baz~bar',
'test-v1:120',
'-second',
'first.second',
'TrAcEsTaTE',
'TRACESTATE',
];
validValuesTestCases.forEach(testCase =>
it(`returns true when value contains valid chars ${testCase}`, () => {
assert.ok(validateValue(testCase));
})
);

const invalidValuesTestCases = [
'my_value=5',
'first,second',
'first ',
'k'.repeat(257),
',baz',
'baz,',
'baz=',
'',
];
invalidValuesTestCases.forEach(testCase =>
it(`returns true when value contains invalid chars ${testCase}`, () => {
assert.ok(!validateValue(testCase));
})
);
});
});
Loading