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 all 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 @@ -34,6 +34,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 './tracestate-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;
}
}
45 changes: 45 additions & 0 deletions src/trace/internal/tracestate-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)
);
}
23 changes: 23 additions & 0 deletions src/trace/internal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { TraceStateImpl } from './tracestate-impl';


export function createTraceState(rawTraceState?: string): TraceState {
return new TraceStateImpl(rawTraceState);
}
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/tracestate-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