Skip to content

Commit 791baaf

Browse files
committed
tracecontexthttptestformat now works with integration test
The tracecontexthttptextformat now adheres completely to the w3c tracecontext test suite. moving the test endpoint to a non-root, to ensure that the basic example is clear. Adding unit tests to test_tracecontexhttptextformat that were helpful.
1 parent 590543c commit 791baaf

File tree

6 files changed

+111
-81
lines changed

6 files changed

+111
-81
lines changed

examples/trace/server.py

+14-12
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,17 @@
4545
app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)
4646

4747

48-
@app.route("/", methods=["POST"])
48+
@app.route("/")
4949
def index():
50+
"""An example which starts a span within the span created for
51+
the request."""
52+
with trace.tracer().start_span("parent"):
53+
requests.get("https://www.wikipedia.org/wiki/Rabbit")
54+
return "hello"
55+
56+
57+
@app.route("/verify-tracecontext", methods=["POST"])
58+
def verify_tracecontext():
5059
"""Upon reception of some payload, sends a request back to the designated url.
5160
5261
This route is designed to be testable with the w3c tracecontext server / client test.
@@ -64,15 +73,8 @@ def index():
6473
return "hello"
6574

6675

67-
@app.route("/child_span_example")
68-
def child_span_example():
69-
"""An example which starts a span within the span created for
70-
the request."""
71-
with trace.tracer().start_span("parent"):
72-
requests.get("https://www.wikipedia.org/wiki/Rabbit")
73-
return "hello"
74-
75-
7676
if __name__ == "__main__":
77-
app.run(debug=True)
78-
span_processor.shutdown()
77+
try:
78+
app.run(debug=True)
79+
finally:
80+
span_processor.shutdown()

opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py

+29-23
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + "|" + _KEY_WITH_VENDOR_FORMAT
3737
_VALUE_FORMAT = (
38-
r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]?"
38+
r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{1,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]?"
3939
)
4040

4141
_DELIMITER_FORMAT = "[ \t]*,[ \t]*"
@@ -88,17 +88,10 @@ def extract(
8888
if version == "ff":
8989
return trace.generate_span_context()
9090

91-
tracestate = trace.TraceState()
92-
for tracestate_header in get_from_carrier(
91+
tracestate_headers = get_from_carrier(
9392
carrier, cls._TRACESTATE_HEADER_NAME
94-
):
95-
# typing.Dict's update is not recognized by pylint:
96-
# https://github.com/PyCQA/pylint/issues/2420
97-
tracestate.update( # pylint:disable=E1101
98-
_parse_tracestate(tracestate_header)
99-
)
100-
if len(tracestate) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
101-
tracestate = trace.TraceState()
93+
)
94+
tracestate = _parse_tracestate(tracestate_headers)
10295

10396
span_context = trace.SpanContext(
10497
trace_id=int(trace_id, 16),
@@ -131,8 +124,8 @@ def inject(
131124
)
132125

133126

134-
def _parse_tracestate(string: str) -> trace.TraceState:
135-
"""Parse a w3c tracestate header into a TraceState.
127+
def _parse_tracestate(header_list: typing.List[str]) -> trace.TraceState:
128+
"""Parse one or more w3c tracestate header into a TraceState.
136129
137130
Args:
138131
string: the value of the tracestate header.
@@ -141,19 +134,32 @@ def _parse_tracestate(string: str) -> trace.TraceState:
141134
A valid TraceState that contains values extracted from
142135
the tracestate header.
143136
144-
If the format of the TraceState is illegal, all values will
137+
If the format of one headers is illegal, all values will
145138
be discarded and an empty tracestate will be returned.
139+
140+
If the number of keys is beyond the maximum, all values
141+
will be discarded and an empty tracestate will be returned.
146142
"""
147143
tracestate = trace.TraceState()
148-
for member in re.split(_DELIMITER_FORMAT_RE, string):
149-
match = _MEMBER_FORMAT_RE.fullmatch(member)
150-
if not match:
151-
# TODO: log this?
152-
return trace.TraceState()
153-
key, _eq, value = match.groups()
154-
# typing.Dict's update is not recognized by pylint:
155-
# https://github.com/PyCQA/pylint/issues/2420
156-
tracestate[key] = value # pylint:disable=E1137
144+
for header in header_list:
145+
for member in re.split(_DELIMITER_FORMAT_RE, header):
146+
# empty members are valid, but no need to process further.
147+
if not member:
148+
continue
149+
match = _MEMBER_FORMAT_RE.fullmatch(member)
150+
if not match:
151+
# TODO: log this?
152+
return trace.TraceState()
153+
key, _eq, value = match.groups()
154+
if key in tracestate:
155+
# duplicate keys are not legal in
156+
# the header, so we will remove
157+
return trace.TraceState()
158+
# typing.Dict's update is not recognized by pylint:
159+
# https://github.com/PyCQA/pylint/issues/2420
160+
tracestate[key] = value # pylint:disable=E1137
161+
if len(tracestate) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
162+
tracestate = trace.TraceState()
157163
return tracestate
158164

