diff --git a/docs/conf.py b/docs/conf.py index 538472cf052..451bca4ef07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,7 @@ sys.path[:0] = [ os.path.abspath("../opentelemetry-api/src/"), + os.path.abspath("../opentelemetry-sdk/src/"), os.path.abspath("../ext/opentelemetry-ext-opentracing-shim/src/"), ] diff --git a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py index a9e74dbc58f..0ac92b5aef2 100644 --- a/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py +++ b/ext/opentelemetry-ext-opentracing-shim/src/opentelemetry/ext/opentracing_shim/__init__.py @@ -86,6 +86,7 @@ import opentelemetry.trace as trace_api from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util +from opentelemetry.sdk.trace import Span logger = logging.getLogger(__name__) @@ -323,7 +324,8 @@ class ScopeShim(opentracing.Scope): store the context manager as an attribute so that it can later be closed by calling its ``__exit__()`` method. Defaults to `None`. - TODO: Is :class:`contextlib.AbstractContextManager` the correct type for *span_cm*? + TODO: Is :class:`contextlib.AbstractContextManager` the correct type for + *span_cm*? """ def __init__(self, manager, span, span_cm=None): @@ -660,12 +662,14 @@ def inject(self, span_context, format, carrier): # uses the configured propagators in opentelemetry.propagators. # TODO: Support Format.BINARY once it is supported in # opentelemetry-python. + if format not in self._supported_formats: raise opentracing.UnsupportedFormatException propagator = propagators.get_global_httptextformat() + propagator.inject( - span_context.unwrap(), type(carrier).__setitem__, carrier + Span("", span_context.unwrap()), type(carrier).__setitem__, carrier ) def extract(self, format, carrier): diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 0daabf199a0..1f5794d922e 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -13,7 +13,7 @@ # limitations under the License. import time -import unittest +from unittest import TestCase import opentracing @@ -24,7 +24,7 @@ from opentelemetry.sdk.trace import Tracer -class TestShim(unittest.TestCase): +class TestShim(TestCase): # pylint: disable=too-many-public-methods def setUp(self): @@ -485,6 +485,7 @@ def test_inject_text_map(self): # Verify Format.TEXT_MAP text_map = {} + self.shim.inject(context, opentracing.Format.TEXT_MAP, text_map) self.assertEqual(text_map[MockHTTPTextFormat.TRACE_ID_KEY], str(1220)) self.assertEqual(text_map[MockHTTPTextFormat.SPAN_ID_KEY], str(7478)) @@ -550,6 +551,10 @@ def extract(cls, get_from_carrier, carrier): ) @classmethod - def inject(cls, context, set_in_carrier, carrier): - set_in_carrier(carrier, cls.TRACE_ID_KEY, str(context.trace_id)) - set_in_carrier(carrier, cls.SPAN_ID_KEY, str(context.span_id)) + def inject(cls, span, set_in_carrier, carrier): + set_in_carrier( + carrier, cls.TRACE_ID_KEY, str(span.get_context().trace_id) + ) + set_in_carrier( + carrier, cls.SPAN_ID_KEY, str(span.get_context().span_id) + ) diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 9b6098a9a42..f93b445a2a4 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -15,7 +15,7 @@ import abc import typing -from opentelemetry.trace import SpanContext +from opentelemetry.trace import Span, SpanContext _T = typing.TypeVar("_T") @@ -95,7 +95,7 @@ def extract( @abc.abstractmethod def inject( - self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T + self, span: Span, set_in_carrier: Setter[_T], carrier: _T ) -> None: """Inject values from a SpanContext into a carrier. diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py index 5d00632ed17..6f50f008394 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py @@ -105,10 +105,13 @@ def extract( @classmethod def inject( cls, - context: trace.SpanContext, + span: trace.Span, set_in_carrier: httptextformat.Setter[_T], carrier: _T, ) -> None: + + context = span.get_context() + if context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index bb75d84c3a4..3974a4cb03a 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -64,7 +64,7 @@ def inject( should know how to set header values on the carrier. """ get_global_httptextformat().inject( - tracer.get_current_span().get_context(), set_in_carrier, carrier + tracer.get_current_span(), set_in_carrier, carrier ) diff --git a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py index ed952e0dbab..c39fd3182ca 100644 --- a/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/context/propagation/test_tracecontexthttptextformat.py @@ -14,6 +14,7 @@ import typing import unittest +from unittest.mock import Mock from opentelemetry import trace from opentelemetry.context.propagation import tracecontexthttptextformat @@ -38,7 +39,8 @@ def test_no_traceparent_header(self): RFC 4.2.2: - If no traceparent header is received, the vendor creates a new trace-id and parent-id that represents the current request. + If no traceparent header is received, the vendor creates a new + trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] span_context = FORMAT.extract(get_as_list, output) @@ -66,8 +68,10 @@ def test_headers_with_tracestate(self): span_context.trace_state, {"foo": "1", "bar": "2", "baz": "3"} ) + mock_span = Mock() + mock_span.configure_mock(**{"get_context.return_value": span_context}) output = {} # type:typing.Dict[str, str] - FORMAT.inject(span_context, dict.__setitem__, output) + FORMAT.inject(mock_span, dict.__setitem__, output) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: self.assertIn(pair, output["tracestate"]) @@ -81,13 +85,16 @@ def test_invalid_trace_id(self): RFC 3.2.2.3 - If the trace-id value is invalid (for example if it contains non-allowed characters or all - zeros), vendors MUST ignore the traceparent. + If the trace-id value is invalid (for example if it contains + non-allowed characters or all zeros), vendors MUST ignore the + traceparent. RFC 3.3 - If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. - Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + If the vendor failed to parse traceparent, it MUST NOT attempt to + parse tracestate. + Note that the opposite is not true: failure to parse tracestate MUST + NOT affect the parsing of traceparent. """ span_context = FORMAT.extract( get_as_list, @@ -101,19 +108,22 @@ def test_invalid_trace_id(self): self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) def test_invalid_parent_id(self): - """If the parent id is invalid, we must ignore the full traceparent header. + """If the parent id is invalid, we must ignore the full traceparent + header. Also ignore any tracestate. RFC 3.2.2.3 - Vendors MUST ignore the traceparent when the parent-id is invalid (for example, - if it contains non-lowercase hex characters). + Vendors MUST ignore the traceparent when the parent-id is invalid (for + example, if it contains non-lowercase hex characters). RFC 3.3 - If the vendor failed to parse traceparent, it MUST NOT attempt to parse tracestate. - Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. + If the vendor failed to parse traceparent, it MUST NOT attempt to parse + tracestate. + Note that the opposite is not true: failure to parse tracestate MUST + NOT affect the parsing of traceparent. """ span_context = FORMAT.extract( get_as_list, @@ -131,15 +141,19 @@ def test_no_send_empty_tracestate(self): RFC 3.3.1.1 - Empty and whitespace-only list members are allowed. Vendors MUST accept empty - tracestate headers but SHOULD avoid sending them. + Empty and whitespace-only list members are allowed. Vendors MUST accept + empty tracestate headers but SHOULD avoid sending them. """ output = {} # type:typing.Dict[str, str] - FORMAT.inject( - trace.SpanContext(self.TRACE_ID, self.SPAN_ID), - dict.__setitem__, - output, + mock_span = Mock() + mock_span.configure_mock( + **{ + "get_context.return_value": trace.SpanContext( + self.TRACE_ID, self.SPAN_ID + ) + } ) + FORMAT.inject(mock_span, dict.__setitem__, output) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -155,7 +169,8 @@ def test_format_not_supported(self): get_as_list, { "traceparent": [ - "00-12345678901234567890123456789012-1234567890123456-00-residue" + "00-12345678901234567890123456789012-" + "1234567890123456-00-residue" ], "tracestate": ["foo=1,bar=2,foo=3"], }, @@ -163,14 +178,14 @@ def test_format_not_supported(self): self.assertEqual(span_context, trace.INVALID_SPAN_CONTEXT) def test_propagate_invalid_context(self): - """Do not propagate invalid trace context. - """ + """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - FORMAT.inject(trace.INVALID_SPAN_CONTEXT, dict.__setitem__, output) + FORMAT.inject(trace.Span(), dict.__setitem__, output) self.assertFalse("traceparent" in output) def test_tracestate_empty_header(self): - """Test tracestate with an additional empty header (should be ignored)""" + """Test tracestate with an additional empty header (should be ignored) + """ span_context = FORMAT.extract( get_as_list, { diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py index 7d59fddb9e5..f85690d3a05 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -27,6 +27,7 @@ class B3Format(HTTPTextFormat): SINGLE_HEADER_KEY = "b3" TRACE_ID_KEY = "x-b3-traceid" SPAN_ID_KEY = "x-b3-spanid" + PARENT_SPAN_ID_KEY = "x-b3-parentspanid" SAMPLED_KEY = "x-b3-sampled" FLAGS_KEY = "x-b3-flags" _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) @@ -55,7 +56,7 @@ def extract(cls, get_from_carrier, carrier): elif len(fields) == 3: trace_id, span_id, sampled = fields elif len(fields) == 4: - trace_id, span_id, sampled, _parent_span_id = fields + trace_id, span_id, sampled, _ = fields else: return trace.INVALID_SPAN_CONTEXT else: @@ -100,14 +101,24 @@ def extract(cls, get_from_carrier, carrier): ) @classmethod - def inject(cls, context, set_in_carrier, carrier): - sampled = (trace.TraceOptions.SAMPLED & context.trace_options) != 0 + def inject( + cls, span, set_in_carrier, carrier + ): # pylint: disable=arguments-differ + sampled = ( + trace.TraceOptions.SAMPLED & span.context.trace_options + ) != 0 set_in_carrier( - carrier, cls.TRACE_ID_KEY, format_trace_id(context.trace_id) + carrier, cls.TRACE_ID_KEY, format_trace_id(span.context.trace_id) ) set_in_carrier( - carrier, cls.SPAN_ID_KEY, format_span_id(context.span_id) + carrier, cls.SPAN_ID_KEY, format_span_id(span.context.span_id) ) + if span.parent is not None: + set_in_carrier( + carrier, + cls.PARENT_SPAN_ID_KEY, + format_span_id(span.parent.context.span_id), + ) set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 12155082692..17f7fdf7cae 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -26,6 +26,28 @@ def get_as_list(dict_object, key): return [value] if value is not None else [] +def get_child_parent_new_carrier(old_carrier): + + parent_context = FORMAT.extract(get_as_list, old_carrier) + + parent = trace.Span("parent", parent_context) + child = trace.Span( + "child", + trace_api.SpanContext( + parent_context.trace_id, + trace.generate_span_id(), + trace_options=parent_context.trace_options, + trace_state=parent_context.trace_state, + ), + parent=parent, + ) + + new_carrier = {} + FORMAT.inject(child, dict.__setitem__, new_carrier) + + return child, parent, new_carrier + + class TestB3Format(unittest.TestCase): @classmethod def setUpClass(cls): @@ -35,40 +57,76 @@ def setUpClass(cls): cls.serialized_span_id = b3_format.format_span_id( trace.generate_span_id() ) + cls.serialized_parent_id = b3_format.format_span_id( + trace.generate_span_id() + ) def test_extract_multi_header(self): """Test the extraction of B3 headers.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.PARENT_SPAN_ID_KEY: self.serialized_parent_id, + FORMAT.SAMPLED_KEY: "1", + } + ) + self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual( + new_carrier[FORMAT.PARENT_SPAN_ID_KEY], + b3_format.format_span_id(parent.context.span_id), ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_single_header(self): """Test the extraction from a single b3 header.""" - carrier = { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( - self.serialized_trace_id, self.serialized_span_id - ) - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + self.serialized_trace_id, self.serialized_span_id + ) + } + ) + self.assertEqual( - new_carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), ) self.assertEqual( - new_carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + + child, parent, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}-1-{}".format( + self.serialized_trace_id, + self.serialized_span_id, + self.serialized_parent_id, + ) + } + ) + + self.assertEqual( + new_carrier[FORMAT.TRACE_ID_KEY], + b3_format.format_trace_id(child.context.trace_id), + ) + self.assertEqual( + new_carrier[FORMAT.SPAN_ID_KEY], + b3_format.format_span_id(child.context.span_id), + ) + self.assertEqual( + new_carrier[FORMAT.PARENT_SPAN_ID_KEY], + b3_format.format_span_id(parent.context.span_id), ) self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") @@ -77,17 +135,18 @@ def test_extract_header_precedence(self): headers. """ single_header_trace_id = self.serialized_trace_id[:-3] + "123" - carrier = { - FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( - single_header_trace_id, self.serialized_span_id - ), - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.SINGLE_HEADER_KEY: "{}-{}".format( + single_header_trace_id, self.serialized_span_id + ), + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: "1", + } + ) + self.assertEqual( new_carrier[FORMAT.TRACE_ID_KEY], single_header_trace_id ) @@ -95,64 +154,65 @@ def test_extract_header_precedence(self): def test_enabled_sampling(self): """Test b3 sample key variants that turn on sampling.""" for variant in ["1", "True", "true", "d"]: - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_disabled_sampling(self): """Test b3 sample key variants that turn off sampling.""" for variant in ["0", "False", "false", None]: - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.SAMPLED_KEY: variant, - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.SAMPLED_KEY: variant, + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "0") def test_flags(self): """x-b3-flags set to "1" should result in propagation.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_flags_and_sampling(self): """Propagate if b3 flags and sampling are set.""" - carrier = { - FORMAT.TRACE_ID_KEY: self.serialized_trace_id, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: self.serialized_trace_id, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" trace_id_64_bit = self.serialized_trace_id[:16] - carrier = { - FORMAT.TRACE_ID_KEY: trace_id_64_bit, - FORMAT.SPAN_ID_KEY: self.serialized_span_id, - FORMAT.FLAGS_KEY: "1", - } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) + + _, _, new_carrier = get_child_parent_new_carrier( + { + FORMAT.TRACE_ID_KEY: trace_id_64_bit, + FORMAT.SPAN_ID_KEY: self.serialized_span_id, + FORMAT.FLAGS_KEY: "1", + } + ) + self.assertEqual( new_carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit )