diff --git a/opencensus/trace/propagation/trace_context_http_header_format.py b/opencensus/trace/propagation/trace_context_http_header_format.py index c3b07f4de..4b7afa100 100644 --- a/opencensus/trace/propagation/trace_context_http_header_format.py +++ b/opencensus/trace/propagation/trace_context_http_header_format.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import re from opencensus.trace.span_context import SpanContext @@ -22,64 +21,15 @@ _TRACEPARENT_HEADER_NAME = 'traceparent' _TRACESTATE_HEADER_NAME = 'tracestate' -_TRACE_CONTEXT_HEADER_FORMAT = \ - '([0-9a-f]{2})(-([0-9a-f]{32}))(-([0-9a-f]{16}))?(-([0-9a-f]{2}))?' -_TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT) +_TRACEPARENT_HEADER_FORMAT = \ + '^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})' + \ + '(-.*)?[ \t]*$' +_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) class TraceContextPropagator(object): """Propagator for processing the trace context HTTP header format.""" - def from_header(self, header): - """Generate a SpanContext object using the trace context header. - - :type header: str - :param header: Trace context header which was extracted from the HTTP - request headers. - - :rtype: :class:`~opencensus.trace.span_context.SpanContext` - :returns: SpanContext generated from the trace context header. - """ - if header is None: - return SpanContext() - - try: - match = re.search(_TRACE_CONTEXT_HEADER_RE, header) - except TypeError: - logging.warning( - 'Header should be str, got {}. Cannot parse the header.' - .format(header.__class__.__name__)) - raise - - if match: - version = match.group(1) - - if version == '00': - trace_id = match.group(3) - span_id = match.group(5) - trace_options = match.group(7) - - if trace_options is None: - trace_options = 1 - - # Need to convert span_id from hex string to int - span_context = SpanContext( - trace_id=trace_id, - span_id=span_id, - trace_options=TraceOptions(trace_options), - from_header=True) - return span_context - else: - logging.warning( - 'Header format version {} is not supported, generate a new' - 'context instead.'.format(version)) - else: - logging.warning( - 'Cannot parse the header {}, generate a new context instead.' - .format(header)) - - return SpanContext() - def from_headers(self, headers): """Generate a SpanContext object using the W3C Distributed Tracing headers. @@ -91,42 +41,46 @@ def from_headers(self, headers): """ if headers is None: return SpanContext() + header = headers.get(_TRACEPARENT_HEADER_NAME) if header is None: return SpanContext() - header = str(header.encode('utf-8')) - - span_context = self.from_header(header) - header = headers.get(_TRACESTATE_HEADER_NAME) - if header is not None: - span_context.tracestate = \ - TracestateStringFormatter().from_string(header) - - return span_context + match = re.search(_TRACEPARENT_HEADER_FORMAT_RE, header) + if not match: + return SpanContext() - def to_header(self, span_context): - """Convert a SpanContext object to header string, using version 0. + version = match.group(1) + trace_id = match.group(2) + span_id = match.group(3) + trace_options = match.group(4) - :type span_context: - :class:`~opencensus.trace.span_context.SpanContext` - :param span_context: SpanContext object. + if trace_id == '0' * 32 or span_id == '0' * 16: + return SpanContext() - :rtype: str - :returns: A trace context header string in trace context HTTP format. - """ - trace_id = span_context.trace_id - span_id = span_context.span_id - trace_options = span_context.trace_options.enabled + if version == '00': + if match.group(5): + return SpanContext() + if version == 'ff': + return SpanContext() - # Convert the trace options - trace_options = '01' if trace_options else '00' + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + trace_options=TraceOptions(trace_options), + from_header=True) - header = '00-{}-{}-{}'.format( - trace_id, - span_id, - trace_options) - return header + header = headers.get(_TRACESTATE_HEADER_NAME) + if header is None: + return span_context + try: + tracestate = TracestateStringFormatter().from_string(header) + if tracestate.is_valid(): + span_context.tracestate = \ + TracestateStringFormatter().from_string(header) + except ValueError: + pass + return span_context def to_headers(self, span_context): """Convert a SpanContext object to W3C Distributed Tracing headers, @@ -139,8 +93,19 @@ def to_headers(self, span_context): :rtype: dict :returns: W3C Distributed Tracing headers. """ + trace_id = span_context.trace_id + span_id = span_context.span_id + trace_options = span_context.trace_options.enabled + + # Convert the trace options + trace_options = '01' if trace_options else '00' + headers = { - _TRACEPARENT_HEADER_NAME: self.to_header(span_context), + _TRACEPARENT_HEADER_NAME: '00-{}-{}-{}'.format( + trace_id, + span_id, + trace_options + ), } tracestate = span_context.tracestate if tracestate: diff --git a/opencensus/trace/propagation/tracestate_string_format.py b/opencensus/trace/propagation/tracestate_string_format.py index 4aa83ae31..da2882880 100644 --- a/opencensus/trace/propagation/tracestate_string_format.py +++ b/opencensus/trace/propagation/tracestate_string_format.py @@ -32,6 +32,8 @@ def from_string(self, string): if not match: raise ValueError('illegal key-value format %r' % (member)) key, eq, value = match.groups() + if key in tracestate: + raise ValueError('conflict key {!r}'.format(key)) tracestate[key] = value return tracestate diff --git a/opencensus/trace/tracestate.py b/opencensus/trace/tracestate.py index bcf9dad21..7b86bbdcc 100644 --- a/opencensus/trace/tracestate.py +++ b/opencensus/trace/tracestate.py @@ -15,8 +15,12 @@ from collections import OrderedDict import re -_KEY_FORMAT = r'[a-z][_0-9a-z\-\*\/]{0,255}' -_VALUE_FORMAT = r'[\x20-\x2b\x2d-\x3c\x3e-\x7e]{1,256}' +_KEY_WITHOUT_VENDOR_FORMAT = r'[a-z][_0-9a-z\-\*\/]{0,255}' +_KEY_WITH_VENDOR_FORMAT = \ + r'[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}' +_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + '|' + _KEY_WITH_VENDOR_FORMAT +_VALUE_FORMAT = \ + r'[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]' _KEY_VALIDATION_RE = re.compile('^' + _KEY_FORMAT + '$') _VALUE_VALIDATION_RE = re.compile('^' + _VALUE_FORMAT + '$') diff --git a/tests/unit/trace/propagation/test_trace_context_http_header_format.py b/tests/unit/trace/propagation/test_trace_context_http_header_format.py index 8e0f512e8..35aa5790b 100644 --- a/tests/unit/trace/propagation/test_trace_context_http_header_format.py +++ b/tests/unit/trace/propagation/test_trace_context_http_header_format.py @@ -19,20 +19,12 @@ class TestTraceContextPropagator(unittest.TestCase): - def test_from_header_no_header(self): - from opencensus.trace.span_context import SpanContext - - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - span_context = propagator.from_header(None) - - self.assertTrue(isinstance(span_context, SpanContext)) - def test_from_headers_none(self): from opencensus.trace.span_context import SpanContext propagator = trace_context_http_header_format.\ TraceContextPropagator() + span_context = propagator.from_headers(None) self.assertTrue(isinstance(span_context, SpanContext)) @@ -42,6 +34,7 @@ def test_from_headers_empty(self): propagator = trace_context_http_header_format.\ TraceContextPropagator() + span_context = propagator.from_headers({}) self.assertTrue(isinstance(span_context, SpanContext)) @@ -51,135 +44,141 @@ def test_from_headers_with_tracestate(self): propagator = trace_context_http_header_format.\ TraceContextPropagator() + span_context = propagator.from_headers({ - 'traceparent': '00-a66ee7820d074463aff4c617a63e929f-91e072af6a404137-01', + 'traceparent': + '00-12345678901234567890123456789012-1234567890123456-00', 'tracestate': 'foo=1,bar=2,baz=3', }) self.assertTrue(isinstance(span_context, SpanContext)) self.assertTrue(span_context.tracestate) - def test_header_type_error(self): - header = 1234 + def test_from_headers_tracestate_limit(self): + propagator = trace_context_http_header_format.\ + TraceContextPropagator() + span_context = propagator.from_headers({ + 'traceparent': + '00-12345678901234567890123456789012-1234567890123456-00', + 'tracestate': ','.join([ + 'a00=0,a01=1,a02=2,a03=3,a04=4,a05=5,a06=6,a07=7,a08=8,a09=9', + 'b00=0,b01=1,b02=2,b03=3,b04=4,b05=5,b06=6,b07=7,b08=8,b09=9', + 'c00=0,c01=1,c02=2,c03=3,c04=4,c05=5,c06=6,c07=7,c08=8,c09=9', + 'd00=0,d01=1,d02=2', + ]), + }) + + self.assertFalse(span_context.tracestate) + + def test_from_headers_tracestate_duplicated_keys(self): propagator = trace_context_http_header_format.\ TraceContextPropagator() - with self.assertRaises(TypeError): - propagator.from_header(header) + span_context = propagator.from_headers({ + 'traceparent': + '00-12345678901234567890123456789012-1234567890123456-00', + 'tracestate': 'foo=1,bar=2,foo=3', + }) + + self.assertFalse(span_context.tracestate) - def test_header_version_not_support(self): + def test_header_all_zero(self): from opencensus.trace.span_context import SpanContext - header = '01-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7-00' propagator = trace_context_http_header_format. \ TraceContextPropagator() - span_context = propagator.from_header(header) - self.assertTrue(isinstance(span_context, SpanContext)) + trace_id = '00000000000000000000000000000000' + span_context = propagator.from_headers({ + 'traceparent': + '00-00000000000000000000000000000000-1234567890123456-00', + }) - def test_header_match(self): - # Trace option is not enabled. - header = '00-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7-00' - expected_trace_id = '6e0c63257de34c92bf9efcd03927272e' - expected_span_id = '00f067aa0ba902b7' + self.assertNotEqual(span_context.trace_id, trace_id) - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - span_context = propagator.from_header(header) + span_id = '0000000000000000' + span_context = propagator.from_headers({ + 'traceparent': + '00-12345678901234567890123456789012-0000000000000000-00', + }) - self.assertEqual(span_context.trace_id, expected_trace_id) - self.assertEqual(span_context.span_id, expected_span_id) - self.assertFalse(span_context.trace_options.enabled) + self.assertNotEqual(span_context.span_id, span_id) - # Trace option is enabled. - header = '00-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7-01' - expected_trace_id = '6e0c63257de34c92bf9efcd03927272e' - expected_span_id = '00f067aa0ba902b7' + def test_header_version_not_supported(self): + from opencensus.trace.span_context import SpanContext - propagator = trace_context_http_header_format.\ + propagator = trace_context_http_header_format. \ TraceContextPropagator() - span_context = propagator.from_header(header) - - self.assertEqual(span_context.trace_id, expected_trace_id) - self.assertEqual(span_context.span_id, expected_span_id) - self.assertTrue(span_context.trace_options.enabled) - def test_header_match_no_option(self): - header = '00-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7' - expected_trace_id = '6e0c63257de34c92bf9efcd03927272e' - expected_span_id = '00f067aa0ba902b7' + trace_id = '12345678901234567890123456789012' + span_context = propagator.from_headers({ + 'traceparent': + 'ff-12345678901234567890123456789012-1234567890123456-00', + }) - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - span_context = propagator.from_header(header) + self.assertNotEqual(span_context.trace_id, trace_id) - self.assertEqual(span_context.trace_id, expected_trace_id) - self.assertEqual(span_context.span_id, expected_span_id) - self.assertTrue(span_context.trace_options.enabled) + span_context = propagator.from_headers({ + 'traceparent': + '00-12345678901234567890123456789012-1234567890123456-00-residue', + }) - def test_header_not_match(self): - header = '00-invalid_trace_id-66666-00' - trace_id = 'invalid_trace_id' + self.assertNotEqual(span_context.trace_id, trace_id) + def test_header_match(self): propagator = trace_context_http_header_format.\ TraceContextPropagator() - span_context = propagator.from_header(header) - self.assertNotEqual(span_context.trace_id, trace_id) + trace_id = '12345678901234567890123456789012' + span_id = '1234567890123456' - def test_headers_match(self): - # Trace option is enabled. - headers = { + # Trace option is not enabled. + span_context = propagator.from_headers({ 'traceparent': - '00-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7-01', - } - expected_trace_id = '6e0c63257de34c92bf9efcd03927272e' - expected_span_id = '00f067aa0ba902b7' - - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - span_context = propagator.from_headers(headers) + '00-12345678901234567890123456789012-1234567890123456-00', + }) - self.assertEqual(span_context.trace_id, expected_trace_id) - self.assertEqual(span_context.span_id, expected_span_id) - self.assertTrue(span_context.trace_options.enabled) + self.assertEqual(span_context.trace_id, trace_id) + self.assertEqual(span_context.span_id, span_id) + self.assertFalse(span_context.trace_options.enabled) - def test_to_header(self): - from opencensus.trace import span_context - from opencensus.trace import trace_options + # Trace option is enabled. + span_context = propagator.from_headers({ + 'traceparent': + '00-12345678901234567890123456789012-1234567890123456-01', + }) - trace_id = '6e0c63257de34c92bf9efcd03927272e' - span_id_hex = '00f067aa0ba902b7' - span_context = span_context.SpanContext( - trace_id=trace_id, - span_id=span_id_hex, - trace_options=trace_options.TraceOptions('1')) + self.assertEqual(span_context.trace_id, trace_id) + self.assertEqual(span_context.span_id, span_id) + self.assertTrue(span_context.trace_options.enabled) + def test_header_not_match(self): propagator = trace_context_http_header_format.\ TraceContextPropagator() - header = propagator.to_header(span_context) - expected_header = '00-{}-{}-01'.format( - trace_id, - span_id_hex) + trace_id = 'invalid_trace_id' + span_context = propagator.from_headers({ + 'traceparent': + '00-invalid_trace_id-66666-00', + }) - self.assertEqual(header, expected_header) + self.assertNotEqual(span_context.trace_id, trace_id) def test_to_headers_without_tracestate(self): from opencensus.trace import span_context from opencensus.trace import trace_options - trace_id = '6e0c63257de34c92bf9efcd03927272e' - span_id_hex = '00f067aa0ba902b7' + propagator = trace_context_http_header_format.\ + TraceContextPropagator() + + trace_id = '12345678901234567890123456789012' + span_id_hex = '1234567890123456' span_context = span_context.SpanContext( trace_id=trace_id, span_id=span_id_hex, trace_options=trace_options.TraceOptions('1')) - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - headers = propagator.to_headers(span_context) self.assertTrue('traceparent' in headers) @@ -193,17 +192,17 @@ def test_to_headers_with_empty_tracestate(self): from opencensus.trace import trace_options from opencensus.trace.tracestate import Tracestate - trace_id = '6e0c63257de34c92bf9efcd03927272e' - span_id_hex = '00f067aa0ba902b7' + propagator = trace_context_http_header_format.\ + TraceContextPropagator() + + trace_id = '12345678901234567890123456789012' + span_id_hex = '1234567890123456' span_context = span_context.SpanContext( trace_id=trace_id, span_id=span_id_hex, tracestate=Tracestate(), trace_options=trace_options.TraceOptions('1')) - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - headers = propagator.to_headers(span_context) self.assertTrue('traceparent' in headers) @@ -217,17 +216,17 @@ def test_to_headers_with_tracestate(self): from opencensus.trace import trace_options from opencensus.trace.tracestate import Tracestate - trace_id = '6e0c63257de34c92bf9efcd03927272e' - span_id_hex = '00f067aa0ba902b7' + propagator = trace_context_http_header_format.\ + TraceContextPropagator() + + trace_id = '12345678901234567890123456789012' + span_id_hex = '1234567890123456' span_context = span_context.SpanContext( trace_id=trace_id, span_id=span_id_hex, tracestate=Tracestate(foo = "xyz"), trace_options=trace_options.TraceOptions('1')) - propagator = trace_context_http_header_format.\ - TraceContextPropagator() - headers = propagator.to_headers(span_context) self.assertTrue('traceparent' in headers)