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

Commit b2c829a

Browse files
reyangSergeyKanzhelev
authored andcommitted
support W3C distributed tracing (part 1) (#251)
1 parent 6ba99d5 commit b2c829a

3 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2018, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import re
16+
from opencensus.trace.tracestate import Tracestate
17+
from opencensus.trace.tracestate import _KEY_FORMAT
18+
from opencensus.trace.tracestate import _VALUE_FORMAT
19+
20+
_DELIMITER_FORMAT = '[ \t]*,[ \t]*'
21+
_MEMBER_FORMAT = '(%s)(=)(%s)' % (_KEY_FORMAT, _VALUE_FORMAT)
22+
23+
_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT)
24+
_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT)
25+
26+
27+
class TracestateStringFormatter(object):
28+
def from_string(self, string):
29+
tracestate = Tracestate()
30+
for member in re.split(_DELIMITER_FORMAT_RE, string):
31+
match = _MEMBER_FORMAT_RE.match(member)
32+
if not match:
33+
raise ValueError('illegal key-value format %r' % (member))
34+
key, eq, value = match.groups()
35+
tracestate[key] = value
36+
return tracestate
37+
38+
def to_string(self, tracestate):
39+
return ','.join(map(
40+
lambda key: key + '=' + tracestate[key],
41+
tracestate
42+
))