159165

opentelemetry-api/src/opentelemetry/trace/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -360,10 +360,11 @@ def __init__(
360360
self.trace_state = trace_state
361361

362362
def __repr__(self) -> str:
363-
return "{}(trace_id={}, span_id={})".format(
363+
return "{}(trace_id={}, span_id={}, trace_state={})".format(
364364
type(self).__name__,
365365
format_trace_id(self.trace_id),
366366
format_span_id(self.span_id),
367+
repr(self.trace_state),
367368
)
368369

369370
def is_valid(self) -> bool:

opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py

+59-40
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222

2323

2424
def get_as_list(
25-
dict_object: typing.Dict[str, str], key: str
25+
dict_object: typing.Dict[str, typing.List[str]], key: str
2626
) -> typing.List[str]:
2727
value = dict_object.get(key)
28-
return [value] if value is not None else []
28+
return value if value is not None else []
2929

3030

3131
class TestTraceContextFormat(unittest.TestCase):
@@ -44,33 +44,6 @@ def test_no_traceparent_header(self):
4444
span_context = FORMAT.extract(get_as_list, output)
4545
self.assertTrue(isinstance(span_context, trace.SpanContext))
4646

47-
def test_from_headers_tracestate_duplicated_keys(self):
48-
"""If a duplicate tracestate header is present, the most recent entry
49-
is used.
50-
51-
RFC 3.3.1.4
52-
53-
Only one entry per key is allowed because the entry represents that last position in the trace.
54-
Hence vendors must overwrite their entry upon reentry to their tracing system.
55-
56-
For example, if a vendor name is Congo and a trace started in their system and then went through
57-
a system named Rojo and later returned to Congo, the tracestate value would not be:
58-
59-
congo=congosFirstPosition,rojo=rojosFirstPosition,congo=congosSecondPosition
60-
61-
Instead, the entry would be rewritten to only include the most recent position:
62-
63-
congo=congosSecondPosition,rojo=rojosFirstPosition
64-
"""
65-
span_context = FORMAT.extract(
66-
get_as_list,
67-
{
68-
"traceparent": "00-12345678901234567890123456789012-1234567890123456-00",
69-
"tracestate": "foo=1,bar=2,foo=3",
70-
},
71-
)
72-
self.assertEqual(span_context.trace_state, {"foo": "3", "bar": "2"})
73-
7447
def test_headers_with_tracestate(self):
7548
"""When there is a traceparent and tracestate header, data from
7649
both should be addded to the SpanContext.
@@ -82,7 +55,10 @@ def test_headers_with_tracestate(self):
8255
tracestate_value = "foo=1,bar=2,baz=3"
8356
span_context = FORMAT.extract(
8457
get_as_list,
85-
{"traceparent": traceparent_value, "tracestate": tracestate_value},
58+
{
59+
"traceparent": [traceparent_value],
60+
"tracestate": [tracestate_value],
61+
},
8662
)
8763
self.assertEqual(span_context.trace_id, self.TRACE_ID)
8864
self.assertEqual(span_context.span_id, self.SPAN_ID)
@@ -98,7 +74,8 @@ def test_headers_with_tracestate(self):
9874
self.assertEqual(output["tracestate"].count(","), 2)
9975

10076
def test_invalid_trace_id(self):
101-
"""If the trace id is invalid, we must ignore the full traceparent header.
77+
"""If the trace id is invalid, we must ignore the full traceparent header,
78+
and return a random, valid trace.
10279
10380
Also ignore any tracestate.
10481
@@ -115,11 +92,19 @@ def test_invalid_trace_id(self):
11592
span_context = FORMAT.extract(
11693
get_as_list,
11794
{
118-
"traceparent": "00-00000000000000000000000000000000-1234567890123456-00",
119-
"tracestate": "foo=1,bar=2,foo=3",
95+
"traceparent": [
96+
"00-00000000000000000000000000000000-1234567890123456-00"
97+
],
98+
"tracestate": ["foo=1,bar=2,foo=3"],
12099
},
121100
)
122-
self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT)
101+
self.assertNotEqual(
102+
span_context.span_id, trace.INVALID_SPAN_CONTEXT.span_id
103+
)
104+
self.assertNotEqual(
105+
span_context.trace_id, trace.INVALID_SPAN_CONTEXT.trace_id
106+
)
107+
self.assertNotEqual(span_context.span_id, "1234567890123456")
123108

124109
def test_invalid_parent_id(self):
125110
"""If the parent id is invalid, we must ignore the full traceparent header.
@@ -139,11 +124,14 @@ def test_invalid_parent_id(self):
139124
span_context = FORMAT.extract(
140125
get_as_list,
141126
{
142-
"traceparent": "00-00000000000000000000000000000000-0000000000000000-00",
143-
"tracestate": "foo=1,bar=2,foo=3",
127+
"traceparent": [
128+
"00-00000000000000000000000000000000-0000000000000000-00"
129+
],
130+
"tracestate": ["foo=1,bar=2,foo=3"],
144131
},
145132
)
146-
self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT)
133+
self.assertNotEqual(span_context, trace.INVALID_SPAN_CONTEXT)
134+
self.assertEqual(span_context.trace_state, trace.TraceState())
147135

148136
def test_no_send_empty_tracestate(self):
149137
"""If the tracestate is empty, do not set the header.
@@ -174,15 +162,46 @@ def test_format_not_supported(self):
174162
span_context = FORMAT.extract(
175163
get_as_list,
176164
{
177-
"traceparent": "00-12345678901234567890123456789012-1234567890123456-00-residue",
178-
"tracestate": "foo=1,bar=2,foo=3",
165+
"traceparent": [
166+
"00-12345678901234567890123456789012-1234567890123456-00-residue"
167+
],
168+
"tracestate": ["foo=1,bar=2,foo=3"],
179169
},
180170
)
181-
self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT)
171+
self.assertNotEqual(span_context, trace.INVALID_SPAN_CONTEXT)
172+
self.assertNotEqual(span_context.trace_id, self.TRACE_ID)
182173

183174
def test_propagate_invalid_context(self):
184175
"""Do not propagate invalid trace context.
185176
"""
186177
output = {} # type:typing.Dict[str, str]
187178
FORMAT.inject(trace.INVALID_SPAN_CONTEXT, dict.__setitem__, output)
188179
self.assertFalse("traceparent" in output)
180+
181+
def test_tracestate_empty_header(self):
182+
"""Do not propagate invalid trace context.
183+
"""
184+
span_context = FORMAT.extract(
185+
get_as_list,
186+
{
187+
"traceparent": [
188+
"00-12345678901234567890123456789012-1234567890123456-00"
189+
],
190+
"tracestate": ["foo=1", ""],
191+
},
192+
)
193+
self.assertEqual(span_context.trace_state["foo"], "1")
194+
195+
def test_tracestate_header_with_trailing_comma(self):
196+
"""Do not propagate invalid trace context.
197+
"""
198+
span_context = FORMAT.extract(
199+
get_as_list,
200+
{
201+
"traceparent": [
202+
"00-12345678901234567890123456789012-1234567890123456-00"
203+
],
204+
"tracestate": ["foo=1,"],
205+
},
206+
)
207+
self.assertEqual(span_context.trace_state["foo"], "1")

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Copyright 2019, OpenTelemetry Authors
2-
#
32
# Licensed under the Apache License, Version 2.0 (the "License");
43
# you may not use this file except in compliance with the License.
54
# You may obtain a copy of the License at
+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
set -e
2+
# set -e
33
# hard-coding the git tag to ensure stable builds.
44
TRACECONTEXT_GIT_TAG="98f210efd89c63593dce90e2bae0a1bdcb986f51"
55
# clone w3c tracecontext tests
@@ -9,15 +9,18 @@ git clone https://github.com/w3c/trace-context ./target/trace-context
99
cd ./target/trace-context && git checkout $TRACECONTEXT_GIT_TAG && cd -
1010
# start example opentelemetry service, which propagates trace-context by
1111
# default.
12-
python ./examples/trace/server.py &
12+
python ./examples/trace/server.py 1>&2 &
1313
EXAMPLE_SERVER_PID=$!
1414
# give the app server a little time to start up. Not adding some sort
1515
# of delay would cause many of the tracecontext tests to fail being
1616
# unable to connect.
1717
sleep 1
1818
function on-shutdown {
19-
kill $EXAMPLE_SERVER_PID
19+
# send a sigint, to ensure
20+
# it is caught as a KeyboardInterrupt in the
21+
# example service.
22+
kill -2 $EXAMPLE_SERVER_PID
2023
}
2124
trap on-shutdown EXIT
2225
cd ./target/trace-context/test
23-
python test.py http://127.0.0.1:5000/
26+
python test.py http://127.0.0.1:5000/verify-tracecontext

0 commit comments

Comments
 (0)