Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit 82842c7

Browse files
feat: add tracestate implementation to api (#147)
Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
1 parent aa65fc6 commit 82842c7

File tree

6 files changed

+410
-0
lines changed

6 files changed

+410
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export * from './trace/SpanOptions';
3434
export * from './trace/status';
3535
export * from './trace/trace_flags';
3636
export * from './trace/trace_state';
37+
export { createTraceState } from './trace/internal/utils';
3738
export * from './trace/tracer_provider';
3839
export * from './trace/tracer';
3940

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { TraceState } from '../trace_state';
18+
import { validateKey, validateValue } from './tracestate-validators';
19+
20+
const MAX_TRACE_STATE_ITEMS = 32;
21+
const MAX_TRACE_STATE_LEN = 512;
22+
const LIST_MEMBERS_SEPARATOR = ',';
23+
const LIST_MEMBER_KEY_VALUE_SPLITTER = '=';
24+
25+
/**
26+
* TraceState must be a class and not a simple object type because of the spec
27+
* requirement (https://www.w3.org/TR/trace-context/#tracestate-field).
28+
*
29+
* Here is the list of allowed mutations:
30+
* - New key-value pair should be added into the beginning of the list
31+
* - The value of any key can be updated. Modified keys MUST be moved to the
32+
* beginning of the list.
33+
*/
34+
export class TraceStateImpl implements TraceState {
35+
private _internalState: Map<string, string> = new Map();
36+
37+
constructor(rawTraceState?: string) {
38+
if (rawTraceState) this._parse(rawTraceState);
39+
}
40+
41+
set(key: string, value: string): TraceStateImpl {
42+
// TODO: Benchmark the different approaches(map vs list) and
43+
// use the faster one.
44+
const traceState = this._clone();
45+
if (traceState._internalState.has(key)) {
46+
traceState._internalState.delete(key);
47+
}
48+
traceState._internalState.set(key, value);
49+
return traceState;
50+
}
51+
52+
unset(key: string): TraceStateImpl {
53+
const traceState = this._clone();
54+
traceState._internalState.delete(key);
55+
return traceState;
56+
}
57+
58+
get(key: string): string | undefined {
59+
return this._internalState.get(key);
60+
}
61+
62+
serialize(): string {
63+
return this._keys()
64+
.reduce((agg: string[], key) => {
65+
agg.push(key + LIST_MEMBER_KEY_VALUE_SPLITTER + this.get(key));
66+
return agg;
67+
}, [])
68+
.join(LIST_MEMBERS_SEPARATOR);
69+
}
70+
71+
private _parse(rawTraceState: string) {
72+
if (rawTraceState.length > MAX_TRACE_STATE_LEN) return;
73+
this._internalState = rawTraceState
74+
.split(LIST_MEMBERS_SEPARATOR)
75+
.reverse() // Store in reverse so new keys (.set(...)) will be placed at the beginning
76+
.reduce((agg: Map<string, string>, part: string) => {
77+
const listMember = part.trim(); // Optional Whitespace (OWS) handling
78+
const i = listMember.indexOf(LIST_MEMBER_KEY_VALUE_SPLITTER);
79+
if (i !== -1) {
80+
const key = listMember.slice(0, i);
81+
const value = listMember.slice(i + 1, part.length);
82+
if (validateKey(key) && validateValue(value)) {
83+
agg.set(key, value);
84+
} else {
85+
// TODO: Consider to add warning log
86+
}
87+
}
88+
return agg;
89+
}, new Map());
90+
91+
// Because of the reverse() requirement, trunc must be done after map is created
92+
if (this._internalState.size > MAX_TRACE_STATE_ITEMS) {
93+
this._internalState = new Map(
94+
Array.from(this._internalState.entries())
95+
.reverse() // Use reverse same as original tracestate parse chain
96+
.slice(0, MAX_TRACE_STATE_ITEMS)
97+
);
98+
}
99+
}
100+
101+
private _keys(): string[] {
102+
return Array.from(this._internalState.keys()).reverse();
103+
}
104+
105+
private _clone(): TraceStateImpl {
106+
const traceState = new TraceStateImpl();
107+
traceState._internalState = new Map(this._internalState);
108+
return traceState;
109+
}
110+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const VALID_KEY_CHAR_RANGE = '[_0-9a-z-*/]';
18+
const VALID_KEY = `[a-z]${VALID_KEY_CHAR_RANGE}{0,255}`;
19+
const VALID_VENDOR_KEY = `[a-z0-9]${VALID_KEY_CHAR_RANGE}{0,240}@[a-z]${VALID_KEY_CHAR_RANGE}{0,13}`;
20+
const VALID_KEY_REGEX = new RegExp(`^(?:${VALID_KEY}|${VALID_VENDOR_KEY})$`);
21+
const VALID_VALUE_BASE_REGEX = /^[ -~]{0,255}[!-~]$/;
22+
const INVALID_VALUE_COMMA_EQUAL_REGEX = /,|=/;
23+
24+
/**
25+
* Key is opaque string up to 256 characters printable. It MUST begin with a
26+
* lowercase letter, and can only contain lowercase letters a-z, digits 0-9,
27+
* underscores _, dashes -, asterisks *, and forward slashes /.
28+
* For multi-tenant vendor scenarios, an at sign (@) can be used to prefix the
29+
* vendor name. Vendors SHOULD set the tenant ID at the beginning of the key.
30+
* see https://www.w3.org/TR/trace-context/#key
31+
*/
32+
export function validateKey(key: string): boolean {
33+
return VALID_KEY_REGEX.test(key);
34+
}
35+
36+
/**
37+
* Value is opaque string up to 256 characters printable ASCII RFC0020
38+
* characters (i.e., the range 0x20 to 0x7E) except comma , and =.
39+
*/
40+
export function validateValue(value: string): boolean {
41+
return (
42+
VALID_VALUE_BASE_REGEX.test(value) &&
43+
!INVALID_VALUE_COMMA_EQUAL_REGEX.test(value)
44+
);
45+
}

src/trace/internal/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { TraceState } from '../trace_state';
18+
import { TraceStateImpl } from './tracestate-impl';
19+
20+
21+
export function createTraceState(rawTraceState?: string): TraceState {
22+
return new TraceStateImpl(rawTraceState);
23+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { validateKey, validateValue } from '../../src/trace/internal/tracestate-validators';
19+
20+
describe('validators', () => {
21+
describe('validateKey', () => {
22+
const validKeysTestCases = [
23+
'abcdefghijklmnopqrstuvwxyz0123456789-_*/',
24+
'baz-',
25+
'baz_',
26+
'baz*',
27+
'baz*bar',
28+
'baz/',
29+
'tracestate',
30+
'fw529a3039@dt',
31+
'6cab5bb-29a@dt',
32+
];
33+
validKeysTestCases.forEach(testCase =>
34+
it(`returns true when key contains valid chars ${testCase}`, () => {
35+
assert.ok(validateKey(testCase), `${testCase} should be valid`);
36+
})
37+
);
38+
39+
const invalidKeysTestCases = [
40+
'1_key',
41+
'kEy_1',
42+
'k'.repeat(257),
43+
'key,',
44+
'TrAcEsTaTE',
45+
'TRACESTATE',
46+
'',
47+
'6num',
48+
];
49+
invalidKeysTestCases.forEach(testCase =>
50+
it(`returns true when key contains invalid chars ${testCase}`, () => {
51+
assert.ok(!validateKey(testCase), `${testCase} should be invalid`);
52+
})
53+
);
54+
});
55+
56+
describe('validateValue', () => {
57+
const validValuesTestCases = [
58+
'first second',
59+
'baz*',
60+
'baz$',
61+
'baz@',
62+
'first-second',
63+
'baz~bar',
64+
'test-v1:120',
65+
'-second',
66+
'first.second',
67+
'TrAcEsTaTE',
68+
'TRACESTATE',
69+
];
70+
validValuesTestCases.forEach(testCase =>
71+
it(`returns true when value contains valid chars ${testCase}`, () => {
72+
assert.ok(validateValue(testCase));
73+
})
74+
);
75+
76+
const invalidValuesTestCases = [
77+
'my_value=5',
78+
'first,second',
79+
'first ',
80+
'k'.repeat(257),
81+
',baz',
82+
'baz,',
83+
'baz=',
84+
'',
85+
];
86+
invalidValuesTestCases.forEach(testCase =>
87+
it(`returns true when value contains invalid chars ${testCase}`, () => {
88+
assert.ok(!validateValue(testCase));
89+
})
90+
);
91+
});
92+
});

0 commit comments

Comments
 (0)