opencensus/trace/tracestate.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2018, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import OrderedDict
16+
import re
17+
18+
_KEY_FORMAT = r'[a-z][_0-9a-z\-\*\/]{0,255}'
19+
_VALUE_FORMAT = r'[\x20-\x2b\x2d-\x3c\x3e-\x7e]{1,256}'
20+
21+
_KEY_VALIDATION_RE = re.compile('^' + _KEY_FORMAT + '$')
22+
_VALUE_VALIDATION_RE = re.compile('^' + _VALUE_FORMAT + '$')
23+
24+
25+
class Tracestate(OrderedDict):
26+
def __setitem__(self, key, value):
27+
if not isinstance(key, str):
28+
raise ValueError('key must be an instance of str')
29+
if not re.match(_KEY_VALIDATION_RE, key):
30+
raise ValueError('illegal key provided')
31+
if not isinstance(value, str):
32+
raise ValueError('value must be an instance of str')
33+
if not re.match(_VALUE_VALIDATION_RE, value):
34+
raise ValueError('illegal value provided')
35+
super(Tracestate, self).__setitem__(key, value)
36+
37+
def append(self, key, value):
38+
if self.get(key):
39+
del self[key]
40+
self[key] = value
41+
42+
# make this an optional choice instead of enforcement during put/update
43+
# if the tracestate value size is bigger than 512 characters, the tracer
44+
# CAN decide to forward the tracestate
45+
def is_valid(self):
46+
if len(self) is 0:
47+
return False
48+
# there can be a maximum of 32 list-members in a list
49+
if len(self) > 32:
50+
return False
51+
return True
52+
53+
def prepend(self, key, value):
54+
self[key] = value
55+
if hasattr(self, 'move_to_end'):
56+
self.move_to_end(key, last=False)
57+
else: # less performant way for Python 2.x
58+
copy = OrderedDict(self)
59+
self.clear()
60+
self[key] = value
61+
self.update(copy)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Copyright 2018, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import sys
16+
import unittest
17+
18+
from opencensus.trace.tracestate import Tracestate
19+
from opencensus.trace.propagation.tracestate_string_format \
20+
import TracestateStringFormatter
21+
22+
formatter = TracestateStringFormatter()
23+
24+
class TestTracestate(unittest.TestCase):
25+
26+
def test_ctor_no_arg(self):
27+
state = Tracestate()
28+
self.assertEqual(formatter.to_string(state), '')
29+
30+
def test_ctor_with_dict(self):
31+
state = Tracestate({'foo': '1'})
32+
self.assertEqual(formatter.to_string(state), 'foo=1')
33+
34+
def test_cctor(self):
35+
state = Tracestate(formatter.from_string('foo=1,bar=2,baz=3'))
36+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2,baz=3')
37+
38+
def test_all_allowed_chars(self):
39+
header = ''.join([
40+
# key
41+
''.join(map(chr, range(0x61, 0x7A + 1))), # lcalpha
42+
'0123456789', # DIGIT
43+
'_',
44+
'-',
45+
'*',
46+
'/',
47+
# "="
48+
'=',
49+
# value
50+
''.join(map(chr, range(0x20, 0x2B + 1))),
51+
''.join(map(chr, range(0x2D, 0x3C + 1))),
52+
''.join(map(chr, range(0x3E, 0x7E + 1))),
53+
])
54+
state = formatter.from_string(header)
55+
self.assertEqual(formatter.to_string(state), header)
56+
57+
def test_delimiter(self):
58+
state = formatter.from_string('foo=1, \t bar=2')
59+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2')
60+
61+
state = formatter.from_string('foo=1,\t \tbar=2')
62+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2')
63+
64+
def test_get(self):
65+
state = Tracestate({'foo': '1'})
66+
self.assertEqual(state['foo'], '1')
67+
68+
def test_method_append(self):
69+
state = Tracestate()
70+
state.append('foo', '1')
71+
state.append('bar', '2')
72+
state.append('baz', '3')
73+
state.append('bar', '2')
74+
self.assertEqual(formatter.to_string(state), 'foo=1,baz=3,bar=2')
75+
76+
def test_method_from_string(self):
77+
state = formatter.from_string('foo=1,bar=2,baz=3')
78+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2,baz=3')
79+
80+
self.assertRaises(ValueError, lambda: formatter.from_string('#=#'))
81+
82+
def test_method_get(self):
83+
state = formatter.from_string('foo=1, bar=2, baz=3')
84+
self.assertEqual(state.get('bar'), '2')
85+
86+
def test_method_is_valid(self):
87+
state = Tracestate()
88+
89+
# empty state not allowed
90+
self.assertFalse(state.is_valid())
91+
92+
state['foo'] = 'x' * 256
93+
self.assertTrue(state.is_valid())
94+
95+
# exceeds 32 elements
96+
for i in range(0xa0, 0xa0 + 31):
97+
state['%x' % (i)] = 'E'
98+
self.assertEqual(len(state), 32)
99+
self.assertTrue(state.is_valid())
100+
state['ff'] = 'E'
101+
self.assertFalse(state.is_valid())
102+
103+
def test_method_prepend(self):
104+
state = Tracestate()
105+
state.prepend('foo', '1')
106+
state.prepend('baz', '3')
107+
state.prepend('bar', '2')
108+
self.assertEqual(formatter.to_string(state), 'bar=2,baz=3,foo=1')
109+
110+
# modified key-value pair MUST be moved to the beginning of the list
111+
state.prepend('foo', '1')
112+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2,baz=3')
113+
114+
def test_pop(self):
115+
state = formatter.from_string('foo=1,bar=2,baz=3')
116+
state.popitem()
117+
self.assertEqual(formatter.to_string(state), 'foo=1,bar=2')
118+
state.popitem()
119+
self.assertEqual(formatter.to_string(state), 'foo=1')
120+
state.popitem()
121+
self.assertEqual(formatter.to_string(state), '')
122+
# raise KeyError exception while trying to pop from nothing
123+
self.assertRaises(KeyError, lambda: state.popitem())
124+
125+
def test_set(self):
126+
state = Tracestate({'bar': '0'})
127+
state['foo'] = '1'
128+
state['bar'] = '2'
129+
state['baz'] = '3'
130+
self.assertEqual(formatter.to_string(state), 'bar=2,foo=1,baz=3')
131+
132+
# key SHOULD be string
133+
self.assertRaises(ValueError, lambda: state.__setitem__(123, 'abc'))
134+
# value SHOULD NOT be empty string
135+
self.assertRaises(ValueError, lambda: state.__setitem__('', 'abc'))
136+
# key SHOULD start with a letter
137+
self.assertRaises(ValueError, lambda: state.__setitem__('123', 'abc'))
138+
# key SHOULD NOT have uppercase
139+
self.assertRaises(ValueError, lambda: state.__setitem__('FOO', 'abc'))
140+
141+
# value SHOULD be string
142+
self.assertRaises(ValueError, lambda: state.__setitem__('foo', 123))
143+
# value SHOULD NOT be empty string
144+
self.assertRaises(ValueError, lambda: state.__setitem__('foo', ''))
145+
146+
state['foo'] = 'x' * 256
147+
# throw if value exceeds 256 bytes
148+
self.assertRaises(ValueError, lambda: state.__setitem__('foo', 'x' * 257))

0 commit comments

Comments
 (0)