This repository has been archived by the owner on Nov 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add tracestate implementation to api (#147)
Co-authored-by: Valentin Marchaud <[email protected]>
- Loading branch information
1 parent
aa65fc6
commit 82842c7
Showing
6 changed files
with
410 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* 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)) { | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}) | ||
); | ||
}); | ||
}); |
Oops, something went wrong.