diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a96c835af5..9ad9ddf09e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,8 @@ The Python special interest group (SIG) meets regularly. See the OpenTelemetry [community](https://github.com/open-telemetry/community#python-sdk) repo for information on this and other language SIGs. -See the [public meeting notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) +See the [public meeting +notes](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit) for a summary description of past meetings. To request edit access join the meeting or get in touch on Gitter. @@ -22,9 +23,9 @@ The standard Python unittest module is used to author unit tests. ## Design Choices As with other OpenTelemetry clients, opentelemetry-python follows the -[opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification). +(opentelemetry-specification)[https://github.com/open-telemetry/opentelemetry-specification]. -It's especially valuable to read through the [library guidelines](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md). +It's especially valuable to read through the (library guidelines)[https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/library-guidelines.md]. ### Focus on Capabilities, Not Structure Compliance @@ -43,4 +44,4 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp ## Styleguide * docstrings should adhere to the Google styleguide as specified - with the [napolean extension](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy) extension in [Sphinx](http://www.sphinx-doc.org/en/master/index.html). \ No newline at end of file + with the (napolean extension)[http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#google-vs-numpy] extension in (Sphinx)[http://www.sphinx-doc.org/en/master/index.html]. \ No newline at end of file diff --git a/README.md b/README.md index edea6801e32..e04d9e3cc6d 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,4 @@ The Python [OpenTelemetry](https://opentelemetry.io/) client. ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +See (CONTRIBUTING.md)[CONTRIBUTING.md] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 8991f2b5df5..90385dba4a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,9 +15,8 @@ abstract types for OpenTelemetry implementations. :maxdepth: 1 :caption: Contents: - opentelemetry.context - opentelemetry.loader opentelemetry.trace + opentelemetry.loader Indices and tables diff --git a/docs/opentelemetry.context.base_context.rst b/docs/opentelemetry.context.base_context.rst deleted file mode 100644 index ac28d40008e..00000000000 --- a/docs/opentelemetry.context.base_context.rst +++ /dev/null @@ -1,7 +0,0 @@ -opentelemetry.context.base\_context module -========================================== - -.. automodule:: opentelemetry.context.base_context - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/opentelemetry.context.rst b/docs/opentelemetry.context.rst deleted file mode 100644 index 7bc738a0500..00000000000 --- a/docs/opentelemetry.context.rst +++ /dev/null @@ -1,14 +0,0 @@ -opentelemetry.context package -============================= - -Submodules ----------- - -.. toctree:: - - opentelemetry.context.base_context - -Module contents ---------------- - -.. automodule:: opentelemetry.context diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 4edbf2bd048..9a58d4f23b5 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -11,15 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - - """ The OpenTelemetry context module provides abstraction layer on top of thread-local storage and contextvars. The long term direction is to switch to contextvars provided by the Python runtime library. A global object ``Context`` is provided to access all the context related -functionalities:: +functionalities: >>> from opentelemetry.context import Context >>> Context.foo = 1 @@ -27,9 +25,8 @@ >>> Context.foo 2 -When explicit thread is used, a helper function -``Context.with_current_context`` can be used to carry the context across -threads:: +When explicit thread is used, a helper function `Context.with_current_context` +can be used to carry the context across threads: from threading import Thread from opentelemetry.context import Context @@ -62,7 +59,7 @@ def work(name): print('Main thread:', Context) -Here goes another example using thread pool:: +Here goes another example using thread pool: import time import threading @@ -97,7 +94,7 @@ def work(name): pool.join() println('Main thread: {}'.format(Context)) -Here goes a simple demo of how async could work in Python 3.7+:: +Here goes a simple demo of how async could work in Python 3.7+: import asyncio @@ -141,9 +138,9 @@ async def main(): import typing from .base_context import BaseRuntimeContext +from .unified_context import UnifiedContext -__all__ = ['Context'] - +__all__ = ['Context', 'UnifiedContext'] Context: typing.Optional[BaseRuntimeContext] diff --git a/opentelemetry-api/src/opentelemetry/context/base_context.py b/opentelemetry-api/src/opentelemetry/context/base_context.py index bb5703e7c32..35ee179a4b8 100644 --- a/opentelemetry-api/src/opentelemetry/context/base_context.py +++ b/opentelemetry-api/src/opentelemetry/context/base_context.py @@ -37,7 +37,7 @@ def set(self, value: 'object') -> None: raise NotImplementedError _lock = threading.Lock() - _slots: typing.Dict[str, 'BaseRuntimeContext.Slot'] = {} + _slots: typing.Dict[str, Slot] = {} @classmethod def clear(cls) -> None: @@ -48,11 +48,7 @@ def clear(cls) -> None: slot.clear() @classmethod - def register_slot( - cls, - name: str, - default: 'object' = None, - ) -> 'BaseRuntimeContext.Slot': + def register_slot(cls, name: str, default: 'object' = None) -> 'Slot': """Register a context slot with an optional default value. :type name: str diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py index f05ef699721..b3e8b27ec3d 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py @@ -13,9 +13,8 @@ # limitations under the License. import abc -import typing -from opentelemetry.trace import SpanContext +from opentelemetry.context import UnifiedContext class BinaryFormat(abc.ABC): @@ -26,14 +25,14 @@ class BinaryFormat(abc.ABC): """ @staticmethod @abc.abstractmethod - def to_bytes(context: SpanContext) -> bytes: + def to_bytes(context: UnifiedContext) -> bytes: """Creates a byte representation of a SpanContext. to_bytes should read values from a SpanContext and return a data format to represent it, in bytes. Args: - context: the SpanContext to serialize + context: the SpanContext to serialize. Returns: A bytes representation of the SpanContext. @@ -41,15 +40,17 @@ def to_bytes(context: SpanContext) -> bytes: """ @staticmethod @abc.abstractmethod - def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]: - """Return a SpanContext that was represented by bytes. + def from_bytes(context: UnifiedContext, + byte_representation: bytes) -> None: + """Populate UnifiedContext that was represented by bytes. - from_bytes should return back a SpanContext that was constructed from - the data serialized in the byte_representation passed. If it is not + from_bytes should populated UnifiedContext with data that was + serialized in the byte_representation passed. If it is not possible to read in a proper SpanContext, return None. Args: - byte_representation: the bytes to deserialize + context: The UnifiedContext to populate. + byte_representation: the bytes to deserialize. Returns: A bytes representation of the SpanContext if it is valid. diff --git a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py index 860498fe35d..023e0bab728 100644 --- a/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py +++ b/opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py @@ -15,6 +15,7 @@ import abc import typing +from opentelemetry.context import UnifiedContext from opentelemetry.trace import SpanContext Setter = typing.Callable[[object, str, str], None] @@ -35,11 +36,12 @@ class HTTPTextFormat(abc.ABC): import flask import requests from opentelemetry.context.propagation import HTTPTextFormat + from opentelemetry.trace import tracer + from opentelemetry.context import UnifiedContext PROPAGATOR = HTTPTextFormat() - def get_header_from_flask_request(request, key): return request.headers.get_all(key) @@ -48,15 +50,17 @@ def set_header_into_requests_request(request: requests.Request, request.headers[key] = value def example_route(): - span_context = PROPAGATOR.extract( - get_header_from_flask_request, + span = tracer().create_span("") + context = UnifiedContext.create(span) + PROPAGATOR.extract( + context, get_header_from_flask_request, flask.request ) request_to_downstream = requests.Request( "GET", "http://httpbin.org/get" ) PROPAGATOR.inject( - span_context, + context, set_header_into_requests_request, request_to_downstream ) @@ -68,15 +72,17 @@ def example_route(): https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md """ @abc.abstractmethod - def extract(self, get_from_carrier: Getter, - carrier: object) -> SpanContext: - """Create a SpanContext from values in the carrier. + def extract(self, context: UnifiedContext, get_from_carrier: Getter, + carrier: object) -> None: + """Extract values from the carrier into the context. The extract function should retrieve values from the carrier - object using get_from_carrier, and use values to populate a - SpanContext value and return it. + object using get_from_carrier, and use values to populate + attributes of the UnifiedContext passed in. Args: + context: A UnifiedContext instance that will be + populated with values from the carrier. get_from_carrier: a function that can retrieve zero or more values from the carrier. In the case that the value does not exist, return an empty list. diff --git a/opentelemetry-api/src/opentelemetry/context/unified_context.py b/opentelemetry-api/src/opentelemetry/context/unified_context.py new file mode 100644 index 00000000000..7da5200d254 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/context/unified_context.py @@ -0,0 +1,65 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.distributedcontext import DistributedContext +from opentelemetry.trace import SpanContext + + +class UnifiedContext: + """A unified context object that contains all context relevant to + telemetry. + + The UnifiedContext is a single object that composes all contexts that + are needed by the various forms of telemetry. It is intended to be an + object that can be passed as the argument to any component that needs + to read or modify content values (such as propagators). By unifying + all context in a composed data structure, it expands the flexibility + of the APIs that modify it. + + As it is designed to carry context specific to all telemetry use + cases, it's schema is explicit. Note that this is not intended to + be an object that acts as a singleton that returns different results + based on the thread or coroutine of execution. For that, see `Context`. + + + Args: + distributed: The DistributedContext for this instance. + span: The SpanContext for this instance. + """ + __slots__ = ["distributed", "span"] + + def __init__(self, distributed: DistributedContext, span: SpanContext): + self.distributed = distributed + self.span = span + + @staticmethod + def create(span: SpanContext) -> "UnifiedContext": + """Create an unpopulated UnifiedContext object. + + Example: + + from opentelemetry.trace import tracer + span = tracer.create_span("") + context = UnifiedContext.create(span) + + + Args: + parent_span: the parent SpanContext that will be the + parent of the span in the UnifiedContext. + """ + return UnifiedContext(DistributedContext(), span) + + def __repr__(self) -> str: + return "{}(distributed={}, span={})".format( + type(self).__name__, repr(self.distributed), repr(self.span)) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py index d853a7bcf65..9efd00db658 100644 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py @@ -11,3 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + + +class DistributedContext: + """A container for values for w3c's Correlation Context.""" + def __repr__(self) -> str: + return "{}()".format(type(self).__name__) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index aed421307ee..f5f082b77ca 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ The OpenTelemetry tracing API describes the classes used to generate distributed traces. @@ -33,9 +32,9 @@ from opentelemetry.trace import tracer # Create a new root span, set it as the current span in context - with tracer.start_span("parent"): + with tracer().start_span("parent"): # Attach a new child and update the current span - with tracer.start_span("child"): + with tracer().start_span("child"): do_work(): # Close child span, set parent as current # Close parent span, set default span as current @@ -46,7 +45,7 @@ from opentelemetry.api.trace import tracer # Explicit parent span assignment - span = tracer.create_span("child", parent=parent) as child: + span = tracer().create_span("child", parent=parent) as child: # The caller is responsible for starting and ending the span span.start() @@ -65,7 +64,6 @@ import typing from opentelemetry import loader -from opentelemetry import types # TODO: quarantine ParentSpan = typing.Optional[typing.Union['Span', 'SpanContext']] @@ -73,7 +71,6 @@ class Span: """A span represents a single operation within a trace.""" - def start(self) -> None: """Sets the current time as the span's start time. @@ -83,7 +80,6 @@ def start(self) -> None: Only the first call to `start` should modify the span, and implementations are free to ignore or raise on further calls. """ - def end(self) -> None: """Sets the current time as the span's end time. @@ -92,7 +88,6 @@ def end(self) -> None: Only the first call to `end` should modify the span, and implementations are free to ignore or raise on further calls. """ - def get_context(self) -> 'SpanContext': """Gets the span's SpanContext. @@ -103,35 +98,6 @@ def get_context(self) -> 'SpanContext': A :class:`.SpanContext` with a copy of this span's immutable state. """ - def set_attribute(self: 'Span', - key: str, - value: types.AttributeValue, - ) -> None: - """Sets an Attribute. - - Sets a single Attribute with the key and value passed as arguments. - """ - - def add_event(self: 'Span', - name: str, - attributes: types.Attributes = None, - ) -> None: - """Adds an Event. - - Adds a single Event with the name and, optionally, attributes passed - as arguments. - """ - - def add_link(self: 'Span', - link_target_context: 'SpanContext', - attributes: types.Attributes = None, - ) -> None: - """Adds a Link to another span. - - Adds a single Link from this Span to another Span identified by the - `SpanContext` passed as argument. - """ - class TraceOptions(int): """A bitmask that represents options specific to the trace. @@ -165,7 +131,6 @@ class TraceState(typing.Dict[str, str]): .. _W3C Trace Context - Tracestate: https://www.w3.org/TR/trace-context/#tracestate-field """ - @classmethod def get_default(cls) -> 'TraceState': return cls() @@ -194,13 +159,11 @@ class SpanContext: options: Trace options to propagate. state: Tracing-system-specific info to propagate. """ - def __init__(self, trace_id: int, span_id: int, trace_options: 'TraceOptions' = None, - trace_state: 'TraceState' = None - ) -> None: + trace_state: 'TraceState' = None) -> None: if trace_options is None: trace_options = DEFAULT_TRACE_OPTIONS if trace_state is None: @@ -211,12 +174,9 @@ def __init__(self, self.trace_state = trace_state def __repr__(self) -> str: - return ("{}(trace_id={}, span_id={})" - .format( - type(self).__name__, - format_trace_id(self.trace_id), - format_span_id(self.span_id) - )) + return ("{}(trace_id={}, span_id={})".format( + type(self).__name__, format_trace_id(self.trace_id), + format_span_id(self.span_id))) def is_valid(self) -> bool: """Get whether this `SpanContext` is valid. @@ -227,8 +187,8 @@ def is_valid(self) -> bool: Returns: True if the `SpanContext` is valid, false otherwise. """ - return (self.trace_id != INVALID_TRACE_ID and - self.span_id != INVALID_SPAN_ID) + return (self.trace_id != INVALID_TRACE_ID + and self.span_id != INVALID_SPAN_ID) class DefaultSpan(Span): @@ -271,13 +231,8 @@ def get_current_span(self) -> 'Span': The currently active :class:`.Span`, or a placeholder span with an invalid :class:`.SpanContext`. """ - # pylint: disable=no-self-use - return INVALID_SPAN - @contextmanager # type: ignore - def start_span(self, - name: str, - parent: ParentSpan = CURRENT_SPAN + def start_span(self, name: str, parent: ParentSpan = CURRENT_SPAN ) -> typing.Iterator['Span']: """Context manager for span creation. @@ -321,13 +276,8 @@ def start_span(self, Yields: The newly-created span. """ - # pylint: disable=unused-argument,no-self-use - yield INVALID_SPAN - - def create_span(self, - name: str, - parent: ParentSpan = CURRENT_SPAN - ) -> 'Span': + def create_span(self, name: str, + parent: ParentSpan = CURRENT_SPAN) -> 'Span': """Creates a span. Creating the span does not start it, and should not affect the tracer's @@ -358,9 +308,6 @@ def create_span(self, Returns: The newly-created span. """ - # pylint: disable=unused-argument,no-self-use - return INVALID_SPAN - @contextmanager # type: ignore def use_span(self, span: 'Span') -> typing.Iterator[None]: """Context manager for controlling a span's lifetime. @@ -374,8 +321,6 @@ def use_span(self, span: 'Span') -> typing.Iterator[None]: Args: span: The span to start and make current. """ - # pylint: disable=unused-argument,no-self-use - yield _TRACER: typing.Optional[Tracer] = None @@ -399,9 +344,8 @@ def tracer() -> Tracer: def set_preferred_tracer_implementation( - factory: typing.Callable[ - [typing.Type[Tracer]], typing.Optional[Tracer]] - ) -> None: + factory: typing.Callable[[typing.Type[Tracer]], typing. + Optional[Tracer]]) -> None: """Set the factory to be used to create the tracer. See :mod:`opentelemetry.loader` for details. diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py deleted file mode 100644 index f1aa402ffa5..00000000000 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2019, OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - -from opentelemetry import trace - - -class TestTracer(unittest.TestCase): - def setUp(self): - self.tracer = trace.Tracer() - - def test_get_current_span(self): - span = self.tracer.get_current_span() - self.assertIsInstance(span, trace.Span) - - def test_start_span(self): - with self.tracer.start_span("") as span: - self.assertIsInstance(span, trace.Span) - - def test_create_span(self): - span = self.tracer.create_span("") - self.assertIsInstance(span, trace.Span) - - def test_use_span(self): - span = trace.Span() - with self.tracer.use_span(span): - pass 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 eaeeb577d2b..23b2e5a73de 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -32,7 +32,7 @@ class B3Format(HTTPTextFormat): _SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"]) @classmethod - def extract(cls, get_from_carrier, carrier): + def extract(cls, context, get_from_carrier, carrier): trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) sampled = 0 @@ -56,7 +56,7 @@ def extract(cls, get_from_carrier, carrier): elif len(fields) == 4: trace_id, span_id, sampled, _parent_span_id = fields else: - return trace.INVALID_SPAN_CONTEXT + return else: trace_id = _extract_first_element( get_from_carrier(carrier, cls.TRACE_ID_KEY)) or trace_id @@ -75,7 +75,7 @@ def extract(cls, get_from_carrier, carrier): if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceOptions.RECORDED - return trace.SpanContext( + context.span = trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=int(trace_id, 16), span_id=int(span_id, 16), @@ -85,11 +85,12 @@ def extract(cls, get_from_carrier, carrier): @classmethod def inject(cls, context, set_in_carrier, carrier): - sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0 + sampled = (trace.TraceOptions.RECORDED + & context.span.trace_options) != 0 set_in_carrier(carrier, cls.TRACE_ID_KEY, - format_trace_id(context.trace_id)) + format_trace_id(context.span.trace_id)) set_in_carrier(carrier, cls.SPAN_ID_KEY, - format_span_id(context.span_id)) + format_span_id(context.span.span_id)) set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0a85c14c67b..44f321e6c96 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -230,7 +230,7 @@ def get_context(self): def set_attribute(self: 'Span', key: str, - value: types.AttributeValue, + value: 'types.AttributeValue' ) -> None: if self.attributes is Span.empty_attributes: self.attributes = BoundedDict(MAX_NUM_ATTRIBUTES) @@ -238,26 +238,22 @@ def set_attribute(self: 'Span', def add_event(self: 'Span', name: str, - attributes: types.Attributes = None, + attributes: 'types.Attributes', ) -> None: if self.events is Span.empty_events: self.events = BoundedList(MAX_NUM_EVENTS) - if attributes is None: - attributes = Span.empty_attributes self.events.append(Event(name, attributes)) def add_link(self: 'Span', - link_target_context: 'trace_api.SpanContext', - attributes: types.Attributes = None, + context: 'trace_api.SpanContext', + attributes: 'types.Attributes', ) -> None: if self.links is Span.empty_links: self.links = BoundedList(MAX_NUM_LINKS) - if attributes is None: - attributes = Span.empty_attributes - self.links.append(Link(link_target_context, attributes)) + self.links.append(Link(context, attributes)) def start(self): - if self.start_time is None: + if self.end_time is None: self.start_time = util.time_ns() def end(self): diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index a24dd01c668..f98b078742e 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -17,6 +17,9 @@ import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace +from opentelemetry.context import UnifiedContext +from opentelemetry.sdk.trace import tracer + FORMAT = b3_format.B3Format() @@ -33,6 +36,11 @@ def setUpClass(cls): cls.serialized_span_id = b3_format.format_span_id( trace.generate_span_id()) + def setUp(self): + span_context = tracer.create_span("").context + self.context = UnifiedContext.create(span_context) + self.carrier = {} + def test_extract_multi_header(self): """Test the extraction of B3 headers.""" carrier = { @@ -40,14 +48,13 @@ def test_extract_multi_header(self): 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) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id) - self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], + self.assertEqual(self.carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_single_header(self): """Test the extraction from a single b3 header.""" @@ -55,14 +62,13 @@ def test_extract_single_header(self): 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) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.TRACE_ID_KEY], self.serialized_trace_id) - self.assertEqual(new_carrier[FORMAT.SPAN_ID_KEY], + self.assertEqual(self.carrier[FORMAT.SPAN_ID_KEY], self.serialized_span_id) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "1") def test_extract_header_precedence(self): """A single b3 header should take precedence over multiple @@ -79,10 +85,9 @@ def test_extract_header_precedence(self): FORMAT.SAMPLED_KEY: "1", } - span_context = FORMAT.extract(get_as_list, carrier) - new_carrier = {} - FORMAT.inject(span_context, dict.__setitem__, new_carrier) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.TRACE_ID_KEY], single_header_trace_id) def test_enabled_sampling(self): @@ -93,10 +98,9 @@ def test_enabled_sampling(self): 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) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "1") def test_disabled_sampling(self): """Test b3 sample key variants that turn off sampling.""" @@ -106,10 +110,9 @@ def test_disabled_sampling(self): 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) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "0") + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "0") def test_flags(self): """x-b3-flags set to "1" should result in propagation.""" @@ -118,10 +121,9 @@ def test_flags(self): 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) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "1") def test_flags_and_sampling(self): """Propagate if b3 flags and sampling are set.""" @@ -130,10 +132,9 @@ def test_flags_and_sampling(self): 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) - self.assertEqual(new_carrier[FORMAT.SAMPLED_KEY], "1") + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.SAMPLED_KEY], "1") def test_64bit_trace_id(self): """64 bit trace ids should be padded to 128 bit trace ids.""" @@ -143,20 +144,22 @@ def test_64bit_trace_id(self): 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) - self.assertEqual(new_carrier[FORMAT.TRACE_ID_KEY], + FORMAT.extract(self.context, get_as_list, carrier) + FORMAT.inject(self.context, dict.__setitem__, self.carrier) + self.assertEqual(self.carrier[FORMAT.TRACE_ID_KEY], "0" * 16 + trace_id_64_bit) def test_invalid_single_header(self): """If an invalid single header is passed, return an invalid SpanContext. """ + self.context.span.trace_id = api_trace.INVALID_TRACE_ID + self.context.span.span_id = api_trace.INVALID_SPAN_ID carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} - span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) - self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) + FORMAT.extract(self.context, get_as_list, carrier) + self.assertEqual(self.context.span.trace_id, + api_trace.INVALID_TRACE_ID) + self.assertEqual(self.context.span.span_id, api_trace.INVALID_SPAN_ID) def test_missing_trace_id(self): """If a trace id is missing, populate an invalid trace id.""" @@ -164,8 +167,9 @@ def test_missing_trace_id(self): FORMAT.SPAN_ID_KEY: self.serialized_span_id, FORMAT.FLAGS_KEY: "1" } - span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.trace_id, api_trace.INVALID_TRACE_ID) + FORMAT.extract(self.context, get_as_list, carrier) + self.assertEqual(self.context.span.trace_id, + api_trace.INVALID_TRACE_ID) def test_missing_span_id(self): """If a trace id is missing, populate an invalid trace id.""" @@ -173,5 +177,5 @@ def test_missing_span_id(self): FORMAT.TRACE_ID_KEY: self.serialized_trace_id, FORMAT.FLAGS_KEY: "1" } - span_context = FORMAT.extract(get_as_list, carrier) - self.assertEqual(span_context.span_id, api_trace.INVALID_SPAN_ID) + FORMAT.extract(self.context, get_as_list, carrier) + self.assertEqual(self.context.span.span_id, api_trace.INVALID_SPAN_ID) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 7a4faf78d11..7a91d23765e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -114,74 +114,6 @@ def test_start_span_explicit(self): self.assertIs(tracer.get_current_span(), root) self.assertIsNotNone(child.end_time) - def test_span_members(self): - context = contextvars.ContextVar('test_span_members') - tracer = trace.Tracer(context) - - other_context1 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id() - ) - other_context2 = trace_api.SpanContext( - trace_id=trace.generate_trace_id(), - span_id=trace.generate_span_id() - ) - - self.assertIsNone(tracer.get_current_span()) - - with tracer.start_span('root') as root: - root.set_attribute('component', 'http') - root.set_attribute('http.method', 'GET') - root.set_attribute('http.url', - 'https://example.com:779/path/12/?q=d#123') - root.set_attribute('http.status_code', 200) - root.set_attribute('http.status_text', 'OK') - root.set_attribute('misc.pi', 3.14) - - # Setting an attribute with the same key as an existing attribute - # SHOULD overwrite the existing attribute's value. - root.set_attribute('attr-key', 'attr-value1') - root.set_attribute('attr-key', 'attr-value2') - - root.add_event('event0') - root.add_event('event1', {'name': 'birthday'}) - - root.add_link(other_context1) - root.add_link(other_context2, {'name': 'neighbor'}) - - # The public API does not expose getters. - # Checks by accessing the span members directly - - self.assertEqual(len(root.attributes), 7) - self.assertEqual(root.attributes['component'], 'http') - self.assertEqual(root.attributes['http.method'], 'GET') - self.assertEqual(root.attributes['http.url'], - 'https://example.com:779/path/12/?q=d#123') - self.assertEqual(root.attributes['http.status_code'], 200) - self.assertEqual(root.attributes['http.status_text'], 'OK') - self.assertEqual(root.attributes['misc.pi'], 3.14) - self.assertEqual(root.attributes['attr-key'], 'attr-value2') - - self.assertEqual(len(root.events), 2) - self.assertEqual(root.events[0], - trace.Event(name='event0', - attributes={})) - self.assertEqual(root.events[1], - trace.Event(name='event1', - attributes={'name': 'birthday'})) - - self.assertEqual(len(root.links), 2) - self.assertEqual(root.links[0].context.trace_id, - other_context1.trace_id) - self.assertEqual(root.links[0].context.span_id, - other_context1.span_id) - self.assertEqual(root.links[0].attributes, {}) - self.assertEqual(root.links[1].context.trace_id, - other_context2.trace_id) - self.assertEqual(root.links[1].context.span_id, - other_context2.span_id) - self.assertEqual(root.links[1].attributes, {'name': 'neighbor'}) - class TestSpan(unittest.TestCase):