From 46bcf63da5def36cd095efb02efa5dd1a8e51009 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 3 Apr 2020 21:37:18 -0700 Subject: [PATCH 01/25] api: adding trace.get_current_span The span context is no longer coupled with the tracer itself. As such, providing a get_current_span method bound to the trace api module rather than a specific tracer is semantically correct, and removes a hurdle where someone who wants to retrieve the current trace would have to create a tracer to do so. renaming and export get_span_in_context to get_current_span, as the intention of the API is similar, and reduces unneeded aliasing and duplication. set_span_in_context is not renamed, as set_current_span would have implied that the span would then be active in the default context, which is only true after attaching the resulting context returned by set_span_in_context. Keeping that name at least implies some asymmetric behavior from get_current_span. --- .../tests/test_server_interceptor.py | 10 +- .../ext/opentracing_shim/__init__.py | 7 +- .../tests/test_shim.py | 7 +- .../src/opentelemetry/ext/wsgi/__init__.py | 1 - .../src/opentelemetry/trace/__init__.py | 289 ++---------------- .../trace/propagation/__init__.py | 11 +- .../propagation/tracecontexthttptextformat.py | 8 +- .../src/opentelemetry/trace/span.py | 274 +++++++++++++++++ .../propagators/test_global_httptextformat.py | 8 +- .../test_tracecontexthttptextformat.py | 17 +- opentelemetry-api/tests/trace/test_globals.py | 22 +- .../sdk/context/propagation/b3_format.py | 7 +- .../src/opentelemetry/sdk/trace/__init__.py | 4 +- .../context/propagation/test_b3_format.py | 13 +- opentelemetry-sdk/tests/trace/test_trace.py | 38 +-- 15 files changed, 370 insertions(+), 346 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/trace/span.py diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index 8dabd11fdf0..966289b4999 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -100,7 +100,7 @@ def test_span_lifetime(self): def handler(request, context): nonlocal active_span_in_handler - active_span_in_handler = tracer.get_current_span() + active_span_in_handler = trace.get_current_span() return b"" server = grpc.server( @@ -113,13 +113,13 @@ def handler(request, context): port = server.add_insecure_port("[::]:0") channel = grpc.insecure_channel("localhost:{:d}".format(port)) - active_span_before_call = tracer.get_current_span() + active_span_before_call = trace.get_current_span() try: server.start() channel.unary_unary("")(b"") finally: server.stop(None) - active_span_after_call = tracer.get_current_span() + active_span_after_call = trace.get_current_span() self.assertIsNone(active_span_before_call) self.assertIsNone(active_span_after_call) @@ -138,7 +138,7 @@ def test_sequential_server_spans(self): active_spans_in_handler = [] def handler(request, context): - active_spans_in_handler.append(tracer.get_current_span()) + active_spans_in_handler.append(trace.get_current_span()) return b"" server = grpc.server( @@ -188,7 +188,7 @@ def test_concurrent_server_spans(self): def handler(request, context): latch() - active_spans_in_handler.append(tracer.get_current_span()) + active_spans_in_handler.append(trace.get_current_span()) return b"" server = grpc.server( 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 9bb6fc8d6bc..8357df56a07 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 @@ -95,10 +95,7 @@ from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ from opentelemetry.trace import DefaultSpan -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace.propagation import set_span_in_context logger = logging.getLogger(__name__) @@ -704,6 +701,6 @@ def get_as_list(dict_object, key): propagator = propagators.get_global_httptextformat() ctx = propagator.extract(get_as_list, carrier) - otel_context = get_span_from_context(ctx).get_context() + otel_context = trace_api.get_current_span(ctx).get_context() return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index 9f62b90ddf1..cfac8b2a3c4 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -26,10 +26,7 @@ from opentelemetry.context import Context from opentelemetry.ext.opentracing_shim import util from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -578,7 +575,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context) + span = trace.get_current_span(context) set_in_carrier( carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 61e8126c196..bd4b24ffb41 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -61,7 +61,6 @@ def hello(): from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ -from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 856745e0771..2150477708e 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -78,6 +78,20 @@ from logging import getLogger from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.trace.propagation import get_current_span +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN, + INVALID_SPAN_CONTEXT, + INVALID_SPAN_ID, + INVALID_TRACE_ID, + DefaultSpan, + Span, + SpanContext, + TraceFlags, + TraceState, +) from opentelemetry.trace.status import Status from opentelemetry.util import types @@ -171,275 +185,6 @@ class SpanKind(enum.Enum): CONSUMER = 4 -class Span(abc.ABC): - """A span represents a single operation within a trace.""" - - @abc.abstractmethod - def end(self, end_time: typing.Optional[int] = None) -> None: - """Sets the current time as the span's end time. - - The span's end time is the wall time at which the operation finished. - - Only the first call to `end` should modify the span, and - implementations are free to ignore or raise on further calls. - """ - - @abc.abstractmethod - def get_context(self) -> "SpanContext": - """Gets the span's SpanContext. - - Get an immutable, serializable identifier for this span that can be - used to create new child spans. - - Returns: - A :class:`.SpanContext` with a copy of this span's immutable state. - """ - - @abc.abstractmethod - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - """Sets an Attribute. - - Sets a single Attribute with the key and value passed as arguments. - """ - - @abc.abstractmethod - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: typing.Optional[int] = None, - ) -> None: - """Adds an `Event`. - - Adds a single `Event` with the name and, optionally, a timestamp and - attributes passed as arguments. Implementations should generate a - timestamp if the `timestamp` argument is omitted. - """ - - @abc.abstractmethod - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - """Adds an `Event`. - - Adds a single `Event` with the name, an event formatter that calculates - the attributes lazily and, optionally, a timestamp. Implementations - should generate a timestamp if the `timestamp` argument is omitted. - """ - - @abc.abstractmethod - def update_name(self, name: str) -> None: - """Updates the `Span` name. - - This will override the name provided via :func:`Tracer.start_span`. - - Upon this update, any sampling behavior based on Span name will depend - on the implementation. - """ - - @abc.abstractmethod - def is_recording_events(self) -> bool: - """Returns whether this span will be recorded. - - Returns true if this Span is active and recording information like - events with the add_event operation and attributes using set_attribute. - """ - - @abc.abstractmethod - def set_status(self, status: Status) -> None: - """Sets the Status of the Span. If used, this will override the default - Span status, which is OK. - """ - - def __enter__(self) -> "Span": - """Invoked when `Span` is used as a context manager. - - Returns the `Span` itself. - """ - return self - - def __exit__( - self, - exc_type: typing.Optional[typing.Type[BaseException]], - exc_val: typing.Optional[BaseException], - exc_tb: typing.Optional[python_types.TracebackType], - ) -> None: - """Ends context manager and calls `end` on the `Span`.""" - - self.end() - - -class TraceFlags(int): - """A bitmask that represents options specific to the trace. - - The only supported option is the "sampled" flag (``0x01``). If set, this - flag indicates that the trace may have been sampled upstream. - - See the `W3C Trace Context - Traceparent`_ spec for details. - - .. _W3C Trace Context - Traceparent: - https://www.w3.org/TR/trace-context/#trace-flags - """ - - DEFAULT = 0x00 - SAMPLED = 0x01 - - @classmethod - def get_default(cls) -> "TraceFlags": - return cls(cls.DEFAULT) - - @property - def sampled(self) -> bool: - return bool(self & TraceFlags.SAMPLED) - - -DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() - - -class TraceState(typing.Dict[str, str]): - """A list of key-value pairs representing vendor-specific trace info. - - Keys and values are strings of up to 256 printable US-ASCII characters. - Implementations should conform to the `W3C Trace Context - Tracestate`_ - spec, which describes additional restrictions on valid field values. - - .. _W3C Trace Context - Tracestate: - https://www.w3.org/TR/trace-context/#tracestate-field - """ - - @classmethod - def get_default(cls) -> "TraceState": - return cls() - - -DEFAULT_TRACE_STATE = TraceState.get_default() - - -def format_trace_id(trace_id: int) -> str: - return "0x{:032x}".format(trace_id) - - -def format_span_id(span_id: int) -> str: - return "0x{:016x}".format(span_id) - - -class SpanContext: - """The state of a Span to propagate between processes. - - This class includes the immutable attributes of a :class:`.Span` that must - be propagated to a span's children and across process boundaries. - - Args: - trace_id: The ID of the trace that this span belongs to. - span_id: This span's ID. - trace_flags: Trace options to propagate. - trace_state: Tracing-system-specific info to propagate. - is_remote: True if propagated from a remote parent. - """ - - def __init__( - self, - trace_id: int, - span_id: int, - is_remote: bool, - trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, - trace_state: "TraceState" = DEFAULT_TRACE_STATE, - ) -> None: - if trace_flags is None: - trace_flags = DEFAULT_TRACE_OPTIONS - if trace_state is None: - trace_state = DEFAULT_TRACE_STATE - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.trace_state = trace_state - self.is_remote = is_remote - - def __repr__(self) -> str: - return ( - "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" - ).format( - type(self).__name__, - format_trace_id(self.trace_id), - format_span_id(self.span_id), - self.trace_state, - self.is_remote, - ) - - def is_valid(self) -> bool: - """Get whether this `SpanContext` is valid. - - A `SpanContext` is said to be invalid if its trace ID or span ID is - invalid (i.e. ``0``). - - Returns: - True if the `SpanContext` is valid, false otherwise. - """ - return ( - self.trace_id != INVALID_TRACE_ID - and self.span_id != INVALID_SPAN_ID - ) - - -class DefaultSpan(Span): - """The default Span that is used when no Span implementation is available. - - All operations are no-op except context propagation. - """ - - def __init__(self, context: "SpanContext") -> None: - self._context = context - - def get_context(self) -> "SpanContext": - return self._context - - def is_recording_events(self) -> bool: - return False - - def end(self, end_time: typing.Optional[int] = None) -> None: - pass - - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - pass - - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: typing.Optional[int] = None, - ) -> None: - pass - - def add_lazy_event( - self, - name: str, - event_formatter: types.AttributesFormatter, - timestamp: typing.Optional[int] = None, - ) -> None: - pass - - def update_name(self, name: str) -> None: - pass - - def set_status(self, status: Status) -> None: - pass - - -INVALID_SPAN_ID = 0x0000000000000000 -INVALID_TRACE_ID = 0x00000000000000000000000000000000 -INVALID_SPAN_CONTEXT = SpanContext( - trace_id=INVALID_TRACE_ID, - span_id=INVALID_SPAN_ID, - is_remote=False, - trace_flags=DEFAULT_TRACE_OPTIONS, - trace_state=DEFAULT_TRACE_STATE, -) -INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) - - class TracerProvider(abc.ABC): @abc.abstractmethod def get_tracer( @@ -497,7 +242,7 @@ class Tracer(abc.ABC): CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) @abc.abstractmethod - def get_current_span(self) -> "Span": + def get_current_span(self) -> "Span": # noqa """Gets the currently active span from the context. If there is no current span, return a placeholder span with an invalid @@ -533,7 +278,7 @@ def start_span( Example:: - # tracer.get_current_span() will be used as the implicit parent. + # trace.get_current_span() will be used as the implicit parent. # If none is found, the created span will be a root instance. with tracer.start_span("one") as child: child.add_event("child's event") @@ -637,7 +382,7 @@ class DefaultTracer(Tracer): All operations are no-op. """ - def get_current_span(self) -> "Span": + def get_current_span(self) -> "Span": # noqa # pylint: disable=no-self-use return INVALID_SPAN diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index f17350c7455..436c510fb2b 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -16,19 +16,22 @@ from opentelemetry import trace as trace_api from opentelemetry.context import get_value, set_value from opentelemetry.context.context import Context +from opentelemetry.trace.span import INVALID_SPAN, Span SPAN_KEY = "current-span" def set_span_in_context( - span: trace_api.Span, context: Optional[Context] = None + span: Span, context: Optional[Context] = None ) -> Context: ctx = set_value(SPAN_KEY, span, context=context) return ctx -def get_span_from_context(context: Optional[Context] = None) -> trace_api.Span: +def get_current_span(context: Optional[Context] = None) -> Optional[Span]: span = get_value(SPAN_KEY, context=context) - if not isinstance(span, trace_api.Span): - return trace_api.INVALID_SPAN + if span is None: + return None + if not isinstance(span, Span): + return INVALID_SPAN return span diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 732ce96c665..cb9470f02c6 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -17,11 +17,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - httptextformat, - set_span_in_context, -) +from opentelemetry.trace.propagation import httptextformat, set_span_in_context # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -121,7 +117,7 @@ def inject( See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ - span_context = get_span_from_context(context).get_context() + span_context = trace.get_current_span(context).get_context() if span_context == trace.INVALID_SPAN_CONTEXT: return diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py new file mode 100644 index 00000000000..c02c9a3f337 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -0,0 +1,274 @@ +import abc +import types as python_types +import typing + +from opentelemetry.trace.status import Status +from opentelemetry.util import types + + +class Span(abc.ABC): + """A span represents a single operation within a trace.""" + + @abc.abstractmethod + def end(self, end_time: typing.Optional[int] = None) -> None: + """Sets the current time as the span's end time. + + The span's end time is the wall time at which the operation finished. + + Only the first call to `end` should modify the span, and + implementations are free to ignore or raise on further calls. + """ + + @abc.abstractmethod + def get_context(self) -> "SpanContext": + """Gets the span's SpanContext. + + Get an immutable, serializable identifier for this span that can be + used to create new child spans. + + Returns: + A :class:`.SpanContext` with a copy of this span's immutable state. + """ + + @abc.abstractmethod + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + """Sets an Attribute. + + Sets a single Attribute with the key and value passed as arguments. + """ + + @abc.abstractmethod + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: + """Adds an `Event`. + + Adds a single `Event` with the name and, optionally, a timestamp and + attributes passed as arguments. Implementations should generate a + timestamp if the `timestamp` argument is omitted. + """ + + @abc.abstractmethod + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: + """Adds an `Event`. + Adds a single `Event` with the name, an event formatter that calculates + the attributes lazily and, optionally, a timestamp. Implementations + should generate a timestamp if the `timestamp` argument is omitted. + """ + + @abc.abstractmethod + def update_name(self, name: str) -> None: + """Updates the `Span` name. + + This will override the name provided via :func:`Tracer.start_span`. + + Upon this update, any sampling behavior based on Span name will depend + on the implementation. + """ + + @abc.abstractmethod + def is_recording_events(self) -> bool: + """Returns whether this span will be recorded. + + Returns true if this Span is active and recording information like + events with the add_event operation and attributes using set_attribute. + """ + + @abc.abstractmethod + def set_status(self, status: Status) -> None: + """Sets the Status of the Span. If used, this will override the default + Span status, which is OK. + """ + + def __enter__(self) -> "Span": + """Invoked when `Span` is used as a context manager. + + Returns the `Span` itself. + """ + return self + + def __exit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[python_types.TracebackType], + ) -> None: + """Ends context manager and calls `end` on the `Span`.""" + + self.end() + + +class TraceFlags(int): + """A bitmask that represents options specific to the trace. + + The only supported option is the "sampled" flag (``0x01``). If set, this + flag indicates that the trace may have been sampled upstream. + + See the `W3C Trace Context - Traceparent`_ spec for details. + + .. _W3C Trace Context - Traceparent: + https://www.w3.org/TR/trace-context/#trace-flags + """ + + DEFAULT = 0x00 + SAMPLED = 0x01 + + @classmethod + def get_default(cls) -> "TraceFlags": + return cls(cls.DEFAULT) + + @property + def sampled(self) -> bool: + return bool(self & TraceFlags.SAMPLED) + + +DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() + + +class TraceState(typing.Dict[str, str]): + """A list of key-value pairs representing vendor-specific trace info. + + Keys and values are strings of up to 256 printable US-ASCII characters. + Implementations should conform to the `W3C Trace Context - Tracestate`_ + spec, which describes additional restrictions on valid field values. + + .. _W3C Trace Context - Tracestate: + https://www.w3.org/TR/trace-context/#tracestate-field + """ + + @classmethod + def get_default(cls) -> "TraceState": + return cls() + + +DEFAULT_TRACE_STATE = TraceState.get_default() + + +class SpanContext: + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + trace_flags: Trace options to propagate. + trace_state: Tracing-system-specific info to propagate. + is_remote: True if propagated from a remote parent. + """ + + def __init__( + self, + trace_id: int, + span_id: int, + is_remote: bool, + trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, + trace_state: "TraceState" = DEFAULT_TRACE_STATE, + ) -> None: + if trace_flags is None: + trace_flags = DEFAULT_TRACE_OPTIONS + if trace_state is None: + trace_state = DEFAULT_TRACE_STATE + self.trace_id = trace_id + self.span_id = span_id + self.trace_flags = trace_flags + self.trace_state = trace_state + self.is_remote = is_remote + + def __repr__(self) -> str: + return ( + "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" + ).format( + type(self).__name__, + _format_trace_id(self.trace_id), + _format_span_id(self.span_id), + self.trace_state, + self.is_remote, + ) + + def is_valid(self) -> bool: + """Get whether this `SpanContext` is valid. + + A `SpanContext` is said to be invalid if its trace ID or span ID is + invalid (i.e. ``0``). + + Returns: + True if the `SpanContext` is valid, false otherwise. + """ + return ( + self.trace_id != INVALID_TRACE_ID + and self.span_id != INVALID_SPAN_ID + ) + + +class DefaultSpan(Span): + """The default Span that is used when no Span implementation is available. + + All operations are no-op except context propagation. + """ + + def __init__(self, context: "SpanContext") -> None: + self._context = context + + def get_context(self) -> "SpanContext": + return self._context + + def is_recording_events(self) -> bool: + return False + + def end(self, end_time: typing.Optional[int] = None) -> None: + pass + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + pass + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: typing.Optional[int] = None, + ) -> None: + pass + + def add_lazy_event( + self, + name: str, + event_formatter: types.AttributesFormatter, + timestamp: typing.Optional[int] = None, + ) -> None: + pass + + def update_name(self, name: str) -> None: + pass + + def set_status(self, status: Status) -> None: + pass + + +INVALID_SPAN_ID = 0x0000000000000000 +INVALID_TRACE_ID = 0x00000000000000000000000000000000 +INVALID_SPAN_CONTEXT = SpanContext( + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +) +INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) + + +def _format_trace_id(trace_id: int) -> str: + return "0x{:032x}".format(trace_id) + + +def _format_span_id(span_id: int) -> str: + return "0x{:016x}".format(span_id) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index cac24f30a08..bb4d2040751 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -17,10 +17,8 @@ from opentelemetry import correlationcontext, trace from opentelemetry.propagators import extract, inject -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace import get_current_span +from opentelemetry.trace.propagation import set_span_in_context def get_as_list( @@ -52,7 +50,7 @@ def test_propagation(self): correlations = correlationcontext.get_correlations(context=ctx) expected = {"key1": "val1", "key2": "val2"} self.assertEqual(correlations, expected) - span_context = get_span_from_context(context=ctx).get_context() + span_context = get_current_span(context=ctx).get_context() self.assertEqual(span_context.trace_id, self.TRACE_ID) self.assertEqual(span_context.span_id, self.SPAN_ID) diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 11a8ecd56e9..36946736127 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -17,7 +17,6 @@ from opentelemetry import trace from opentelemetry.trace.propagation import ( - get_span_from_context, set_span_in_context, tracecontexthttptextformat, ) @@ -46,7 +45,7 @@ def test_no_traceparent_header(self): trace-id and parent-id that represents the current request. """ output = {} # type:typing.Dict[str, typing.List[str]] - span = get_span_from_context(FORMAT.extract(get_as_list, output)) + span = trace.get_current_span(FORMAT.extract(get_as_list, output)) self.assertIsInstance(span.get_context(), trace.SpanContext) def test_headers_with_tracestate(self): @@ -58,7 +57,7 @@ def test_headers_with_tracestate(self): span_id=format(self.SPAN_ID, "016x"), ) tracestate_value = "foo=1,bar=2,baz=3" - span_context = get_span_from_context( + span_context = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -102,7 +101,7 @@ def test_invalid_trace_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -133,7 +132,7 @@ def test_invalid_parent_id(self): Note that the opposite is not true: failure to parse tracestate MUST NOT affect the parsing of traceparent. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -171,7 +170,7 @@ def test_format_not_supported(self): If the version cannot be parsed, return an invalid trace header. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -195,7 +194,7 @@ def test_propagate_invalid_context(self): def test_tracestate_empty_header(self): """Test tracestate with an additional empty header (should be ignored) """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -211,7 +210,7 @@ def test_tracestate_empty_header(self): def test_tracestate_header_with_trailing_comma(self): """Do not propagate invalid trace context. """ - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { @@ -235,7 +234,7 @@ def test_tracestate_keys(self): "foo-_*/bar=bar4", ] ) - span = get_span_from_context( + span = trace.get_current_span( FORMAT.extract( get_as_list, { diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 2e0339b99dd..0d81eb0993c 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -1,7 +1,8 @@ import unittest from unittest.mock import patch -from opentelemetry import trace +from opentelemetry import context, trace +from opentelemetry.trace.propagation import set_span_in_context class TestGlobals(unittest.TestCase): @@ -16,3 +17,22 @@ def test_get_tracer(self): """trace.get_tracer should proxy to the global tracer provider.""" trace.get_tracer("foo", "var") self._mock_tracer_provider.get_tracer.assert_called_with("foo", "var") + + +class TestTracer(unittest.TestCase): + def setUp(self): + self.tracer = trace.DefaultTracer() + + def test_get_current_span(self): + """DefaultTracer's start_span will also + be retrievable via get_current_span + """ + self.assertIs(trace.get_current_span(), None) + span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) + ctx = set_span_in_context(span) + token = context.attach(ctx) + try: + self.assertIs(trace.get_current_span(), span) + finally: + context.detach(token) + self.assertIs(trace.get_current_span(), None) 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 e082ed03e4e..7d92d6da737 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py @@ -16,10 +16,7 @@ import opentelemetry.trace as trace from opentelemetry.context import Context -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -125,7 +122,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context=context) + span = trace.get_current_span(context=context) sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index c162ea5c1ca..1226201c7d9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -497,7 +497,7 @@ def __init__( self.source = source self.instrumentation_info = instrumentation_info - def get_current_span(self): + def get_current_span(self): # pylint: disable return self.source.get_current_span() def start_as_current_span( @@ -655,7 +655,7 @@ def get_tracer( @staticmethod def get_current_span() -> Span: - return context_api.get_value(SPAN_KEY) # type: ignore + return trace_api.get_current_span() def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py index 8f06912b9ec..5fcbb33dc5d 100644 --- a/opentelemetry-sdk/tests/context/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/context/propagation/test_b3_format.py @@ -17,10 +17,7 @@ import opentelemetry.sdk.context.propagation.b3_format as b3_format import opentelemetry.sdk.trace as trace import opentelemetry.trace as trace_api -from opentelemetry.trace.propagation import ( - get_span_from_context, - set_span_in_context, -) +from opentelemetry.trace.propagation import set_span_in_context FORMAT = b3_format.B3Format() @@ -33,7 +30,7 @@ def get_as_list(dict_object, key): def get_child_parent_new_carrier(old_carrier): ctx = FORMAT.extract(get_as_list, old_carrier) - parent_context = get_span_from_context(ctx).get_context() + parent_context = trace_api.get_current_span(ctx).get_context() parent = trace.Span("parent", parent_context) child = trace.Span( @@ -233,7 +230,7 @@ def test_invalid_single_header(self): """ carrier = {FORMAT.SINGLE_HEADER_KEY: "0-1-2-3-4-5-6-7"} ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) @@ -245,7 +242,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_span_id(self): @@ -256,5 +253,5 @@ def test_missing_span_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index ccb1ccd1e4e..e3faa5367c8 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -212,15 +212,17 @@ def test_get_current_span_multiple_tracers(self): with tracer_1.use_span(root, True): self.assertIs(tracer_1.get_current_span(), root) self.assertIs(tracer_2.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) # outside of the loop, both should not reference a span. self.assertIs(tracer_1.get_current_span(), None) self.assertIs(tracer_2.get_current_span(), None) + self.assertIs(trace_api.get_current_span(), None) def test_start_span_implicit(self): tracer = new_tracer() - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -228,7 +230,7 @@ def test_start_span_implicit(self): self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL) with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_span( "child", kind=trace_api.SpanKind.CLIENT @@ -255,7 +257,7 @@ def test_start_span_implicit(self): ) # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) @@ -272,7 +274,7 @@ def test_start_span_explicit(self): trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), ) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) root = tracer.start_span("root") self.assertIsNotNone(root.start_time) @@ -280,7 +282,7 @@ def test_start_span_explicit(self): # Test with the implicit root span with tracer.use_span(root, True): - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_span("stepchild", other_parent) as child: # The child's parent should be the one passed in, @@ -306,30 +308,30 @@ def test_start_span_explicit(self): ) # Verify start_span() did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) # Verify ending the child did not set the current span. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_start_as_current_span_implicit(self): tracer = new_tracer() - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) with tracer.start_as_current_span("child") as child: - self.assertIs(tracer.get_current_span(), child) + self.assertIs(trace_api.get_current_span(), child) self.assertIs(child.parent, root) # After exiting the child's scope the parent should become the # current span again. - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): @@ -341,11 +343,11 @@ def test_start_as_current_span_explicit(self): is_remote=False, ) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) # Test with the implicit root span with tracer.start_as_current_span("root") as root: - self.assertIs(tracer.get_current_span(), root) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(root.start_time) self.assertIsNone(root.end_time) @@ -356,14 +358,14 @@ def test_start_as_current_span_explicit(self): # The child should become the current span as usual, but its # parent should be the one passed in, not the # previously-current span. - self.assertIs(tracer.get_current_span(), child) + self.assertIs(trace_api.get_current_span(), child) self.assertNotEqual(child.parent, root) self.assertIs(child.parent, other_parent) # After exiting the child's scope the last span on the stack should # become current, not the child's parent. - self.assertNotEqual(tracer.get_current_span(), other_parent) - self.assertIs(tracer.get_current_span(), root) + self.assertNotEqual(trace_api.get_current_span(), other_parent) + self.assertIs(trace_api.get_current_span(), root) self.assertIsNotNone(child.end_time) def test_explicit_span_resource(self): @@ -543,7 +545,7 @@ def test_sampling_attributes(self): self.assertEqual(root.attributes["attr-in-both"], "decision-attr") def test_events(self): - self.assertIsNone(self.tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with self.tracer.start_as_current_span("root") as root: # only event name From 3480e412cd857bcfcf2615825c8e832d0aa1da71 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 4 May 2020 22:38:03 -0700 Subject: [PATCH 02/25] fixing lint --- .../tests/test_server_interceptor.py | 10 ---------- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 ++ opentelemetry-api/src/opentelemetry/trace/span.py | 8 ++++---- .../util/src/opentelemetry/test/mock_httptextformat.py | 4 ++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py index a7a28995402..67ff0053bf3 100644 --- a/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py +++ b/ext/opentelemetry-ext-grpc/tests/test_server_interceptor.py @@ -91,19 +91,13 @@ def test_span_lifetime(self): """Check that the span is active for the duration of the call.""" interceptor = server_interceptor() - tracer = self.tracer_provider.get_tracer(__name__) # To capture the current span at the time the handler is called active_span_in_handler = None def handler(request, context): nonlocal active_span_in_handler -<<<<<<< HEAD active_span_in_handler = trace.get_current_span() -======= - # The current span is shared among all the tracers. - active_span_in_handler = tracer.get_current_span() ->>>>>>> source/master return b"" server = grpc.server( @@ -132,8 +126,6 @@ def handler(request, context): def test_sequential_server_spans(self): """Check that sequential RPCs get separate server spans.""" - tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor() # Capture the currently active span in each thread @@ -179,8 +171,6 @@ def test_concurrent_server_spans(self): context. """ - tracer = self.tracer_provider.get_tracer(__name__) - interceptor = server_interceptor() # Capture the currently active span in each thread diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 6c9333ed5cf..5f0e67fe5ca 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -91,6 +91,8 @@ SpanContext, TraceFlags, TraceState, + format_span_id, + format_trace_id, ) from opentelemetry.trace.status import Status from opentelemetry.util import _load_provider, types diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index c02c9a3f337..5eae7f170c8 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -189,8 +189,8 @@ def __repr__(self) -> str: "{}(trace_id={}, span_id={}, trace_state={!r}, is_remote={})" ).format( type(self).__name__, - _format_trace_id(self.trace_id), - _format_span_id(self.span_id), + format_trace_id(self.trace_id), + format_span_id(self.span_id), self.trace_state, self.is_remote, ) @@ -266,9 +266,9 @@ def set_status(self, status: Status) -> None: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) -def _format_trace_id(trace_id: int) -> str: +def format_trace_id(trace_id: int) -> str: return "0x{:032x}".format(trace_id) -def _format_span_id(span_id: int) -> str: +def format_span_id(span_id: int) -> str: return "0x{:016x}".format(span_id) diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_httptextformat.py index 1d4b1d5d511..b1b281c7a60 100644 --- a/tests/util/src/opentelemetry/test/mock_httptextformat.py +++ b/tests/util/src/opentelemetry/test/mock_httptextformat.py @@ -17,7 +17,7 @@ from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.trace.propagation import ( - get_span_from_context, + get_current_span, set_span_in_context, ) from opentelemetry.trace.propagation.httptextformat import ( @@ -62,7 +62,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context) + span = get_current_span(context) set_in_carrier( carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) From 885228cff56f57914e1a60c875a1c99397f351fa Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 4 May 2020 23:01:10 -0700 Subject: [PATCH 03/25] fixing test --- opentelemetry-sdk/tests/trace/test_trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index e4eebfef81a..35eea6f3fda 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -334,7 +334,7 @@ def test_start_as_current_span_implicit(self): with tracer.start_as_current_span("child") as child: self.assertIs(trace_api.get_current_span(), child) - self.assertIs(child.parent, root) + self.assertIs(child.parent, root.get_context()) # After exiting the child's scope the parent should become the # current span again. From aa128fe62ad2545e572613caebccb0e5c4e332d9 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 5 May 2020 09:27:54 -0700 Subject: [PATCH 04/25] Fixing mypy. mypy needs re-exports called out explicitly in one of two ways: * in the __all__ param * using the "import y as x" syntax. Modifying the imports to work in this fashion solves the issue. --- .../src/opentelemetry/__init__.py | 0 .../src/opentelemetry/trace/__init__.py | 29 +++++++++++++++++++ .../propagation/tracecontexthttptextformat.py | 6 ++-- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/__init__.py diff --git a/opentelemetry-api/src/opentelemetry/__init__.py b/opentelemetry-api/src/opentelemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 5f0e67fe5ca..de7e33adefc 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -70,6 +70,35 @@ `set_tracer_provider`. """ +__all__ = [ + "DEFAULT_TRACE_OPTIONS", + "DEFAULT_TRACE_STATE", + "INVALID_SPAN", + "INVALID_SPAN_CONTEXT", + "INVALID_SPAN_ID", + "INVALID_TRACE_ID", + "DefaultSpan", + "DefaultTracer", + "DefaultTracerProvider", + "LazyLink", + "Link", + "LinkBase", + "ParentSpan", + "Span", + "SpanContext", + "SpanKind", + "TraceFlags", + "TraceState", + "TracerProvider", + "Tracer", + "format_span_id", + "format_trace_id", + "get_current_span", + "get_tracer", + "get_tracer_provider", + "set_tracer_provider", +] + import abc import enum import types as python_types diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index cb9470f02c6..1d61c970283 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -117,8 +117,10 @@ def inject( See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` """ - span_context = trace.get_current_span(context).get_context() - + span = trace.get_current_span(context) + if span is None: + return + span_context = span.get_context() if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{:032x}-{:016x}-{:02x}".format( From 167482c23d5c512a49399e3c4c978d3af2aec7de Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 5 May 2020 21:21:52 -0700 Subject: [PATCH 05/25] removing __init__.py It was causing issues with the module import order. --- opentelemetry-api/src/opentelemetry/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 opentelemetry-api/src/opentelemetry/__init__.py diff --git a/opentelemetry-api/src/opentelemetry/__init__.py b/opentelemetry-api/src/opentelemetry/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From ec1c12054b8105528a6271fb042c263d893f9b86 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 5 May 2020 21:37:59 -0700 Subject: [PATCH 06/25] removing recursive import. --- .../src/opentelemetry/trace/propagation/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 436c510fb2b..2a8a5eb921c 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. from typing import Optional -from opentelemetry import trace as trace_api from opentelemetry.context import get_value, set_value from opentelemetry.context.context import Context from opentelemetry.trace.span import INVALID_SPAN, Span From 95b36637b9736630267d62140d1c89383153a38a Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 14 May 2020 22:47:44 -0700 Subject: [PATCH 07/25] Removing get_current_span from Tracer and TracerProvider After discussion in the SIG, we decided to remove the legacy get_current_span APIs from Tracer and TracerProvider to reduce long-term confusion of how to idiomatically retrieve the span. --- .../src/opentelemetry/trace/__init__.py | 24 ++++--------------- opentelemetry-api/tests/trace/test_tracer.py | 4 ---- .../src/opentelemetry/sdk/trace/__init__.py | 5 +--- opentelemetry-sdk/tests/trace/test_trace.py | 22 ++--------------- 4 files changed, 7 insertions(+), 48 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index de7e33adefc..e66054ed577 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -272,18 +272,6 @@ class Tracer(abc.ABC): # This is the default behavior when creating spans. CURRENT_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) - @abc.abstractmethod - def get_current_span(self) -> "Span": # noqa - """Gets the currently active span from the context. - - If there is no current span, return a placeholder span with an invalid - context. - - Returns: - The currently active :class:`.Span`, or a placeholder span with an - invalid :class:`.SpanContext`. - """ - @abc.abstractmethod def start_span( self, @@ -355,11 +343,11 @@ def start_as_current_span( with tracer.start_as_current_span("one") as parent: parent.add_event("parent's event") - with tracer.start_as_current_span("two") as child: + with trace.start_as_current_span("two") as child: child.add_event("child's event") - tracer.get_current_span() # returns child - tracer.get_current_span() # returns parent - tracer.get_current_span() # returns previously active span + trace.get_current_span() # returns child + trace.get_current_span() # returns parent + trace.get_current_span() # returns previously active span This is a convenience method for creating spans attached to the tracer's context. Applications that need more control over the span @@ -413,10 +401,6 @@ class DefaultTracer(Tracer): All operations are no-op. """ - def get_current_span(self) -> "Span": # noqa - # pylint: disable=no-self-use - return INVALID_SPAN - def start_span( self, name: str, diff --git a/opentelemetry-api/tests/trace/test_tracer.py b/opentelemetry-api/tests/trace/test_tracer.py index 4fe3d20f78c..1eb15062305 100644 --- a/opentelemetry-api/tests/trace/test_tracer.py +++ b/opentelemetry-api/tests/trace/test_tracer.py @@ -21,10 +21,6 @@ class TestTracer(unittest.TestCase): def setUp(self): self.tracer = trace.DefaultTracer() - 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) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1187ce94250..86e11205cc1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -593,9 +593,6 @@ def __init__( self.source = source self.instrumentation_info = instrumentation_info - def get_current_span(self): # pylint: disable - return self.source.get_current_span() - def start_as_current_span( self, name: str, @@ -618,7 +615,7 @@ def start_span( # pylint: disable=too-many-locals set_status_on_exception: bool = True, ) -> trace_api.Span: if parent is Tracer.CURRENT_SPAN: - parent = self.get_current_span() + parent = trace_api.get_current_span() parent_context = parent if isinstance(parent_context, trace_api.Span): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 50b794547f4..8f8ff4c6a16 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -211,24 +211,6 @@ def test_span_processor_for_source(self): span2.span_processor, tracer_provider._active_span_processor ) - def test_get_current_span_multiple_tracers(self): - """In the case where there are multiple tracers, - get_current_span will return the same active span - for both tracers. - """ - tracer_1 = new_tracer() - tracer_2 = new_tracer() - root = tracer_1.start_span("root") - with tracer_1.use_span(root, True): - self.assertIs(tracer_1.get_current_span(), root) - self.assertIs(tracer_2.get_current_span(), root) - self.assertIs(trace_api.get_current_span(), root) - - # outside of the loop, both should not reference a span. - self.assertIs(tracer_1.get_current_span(), None) - self.assertIs(tracer_2.get_current_span(), None) - self.assertIs(trace_api.get_current_span(), None) - def test_start_span_implicit(self): tracer = new_tracer() @@ -271,7 +253,7 @@ def test_start_span_implicit(self): self.assertIsNotNone(child.end_time) - self.assertIsNone(tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): @@ -599,7 +581,7 @@ def event_formatter(): self.assertEqual(root.events[4].timestamp, now) def test_invalid_event_attributes(self): - self.assertIsNone(self.tracer.get_current_span()) + self.assertIsNone(trace_api.get_current_span()) with self.tracer.start_as_current_span("root") as root: root.add_event("event0", {"attr1": True, "attr2": ["hi", False]}) From a901b8adbeaed2e35eb74b967adad41935839b4a Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 15 May 2020 20:32:53 -0700 Subject: [PATCH 08/25] removing missed reference to tracer.get_current_span --- .../src/opentelemetry/ext/opentracing_shim/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2403205774..3f52e49cf90 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 @@ -470,7 +470,7 @@ def active(self): shim and is likely to be handled in future versions. """ - span = self._tracer.unwrap().get_current_span() + span = trace_api.get_current_span() if span is None: return None From 791c31b7732ebb8050957ae67b2213974c37078c Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 21 May 2020 22:39:29 -0700 Subject: [PATCH 09/25] docs: fixing doc references Missing references in the documentation were causing issues with dependent class definitions. --- docs/api/trace.rst | 3 ++- docs/api/trace.span.rst | 7 +++++++ opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- opentelemetry-api/src/opentelemetry/trace/span.py | 4 ++-- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 docs/api/trace.span.rst diff --git a/docs/api/trace.rst b/docs/api/trace.rst index 00823aa0362..411e31023ec 100644 --- a/docs/api/trace.rst +++ b/docs/api/trace.rst @@ -8,8 +8,9 @@ Submodules trace.sampling trace.status + trace.span Module contents --------------- -.. automodule:: opentelemetry.trace +.. automodule:: opentelemetry.trace \ No newline at end of file diff --git a/docs/api/trace.span.rst b/docs/api/trace.span.rst new file mode 100644 index 00000000000..e816928287f --- /dev/null +++ b/docs/api/trace.span.rst @@ -0,0 +1,7 @@ +opentelemetry.trace.span +========================== + +.. automodule:: opentelemetry.trace.span + :members: + :undoc-members: + :show-inheritance: diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index e66054ed577..52dc9bbae0a 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -289,7 +289,7 @@ def start_span( span in this tracer's context. By default the current span will be used as parent, but an explicit - parent can also be specified, either a `Span` or a `SpanContext`. If + parent can also be specified, either a `Span` or a `opentelemetry.trace.SpanContext`. If the specified value is `None`, the created span will be a root span. The span can be used as context manager. On exiting, the span will be diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 5eae7f170c8..7f0f39d92a1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -27,7 +27,7 @@ def get_context(self) -> "SpanContext": used to create new child spans. Returns: - A :class:`.SpanContext` with a copy of this span's immutable state. + A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state. """ @abc.abstractmethod @@ -68,7 +68,7 @@ def add_lazy_event( def update_name(self, name: str) -> None: """Updates the `Span` name. - This will override the name provided via :func:`Tracer.start_span`. + This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`. Upon this update, any sampling behavior based on Span name will depend on the implementation. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 86e11205cc1..dde30031d53 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -240,7 +240,7 @@ class Span(trace_api.Span): Args: name: The name of the operation this span represents context: The immutable span context - parent: This span's parent's `SpanContext`, or + parent: This span's parent's `opentelemetry.trace.SpanContext`, or null if this is a root span sampler: The sampler used to create this span trace_config: TODO From e7472e7564078f571b54d8c73d2133a50b241f95 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 23 May 2020 21:36:57 -0700 Subject: [PATCH 10/25] Applying API changes to datadog propagator --- .../src/opentelemetry/ext/datadog/propagator.py | 4 ++-- .../tests/test_datadog_format.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py index d6595e8d93a..86b58b02e9e 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py @@ -17,7 +17,7 @@ from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.trace.propagation import ( - get_span_from_context, + get_current_span, set_span_in_context, ) from opentelemetry.trace.propagation.httptextformat import ( @@ -88,7 +88,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_span_from_context(context=context) + span = get_current_span(context) sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py index cf2fbf42208..9a9b3620826 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py @@ -18,7 +18,7 @@ from opentelemetry.ext.datadog import constants, propagator from opentelemetry.sdk import trace from opentelemetry.trace.propagation import ( - get_span_from_context, + get_current_span, set_span_in_context, ) @@ -45,7 +45,7 @@ def test_malformed_headers(self): """Test with no Datadog headers""" malformed_trace_id_key = FORMAT.TRACE_ID_KEY + "-x" malformed_parent_id_key = FORMAT.PARENT_ID_KEY + "-x" - context = get_span_from_context( + context = get_current_span( FORMAT.extract( get_as_list, { @@ -66,7 +66,7 @@ def test_missing_trace_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = get_current_span(ctx).get_context() self.assertEqual(span_context.trace_id, trace_api.INVALID_TRACE_ID) def test_missing_parent_id(self): @@ -76,12 +76,12 @@ def test_missing_parent_id(self): } ctx = FORMAT.extract(get_as_list, carrier) - span_context = get_span_from_context(ctx).get_context() + span_context = get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) def test_context_propagation(self): """Test the propagation of Datadog headers.""" - parent_context = get_span_from_context( + parent_context = get_current_span( FORMAT.extract( get_as_list, { @@ -138,7 +138,7 @@ def test_context_propagation(self): def test_sampling_priority_auto_reject(self): """Test sampling priority rejected.""" - parent_context = get_span_from_context( + parent_context = get_current_span( FORMAT.extract( get_as_list, { From 236223d839d0890d387faed343c803b704674e04 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 4 Jun 2020 22:45:00 -0700 Subject: [PATCH 11/25] fixing possiblity of empty span for b3 --- .../src/opentelemetry/sdk/trace/propagation/b3_format.py | 4 ++++ .../tests/trace/propagation/test_b3_format.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 984abb34dd7..13120f3209d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -123,6 +123,10 @@ def inject( context: typing.Optional[Context] = None, ) -> None: span = trace.get_current_span(context=context) + + if span is None: + return + sampled = (trace.TraceFlags.SAMPLED & span.context.trace_flags) != 0 set_in_carrier( carrier, self.TRACE_ID_KEY, format_trace_id(span.context.trace_id), diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index 627b5d5b76d..c972fcd24b5 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -18,6 +18,7 @@ import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.trace.propagation import set_span_in_context +from opentelemetry.context import get_current FORMAT = b3_format.B3Format() @@ -255,3 +256,9 @@ def test_missing_span_id(self): ctx = FORMAT.extract(get_as_list, carrier) span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) + + def test_inject_empty_context(self): + """If the current context has no span, don't add headers""" + new_carrier = {} + FORMAT.inject(dict.__setitem__, new_carrier, get_current()) + assert len(new_carrier) == 0 From aad61afe7b88cfea78012c63b2a83a1128607fe8 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 1 Jun 2020 13:04:15 -0700 Subject: [PATCH 12/25] docs: Fix broken link (#763) --- docs/examples/basic_meter/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_meter/README.rst b/docs/examples/basic_meter/README.rst index a48e5bb612d..d77fbb7c4bb 100644 --- a/docs/examples/basic_meter/README.rst +++ b/docs/examples/basic_meter/README.rst @@ -13,7 +13,7 @@ There are three different examples: * observer: Shows how to use the observer instrument. -The source files of these examples are available :scm_web:`here `. +The source files of these examples are available :scm_web:`here `. Installation ------------ From 2f9d22985dcc431885a7e73ce753383515411b5e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 2 Jun 2020 09:49:58 -0700 Subject: [PATCH 13/25] Rename Measure to ValueRecorder (#761) --- docs/examples/basic_meter/basic_metrics.py | 4 +- .../basic_meter/calling_conventions.py | 4 +- .../test_otcollector_metrics_exporter.py | 10 +-- .../opentelemetry/ext/prometheus/__init__.py | 4 +- opentelemetry-api/CHANGELOG.md | 3 + .../src/opentelemetry/metrics/__init__.py | 55 +++++------- .../tests/metrics/test_metrics.py | 20 ++--- opentelemetry-sdk/CHANGELOG.md | 3 + .../src/opentelemetry/sdk/metrics/__init__.py | 12 +-- .../sdk/metrics/export/aggregate.py | 2 +- .../sdk/metrics/export/batcher.py | 4 +- .../tests/metrics/test_metrics.py | 84 ++++++++++--------- 12 files changed, 103 insertions(+), 102 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 5d137bf01e4..6e8a5c040f3 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -24,7 +24,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -80,7 +80,7 @@ def usage(argv): description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/docs/examples/basic_meter/calling_conventions.py b/docs/examples/basic_meter/calling_conventions.py index 15b57fdafcb..f8cc3dddbb1 100644 --- a/docs/examples/basic_meter/calling_conventions.py +++ b/docs/examples/basic_meter/calling_conventions.py @@ -19,7 +19,7 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider +from opentelemetry.sdk.metrics import Counter, MeterProvider, ValueRecorder from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController @@ -43,7 +43,7 @@ description="size of requests", unit="1", value_type=int, - metric_type=Measure, + metric_type=ValueRecorder, label_keys=("environment",), ) diff --git a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py index 63ea28cd935..18b4a328067 100644 --- a/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py +++ b/ext/opentelemetry-ext-opencensusexporter/tests/test_otcollector_metrics_exporter.py @@ -23,8 +23,8 @@ from opentelemetry.ext.opencensusexporter import metrics_exporter from opentelemetry.sdk.metrics import ( Counter, - Measure, MeterProvider, + ValueRecorder, get_labels_as_key, ) from opentelemetry.sdk.metrics.export import ( @@ -76,7 +76,7 @@ def test_get_collector_metric_type(self): ) self.assertIs(result, metrics_pb2.MetricDescriptor.CUMULATIVE_DOUBLE) result = metrics_exporter.get_collector_metric_type( - Measure("testName", "testDescription", "unit", None, None) + ValueRecorder("testName", "testDescription", "unit", None, None) ) self.assertIs(result, metrics_pb2.MetricDescriptor.UNSPECIFIED) @@ -88,8 +88,8 @@ def test_get_collector_point(self): float_counter = self._meter.create_metric( "testName", "testDescription", "unit", float, Counter ) - measure = self._meter.create_metric( - "testName", "testDescription", "unit", float, Measure + valuerecorder = self._meter.create_metric( + "testName", "testDescription", "unit", float, ValueRecorder ) result = metrics_exporter.get_collector_point( MetricRecord(aggregator, self._key_labels, int_counter) @@ -106,7 +106,7 @@ def test_get_collector_point(self): self.assertRaises( TypeError, metrics_exporter.get_collector_point( - MetricRecord(aggregator, self._key_labels, measure) + MetricRecord(aggregator, self._key_labels, valuerecorder) ), ) diff --git a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py index f6f91fc5868..cc44621ac48 100644 --- a/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py +++ b/ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py @@ -79,7 +79,7 @@ UnknownMetricFamily, ) -from opentelemetry.metrics import Counter, Measure, Metric +from opentelemetry.metrics import Counter, Metric, ValueRecorder from opentelemetry.sdk.metrics.export import ( MetricRecord, MetricsExporter, @@ -164,7 +164,7 @@ def _translate_to_prometheus(self, metric_record: MetricRecord): labels=label_values, value=metric_record.aggregator.checkpoint ) # TODO: Add support for histograms when supported in OT - elif isinstance(metric_record.metric, Measure): + elif isinstance(metric_record.metric, ValueRecorder): prometheus_metric = UnknownMetricFamily( name=metric_name, documentation=metric_record.metric.description, diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 386bd75d470..4b678cd4226 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index b7ad62adb2e..a91a4ce7b76 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -19,16 +19,13 @@ The `Meter` class is used to construct `Metric` s to record raw statistics as well as metrics with predefined aggregation. +`Meter` s can be obtained via the `MeterProvider`, corresponding to the name +of the instrumenting library and (optionally) a version. + See the `metrics api`_ spec for terminology and context clarification. .. _metrics api: - https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md - -.. versionadded:: 0.1.0 -.. versionchanged:: 0.5.0 - ``meter_provider`` was replaced by `get_meter_provider`, - ``set_preferred_meter_provider_implementation`` was replaced by - `set_meter_provider`. + https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/api.md """ import abc from logging import getLogger @@ -54,7 +51,7 @@ def add(self, value: ValueT) -> None: """ def record(self, value: ValueT) -> None: - """No-op implementation of `BoundMeasure` record. + """No-op implementation of `BoundValueRecorder` record. Args: value: The value to record to the bound metric instrument. @@ -73,12 +70,12 @@ def add(self, value: ValueT) -> None: """ -class BoundMeasure: +class BoundValueRecorder: def record(self, value: ValueT) -> None: - """Records the given ``value`` to this bound measure. + """Records the given ``value`` to this bound valuerecorder. Args: - value: The value to record to the bound measure. + value: The value to record to the bound valuerecorder. """ @@ -94,12 +91,7 @@ def bind(self, labels: Dict[str, str]) -> "object": """Gets a bound metric instrument. Bound metric instruments are useful to reduce the cost of repeatedly - recording a metric with a pre-defined set of label values. All metric - kinds (counter, measure) support declaring a set of required label - keys. The values corresponding to these keys should be specified in - every bound metric instrument. "Unspecified" label values, in cases - where a bound metric instrument is requested but a value was not - provided are permitted. + recording a metric with a pre-defined set of label values. Args: labels: Labels to associate with the bound instrument. @@ -126,10 +118,10 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """No-op implementation of `Measure` record. + """No-op implementation of `ValueRecorder` record. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -150,21 +142,18 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None: """ -class Measure(Metric): - """A measure type metric that represent raw stats that are recorded. - - Measure metrics represent raw statistics that are recorded. - """ +class ValueRecorder(Metric): + """A valuerecorder type metric that represent raw stats.""" - def bind(self, labels: Dict[str, str]) -> "BoundMeasure": - """Gets a `BoundMeasure`.""" - return BoundMeasure() + def bind(self, labels: Dict[str, str]) -> "BoundValueRecorder": + """Gets a `BoundValueRecorder`.""" + return BoundValueRecorder() def record(self, value: ValueT, labels: Dict[str, str]) -> None: - """Records the ``value`` to the measure. + """Records the ``value`` to the valuerecorder. Args: - value: The value to record to this measure metric. + value: The value to record to this valuerecorder metric. labels: Labels to associate with the bound instrument. """ @@ -251,7 +240,7 @@ def get_meter( return DefaultMeter() -MetricT = TypeVar("MetricT", Counter, Measure, Observer) +MetricT = TypeVar("MetricT", Counter, ValueRecorder, Observer) ObserverCallbackT = Callable[[Observer], None] @@ -259,9 +248,9 @@ def get_meter( class Meter(abc.ABC): """An interface to allow the recording of metrics. - `Metric` s are used for recording pre-defined aggregation (counter), - or raw values (measure) in which the aggregation and labels - for the exported metric are deferred. + `Metric` s or metric instruments, are devices used for capturing raw + measurements. Each metric instrument supports a single method, each with + fixed interpretation to capture measurements. """ @abc.abstractmethod diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3e760d3d98b..897c7492e42 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -35,14 +35,14 @@ def test_counter_add(self): counter = metrics.Counter() counter.add(1, {}) - def test_measure(self): - measure = metrics.Measure() - bound_measure = measure.bind({}) - self.assertIsInstance(bound_measure, metrics.BoundMeasure) + def test_valuerecorder(self): + valuerecorder = metrics.ValueRecorder() + bound_valuerecorder = valuerecorder.bind({}) + self.assertIsInstance(bound_valuerecorder, metrics.BoundValueRecorder) - def test_measure_record(self): - measure = metrics.Measure() - measure.record(1, {}) + def test_valuerecorder_record(self): + valuerecorder = metrics.ValueRecorder() + valuerecorder.record(1, {}) def test_default_bound_metric(self): bound_instrument = metrics.DefaultBoundInstrument() @@ -52,9 +52,9 @@ def test_bound_counter(self): bound_counter = metrics.BoundCounter() bound_counter.add(1) - def test_bound_measure(self): - bound_measure = metrics.BoundMeasure() - bound_measure.record(1) + def test_bound_valuerecorder(self): + bound_valuerecorder = metrics.BoundValueRecorder() + bound_valuerecorder.record(1) def test_observer(self): observer = metrics.DefaultObserver() diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 50ea751e15d..29aaed01dcc 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Rename Measure to ValueRecorder in metrics + ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) + ## 0.8b0 Released 2020-05-27 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 1d35648fd35..b9284bae9fe 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -97,9 +97,9 @@ def add(self, value: metrics_api.ValueT) -> None: self.update(value) -class BoundMeasure(metrics_api.BoundMeasure, BaseBoundInstrument): +class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument): def record(self, value: metrics_api.ValueT) -> None: - """See `opentelemetry.metrics.BoundMeasure.record`.""" + """See `opentelemetry.metrics.BoundValueRecorder.record`.""" if self._validate_update(value): self.update(value) @@ -174,15 +174,15 @@ def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None: UPDATE_FUNCTION = add -class Measure(Metric, metrics_api.Measure): - """See `opentelemetry.metrics.Measure`.""" +class ValueRecorder(Metric, metrics_api.ValueRecorder): + """See `opentelemetry.metrics.ValueRecorder`.""" - BOUND_INSTR_TYPE = BoundMeasure + BOUND_INSTR_TYPE = BoundValueRecorder def record( self, value: metrics_api.ValueT, labels: Dict[str, str] ) -> None: - """See `opentelemetry.metrics.Measure.record`.""" + """See `opentelemetry.metrics.ValueRecorder.record`.""" bound_intrument = self.bind(labels) bound_intrument.record(value) bound_intrument.release() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py index ea8c40a7e72..7e1baba2c77 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/aggregate.py @@ -72,7 +72,7 @@ def merge(self, other): class MinMaxSumCountAggregator(Aggregator): - """Agregator for Measure metrics that keeps min, max, sum and count.""" + """Aggregator for ValueRecorder metrics that keeps min, max, sum, count.""" _TYPE = namedtuple("minmaxsumcount", "min max sum count") _EMPTY = _TYPE(None, None, None, 0) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py index 7b599f4c7da..eda504d5684 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py @@ -15,7 +15,7 @@ import abc from typing import Sequence, Type -from opentelemetry.metrics import Counter, Measure, MetricT, Observer +from opentelemetry.metrics import Counter, MetricT, Observer, ValueRecorder from opentelemetry.sdk.metrics.export import MetricRecord from opentelemetry.sdk.metrics.export.aggregate import ( Aggregator, @@ -49,7 +49,7 @@ def aggregator_for(self, metric_type: Type[MetricT]) -> Aggregator: # pylint:disable=R0201 if issubclass(metric_type, Counter): return CounterAggregator() - if issubclass(metric_type, Measure): + if issubclass(metric_type, ValueRecorder): return MinMaxSumCountAggregator() if issubclass(metric_type, Observer): return ObserverAggregator() diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 32980647055..a3c0f4294d9 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -109,14 +109,14 @@ def test_record_batch_multiple(self): counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys ) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - record_tuples = [(counter, 1.0), (measure, 3.0)] + record_tuples = [(counter, 1.0), (valuerecorder, 3.0)] meter.record_batch(labels, record_tuples) self.assertEqual(counter.bind(labels).aggregator.current, 1.0) self.assertEqual( - measure.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) + valuerecorder.bind(labels).aggregator.current, (3.0, 3.0, 3.0, 1) ) def test_record_batch_exists(self): @@ -145,14 +145,14 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") self.assertIs(counter.meter.resource, resource) - def test_create_measure(self): + def test_create_valuerecorder(self): meter = metrics.MeterProvider().get_meter(__name__) - measure = meter.create_metric( - "name", "desc", "unit", float, metrics.Measure, () + valuerecorder = meter.create_metric( + "name", "desc", "unit", float, metrics.ValueRecorder, () ) - self.assertIsInstance(measure, metrics.Measure) - self.assertEqual(measure.value_type, float) - self.assertEqual(measure.name, "name") + self.assertIsInstance(valuerecorder, metrics.ValueRecorder) + self.assertEqual(valuerecorder.value_type, float) + self.assertEqual(valuerecorder.name, "name") def test_register_observer(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -197,19 +197,19 @@ def test_direct_call_release_bound_instrument(self): meter.metrics.add(counter) counter.add(4.0, labels) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - measure.record(42.0, labels) + meter.metrics.add(valuerecorder) + valuerecorder.record(42.0, labels) self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) def test_release_bound_instrument(self): meter = metrics.MeterProvider().get_meter(__name__) @@ -223,30 +223,30 @@ def test_release_bound_instrument(self): bound_counter = counter.bind(labels) bound_counter.add(4.0) - measure = metrics.Measure( + valuerecorder = metrics.ValueRecorder( "name", "desc", "unit", float, meter, label_keys ) - meter.metrics.add(measure) - bound_measure = measure.bind(labels) - bound_measure.record(42) + meter.metrics.add(valuerecorder) + bound_valuerecorder = valuerecorder.bind(labels) + bound_valuerecorder.record(42) bound_counter.release() - bound_measure.release() + bound_valuerecorder.release() # be sure that bound instruments are only released after collection self.assertEqual(len(counter.bound_instruments), 1) - self.assertEqual(len(measure.bound_instruments), 1) + self.assertEqual(len(valuerecorder.bound_instruments), 1) meter.collect() self.assertEqual(len(counter.bound_instruments), 0) - self.assertEqual(len(measure.bound_instruments), 0) + self.assertEqual(len(valuerecorder.bound_instruments), 0) class TestMetric(unittest.TestCase): def test_bind(self): meter = metrics.MeterProvider().get_meter(__name__) - metric_types = [metrics.Counter, metrics.Measure] + metric_types = [metrics.Counter, metrics.ValueRecorder] labels = {"key": "value"} key_labels = tuple(sorted(labels.items())) for _type in metric_types: @@ -268,17 +268,19 @@ def test_add(self): self.assertEqual(bound_counter.aggregator.current, 5) -class TestMeasure(unittest.TestCase): +class TestValueRecorder(unittest.TestCase): def test_record(self): meter = metrics.MeterProvider().get_meter(__name__) - metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) + metric = metrics.ValueRecorder( + "name", "desc", "unit", int, meter, ("key",) + ) labels = {"key": "value"} - bound_measure = metric.bind(labels) + bound_valuerecorder = metric.bind(labels) values = (37, 42, 7) for val in values: metric.record(val, labels) self.assertEqual( - bound_measure.aggregator.current, + bound_valuerecorder.aggregator.current, (min(values), max(values), sum(values), len(values)), ) @@ -375,33 +377,37 @@ def test_update(self): self.assertEqual(bound_counter.aggregator.current, 4.0) -class TestBoundMeasure(unittest.TestCase): +class TestBoundValueRecorder(unittest.TestCase): def test_record(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3) - self.assertEqual(bound_measure.aggregator.current, (3, 3, 3, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3) + self.assertEqual(bound_valuerecorder.aggregator.current, (3, 3, 3, 1)) def test_record_disabled(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, False, aggregator) - bound_measure.record(3) + bound_valuerecorder = metrics.BoundValueRecorder( + int, False, aggregator + ) + bound_valuerecorder.record(3) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) @mock.patch("opentelemetry.sdk.metrics.logger") def test_record_incorrect_type(self, logger_mock): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.record(3.0) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.record(3.0) self.assertEqual( - bound_measure.aggregator.current, (None, None, None, 0) + bound_valuerecorder.aggregator.current, (None, None, None, 0) ) self.assertTrue(logger_mock.warning.called) def test_update(self): aggregator = export.aggregate.MinMaxSumCountAggregator() - bound_measure = metrics.BoundMeasure(int, True, aggregator) - bound_measure.update(4.0) - self.assertEqual(bound_measure.aggregator.current, (4.0, 4.0, 4.0, 1)) + bound_valuerecorder = metrics.BoundValueRecorder(int, True, aggregator) + bound_valuerecorder.update(4.0) + self.assertEqual( + bound_valuerecorder.aggregator.current, (4.0, 4.0, 4.0, 1) + ) From 1086c8314403d85a23dd156a2e69c0ded2376e26 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 2 Jun 2020 16:37:56 -0600 Subject: [PATCH 14/25] ext/boto: Add boto instrumentation (#665) --- docs-requirements.txt | 1 + docs/ext/boto/boto.rst | 7 + ext/opentelemetry-ext-boto/CHANGELOG.md | 5 + ext/opentelemetry-ext-boto/LICENSE | 201 ++++++++++++++ ext/opentelemetry-ext-boto/MANIFEST.in | 9 + ext/opentelemetry-ext-boto/README.rst | 23 ++ ext/opentelemetry-ext-boto/setup.cfg | 58 +++++ ext/opentelemetry-ext-boto/setup.py | 26 ++ .../src/opentelemetry/ext/boto/__init__.py | 245 ++++++++++++++++++ .../src/opentelemetry/ext/boto/version.py | 15 ++ ext/opentelemetry-ext-boto/tests/__init__.py | 0 ext/opentelemetry-ext-boto/tests/conftest.py | 31 +++ .../tests/test_boto_instrumentation.py | 242 +++++++++++++++++ tox.ini | 9 + 14 files changed, 872 insertions(+) create mode 100644 docs/ext/boto/boto.rst create mode 100644 ext/opentelemetry-ext-boto/CHANGELOG.md create mode 100644 ext/opentelemetry-ext-boto/LICENSE create mode 100644 ext/opentelemetry-ext-boto/MANIFEST.in create mode 100644 ext/opentelemetry-ext-boto/README.rst create mode 100644 ext/opentelemetry-ext-boto/setup.cfg create mode 100644 ext/opentelemetry-ext-boto/setup.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py create mode 100644 ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py create mode 100644 ext/opentelemetry-ext-boto/tests/__init__.py create mode 100644 ext/opentelemetry-ext-boto/tests/conftest.py create mode 100644 ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py diff --git a/docs-requirements.txt b/docs-requirements.txt index cc999f13832..a61f0beedb3 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -20,3 +20,4 @@ sqlalchemy>=1.0 thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 +boto~=2.0 diff --git a/docs/ext/boto/boto.rst b/docs/ext/boto/boto.rst new file mode 100644 index 00000000000..8bf40c75665 --- /dev/null +++ b/docs/ext/boto/boto.rst @@ -0,0 +1,7 @@ +OpenTelemetry Boto Integration +============================== + +.. automodule:: opentelemetry.ext.boto + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-boto/CHANGELOG.md b/ext/opentelemetry-ext-boto/CHANGELOG.md new file mode 100644 index 00000000000..3e04402cea9 --- /dev/null +++ b/ext/opentelemetry-ext-boto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial release diff --git a/ext/opentelemetry-ext-boto/LICENSE b/ext/opentelemetry-ext-boto/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/ext/opentelemetry-ext-boto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/ext/opentelemetry-ext-boto/MANIFEST.in b/ext/opentelemetry-ext-boto/MANIFEST.in new file mode 100644 index 00000000000..aed3e33273b --- /dev/null +++ b/ext/opentelemetry-ext-boto/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/ext/opentelemetry-ext-boto/README.rst b/ext/opentelemetry-ext-boto/README.rst new file mode 100644 index 00000000000..e149ec424e0 --- /dev/null +++ b/ext/opentelemetry-ext-boto/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Boto Tracing +========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-boto.svg + :target: https://pypi.org/project/opentelemetry-ext-boto/ + +This library allows tracing requests made by the Boto library. + +Installation +------------ + +:: + + pip install opentelemetry-ext-boto + + +References +---------- + +* `OpenTelemetry Boto Tracing `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-boto/setup.cfg b/ext/opentelemetry-ext-boto/setup.cfg new file mode 100644 index 00000000000..529e79be990 --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The 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. +# +[metadata] +name = opentelemetry-ext-boto +description = Boto tracing for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-boto +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + boto ~= 2.0 + opentelemetry-api == 0.9.dev0 + opentelemetry-auto-instrumentation == 0.9.dev0 + +[options.extras_require] +test = + boto~=2.0 + moto~=1.0 + opentelemetry-test == 0.9.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + django = opentelemetry.ext.boto:BotoInstrumentor diff --git a/ext/opentelemetry-ext-boto/setup.py b/ext/opentelemetry-ext-boto/setup.py new file mode 100644 index 00000000000..4c78e9b35f2 --- /dev/null +++ b/ext/opentelemetry-ext-boto/setup.py @@ -0,0 +1,26 @@ +# Copyright The 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "boto", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py new file mode 100644 index 00000000000..fa66fda61dd --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/__init__.py @@ -0,0 +1,245 @@ +# Copyright The 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. +""" +Instrument `Boto`_ to trace service requests. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-auto-instrumentation`` executable which will automatically +instrument your Boto client. The second is to programmatically enable +instrumentation via the following code: + +.. _boto: https://pypi.org/project/boto/ + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.ext.boto import BotoInstrumentor + from opentelemetry.sdk.trace import TracerProvider + import boto + + trace.set_tracer_provider(TracerProvider()) + + # Instrument Boto + BotoInstrumentor().instrument(tracer_provider=trace.get_tracer_provider()) + + # This will create a span with Boto-specific attributes + ec2 = boto.ec2.connect_to_region("us-west-2") + ec2.get_all_instances() + +API +--- +""" + +import logging +from inspect import currentframe + +from boto.connection import AWSAuthConnection, AWSQueryConnection +from wrapt import ObjectProxy, wrap_function_wrapper + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.ext.boto.version import __version__ +from opentelemetry.trace import SpanKind, get_tracer + +logger = logging.getLogger(__name__) + + +def _get_instance_region_name(instance): + region = getattr(instance, "region", None) + + if not region: + return None + if isinstance(region, str): + return region.split(":")[1] + return region.name + + +class BotoInstrumentor(BaseInstrumentor): + """A instrumentor for Boto + + See `BaseInstrumentor` + """ + + def __init__(self): + super().__init__() + self._original_boto = None + + def _instrument(self, **kwargs): + # AWSQueryConnection and AWSAuthConnection are two different classes + # called by different services for connection. + # For exemple EC2 uses AWSQueryConnection and S3 uses + # AWSAuthConnection + + # FIXME should the tracer provider be accessed via Configuration + # instead? + # pylint: disable=attribute-defined-outside-init + self._tracer = get_tracer( + __name__, __version__, kwargs.get("tracer_provider") + ) + + wrap_function_wrapper( + "boto.connection", + "AWSQueryConnection.make_request", + self._patched_query_request, + ) + wrap_function_wrapper( + "boto.connection", + "AWSAuthConnection.make_request", + self._patched_auth_request, + ) + + def _uninstrument(self, **kwargs): + unwrap(AWSQueryConnection, "make_request") + unwrap(AWSAuthConnection, "make_request") + + def _common_request( # pylint: disable=too-many-locals + self, + args_name, + traced_args, + operation_name, + original_func, + instance, + args, + kwargs, + ): + + endpoint_name = getattr(instance, "host").split(".")[0] + + with self._tracer.start_as_current_span( + "{}.command".format(endpoint_name), kind=SpanKind.CONSUMER, + ) as span: + if args: + http_method = args[0] + span.resource = "%s.%s" % (endpoint_name, http_method.lower()) + else: + span.resource = endpoint_name + + add_span_arg_tags( + span, endpoint_name, args, args_name, traced_args, + ) + + # Obtaining region name + region_name = _get_instance_region_name(instance) + + meta = { + "aws.agent": "boto", + "aws.operation": operation_name, + } + if region_name: + meta["aws.region"] = region_name + + for key, value in meta.items(): + span.set_attribute(key, value) + + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) + span.set_attribute("http.status_code", getattr(result, "status")) + span.set_attribute("http.method", getattr(result, "_method")) + + return result + + def _patched_query_request(self, original_func, instance, args, kwargs): + + return self._common_request( + ("operation_name", "params", "path", "verb"), + ["operation_name", "params", "path"], + args[0] if args else None, + original_func, + instance, + args, + kwargs, + ) + + def _patched_auth_request(self, original_func, instance, args, kwargs): + operation_name = None + + frame = currentframe().f_back + operation_name = None + while frame: + if frame.f_code.co_name == "make_request": + operation_name = frame.f_back.f_code.co_name + break + frame = frame.f_back + + return self._common_request( + ( + "method", + "path", + "headers", + "data", + "host", + "auth_path", + "sender", + ), + ["path", "data", "host"], + operation_name, + original_func, + instance, + args, + kwargs, + ) + + +def truncate_arg_value(value, max_len=1024): + """Truncate values which are bytes and greater than ``max_len``. + Useful for parameters like "Body" in ``put_object`` operations. + """ + if isinstance(value, bytes) and len(value) > max_len: + return b"..." + + return value + + +def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced): + if endpoint_name not in ["kms", "sts"]: + tags = dict( + (name, value) + for (name, value) in zip(args_names, args) + if name in args_traced + ) + tags = flatten_dict(tags) + for key, value in { + k: truncate_arg_value(v) + for k, v in tags.items() + if k not in {"s3": ["params.Body"]}.get(endpoint_name, []) + }.items(): + span.set_attribute(key, value) + + +def flatten_dict(dict_, sep=".", prefix=""): + """ + Returns a normalized dict of depth 1 with keys in order of embedding + """ + # adapted from https://stackoverflow.com/a/19647596 + return ( + { + prefix + sep + k if prefix else k: v + for kk, vv in dict_.items() + for k, v in flatten_dict(vv, sep, kk).items() + } + if isinstance(dict_, dict) + else {prefix: dict_} + ) + + +def unwrap(obj, attr): + function = getattr(obj, attr, None) + if ( + function + and isinstance(function, ObjectProxy) + and hasattr(function, "__wrapped__") + ): + setattr(obj, attr, function.__wrapped__) diff --git a/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py new file mode 100644 index 00000000000..bcf6a357770 --- /dev/null +++ b/ext/opentelemetry-ext-boto/src/opentelemetry/ext/boto/version.py @@ -0,0 +1,15 @@ +# Copyright The 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. + +__version__ = "0.8.dev0" diff --git a/ext/opentelemetry-ext-boto/tests/__init__.py b/ext/opentelemetry-ext-boto/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-boto/tests/conftest.py b/ext/opentelemetry-ext-boto/tests/conftest.py new file mode 100644 index 00000000000..884c6753c16 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/conftest.py @@ -0,0 +1,31 @@ +# Copyright The 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 os import environ + + +def pytest_sessionstart(session): + # pylint: disable=unused-argument + environ["AWS_ACCESS_KEY_ID"] = "testing" + environ["AWS_SECRET_ACCESS_KEY"] = "testing" + environ["AWS_SECURITY_TOKEN"] = "testing" + environ["AWS_SESSION_TOKEN"] = "testing" + + +def pytest_sessionfinish(session): + # pylint: disable=unused-argument + environ.pop("AWS_ACCESS_KEY_ID") + environ.pop("AWS_SECRET_ACCESS_KEY") + environ.pop("AWS_SECURITY_TOKEN") + environ.pop("AWS_SESSION_TOKEN") diff --git a/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py new file mode 100644 index 00000000000..492fac5a883 --- /dev/null +++ b/ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py @@ -0,0 +1,242 @@ +# Copyright The 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 unittest import skipUnless + +import boto.awslambda +import boto.ec2 +import boto.elasticache +import boto.s3 +import boto.sts + +from moto import ( # pylint: disable=import-error + mock_ec2_deprecated, + mock_lambda_deprecated, + mock_s3_deprecated, + mock_sts_deprecated, +) +from opentelemetry.ext.boto import BotoInstrumentor +from opentelemetry.test.test_base import TestBase + + +def assert_span_http_status_code(span, code): + """Assert on the span's 'http.status_code' tag""" + tag = span.attributes["http.status_code"] + assert tag == code, "%r != %r" % (tag, code) + + +class TestBotoInstrumentor(TestBase): + """Botocore integration testsuite""" + + def setUp(self): + super().setUp() + BotoInstrumentor().instrument() + + def tearDown(self): + BotoInstrumentor().uninstrument() + + @mock_ec2_deprecated + def test_ec2_client(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.attributes["aws.operation"], "DescribeInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + + # Create an instance + ec2.run_instances(21) + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + self.assertEqual(span.attributes["aws.operation"], "RunInstances") + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "ec2.runinstances") + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual(span.name, "ec2.command") + + @mock_ec2_deprecated + def test_analytics_enabled_with_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_ec2_deprecated + def test_analytics_enabled_without_rate(self): + ec2 = boto.ec2.connect_to_region("us-west-2") + + ec2.get_all_instances() + + spans = self.memory_exporter.get_finished_spans() + assert spans + + @mock_s3_deprecated + def test_s3_client(self): + s3 = boto.s3.connect_to_region("us-east-1") + + s3.get_all_buckets() + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.operation"], "get_all_buckets") + + # Create a bucket command + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[1] + assert_span_http_status_code(span, 200) + self.assertEqual(span.attributes["http.method"], "PUT") + self.assertEqual(span.attributes["path"], "/") + self.assertEqual(span.attributes["aws.operation"], "create_bucket") + + # Get the created bucket + s3.get_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 3) + span = spans[2] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "s3.head") + self.assertEqual(span.attributes["http.method"], "HEAD") + self.assertEqual(span.attributes["aws.operation"], "head_bucket") + self.assertEqual(span.name, "s3.command") + + # Checking for resource incase of error + try: + s3.get_bucket("big_bucket") + except Exception: # pylint: disable=broad-except + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[2] + self.assertEqual(span.resource, "s3.head") + + @mock_s3_deprecated + def test_s3_put(self): + s3 = boto.s3.connect_to_region("us-east-1") + s3.create_bucket("mybucket") + bucket = s3.get_bucket("mybucket") + key = boto.s3.key.Key(bucket) + key.key = "foo" + key.set_contents_from_string("bar") + + spans = self.memory_exporter.get_finished_spans() + assert spans + # create bucket + self.assertEqual(len(spans), 3) + self.assertEqual(spans[0].attributes["aws.operation"], "create_bucket") + assert_span_http_status_code(spans[0], 200) + self.assertEqual(spans[0].resource, "s3.put") + # get bucket + self.assertEqual(spans[1].attributes["aws.operation"], "head_bucket") + self.assertEqual(spans[1].resource, "s3.head") + # put object + self.assertEqual( + spans[2].attributes["aws.operation"], "_send_file_internal" + ) + self.assertEqual(spans[2].resource, "s3.put") + + @mock_lambda_deprecated + def test_unpatch(self): + + lamb = boto.awslambda.connect_to_region("us-east-2") + + BotoInstrumentor().uninstrument() + + # multiple calls + lamb.list_functions() + spans = self.memory_exporter.get_finished_spans() + assert not spans, spans + + @mock_s3_deprecated + def test_double_patch(self): + s3 = boto.s3.connect_to_region("us-east-1") + + BotoInstrumentor().instrument() + BotoInstrumentor().instrument() + + # Get the created bucket + s3.create_bucket("cheese") + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 1) + + @mock_lambda_deprecated + def test_lambda_client(self): + lamb = boto.awslambda.connect_to_region("us-east-2") + + # multiple calls + lamb.list_functions() + lamb.list_functions() + + spans = self.memory_exporter.get_finished_spans() + assert spans + self.assertEqual(len(spans), 2) + span = spans[0] + assert_span_http_status_code(span, 200) + self.assertEqual(span.resource, "lambda.get") + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual(span.attributes["aws.region"], "us-east-2") + self.assertEqual(span.attributes["aws.operation"], "list_functions") + + @mock_sts_deprecated + def test_sts_client(self): + sts = boto.sts.connect_to_region("us-west-2") + + sts.get_federation_token(12, duration=10) + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "sts.getfederationtoken") + self.assertEqual(span.attributes["aws.region"], "us-west-2") + self.assertEqual( + span.attributes["aws.operation"], "GetFederationToken" + ) + + # checking for protection on sts against security leak + self.assertTrue("args.path" not in span.attributes.keys()) + + @skipUnless( + False, + ( + "Test to reproduce the case where args sent to patched function " + "are None, can't be mocked: needs AWS credentials" + ), + ) + def test_elasticache_client(self): + elasticache = boto.elasticache.connect_to_region("us-west-2") + + elasticache.describe_cache_clusters() + + spans = self.memory_exporter.get_finished_spans() + assert spans + span = spans[0] + self.assertEqual(span.resource, "elasticache") + self.assertEqual(span.attributes["aws.region"], "us-west-2") diff --git a/tox.ini b/tox.ini index 3cec9fcfbd2..978155616a8 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,10 @@ envlist = py3{4,5,6,7,8}-test-ext-dbapi pypy3-test-ext-dbapi + ; opentelemetry-ext-boto + py3{5,6,7,8}-test-ext-boto + pypy3-test-ext-boto + ; opentelemetry-ext-flask py3{4,5,6,7,8}-test-ext-flask pypy3-test-ext-flask @@ -161,6 +165,7 @@ changedir = test-ext-sqlite3: ext/opentelemetry-ext-sqlite3/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests + test-ext-boto: ext/opentelemetry-ext-boto/tests test-ext-flask: ext/opentelemetry-ext-flask/tests test-example-app: docs/examples/opentelemetry-example-app/tests test-getting-started: docs/getting_started/tests @@ -188,6 +193,10 @@ commands_pre = wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation asgi: pip install {toxinidir}/ext/opentelemetry-ext-asgi + + boto: pip install {toxinidir}/opentelemetry-auto-instrumentation + boto: pip install {toxinidir}/ext/opentelemetry-ext-boto[test] + flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test] From 5c03a97712eb61c72ec2d879c1834c1a8b5e265a Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 2 Jun 2020 20:37:36 -0700 Subject: [PATCH 15/25] opentracing-shim: add testbed for otshim (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit ports the OpenTracing testbed[1] to check that the ot-shim is working as expected using different frameworks. Gevent doesn't support context vars yet[2], so those tests are not compatible with opentelemetry and were not ported. [1] https://github.com/opentracing/opentracing-python/tree/master/testbed [2] https://github.com/gevent/gevent/issues/1407 Co-authored-by: Mauricio Vásquez Co-authored-by: alrex --- dev-requirements.txt | 1 + .../tests/testbed/README.rst | 47 ++++++ .../tests/testbed/__init__.py | 0 .../tests/testbed/otel_ot_shim_tracer.py | 26 ++++ .../test_active_span_replacement/README.rst | 20 +++ .../test_active_span_replacement/__init__.py | 0 .../test_asyncio.py | 54 +++++++ .../test_threads.py | 50 +++++++ .../testbed/test_client_server/README.rst | 19 +++ .../testbed/test_client_server/__init__.py | 0 .../test_client_server/test_asyncio.py | 79 ++++++++++ .../test_client_server/test_threads.py | 75 ++++++++++ .../test_common_request_handler/README.rst | 23 +++ .../test_common_request_handler/__init__.py | 0 .../request_handler.py | 38 +++++ .../test_asyncio.py | 136 ++++++++++++++++++ .../test_threads.py | 119 +++++++++++++++ .../testbed/test_late_span_finish/README.rst | 18 +++ .../testbed/test_late_span_finish/__init__.py | 0 .../test_late_span_finish/test_asyncio.py | 51 +++++++ .../test_late_span_finish/test_threads.py | 44 ++++++ .../test_listener_per_request/README.rst | 19 +++ .../test_listener_per_request/__init__.py | 0 .../response_listener.py | 7 + .../test_listener_per_request/test_asyncio.py | 45 ++++++ .../test_listener_per_request/test_threads.py | 45 ++++++ .../test_multiple_callbacks/README.rst | 44 ++++++ .../test_multiple_callbacks/__init__.py | 0 .../test_multiple_callbacks/test_asyncio.py | 59 ++++++++ .../test_multiple_callbacks/test_threads.py | 59 ++++++++ .../testbed/test_nested_callbacks/README.rst | 47 ++++++ .../testbed/test_nested_callbacks/__init__.py | 0 .../test_nested_callbacks/test_asyncio.py | 57 ++++++++ .../test_nested_callbacks/test_threads.py | 59 ++++++++ .../test_subtask_span_propagation/README.rst | 42 ++++++ .../test_subtask_span_propagation/__init__.py | 0 .../test_asyncio.py | 32 +++++ .../test_threads.py | 33 +++++ .../tests/testbed/testcase.py | 46 ++++++ .../tests/testbed/utils.py | 78 ++++++++++ opentelemetry-api/tests/__init__.py | 9 ++ scripts/coverage.sh | 26 +++- tox.ini | 8 +- 43 files changed, 1506 insertions(+), 9 deletions(-) create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py create mode 100644 ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py diff --git a/dev-requirements.txt b/dev-requirements.txt index b8ae14c89c3..be74d804b3d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,3 +10,4 @@ pytest!=5.2.3 pytest-cov>=2.8 readme-renderer~=24.0 httpretty~=1.0 +opentracing~=2.2.0 \ No newline at end of file diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst new file mode 100644 index 00000000000..ba7119cd68c --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/README.rst @@ -0,0 +1,47 @@ + +Testbed suite for the OpenTelemetry-OpenTracing Bridge +====================================================== + +Testbed suite designed to test the API changes. + +Build and test. +--------------- + +.. code-block:: sh + + tox -e py37-test-opentracing-shim + +Alternatively, due to the organization of the suite, it's possible to run directly the tests using ``py.test``\ : + +.. code-block:: sh + + py.test -s testbed/test_multiple_callbacks/test_threads.py + +Tested frameworks +----------------- + +Currently the examples cover ``threading`` and ``asyncio``. + +List of patterns +---------------- + + +* `Active Span replacement `_ - Start an isolated task and query for its results in another task/thread. +* `Client-Server `_ - Typical client-server example. +* `Common Request Handler `_ - One request handler for all requests. +* `Late Span finish `_ - Late parent ``Span`` finish. +* `Multiple callbacks `_ - Multiple callbacks spawned at the same time. +* `Nested callbacks `_ - One callback at a time, defined in a pipeline fashion. +* `Subtask Span propagation `_ - ``Span`` propagation for subtasks/coroutines. + +Adding new patterns +------------------- + +A new pattern is composed of a directory under *testbed* with the *test_* prefix, and containing the files for each platform, also with the *test_* prefix: + +.. code-block:: + + testbed/ + test_new_pattern/ + test_threads.py + test_asyncio.py diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py new file mode 100644 index 00000000000..b3b4271f022 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/otel_ot_shim_tracer.py @@ -0,0 +1,26 @@ +import opentelemetry.ext.opentracing_shim as opentracingshim +from opentelemetry.sdk import trace +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +class MockTracer(opentracingshim.TracerShim): + """Wrapper of `opentracingshim.TracerShim`. + + MockTracer extends `opentracingshim.TracerShim` by adding a in memory + span exporter that can be used to get the list of finished spans.""" + + def __init__(self): + tracer_provider = trace.TracerProvider() + oteltracer = tracer_provider.get_tracer(__name__) + super(MockTracer, self).__init__(oteltracer) + exporter = InMemorySpanExporter() + span_processor = SimpleExportSpanProcessor(exporter) + tracer_provider.add_span_processor(span_processor) + + self.exporter = exporter + + def finished_spans(self): + return self.exporter.get_finished_spans() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst new file mode 100644 index 00000000000..6bb4d2f35c6 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/README.rst @@ -0,0 +1,20 @@ + +Active Span replacement example. +================================ + +This example shows a ``Span`` being created and then passed to an asynchronous task, which will temporary activate it to finish its processing, and further restore the previously active ``Span``. + +``threading`` implementation: + +.. code-block:: python + + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py new file mode 100644 index 00000000000..cb555dc1092 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_asyncio.py @@ -0,0 +1,54 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 3, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + + async def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.loop.create_task(self.task(span)) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py new file mode 100644 index 00000000000..e382d5d7167 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_active_span_replacement/test_threads.py @@ -0,0 +1,50 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + # use max_workers=3 as a general example even if only one would suffice + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Start an isolated task and query for its result -and finish it- + # in another task/thread + span = self.tracer.start_span("initial") + self.submit_another_task(span) + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["initial", "subtask", "task"]) + + # task/subtask are part of the same trace, + # and subtask is a child of task + self.assertSameTrace(spans[1], spans[2]) + self.assertIsChildOf(spans[1], spans[2]) + + # initial task is not related in any way to those two tasks + self.assertNotSameTrace(spans[0], spans[1]) + self.assertEqual(spans[0].parent, None) + self.assertEqual(spans[2].parent, None) + + def task(self, span): + # Create a new Span for this task + with self.tracer.start_active_span("task"): + + with self.tracer.scope_manager.activate(span, True): + # Simulate work strictly related to the initial Span + pass + + # Use the task span as parent of a new subtask + with self.tracer.start_active_span("subtask"): + pass + + def submit_another_task(self, span): + self.executor.submit(self.task, span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst new file mode 100644 index 00000000000..730fd9295da --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/README.rst @@ -0,0 +1,19 @@ + +Client-Server example. +====================== + +This example shows a ``Span`` created by a ``Client``, which will send a ``Message`` / ``SpanContext`` to a ``Server``, which will in turn extract such context and use it as parent of a new (server-side) ``Span``. + +``Client.send()`` is used to send messages and inject the ``SpanContext`` using the ``TEXT_MAP`` format, and ``Server.process()`` will process received messages and will extract the context used as parent. + +.. code-block:: python + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject(scope.span.context, + opentracing.Format.TEXT_MAP, + message) + self.queue.put(message) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py new file mode 100644 index 00000000000..5379584719a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_asyncio.py @@ -0,0 +1,79 @@ +from __future__ import print_function + +import asyncio + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_tag, stop_loop_when + +logger = get_logger(__name__) + + +class Server: + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.tracer = tracer + self.queue = queue + + async def run(self): + value = await self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + async def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + await self.queue.put(message) + + logger.info("Sent message from client") + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = asyncio.Queue() + self.loop = asyncio.get_event_loop() + self.server = Server(tracer=self.tracer, queue=self.queue) + + def test(self): + client = Client(self.tracer, self.queue) + self.loop.create_task(self.server.run()) + self.loop.create_task(client.send()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py new file mode 100644 index 00000000000..619781edecf --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_client_server/test_threads.py @@ -0,0 +1,75 @@ +from __future__ import print_function + +from queue import Queue +from threading import Thread + +import opentracing +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until, get_logger, get_one_by_tag + +logger = get_logger(__name__) + + +class Server(Thread): + def __init__(self, *args, **kwargs): + tracer = kwargs.pop("tracer") + queue = kwargs.pop("queue") + super(Server, self).__init__(*args, **kwargs) + + self.daemon = True + self.tracer = tracer + self.queue = queue + + def run(self): + value = self.queue.get() + self.process(value) + + def process(self, message): + logger.info("Processing message in server") + + ctx = self.tracer.extract(opentracing.Format.TEXT_MAP, message) + with self.tracer.start_active_span("receive", child_of=ctx) as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + + +class Client: + def __init__(self, tracer, queue): + self.tracer = tracer + self.queue = queue + + def send(self): + with self.tracer.start_active_span("send") as scope: + scope.span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + message = {} + self.tracer.inject( + scope.span.context, opentracing.Format.TEXT_MAP, message + ) + self.queue.put(message) + + logger.info("Sent message from client") + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.queue = Queue() + self.server = Server(tracer=self.tracer, queue=self.queue) + self.server.start() + + def test(self): + client = Client(self.tracer, self.queue) + client.send() + + await_until(lambda: len(self.tracer.finished_spans()) >= 2) + + spans = self.tracer.finished_spans() + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_SERVER) + ) + self.assertIsNotNone( + get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst new file mode 100644 index 00000000000..1bcda539bbd --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/README.rst @@ -0,0 +1,23 @@ + +Common Request Handler example. +=============================== + +This example shows a ``Span`` used with ``RequestHandler``, which is used as a middleware (as in web frameworks) to manage a new ``Span`` per operation through its ``before_request()`` / ``after_response()`` methods. + +Implementation details: + + +* For ``threading``, no active ``Span`` is consumed as the tasks may be run concurrently on different threads, and an explicit ``SpanContext`` has to be saved to be used as parent. + +RequestHandler implementation: + +.. code-block:: python + + def before_request(self, request, request_context): + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + span = self.tracer.start_span("send", + child_of=self.context, + ignore_active_span=True) + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py new file mode 100644 index 00000000000..47ff088025a --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/request_handler.py @@ -0,0 +1,38 @@ +from __future__ import print_function + +from opentracing.ext import tags + +from ..utils import get_logger + +logger = get_logger(__name__) + + +class RequestHandler: + def __init__(self, tracer, context=None, ignore_active_span=True): + self.tracer = tracer + self.context = context + self.ignore_active_span = ignore_active_span + + def before_request(self, request, request_context): + logger.info("Before request %s", request) + + # If we should ignore the active Span, use any passed SpanContext + # as the parent. Else, use the active one. + if self.ignore_active_span: + span = self.tracer.start_span( + "send", child_of=self.context, ignore_active_span=True + ) + else: + span = self.tracer.start_span("send") + + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + request_context["span"] = span + + def after_request(self, request, request_context): + # pylint: disable=no-self-use + logger.info("After request %s", request) + + span = request_context.get("span") + if span is not None: + span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py new file mode 100644 index 00000000000..b0216dd7560 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_asyncio.py @@ -0,0 +1,136 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name, stop_loop_when +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, loop): + self.request_handler = request_handler + self.loop = loop + + async def send_task(self, message): + request_context = {} + + async def before_handler(): + self.request_handler.before_request(message, request_context) + + async def after_handler(): + self.request_handler.after_request(message, request_context) + + await before_handler() + await after_handler() + + return "%s::response" % message + + def send(self, message): + return self.send_task(message) + + def send_sync(self, message): + return self.loop.run_until_complete(self.send_task(message)) + + +class TestAsyncio(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed in different Tasks, and no Span propagation + among them is done automatically. + Therefore we cannot use current active span and activate span. + So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + self.client = Client(RequestHandler(self.tracer), self.loop) + + def test_two_callbacks(self): + res_future1 = self.loop.create_task(self.client.send("message1")) + res_future2 = self.loop.create_task(self.client.send("message2")) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + self.assertEqual("message1::response", res_future1.result()) + self.assertEqual("message2::response", res_future2.result()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + async def do_task(): + with self.tracer.start_active_span("parent"): + response = await self.client.send_task("no_parent") + self.assertEqual("no_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_good_solution_to_set_parent(self): + """Asyncio and contextvars are integrated, in this case it is not needed + to activate current span by hand. + """ + + async def do_task(): + with self.tracer.start_active_span("parent"): + # Set ignore_active_span to False indicating that the + # framework will do it for us. + req_handler = RequestHandler( + self.tracer, ignore_active_span=False, + ) + client = Client(req_handler, self.loop) + response = await client.send_task("correct_parent") + + self.assertEqual("correct_parent::response", response) + + # Send second request, now there is no active parent, + # but it will be set, ups + response = await client.send_task("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + self.loop.run_until_complete(do_task()) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsNotChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py new file mode 100644 index 00000000000..4ab8b2a075e --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_common_request_handler/test_threads.py @@ -0,0 +1,119 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, get_one_by_operation_name +from .request_handler import RequestHandler + +logger = get_logger(__name__) + + +class Client: + def __init__(self, request_handler, executor): + self.request_handler = request_handler + self.executor = executor + + def send_task(self, message): + request_context = {} + + def before_handler(): + self.request_handler.before_request(message, request_context) + + def after_handler(): + self.request_handler.after_request(message, request_context) + + self.executor.submit(before_handler).result() + self.executor.submit(after_handler).result() + + return "%s::response" % message + + def send(self, message): + return self.executor.submit(self.send_task, message) + + def send_sync(self, message, timeout=5.0): + fut = self.executor.submit(self.send_task, message) + return fut.result(timeout=timeout) + + +class TestThreads(OpenTelemetryTestCase): + """ + There is only one instance of 'RequestHandler' per 'Client'. Methods of + 'RequestHandler' are executed concurrently in different threads which are + reused (executor). Therefore we cannot use current active span and + activate span. So one issue here is setting correct parent span. + """ + + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + self.client = Client(RequestHandler(self.tracer), self.executor) + + def test_two_callbacks(self): + response_future1 = self.client.send("message1") + response_future2 = self.client.send("message2") + + self.assertEqual("message1::response", response_future1.result(5.0)) + self.assertEqual("message2::response", response_future2.result(5.0)) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + for span in spans: + self.assertEqual( + span.attributes.get(tags.SPAN_KIND, None), + tags.SPAN_KIND_RPC_CLIENT, + ) + + self.assertNotSameTrace(spans[0], spans[1]) + self.assertIsNone(spans[0].parent) + self.assertIsNone(spans[1].parent) + + def test_parent_not_picked(self): + """Active parent should not be picked up by child.""" + + with self.tracer.start_active_span("parent"): + response = self.client.send_sync("no_parent") + self.assertEqual("no_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + + child_span = get_one_by_operation_name(spans, "send") + self.assertIsNotNone(child_span) + + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + # Here check that there is no parent-child relation. + self.assertIsNotChildOf(child_span, parent_span) + + def test_bad_solution_to_set_parent(self): + """Solution is bad because parent is per client and is not automatically + activated depending on the context. + """ + + with self.tracer.start_active_span("parent") as scope: + client = Client( + # Pass a span context to be used ad the parent. + RequestHandler(self.tracer, scope.span.context), + self.executor, + ) + response = client.send_sync("correct_parent") + self.assertEqual("correct_parent::response", response) + + response = client.send_sync("wrong_parent") + self.assertEqual("wrong_parent::response", response) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + + spans = sorted(spans, key=lambda x: x.start_time) + parent_span = get_one_by_operation_name(spans, "parent") + self.assertIsNotNone(parent_span) + + self.assertIsChildOf(spans[1], parent_span) + self.assertIsChildOf(spans[2], parent_span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst new file mode 100644 index 00000000000..8c4ffd864ac --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/README.rst @@ -0,0 +1,18 @@ + +Late Span finish example. +========================= + +This example shows a ``Span`` for a top-level operation, with independent, unknown lifetime, acting as parent of a few asynchronous subtasks (which must re-activate it but not finish it). + +.. code-block:: python + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py new file mode 100644 index 00000000000..128073b056f --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_asyncio.py @@ -0,0 +1,51 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 2, + timeout=5.0, + ) + self.loop.run_forever() + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + async def task(name): + logger.info("Running %s", name) + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + await asyncio.sleep(0.1) + + self.loop.create_task(task("task1")) + self.loop.create_task(task("task2")) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py new file mode 100644 index 00000000000..5972eb8b924 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_late_span_finish/test_threads.py @@ -0,0 +1,44 @@ +from __future__ import print_function + +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + # Create a Span and use it as (explicit) parent of a pair of subtasks. + parent_span = self.tracer.start_span("parent") + self.submit_subtasks(parent_span) + + # Wait for the threadpool to be done. + self.executor.shutdown(True) + + # Late-finish the parent Span now. + parent_span.finish() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 3) + self.assertNamesEqual(spans, ["task1", "task2", "parent"]) + + for idx in range(2): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + self.assertTrue(spans[idx].end_time <= spans[-1].end_time) + + # Fire away a few subtasks, passing a parent Span whose lifetime + # is not tied at all to the children. + def submit_subtasks(self, parent_span): + def task(name, interval): + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span(name): + time.sleep(interval) + + self.executor.submit(task, "task1", 0.1) + self.executor.submit(task, "task2", 0.3) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst new file mode 100644 index 00000000000..952d1ec51dd --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/README.rst @@ -0,0 +1,19 @@ + +Listener Response example. +========================== + +This example shows a ``Span`` created upon a message being sent to a ``Client``, and its handling along a related, **not shared** ``ResponseListener`` object with a ``on_response(self, response)`` method to finish it. + +.. code-block:: python + + def _task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py new file mode 100644 index 00000000000..dd143c20b8e --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/response_listener.py @@ -0,0 +1,7 @@ +class ResponseListener: + def __init__(self, span): + self.span = span + + def on_response(self, res): + del res + self.span.finish() diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py new file mode 100644 index 00000000000..085c0ea8134 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_asyncio.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +import asyncio + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer, loop): + self.tracer = tracer + self.loop = loop + + async def task(self, message, listener): + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.loop.run_until_complete(self.task(message, listener)) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + client = Client(self.tracer, self.loop) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py new file mode 100644 index 00000000000..8f82e1fb158 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_listener_per_request/test_threads.py @@ -0,0 +1,45 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from opentracing.ext import tags + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_one_by_tag +from .response_listener import ResponseListener + + +class Client: + def __init__(self, tracer): + self.tracer = tracer + self.executor = ThreadPoolExecutor(max_workers=3) + + def _task(self, message, listener): + # pylint: disable=no-self-use + res = "%s::response" % message + listener.on_response(res) + return res + + def send_sync(self, message): + span = self.tracer.start_span("send") + span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + + listener = ResponseListener(span) + return self.executor.submit(self._task, message, listener).result() + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + + def test_main(self): + client = Client(self.tracer) + res = client.send_sync("message") + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + + span = get_one_by_tag(spans, tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) + self.assertIsNotNone(span) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst new file mode 100644 index 00000000000..204f282cf23 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/README.rst @@ -0,0 +1,44 @@ + +Multiple callbacks example. +=========================== + +This example shows a ``Span`` created for a top-level operation, covering a set of asynchronous operations (representing callbacks), and have this ``Span`` finished when **all** of them have been executed. + +``Client.send()`` is used to create a new asynchronous operation (callback), and in turn every operation both restores the active ``Span``, and creates a child ``Span`` (useful for measuring the performance of each callback). + +Implementation details: + + +* For ``threading``, a thread-safe counter is put in each ``Span`` to keep track of the pending callbacks, and call ``Span.finish()`` when the count becomes 0. +* For ``asyncio`` the children corotuines representing the subtasks are simply yielded over, so no counter is needed. + +``threading`` implementation: + +.. code-block:: python + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span._ref_count.decr() == 0: + parent_span.finish() + +``asyncio`` implementation: + +.. code-block:: python + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + # Invoke and yield over the corotuines. + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py new file mode 100644 index 00000000000..36043d0a9bc --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_asyncio.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import asyncio +import random + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import get_logger, stop_loop_when + +random.seed() +logger = get_logger(__name__) + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Need to run within a Task, as the scope manager depends + # on Task.current_task() + async def main_task(): + with self.tracer.start_active_span("parent"): + tasks = self.submit_callbacks() + await asyncio.gather(*tasks) + + self.loop.create_task(main_task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) >= 4, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + async def task(self, interval, parent_span): + logger.info("Starting task") + + with self.tracer.scope_manager.activate(parent_span, False): + with self.tracer.start_active_span("task"): + await asyncio.sleep(interval) + + def submit_callbacks(self): + parent_span = self.tracer.scope_manager.active.span + tasks = [] + for _ in range(3): + interval = 0.1 + random.randint(200, 500) * 0.001 + task = self.loop.create_task(self.task(interval, parent_span)) + tasks.append(task) + + return tasks diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py new file mode 100644 index 00000000000..b24ae643e36 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_multiple_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +import random +import time +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import RefCount, get_logger + +random.seed() +logger = get_logger(__name__) + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + try: + scope = self.tracer.start_active_span( + "parent", finish_on_close=False + ) + scope.span.ref_count = RefCount(1) + self.submit_callbacks(scope.span) + finally: + scope.close() + if scope.span.ref_count.decr() == 0: + scope.span.finish() + + self.executor.shutdown(True) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 4) + self.assertNamesEqual(spans, ["task", "task", "task", "parent"]) + + for idx in range(3): + self.assertSameTrace(spans[idx], spans[-1]) + self.assertIsChildOf(spans[idx], spans[-1]) + + def task(self, interval, parent_span): + logger.info("Starting task") + + try: + scope = self.tracer.scope_manager.activate(parent_span, False) + with self.tracer.start_active_span("task"): + time.sleep(interval) + finally: + scope.close() + if parent_span.ref_count.decr() == 0: + parent_span.finish() + + def submit_callbacks(self, parent_span): + for _ in range(3): + parent_span.ref_count.incr() + self.executor.submit( + self.task, 0.1 + random.randint(200, 500) * 0.001, parent_span + ) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst new file mode 100644 index 00000000000..c191431ccc4 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/README.rst @@ -0,0 +1,47 @@ + +Nested callbacks example. +========================= + +This example shows a ``Span`` for a top-level operation, and how it can be passed down on a list of nested callbacks (always one at a time), have it as the active one for each of them, and finished **only** when the last one executes. For Python, we have decided to do it in a **fire-and-forget** fashion. + +Implementation details: + + +* For ``threading``, the ``Span`` is manually activatted it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + ... + +``asyncio`` implementation: + +.. code-block:: python + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py new file mode 100644 index 00000000000..12eb4362770 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_asyncio.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import stop_loop_when + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + async def task(): + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + self.loop.create_task(task()) + + stop_loop_when( + self.loop, + lambda: len(self.tracer.finished_spans()) == 1, + timeout=5.0, + ) + self.loop.run_forever() + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + async def task1(): + span.set_tag("key1", "1") + + async def task2(): + span.set_tag("key2", "2") + + async def task3(): + span.set_tag("key3", "3") + span.finish() + + self.loop.create_task(task3()) + + self.loop.create_task(task2()) + + self.loop.create_task(task1()) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py new file mode 100644 index 00000000000..a1d35c35d88 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_nested_callbacks/test_threads.py @@ -0,0 +1,59 @@ +from __future__ import print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase +from ..utils import await_until + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def tearDown(self): + self.executor.shutdown(False) + + def test_main(self): + # Start a Span and let the callback-chain + # finish it when the task is done + with self.tracer.start_active_span("one", finish_on_close=False): + self.submit() + + # Cannot shutdown the executor and wait for the callbacks + # to be run, as in such case only the first will be executed, + # and the rest will get canceled. + await_until(lambda: len(self.tracer.finished_spans()) == 1, 5) + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "one") + + for idx in range(1, 4): + self.assertEqual( + spans[0].attributes.get("key%s" % idx, None), str(idx) + ) + + def submit(self): + span = self.tracer.scope_manager.active.span + + def task1(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key1", "1") + + def task2(): + with self.tracer.scope_manager.activate(span, False): + span.set_tag("key2", "2") + + def task3(): + with self.tracer.scope_manager.activate( + span, True + ): + span.set_tag("key3", "3") + + self.executor.submit(task3) + + self.executor.submit(task2) + + self.executor.submit(task1) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst new file mode 100644 index 00000000000..eaeda8e6f81 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/README.rst @@ -0,0 +1,42 @@ + +Subtask Span propagation example. +================================= + +This example shows an active ``Span`` being simply propagated to the subtasks -either threads or coroutines-, and finished **by** the parent task. In real-life scenarios instrumentation libraries may help with ``Span`` propagation **if** not offered by default (see implementation details below), but we show here the case without such help. + +Implementation details: + +* For ``threading``, the ``Span`` is manually passed down the call chain, activating it in each corotuine/task. +* For ``asyncio``, the active ``Span`` is not passed nor activated down the chain as the ``Context`` automatically propagates it. + +``threading`` implementation: + +.. code-block:: python + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + f = self.executor.submit(self.child_task, message, scope.span) + res = f.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message + +``asyncio`` implementation: + +.. code-block:: python + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message + diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py new file mode 100644 index 00000000000..6e544560704 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_asyncio.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, print_function + +import asyncio + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestAsyncio(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.loop = asyncio.get_event_loop() + + def test_main(self): + res = self.loop.run_until_complete(self.parent_task("message")) + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + async def parent_task(self, message): # noqa + with self.tracer.start_active_span("parent"): + res = await self.child_task(message) + + return res + + async def child_task(self, message): + # No need to pass/activate the parent Span, as it stays in the context. + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py new file mode 100644 index 00000000000..1ba5f697cad --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/test_subtask_span_propagation/test_threads.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, print_function + +from concurrent.futures import ThreadPoolExecutor + +from ..otel_ot_shim_tracer import MockTracer +from ..testcase import OpenTelemetryTestCase + + +class TestThreads(OpenTelemetryTestCase): + def setUp(self): + self.tracer = MockTracer() + self.executor = ThreadPoolExecutor(max_workers=3) + + def test_main(self): + res = self.executor.submit(self.parent_task, "message").result() + self.assertEqual(res, "message::response") + + spans = self.tracer.finished_spans() + self.assertEqual(len(spans), 2) + self.assertNamesEqual(spans, ["child", "parent"]) + self.assertIsChildOf(spans[0], spans[1]) + + def parent_task(self, message): + with self.tracer.start_active_span("parent") as scope: + fut = self.executor.submit(self.child_task, message, scope.span) + res = fut.result() + + return res + + def child_task(self, message, span): + with self.tracer.scope_manager.activate(span, False): + with self.tracer.start_active_span("child"): + return "%s::response" % message diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py new file mode 100644 index 00000000000..c1ce6ea5abd --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/testcase.py @@ -0,0 +1,46 @@ +import unittest + +import opentelemetry.trace as trace_api + + +# pylint: disable=C0103 +class OpenTelemetryTestCase(unittest.TestCase): + def assertSameTrace(self, spanA, spanB): + return self.assertEqual(spanA.context.trace_id, spanB.context.trace_id) + + def assertNotSameTrace(self, spanA, spanB): + return self.assertNotEqual( + spanA.context.trace_id, spanB.context.trace_id + ) + + def assertIsChildOf(self, spanA, spanB): + # spanA is child of spanB + self.assertIsNotNone(spanA.parent) + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + return self.assertEqual(ctxA.span_id, ctxB.span_id) + + def assertIsNotChildOf(self, spanA, spanB): + # spanA is NOT child of spanB + if spanA.parent is None: + return + + ctxA = spanA.parent + if isinstance(spanA.parent, trace_api.Span): + ctxA = spanA.parent.context + + ctxB = spanB + if isinstance(ctxB, trace_api.Span): + ctxB = spanB.context + + self.assertNotEqual(ctxA.span_id, ctxB.span_id) + + def assertNamesEqual(self, spans, names): + self.assertEqual(list(map(lambda x: x.name, spans)), names) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py new file mode 100644 index 00000000000..a7b977f3d76 --- /dev/null +++ b/ext/opentelemetry-ext-opentracing-shim/tests/testbed/utils.py @@ -0,0 +1,78 @@ +from __future__ import print_function + +import logging +import threading +import time + + +class RefCount: + """Thread-safe counter""" + + def __init__(self, count=1): + self._lock = threading.Lock() + self._count = count + + def incr(self): + with self._lock: + self._count += 1 + return self._count + + def decr(self): + with self._lock: + self._count -= 1 + return self._count + + +def await_until(func, timeout=5.0): + """Polls for func() to return True""" + end_time = time.time() + timeout + while time.time() < end_time and not func(): + time.sleep(0.01) + + +def stop_loop_when(loop, cond_func, timeout=5.0): + """ + Registers a periodic callback that stops the loop when cond_func() == True. + Compatible with both Tornado and asyncio. + """ + if cond_func() or timeout <= 0.0: + loop.stop() + return + + timeout -= 0.1 + loop.call_later(0.1, stop_loop_when, loop, cond_func, timeout) + + +def get_logger(name): + """Returns a logger with log level set to INFO""" + logging.basicConfig(level=logging.INFO) + return logging.getLogger(name) + + +def get_one_by_tag(spans, key, value): + """Return a single Span with a tag value/key from a list, + errors if more than one is found.""" + + found = [] + for span in spans: + if span.attributes.get(key) == value: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None + + +def get_one_by_operation_name(spans, name): + """Return a single Span with a name from a list, + errors if more than one is found.""" + found = [] + for span in spans: + if span.name == name: + found.append(span) + + if len(found) > 1: + raise RuntimeError("Too many values") + + return found[0] if len(found) > 0 else None diff --git a/opentelemetry-api/tests/__init__.py b/opentelemetry-api/tests/__init__.py index b0a6f428417..bc48946761e 100644 --- a/opentelemetry-api/tests/__init__.py +++ b/opentelemetry-api/tests/__init__.py @@ -11,3 +11,12 @@ # 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 pkg_resources + +# naming the tests module as a namespace package ensures that +# relative imports will resolve properly for other test packages, +# as it enables searching for a composite of multiple test modules. +# +# only the opentelemetry-api directory needs this code, as it is +# the first tests module found by pylint during eachdist.py lint +pkg_resources.declare_namespace(__name__) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 8e09ae23a31..0b45fbf643b 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -3,13 +3,25 @@ set -e function cov { - pytest \ - --ignore-glob=*/setup.py \ - --cov ${1} \ - --cov-append \ - --cov-branch \ - --cov-report='' \ - ${1} + if [ ${TOX_ENV_NAME:0:4} == "py34" ] + then + pytest \ + --ignore-glob=*/setup.py \ + --ignore-glob=ext/opentelemetry-ext-opentracing-shim/tests/testbed/* \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + else + pytest \ + --ignore-glob=*/setup.py \ + --cov ${1} \ + --cov-append \ + --cov-branch \ + --cov-report='' \ + ${1} + fi } PYTHON_VERSION=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') diff --git a/tox.ini b/tox.ini index 978155616a8..381e8604e1e 100644 --- a/tox.ini +++ b/tox.ini @@ -227,10 +227,11 @@ commands_pre = jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger - datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog - + opentracing-shim: pip install {toxinidir}/opentelemetry-sdk opentracing-shim: pip install {toxinidir}/ext/opentelemetry-ext-opentracing-shim + datadog: pip install {toxinidir}/opentelemetry-sdk {toxinidir}/ext/opentelemetry-ext-datadog + zipkin: pip install {toxinidir}/ext/opentelemetry-ext-zipkin sqlalchemy: pip install {toxinidir}/opentelemetry-auto-instrumentation {toxinidir}/ext/opentelemetry-ext-sqlalchemy @@ -258,6 +259,9 @@ commands = ; implicit Any due to unfollowed import would result). mypyinstalled: mypy --namespace-packages opentelemetry-api/tests/mypysmoke.py --strict +[testenv:py34-test-opentracing-shim] +commands = + pytest --ignore-glob='*[asyncio].py' [testenv:lint] basepython: python3.8 From c4f1364bc71c1a83bbd2f81001eea8947b2a5439 Mon Sep 17 00:00:00 2001 From: alrex Date: Tue, 2 Jun 2020 21:44:55 -0700 Subject: [PATCH 16/25] chore: removing Oberon00 from approvers (#770) Co-authored-by: Yusuke Tsutsumi --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b297a77cd1d..613b4e6acbd 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,6 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Carlos Alberto Cortez](https://github.com/carlosalberto), LightStep - [Chris Kleinknecht](https://github.com/c24t), Google -- [Christian Neumüller](https://github.com/Oberon00), Dynatrace - [Diego Hurtado](https://github.com/ocelotl) - [Hector Hernandez](https://github.com/hectorhdzg), Microsoft - [Leighton Chen](https://github.com/lzchen), Microsoft @@ -119,6 +118,12 @@ Maintainers ([@open-telemetry/python-maintainers](https://github.com/orgs/open-t - [Alex Boten](https://github.com/codeboten), LightStep - [Yusuke Tsutsumi](https://github.com/toumorokoshi), Zillow Group +### Thanks to all the people who already contributed! + + + + + *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).* ## Release Schedule From afe61508775bf8e00d58cf319c27c09822fe6eb9 Mon Sep 17 00:00:00 2001 From: Andrew Xue Date: Thu, 4 Jun 2020 00:33:36 -0400 Subject: [PATCH 17/25] cloud-trace: Cloud Trace exporter (#698) Co-authored-by: Cheng-Lung Sung --- docs-requirements.txt | 1 + docs/examples/cloud_trace_exporter/README.rst | 34 ++ .../cloud_trace_exporter/basic_trace.py | 14 + docs/ext/cloud_trace/cloud_trace.rst | 7 + .../README.rst | 43 ++ .../setup.cfg | 47 ++ .../setup.py | 26 ++ .../exporter/cloud_trace/__init__.py | 332 +++++++++++++++ .../exporter/cloud_trace/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_cloud_trace_exporter.py | 401 ++++++++++++++++++ scripts/coverage.sh | 1 + tox.ini | 1 + 13 files changed, 922 insertions(+) create mode 100644 docs/examples/cloud_trace_exporter/README.rst create mode 100644 docs/examples/cloud_trace_exporter/basic_trace.py create mode 100644 docs/ext/cloud_trace/cloud_trace.rst create mode 100644 ext/opentelemetry-exporter-cloud-trace/README.rst create mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.cfg create mode 100644 ext/opentelemetry-exporter-cloud-trace/setup.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/__init__.py create mode 100644 ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py diff --git a/docs-requirements.txt b/docs-requirements.txt index a61f0beedb3..db10f6f9ee4 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -21,3 +21,4 @@ thrift>=0.10.0 wrapt>=1.0.0,<2.0.0 psutil~=5.7.0 boto~=2.0 +google-cloud-trace >=0.23.0 diff --git a/docs/examples/cloud_trace_exporter/README.rst b/docs/examples/cloud_trace_exporter/README.rst new file mode 100644 index 00000000000..871422356a7 --- /dev/null +++ b/docs/examples/cloud_trace_exporter/README.rst @@ -0,0 +1,34 @@ +Cloud Trace Exporter Example +============================ + +These examples show how to use OpenTelemetry to send tracing data to Cloud Trace. + + +Basic Example +------------- + +To use this exporter you first need to: + * A Google Cloud project. You can `create one here. `_ + * Enable Cloud Trace API (aka StackDriver Trace API) in the project `here. `_ + * Enable `Default Application Credentials. `_ + +* Installation + +.. code-block:: sh + + pip install opentelemetry-api + pip install opentelemetry-sdk + pip install opentelemetry-exporter-cloud-trace + +* Run example + +.. code-block:: sh + + python basic_trace.py + +Checking Output +-------------------------- + +After running any of these examples, you can go to `Cloud Trace overview `_ to see the results. + +* `More information about exporters in general `_ \ No newline at end of file diff --git a/docs/examples/cloud_trace_exporter/basic_trace.py b/docs/examples/cloud_trace_exporter/basic_trace.py new file mode 100644 index 00000000000..76840a291ec --- /dev/null +++ b/docs/examples/cloud_trace_exporter/basic_trace.py @@ -0,0 +1,14 @@ +from opentelemetry import trace +from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + +trace.set_tracer_provider(TracerProvider()) + +cloud_trace_exporter = CloudTraceSpanExporter() +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) +) +tracer = trace.get_tracer(__name__) +with tracer.start_as_current_span("foo"): + print("Hello world!") diff --git a/docs/ext/cloud_trace/cloud_trace.rst b/docs/ext/cloud_trace/cloud_trace.rst new file mode 100644 index 00000000000..5914b00d1a4 --- /dev/null +++ b/docs/ext/cloud_trace/cloud_trace.rst @@ -0,0 +1,7 @@ +OpenTelemetry Cloud Trace Exporter +================================== + +.. automodule:: opentelemetry.exporter.cloud_trace + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-exporter-cloud-trace/README.rst b/ext/opentelemetry-exporter-cloud-trace/README.rst new file mode 100644 index 00000000000..001f163007e --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/README.rst @@ -0,0 +1,43 @@ +OpenTelemetry Cloud Trace Exporters +=================================== + +This library provides classes for exporting trace data to Google Cloud Trace. + +Installation +------------ + +:: + + pip install opentelemetry-exporter-cloud-trace + +Usage +----- + +.. code:: python + + from opentelemetry import trace + from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + SimpleExportSpanProcessor, + ) + + trace.set_tracer_provider(TracerProvider()) + + cloud_trace_exporter = CloudTraceSpanExporter( + project_id='my-gcloud-project', + ) + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span('foo'): + print('Hello world!') + + + +References +---------- + +* `Cloud Trace `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.cfg b/ext/opentelemetry-exporter-cloud-trace/setup.cfg new file mode 100644 index 00000000000..df6c2ce587b --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/setup.cfg @@ -0,0 +1,47 @@ +# Copyright 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. +# +[metadata] +name = opentelemetry-exporter-cloud-trace +description = Cloud Trace integration for OpenTelemetry +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-exporter-cloud-trace +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api + opentelemetry-sdk + google-cloud-trace + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-exporter-cloud-trace/setup.py b/ext/opentelemetry-exporter-cloud-trace/setup.py new file mode 100644 index 00000000000..332cf41d01c --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/setup.py @@ -0,0 +1,26 @@ +# Copyright 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "exporter", "cloud_trace", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py new file mode 100644 index 00000000000..7e7aa017cfd --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py @@ -0,0 +1,332 @@ +# Copyright 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. + +"""Cloud Trace Span Exporter for OpenTelemetry. Uses Cloud Trace Client's REST +API to export traces and spans for viewing in Cloud Trace. + +Usage +----- + +.. code-block:: python + + from opentelemetry import trace + from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + + trace.set_tracer_provider(TracerProvider()) + + cloud_trace_exporter = CloudTraceSpanExporter() + trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(cloud_trace_exporter) + ) + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("foo"): + print("Hello world!") + + +API +--- +""" + +import logging +from typing import Any, Dict, List, Optional, Sequence, Tuple + +import google.auth +from google.cloud.trace_v2 import TraceServiceClient +from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue +from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan +from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString +from google.rpc.status_pb2 import Status + +import opentelemetry.trace as trace_api +from opentelemetry.sdk.trace import Event +from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult +from opentelemetry.sdk.util import BoundedDict +from opentelemetry.util import types + +logger = logging.getLogger(__name__) + +MAX_NUM_LINKS = 128 +MAX_NUM_EVENTS = 32 +MAX_EVENT_ATTRS = 4 +MAX_LINK_ATTRS = 32 +MAX_SPAN_ATTRS = 32 + + +class CloudTraceSpanExporter(SpanExporter): + """Cloud Trace span exporter for OpenTelemetry. + + Args: + project_id: ID of the cloud project that will receive the traces. + client: Cloud Trace client. If not given, will be taken from gcloud + default credentials + """ + + def __init__( + self, project_id=None, client=None, + ): + self.client = client or TraceServiceClient() + if not project_id: + _, self.project_id = google.auth.default() + else: + self.project_id = project_id + + def export(self, spans: Sequence[Span]) -> SpanExportResult: + """Export the spans to Cloud Trace. + + See: https://cloud.google.com/trace/docs/reference/v2/rest/v2/projects.traces/batchWrite + + Args: + spans: Tuple of spans to export + """ + cloud_trace_spans = [] + for span in self._translate_to_cloud_trace(spans): + try: + cloud_trace_spans.append(self.client.create_span(**span)) + # pylint: disable=broad-except + except Exception as ex: + logger.error("Error when creating span %s", span, exc_info=ex) + + try: + self.client.batch_write_spans( + "projects/{}".format(self.project_id), cloud_trace_spans, + ) + # pylint: disable=broad-except + except Exception as ex: + logger.error("Error while writing to Cloud Trace", exc_info=ex) + return SpanExportResult.FAILURE + + return SpanExportResult.SUCCESS + + def _translate_to_cloud_trace( + self, spans: Sequence[Span] + ) -> List[Dict[str, Any]]: + """Translate the spans to Cloud Trace format. + + Args: + spans: Tuple of spans to convert + """ + + cloud_trace_spans = [] + + for span in spans: + ctx = span.get_context() + trace_id = _get_hexadecimal_trace_id(ctx.trace_id) + span_id = _get_hexadecimal_span_id(ctx.span_id) + span_name = "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ) + + parent_id = None + if span.parent: + parent_id = _get_hexadecimal_span_id(span.parent.span_id) + + start_time = _get_time_from_ns(span.start_time) + end_time = _get_time_from_ns(span.end_time) + + if len(span.attributes) > MAX_SPAN_ATTRS: + logger.warning( + "Span has more then %s attributes, some will be truncated", + MAX_SPAN_ATTRS, + ) + + cloud_trace_spans.append( + { + "name": span_name, + "span_id": span_id, + "display_name": _get_truncatable_str_object( + span.name, 128 + ), + "start_time": start_time, + "end_time": end_time, + "parent_span_id": parent_id, + "attributes": _extract_attributes( + span.attributes, MAX_SPAN_ATTRS + ), + "links": _extract_links(span.links), + "status": _extract_status(span.status), + "time_events": _extract_events(span.events), + } + ) + # TODO: Leverage more of the Cloud Trace API, e.g. + # same_process_as_parent_span and child_span_count + + return cloud_trace_spans + + def shutdown(self): + pass + + +def _get_hexadecimal_trace_id(trace_id: int) -> str: + return "{:032x}".format(trace_id) + + +def _get_hexadecimal_span_id(span_id: int) -> str: + return "{:016x}".format(span_id) + + +def _get_time_from_ns(nanoseconds: int) -> Dict: + """Given epoch nanoseconds, split into epoch milliseconds and remaining + nanoseconds""" + if not nanoseconds: + return None + seconds, nanos = divmod(nanoseconds, 1e9) + return {"seconds": int(seconds), "nanos": int(nanos)} + + +def _get_truncatable_str_object(str_to_convert: str, max_length: int): + """Truncate the string if it exceeds the length limit and record the + truncated bytes count.""" + truncated, truncated_byte_count = _truncate_str(str_to_convert, max_length) + + return TruncatableString( + value=truncated, truncated_byte_count=truncated_byte_count + ) + + +def _truncate_str(str_to_check: str, limit: int) -> Tuple[str, int]: + """Check the length of a string. If exceeds limit, then truncate it.""" + encoded = str_to_check.encode("utf-8") + truncated_str = encoded[:limit].decode("utf-8", errors="ignore") + return truncated_str, len(encoded) - len(truncated_str.encode("utf-8")) + + +def _extract_status(status: trace_api.Status) -> Optional[Status]: + """Convert a Status object to protobuf object.""" + if not status: + return None + status_dict = {"details": None, "code": status.canonical_code.value} + + if status.description is not None: + status_dict["message"] = status.description + + return Status(**status_dict) + + +def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links: + """Convert span.links""" + if not links: + return None + extracted_links = [] + dropped_links = 0 + if len(links) > MAX_NUM_LINKS: + logger.warning( + "Exporting more then %s links, some will be truncated", + MAX_NUM_LINKS, + ) + dropped_links = len(links) - MAX_NUM_LINKS + links = links[:MAX_NUM_LINKS] + for link in links: + if len(link.attributes) > MAX_LINK_ATTRS: + logger.warning( + "Link has more then %s attributes, some will be truncated", + MAX_LINK_ATTRS, + ) + trace_id = _get_hexadecimal_trace_id(link.context.trace_id) + span_id = _get_hexadecimal_span_id(link.context.span_id) + extracted_links.append( + { + "trace_id": trace_id, + "span_id": span_id, + "type": "TYPE_UNSPECIFIED", + "attributes": _extract_attributes( + link.attributes, MAX_LINK_ATTRS + ), + } + ) + return ProtoSpan.Links( + link=extracted_links, dropped_links_count=dropped_links + ) + + +def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents: + """Convert span.events to dict.""" + if not events: + return None + logs = [] + dropped_annontations = 0 + if len(events) > MAX_NUM_EVENTS: + logger.warning( + "Exporting more then %s annotations, some will be truncated", + MAX_NUM_EVENTS, + ) + dropped_annontations = len(events) - MAX_NUM_EVENTS + events = events[:MAX_NUM_EVENTS] + for event in events: + if len(event.attributes) > MAX_EVENT_ATTRS: + logger.warning( + "Event %s has more then %s attributes, some will be truncated", + event.name, + MAX_EVENT_ATTRS, + ) + logs.append( + { + "time": _get_time_from_ns(event.timestamp), + "annotation": { + "description": _get_truncatable_str_object( + event.name, 256 + ), + "attributes": _extract_attributes( + event.attributes, MAX_EVENT_ATTRS + ), + }, + } + ) + return ProtoSpan.TimeEvents( + time_event=logs, + dropped_annotations_count=dropped_annontations, + dropped_message_events_count=0, + ) + + +def _extract_attributes( + attrs: types.Attributes, num_attrs_limit: int +) -> ProtoSpan.Attributes: + """Convert span.attributes to dict.""" + attributes_dict = BoundedDict(num_attrs_limit) + + for key, value in attrs.items(): + key = _truncate_str(key, 128)[0] + value = _format_attribute_value(value) + + if value is not None: + attributes_dict[key] = value + return ProtoSpan.Attributes( + attribute_map=attributes_dict, + dropped_attributes_count=len(attrs) - len(attributes_dict), + ) + + +def _format_attribute_value(value: types.AttributeValue) -> AttributeValue: + if isinstance(value, bool): + value_type = "bool_value" + elif isinstance(value, int): + value_type = "int_value" + elif isinstance(value, str): + value_type = "string_value" + value = _get_truncatable_str_object(value, 256) + elif isinstance(value, float): + value_type = "string_value" + value = _get_truncatable_str_object("{:0.4f}".format(value), 256) + else: + logger.warning( + "ignoring attribute value %s of type %s. Values type must be one " + "of bool, int, string or float", + value, + type(value), + ) + return None + + return AttributeValue(**{value_type: value}) diff --git a/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py new file mode 100644 index 00000000000..f83f20e7bac --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/version.py @@ -0,0 +1,15 @@ +# Copyright 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. + +__version__ = "0.9.dev0" diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py b/ext/opentelemetry-exporter-cloud-trace/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py new file mode 100644 index 00000000000..5ebd5f3b649 --- /dev/null +++ b/ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py @@ -0,0 +1,401 @@ +# Copyright 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 unittest import mock + +from google.cloud.trace_v2.proto.trace_pb2 import AttributeValue +from google.cloud.trace_v2.proto.trace_pb2 import Span as ProtoSpan +from google.cloud.trace_v2.proto.trace_pb2 import TruncatableString +from google.rpc.status_pb2 import Status + +from opentelemetry.exporter.cloud_trace import ( + MAX_EVENT_ATTRS, + MAX_LINK_ATTRS, + MAX_NUM_EVENTS, + MAX_NUM_LINKS, + CloudTraceSpanExporter, + _extract_attributes, + _extract_events, + _extract_links, + _extract_status, + _format_attribute_value, + _truncate_str, +) +from opentelemetry.sdk.trace import Event, Span +from opentelemetry.trace import Link, SpanContext, SpanKind +from opentelemetry.trace.status import Status as SpanStatus +from opentelemetry.trace.status import StatusCanonicalCode + + +class TestCloudTraceSpanExporter(unittest.TestCase): + def setUp(self): + self.client_patcher = mock.patch( + "opentelemetry.exporter.cloud_trace.TraceServiceClient" + ) + self.client_patcher.start() + self.project_id = "PROJECT" + self.attributes_variety_pack = { + "str_key": "str_value", + "bool_key": False, + "double_key": 1.421, + "int_key": 123, + } + self.extracted_attributes_variety_pack = ProtoSpan.Attributes( + attribute_map={ + "str_key": AttributeValue( + string_value=TruncatableString( + value="str_value", truncated_byte_count=0 + ) + ), + "bool_key": AttributeValue(bool_value=False), + "double_key": AttributeValue( + string_value=TruncatableString( + value="1.4210", truncated_byte_count=0 + ) + ), + "int_key": AttributeValue(int_value=123), + } + ) + + def tearDown(self): + self.client_patcher.stop() + + def test_constructor_default(self): + exporter = CloudTraceSpanExporter(self.project_id) + self.assertEqual(exporter.project_id, self.project_id) + + def test_constructor_explicit(self): + client = mock.Mock() + exporter = CloudTraceSpanExporter(self.project_id, client=client) + + self.assertIs(exporter.client, client) + self.assertEqual(exporter.project_id, self.project_id) + + def test_export(self): + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id = "95bb5edabd45950f" + span_datas = [ + Span( + name="span_name", + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + parent=None, + kind=SpanKind.INTERNAL, + ) + ] + + cloud_trace_spans = { + "name": "projects/{}/traces/{}/spans/{}".format( + self.project_id, trace_id, span_id + ), + "span_id": span_id, + "parent_span_id": None, + "display_name": TruncatableString( + value="span_name", truncated_byte_count=0 + ), + "attributes": ProtoSpan.Attributes(attribute_map={}), + "links": None, + "status": None, + "time_events": None, + "start_time": None, + "end_time": None, + } + + client = mock.Mock() + + exporter = CloudTraceSpanExporter(self.project_id, client=client) + + exporter.export(span_datas) + + client.create_span.assert_called_with(**cloud_trace_spans) + self.assertTrue(client.create_span.called) + + def test_extract_status(self): + self.assertIsNone(_extract_status(None)) + self.assertEqual( + _extract_status(SpanStatus(canonical_code=StatusCanonicalCode.OK)), + Status(details=None, code=0), + ) + self.assertEqual( + _extract_status( + SpanStatus( + canonical_code=StatusCanonicalCode.UNKNOWN, + description="error_desc", + ) + ), + Status(details=None, code=2, message="error_desc"), + ) + + def test_extract_attributes(self): + self.assertEqual( + _extract_attributes({}, 4), ProtoSpan.Attributes(attribute_map={}) + ) + self.assertEqual( + _extract_attributes(self.attributes_variety_pack, 4), + self.extracted_attributes_variety_pack, + ) + # Test ignoring attributes with illegal value type + self.assertEqual( + _extract_attributes({"illegal_attribute_value": dict()}, 4), + ProtoSpan.Attributes(attribute_map={}, dropped_attributes_count=1), + ) + + too_many_attrs = {} + for attr_key in range(5): + too_many_attrs[str(attr_key)] = 0 + proto_attrs = _extract_attributes(too_many_attrs, 4) + self.assertEqual(proto_attrs.dropped_attributes_count, 1) + + def test_extract_events(self): + self.assertIsNone(_extract_events([])) + time_in_ns1 = 1589919268850900051 + time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} + time_in_ns2 = 1589919438550020326 + time_in_ms_and_ns2 = {"seconds": 1589919438, "nanos": 550020352} + event1 = Event( + name="event1", + attributes=self.attributes_variety_pack, + timestamp=time_in_ns1, + ) + event2 = Event( + name="event2", + attributes={"illegal_attr_value": dict()}, + timestamp=time_in_ns2, + ) + self.assertEqual( + _extract_events([event1, event2]), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value="event1", truncated_byte_count=0 + ), + "attributes": self.extracted_attributes_variety_pack, + }, + }, + { + "time": time_in_ms_and_ns2, + "annotation": { + "description": TruncatableString( + value="event2", truncated_byte_count=0 + ), + "attributes": ProtoSpan.Attributes( + attribute_map={}, dropped_attributes_count=1 + ), + }, + }, + ] + ), + ) + + def test_extract_links(self): + self.assertIsNone(_extract_links([])) + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id1 = "95bb5edabd45950f" + span_id2 = "b6b86ad2915c9ddc" + link1 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id1, 16), + is_remote=False, + ), + attributes={}, + ) + link2 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id1, 16), + is_remote=False, + ), + attributes=self.attributes_variety_pack, + ) + link3 = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id2, 16), + is_remote=False, + ), + attributes={"illegal_attr_value": dict(), "int_attr_value": 123}, + ) + self.assertEqual( + _extract_links([link1, link2, link3]), + ProtoSpan.Links( + link=[ + { + "trace_id": trace_id, + "span_id": span_id1, + "type": "TYPE_UNSPECIFIED", + "attributes": ProtoSpan.Attributes(attribute_map={}), + }, + { + "trace_id": trace_id, + "span_id": span_id1, + "type": "TYPE_UNSPECIFIED", + "attributes": self.extracted_attributes_variety_pack, + }, + { + "trace_id": trace_id, + "span_id": span_id2, + "type": "TYPE_UNSPECIFIED", + "attributes": { + "attribute_map": { + "int_attr_value": AttributeValue(int_value=123) + }, + "dropped_attributes_count": 1, + }, + }, + ] + ), + ) + + # pylint:disable=too-many-locals + def test_truncate(self): + """Cloud Trace API imposes limits on the length of many things, + e.g. strings, number of events, number of attributes. We truncate + these things before sending it to the API as an optimization. + """ + str_300 = "a" * 300 + str_256 = "a" * 256 + str_128 = "a" * 128 + self.assertEqual(_truncate_str("aaaa", 1), ("a", 3)) + self.assertEqual(_truncate_str("aaaa", 5), ("aaaa", 0)) + self.assertEqual(_truncate_str("aaaa", 4), ("aaaa", 0)) + self.assertEqual(_truncate_str("中文翻译", 4), ("中", 9)) + + self.assertEqual( + _format_attribute_value(str_300), + AttributeValue( + string_value=TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ) + ), + ) + + self.assertEqual( + _extract_attributes({str_300: str_300}, 4), + ProtoSpan.Attributes( + attribute_map={ + str_128: AttributeValue( + string_value=TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ) + ) + } + ), + ) + + time_in_ns1 = 1589919268850900051 + time_in_ms_and_ns1 = {"seconds": 1589919268, "nanos": 850899968} + event1 = Event(name=str_300, attributes={}, timestamp=time_in_ns1) + self.assertEqual( + _extract_events([event1]), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ), + "attributes": {}, + }, + }, + ] + ), + ) + + trace_id = "6e0c63257de34c92bf9efcd03927272e" + span_id = "95bb5edabd45950f" + link = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + attributes={}, + ) + too_many_links = [link] * (MAX_NUM_LINKS + 1) + self.assertEqual( + _extract_links(too_many_links), + ProtoSpan.Links( + link=[ + { + "trace_id": trace_id, + "span_id": span_id, + "type": "TYPE_UNSPECIFIED", + "attributes": {}, + } + ] + * MAX_NUM_LINKS, + dropped_links_count=len(too_many_links) - MAX_NUM_LINKS, + ), + ) + + link_attrs = {} + for attr_key in range(MAX_LINK_ATTRS + 1): + link_attrs[str(attr_key)] = 0 + attr_link = Link( + context=SpanContext( + trace_id=int(trace_id, 16), + span_id=int(span_id, 16), + is_remote=False, + ), + attributes=link_attrs, + ) + + proto_link = _extract_links([attr_link]) + self.assertEqual( + len(proto_link.link[0].attributes.attribute_map), MAX_LINK_ATTRS + ) + + too_many_events = [event1] * (MAX_NUM_EVENTS + 1) + self.assertEqual( + _extract_events(too_many_events), + ProtoSpan.TimeEvents( + time_event=[ + { + "time": time_in_ms_and_ns1, + "annotation": { + "description": TruncatableString( + value=str_256, truncated_byte_count=300 - 256 + ), + "attributes": {}, + }, + }, + ] + * MAX_NUM_EVENTS, + dropped_annotations_count=len(too_many_events) + - MAX_NUM_EVENTS, + ), + ) + + time_in_ns1 = 1589919268850900051 + event_attrs = {} + for attr_key in range(MAX_EVENT_ATTRS + 1): + event_attrs[str(attr_key)] = 0 + proto_events = _extract_events( + [Event(name="a", attributes=event_attrs, timestamp=time_in_ns1)] + ) + self.assertEqual( + len( + proto_events.time_event[0].annotation.attributes.attribute_map + ), + MAX_EVENT_ATTRS, + ) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 0b45fbf643b..1794cdf01b7 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -36,6 +36,7 @@ cov ext/opentelemetry-ext-flask cov ext/opentelemetry-ext-requests cov ext/opentelemetry-ext-jaeger cov ext/opentelemetry-ext-opentracing-shim +cov ext/opentelemetry-exporter-cloud-trace cov ext/opentelemetry-ext-wsgi cov ext/opentelemetry-ext-zipkin cov docs/examples/opentelemetry-example-app diff --git a/tox.ini b/tox.ini index 381e8604e1e..026d01cc0c1 100644 --- a/tox.ini +++ b/tox.ini @@ -159,6 +159,7 @@ changedir = test-ext-opencensusexporter: ext/opentelemetry-ext-opencensusexporter/tests test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-exporter-cloud-trace: ext/opentelemetry-exporter-cloud-trace/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-asgi: ext/opentelemetry-ext-asgi/tests From 9e5ddbbab30933a3072521a76f6e7c40a52f4a62 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 4 Jun 2020 10:02:37 -0700 Subject: [PATCH 18/25] metrics api/sdk: Move "config" out from Meter into MeterProvider (#751) --- docs/examples/basic_meter/basic_metrics.py | 28 ++++--------------- docs/examples/basic_meter/observer.py | 2 -- opentelemetry-api/CHANGELOG.md | 2 ++ .../src/opentelemetry/metrics/__init__.py | 20 ++++++------- opentelemetry-sdk/CHANGELOG.md | 2 ++ .../src/opentelemetry/sdk/metrics/__init__.py | 25 +++++++++++------ .../tests/metrics/test_metrics.py | 5 ++++ 7 files changed, 39 insertions(+), 45 deletions(-) diff --git a/docs/examples/basic_meter/basic_metrics.py b/docs/examples/basic_meter/basic_metrics.py index 6e8a5c040f3..b9ff8d87417 100644 --- a/docs/examples/basic_meter/basic_metrics.py +++ b/docs/examples/basic_meter/basic_metrics.py @@ -30,33 +30,17 @@ stateful = True - -def usage(argv): - print("usage:") - print("{} [mode]".format(argv[0])) - print("mode: stateful (default) or stateless") - - -if len(sys.argv) >= 2: - batcher_mode = sys.argv[1] - if batcher_mode not in ("stateful", "stateless"): - print("bad mode specified.") - usage(sys.argv) - sys.exit(1) - stateful = batcher_mode == "stateful" - print( "Starting example, values will be printed to the console every 5 seconds." ) - +# Stateful determines whether how metrics are collected: if true, metrics +# accumulate over the process lifetime. If false, metrics are reset at the +# beginning of each collection interval. +metrics.set_meter_provider(MeterProvider(stateful)) # The Meter is responsible for creating and recording metrics. Each meter has a -# unique name, which we set as the module's name here. The second argument -# determines whether how metrics are collected: if true, metrics accumulate -# over the process lifetime. If false, metrics are reset at the beginning of -# each collection interval. -metrics.set_meter_provider(MeterProvider()) -meter = metrics.get_meter(__name__, batcher_mode == "stateful") +# unique name, which we set as the module's name here. +meter = metrics.get_meter(__name__) # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() diff --git a/docs/examples/basic_meter/observer.py b/docs/examples/basic_meter/observer.py index 0490fbe8efb..aa70abe2a44 100644 --- a/docs/examples/basic_meter/observer.py +++ b/docs/examples/basic_meter/observer.py @@ -24,8 +24,6 @@ from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController -# Configure a stateful batcher -batcher = UngroupedBatcher(stateful=True) metrics.set_meter_provider(MeterProvider()) meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 4b678cd4226..a67c840190c 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Move stateful from Meter to MeterProvider + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index a91a4ce7b76..16ac4c096f0 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -29,7 +29,7 @@ """ import abc from logging import getLogger -from typing import Callable, Dict, Sequence, Tuple, Type, TypeVar +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import _load_provider @@ -195,7 +195,6 @@ class MeterProvider(abc.ABC): def get_meter( self, instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. @@ -212,12 +211,6 @@ def get_meter( E.g., instead of ``"requests"``, use ``"opentelemetry.ext.requests"``. - stateful: True/False to indicate whether the meter will be - stateful. True indicates the meter computes checkpoints - from over the process lifetime. False indicates the meter - computes checkpoints which describe the updates of a single - collection period (deltas). - instrumenting_library_version: Optional. The version string of the instrumenting library. Usually this should be the same as ``pkg_resources.get_distribution(instrumenting_library_name).version``. @@ -233,7 +226,6 @@ class DefaultMeterProvider(MeterProvider): def get_meter( self, instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", ) -> "Meter": # pylint:disable=no-self-use,unused-argument @@ -376,15 +368,19 @@ def unregister_observer(self, observer: "Observer") -> None: def get_meter( instrumenting_module_name: str, - stateful: bool = True, instrumenting_library_version: str = "", + meter_provider: Optional[MeterProvider] = None, ) -> "Meter": """Returns a `Meter` for use by the given instrumentation library. This function is a convenience wrapper for opentelemetry.metrics.get_meter_provider().get_meter + + If meter_provider is omitted the current configured one is used. """ - return get_meter_provider().get_meter( - instrumenting_module_name, stateful, instrumenting_library_version + if meter_provider is None: + meter_provider = get_meter_provider() + return meter_provider.get_meter( + instrumenting_module_name, instrumenting_library_version ) diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 29aaed01dcc..3e54a63b3ba 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Move stateful & resource from Meter to MeterProvider + ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index b9284bae9fe..f231182cc28 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -270,22 +270,21 @@ class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: + source: The `MeterProvider` that created this meter. instrumentation_info: The `InstrumentationInfo` for this meter. - stateful: Indicates whether the meter is stateful. """ def __init__( self, + source: "MeterProvider", instrumentation_info: "InstrumentationInfo", - stateful: bool, - resource: Resource = Resource.create_empty(), ): self.instrumentation_info = instrumentation_info + self.batcher = UngroupedBatcher(source.stateful) + self.resource = source.resource self.metrics = set() self.observers = set() - self.batcher = UngroupedBatcher(stateful) self.observers_lock = threading.Lock() - self.resource = resource def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -398,21 +397,29 @@ def unregister_observer(self, observer: "Observer") -> None: class MeterProvider(metrics_api.MeterProvider): - def __init__(self, resource: Resource = Resource.create_empty()): + """See `opentelemetry.metrics.MeterProvider`. + + Args: + stateful: Indicates whether meters created are going to be stateful + resource: Resource for this MeterProvider + """ + + def __init__( + self, stateful=True, resource: Resource = Resource.create_empty(), + ): + self.stateful = stateful self.resource = resource def get_meter( self, instrumenting_module_name: str, - stateful=True, instrumenting_library_version: str = "", ) -> "metrics_api.Meter": if not instrumenting_module_name: # Reject empty strings too. raise ValueError("get_meter called with missing module name.") return Meter( + self, InstrumentationInfo( instrumenting_module_name, instrumenting_library_version ), - stateful=stateful, - resource=self.resource, ) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index a3c0f4294d9..9d5d2b15d8e 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -21,6 +21,11 @@ class TestMeterProvider(unittest.TestCase): + def test_stateful(self): + meter_provider = metrics.MeterProvider(stateful=False) + meter = meter_provider.get_meter(__name__) + self.assertIs(meter.batcher.stateful, False) + def test_resource(self): resource = resources.Resource.create({}) meter_provider = metrics.MeterProvider(resource=resource) From c5b960a152e09db9f7685974278c32efeb595067 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 4 Jun 2020 23:03:04 -0700 Subject: [PATCH 19/25] otel-shim: Adding test for empty context from extract --- .../ext/opentracing_shim/__init__.py | 6 ++++- .../tests/test_shim.py | 19 +++++++++++++- .../src/opentelemetry/sdk/trace/__init__.py | 4 --- .../opentelemetry/test/mock_httptextformat.py | 26 ++++++++++++++++++- 4 files changed, 48 insertions(+), 7 deletions(-) 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 3f52e49cf90..2796fae7f8a 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 @@ -700,6 +700,10 @@ def get_as_list(dict_object, key): propagator = propagators.get_global_httptextformat() ctx = propagator.extract(get_as_list, carrier) - otel_context = trace_api.get_current_span(ctx).get_context() + span = trace_api.get_current_span(ctx) + if span is not None: + otel_context = span.get_context() + else: + otel_context = trace_api.INVALID_SPAN_CONTEXT return SpanContextShim(otel_context) diff --git a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py index abca2e052bc..941d4280690 100644 --- a/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py +++ b/ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py @@ -24,7 +24,10 @@ from opentelemetry import propagators, trace from opentelemetry.ext.opentracing_shim import util from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat +from opentelemetry.test.mock_httptextformat import ( + MockHTTPTextFormat, + NOOPHTTPTextFormat, +) class TestShim(TestCase): @@ -515,6 +518,20 @@ def test_extract_http_headers(self): self.assertEqual(ctx.unwrap().trace_id, 1220) self.assertEqual(ctx.unwrap().span_id, 7478) + def test_extract_empty_context_returns_invalid_context(self): + """In the case where the propagator cannot extract a + SpanContext, extract should return and invalid span context. + """ + _old_propagator = propagators.get_global_httptextformat() + propagators.set_global_httptextformat(NOOPHTTPTextFormat()) + try: + carrier = {} + + ctx = self.shim.extract(opentracing.Format.HTTP_HEADERS, carrier) + self.assertEqual(ctx.unwrap(), trace.INVALID_SPAN_CONTEXT) + finally: + propagators.set_global_httptextformat(_old_propagator) + def test_extract_text_map(self): """Test `extract()` method for Format.TEXT_MAP.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index b70e8d2b3a2..678682f0db0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -747,10 +747,6 @@ def get_tracer( ), ) - @staticmethod - def get_current_span() -> Span: - return trace_api.get_current_span() - def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_httptextformat.py index b1b281c7a60..641f96a8e45 100644 --- a/tests/util/src/opentelemetry/test/mock_httptextformat.py +++ b/tests/util/src/opentelemetry/test/mock_httptextformat.py @@ -15,7 +15,7 @@ import typing from opentelemetry import trace -from opentelemetry.context import Context +from opentelemetry.context import Context, get_current from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, @@ -28,6 +28,30 @@ ) +class NOOPHTTPTextFormat(HTTPTextFormat): + """A propagator that does not extract nor inject. + + This class is useful for catching edge cases assuming + a SpanContext will always be present. + """ + + def extract( + self, + get_from_carrier: Getter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + return get_current() + + def inject( + self, + set_in_carrier: Setter[HTTPTextFormatT], + carrier: HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + return None + + class MockHTTPTextFormat(HTTPTextFormat): """Mock propagator for testing purposes.""" From d8320d582cb8e25b1af17948f244319d154b6d87 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Thu, 4 Jun 2020 23:07:54 -0700 Subject: [PATCH 20/25] Adding changelog entry. --- opentelemetry-api/CHANGELOG.md | 2 ++ opentelemetry-sdk/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index a67c840190c..c6759c1df27 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Adding trace.get_current_span, Removing Tracer.get_current_span + ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) ## 0.8b0 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 3e54a63b3ba..940f1f17b8b 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ ([#751](https://github.com/open-telemetry/opentelemetry-python/pull/751)) - Rename Measure to ValueRecorder in metrics ([#761](https://github.com/open-telemetry/opentelemetry-python/pull/761)) +- Adding trace.get_current_span, Removing Tracer.get_current_span + ([#552](https://github.com/open-telemetry/opentelemetry-python/pull/552)) ## 0.8b0 From 3732f31c987bd0745c1dc44b8e17786674805a44 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Fri, 5 Jun 2020 10:29:02 -0700 Subject: [PATCH 21/25] addressing lint --- opentelemetry-sdk/tests/trace/propagation/test_b3_format.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index c972fcd24b5..f2cab1b1c89 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -17,8 +17,8 @@ import opentelemetry.sdk.trace as trace import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api -from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.context import get_current +from opentelemetry.trace.propagation import set_span_in_context FORMAT = b3_format.B3Format() @@ -257,7 +257,8 @@ def test_missing_span_id(self): span_context = trace_api.get_current_span(ctx).get_context() self.assertEqual(span_context.span_id, trace_api.INVALID_SPAN_ID) - def test_inject_empty_context(self): + @staticmethod + def test_inject_empty_context(): """If the current context has no span, don't add headers""" new_carrier = {} FORMAT.inject(dict.__setitem__, new_carrier, get_current()) From aae0402cc9bcea04892eb0bf0c429703e3596bcd Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 8 Jun 2020 16:46:38 -0700 Subject: [PATCH 22/25] Addressing comments Adding docstrings for propgation span methods. Exporting set_span_in_context in trace. --- .../opentelemetry/ext/aiohttp_client/__init__.py | 2 +- .../src/opentelemetry/ext/datadog/propagator.py | 5 +---- .../tests/test_datadog_format.py | 5 +---- .../ext/opentracing_shim/__init__.py | 3 +-- .../src/opentelemetry/trace/__init__.py | 6 +++++- .../opentelemetry/trace/propagation/__init__.py | 16 ++++++++++++++++ .../propagation/tracecontexthttptextformat.py | 16 +++++++++------- .../propagators/test_global_httptextformat.py | 3 +-- .../test_tracecontexthttptextformat.py | 11 ++++------- opentelemetry-api/tests/trace/test_globals.py | 3 +-- .../sdk/trace/propagation/b3_format.py | 5 ++--- .../tests/trace/propagation/test_b3_format.py | 3 +-- .../opentelemetry/test/mock_httptextformat.py | 10 +++------- 13 files changed, 46 insertions(+), 42 deletions(-) diff --git a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py index 77dadbd6453..54832698b84 100644 --- a/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py +++ b/ext/opentelemetry-ext-aiohttp-client/src/opentelemetry/ext/aiohttp_client/__init__.py @@ -172,7 +172,7 @@ async def on_request_start( ) trace_config_ctx.token = context_api.attach( - trace.propagation.set_span_in_context(trace_config_ctx.span) + trace.set_span_in_context(trace_config_ctx.span) ) propagators.inject(type(params.headers).__setitem__, params.headers) diff --git a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py index 86b58b02e9e..6ff192f4257 100644 --- a/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py +++ b/ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/propagator.py @@ -16,10 +16,7 @@ from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.trace.propagation import ( - get_current_span, - set_span_in_context, -) +from opentelemetry.trace import get_current_span, set_span_in_context from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, diff --git a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py index 9a9b3620826..31633f83701 100644 --- a/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py +++ b/ext/opentelemetry-ext-datadog/tests/test_datadog_format.py @@ -17,10 +17,7 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.datadog import constants, propagator from opentelemetry.sdk import trace -from opentelemetry.trace.propagation import ( - get_current_span, - set_span_in_context, -) +from opentelemetry.trace import get_current_span, set_span_in_context FORMAT = propagator.DatadogFormat() 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 2796fae7f8a..81f25013ff0 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 @@ -94,8 +94,7 @@ from opentelemetry import propagators from opentelemetry.ext.opentracing_shim import util from opentelemetry.ext.opentracing_shim.version import __version__ -from opentelemetry.trace import DefaultSpan -from opentelemetry.trace.propagation import set_span_in_context +from opentelemetry.trace import DefaultSpan, set_span_in_context logger = logging.getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 88131738417..dcf5fa6ad89 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -97,6 +97,7 @@ "get_tracer", "get_tracer_provider", "set_tracer_provider", + "set_span_in_context", ] import abc @@ -107,7 +108,10 @@ from logging import getLogger from opentelemetry.configuration import Configuration # type: ignore -from opentelemetry.trace.propagation import get_current_span +from opentelemetry.trace.propagation import ( + get_current_span, + set_span_in_context, +) from opentelemetry.trace.span import ( DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE, diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py index 2a8a5eb921c..45a07c920db 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py @@ -23,11 +23,27 @@ def set_span_in_context( span: Span, context: Optional[Context] = None ) -> Context: + """Set the span in the given context. + + Args: + span: The Span to set. + context: a Context object. if one is not passed, the + default current context is used instead. + """ ctx = set_value(SPAN_KEY, span, context=context) return ctx def get_current_span(context: Optional[Context] = None) -> Optional[Span]: + """Retrieve the current span. + + Args: + context: A Context object. If one is not passed, the + default current context is used instead. + + Returns: + The Span set in the context if it exists. None otherwise. + """ span = get_value(SPAN_KEY, context=context) if span is None: return None diff --git a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py index 1d61c970283..fa2fae87035 100644 --- a/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py +++ b/opentelemetry-api/src/opentelemetry/trace/propagation/tracecontexthttptextformat.py @@ -17,7 +17,7 @@ import opentelemetry.trace as trace from opentelemetry.context.context import Context -from opentelemetry.trace.propagation import httptextformat, set_span_in_context +from opentelemetry.trace.propagation import httptextformat # Keys and values are strings of up to 256 printable US-ASCII characters. # Implementations should conform to the `W3C Trace Context - Tracestate`_ @@ -73,11 +73,11 @@ def extract( header = get_from_carrier(carrier, self._TRACEPARENT_HEADER_NAME) if not header: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0]) if not match: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) version = match.group(1) trace_id = match.group(2) @@ -85,13 +85,13 @@ def extract( trace_flags = match.group(4) if trace_id == "0" * 32 or span_id == "0" * 16: - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) if version == "00": if match.group(5): - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) if version == "ff": - return set_span_in_context(trace.INVALID_SPAN, context) + return trace.set_span_in_context(trace.INVALID_SPAN, context) tracestate_headers = get_from_carrier( carrier, self._TRACESTATE_HEADER_NAME @@ -105,7 +105,9 @@ def extract( trace_flags=trace.TraceFlags(trace_flags), trace_state=tracestate, ) - return set_span_in_context(trace.DefaultSpan(span_context), context) + return trace.set_span_in_context( + trace.DefaultSpan(span_context), context + ) def inject( self, diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py index bb4d2040751..a7e94302233 100644 --- a/opentelemetry-api/tests/propagators/test_global_httptextformat.py +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -17,8 +17,7 @@ from opentelemetry import correlationcontext, trace from opentelemetry.propagators import extract, inject -from opentelemetry.trace import get_current_span -from opentelemetry.trace.propagation import set_span_in_context +from opentelemetry.trace import get_current_span, set_span_in_context def get_as_list( diff --git a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py index 36946736127..5adc180d9fc 100644 --- a/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py +++ b/opentelemetry-api/tests/trace/propagation/test_tracecontexthttptextformat.py @@ -16,10 +16,7 @@ import unittest from opentelemetry import trace -from opentelemetry.trace.propagation import ( - set_span_in_context, - tracecontexthttptextformat, -) +from opentelemetry.trace.propagation import tracecontexthttptextformat FORMAT = tracecontexthttptextformat.TraceContextHTTPTextFormat() @@ -75,7 +72,7 @@ def test_headers_with_tracestate(self): output = {} # type:typing.Dict[str, str] span = trace.DefaultSpan(span_context) - ctx = set_span_in_context(span) + ctx = trace.set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) self.assertEqual(output["traceparent"], traceparent_value) for pair in ["foo=1", "bar=2", "baz=3"]: @@ -157,7 +154,7 @@ def test_no_send_empty_tracestate(self): span = trace.DefaultSpan( trace.SpanContext(self.TRACE_ID, self.SPAN_ID, is_remote=False) ) - ctx = set_span_in_context(span) + ctx = trace.set_span_in_context(span) FORMAT.inject(dict.__setitem__, output, ctx) self.assertTrue("traceparent" in output) self.assertFalse("tracestate" in output) @@ -187,7 +184,7 @@ def test_format_not_supported(self): def test_propagate_invalid_context(self): """Do not propagate invalid trace context.""" output = {} # type:typing.Dict[str, str] - ctx = set_span_in_context(trace.INVALID_SPAN) + ctx = trace.set_span_in_context(trace.INVALID_SPAN) FORMAT.inject(dict.__setitem__, output, context=ctx) self.assertFalse("traceparent" in output) diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py index 7ad772b1a1a..2f0f88fb280 100644 --- a/opentelemetry-api/tests/trace/test_globals.py +++ b/opentelemetry-api/tests/trace/test_globals.py @@ -2,7 +2,6 @@ from unittest.mock import patch from opentelemetry import context, trace -from opentelemetry.trace.propagation import set_span_in_context class TestGlobals(unittest.TestCase): @@ -32,7 +31,7 @@ def test_get_current_span(self): """ self.assertIs(trace.get_current_span(), None) span = trace.DefaultSpan(trace.INVALID_SPAN_CONTEXT) - ctx = set_span_in_context(span) + ctx = trace.set_span_in_context(span) token = context.attach(ctx) try: self.assertIs(trace.get_current_span(), span) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py index 13120f3209d..3a9722bc36c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/propagation/b3_format.py @@ -16,7 +16,6 @@ import opentelemetry.trace as trace from opentelemetry.context import Context -from opentelemetry.trace.propagation import set_span_in_context from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -69,7 +68,7 @@ def extract( elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: - return set_span_in_context(trace.INVALID_SPAN) + return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = ( _extract_first_element( @@ -103,7 +102,7 @@ def extract( # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED - return set_span_in_context( + return trace.set_span_in_context( trace.DefaultSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted diff --git a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py index f2cab1b1c89..a5bd1baaa48 100644 --- a/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py +++ b/opentelemetry-sdk/tests/trace/propagation/test_b3_format.py @@ -18,7 +18,6 @@ import opentelemetry.sdk.trace.propagation.b3_format as b3_format import opentelemetry.trace as trace_api from opentelemetry.context import get_current -from opentelemetry.trace.propagation import set_span_in_context FORMAT = b3_format.B3Format() @@ -47,7 +46,7 @@ def get_child_parent_new_carrier(old_carrier): ) new_carrier = {} - ctx = set_span_in_context(child) + ctx = trace_api.set_span_in_context(child) FORMAT.inject(dict.__setitem__, new_carrier, context=ctx) return child, parent, new_carrier diff --git a/tests/util/src/opentelemetry/test/mock_httptextformat.py b/tests/util/src/opentelemetry/test/mock_httptextformat.py index 641f96a8e45..76165c3e4b2 100644 --- a/tests/util/src/opentelemetry/test/mock_httptextformat.py +++ b/tests/util/src/opentelemetry/test/mock_httptextformat.py @@ -16,10 +16,6 @@ from opentelemetry import trace from opentelemetry.context import Context, get_current -from opentelemetry.trace.propagation import ( - get_current_span, - set_span_in_context, -) from opentelemetry.trace.propagation.httptextformat import ( Getter, HTTPTextFormat, @@ -68,9 +64,9 @@ def extract( span_id_list = get_from_carrier(carrier, self.SPAN_ID_KEY) if not trace_id_list or not span_id_list: - return set_span_in_context(trace.INVALID_SPAN) + return trace.set_span_in_context(trace.INVALID_SPAN) - return set_span_in_context( + return trace.set_span_in_context( trace.DefaultSpan( trace.SpanContext( trace_id=int(trace_id_list[0]), @@ -86,7 +82,7 @@ def inject( carrier: HTTPTextFormatT, context: typing.Optional[Context] = None, ) -> None: - span = get_current_span(context) + span = trace.get_current_span(context) set_in_carrier( carrier, self.TRACE_ID_KEY, str(span.get_context().trace_id) ) From cc08e7af2d940435e122e4475cf346c28f6df047 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 8 Jun 2020 20:31:49 -0700 Subject: [PATCH 23/25] removing non-existent reference to get_span_from_context --- .../src/opentelemetry/ext/wsgi/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py index 0895bc75b07..c5b0216869c 100644 --- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py +++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py @@ -62,7 +62,6 @@ def hello(): from opentelemetry import context, propagators, trace from opentelemetry.ext.wsgi.version import __version__ from opentelemetry.instrumentation.utils import http_status_to_canonical_code -from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.trace.status import Status, StatusCanonicalCode _HTTP_VERSION_PREFIX = "HTTP/" From 3d309151b46f0469d6c46c4d045ab5da6ad951a3 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Mon, 8 Jun 2020 20:55:47 -0700 Subject: [PATCH 24/25] fixing typing --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index dcf5fa6ad89..fa0bc376e7f 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -107,7 +107,7 @@ from contextlib import contextmanager from logging import getLogger -from opentelemetry.configuration import Configuration # type: ignore +from opentelemetry.configuration import Configuration from opentelemetry.trace.propagation import ( get_current_span, set_span_in_context, From 8e7db7829ad5c34482e175b4fba1711cb5edecfb Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Tue, 9 Jun 2020 08:42:05 -0700 Subject: [PATCH 25/25] fixing doc header bar length --- docs/api/trace.span.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/trace.span.rst b/docs/api/trace.span.rst index e816928287f..94b36930dfb 100644 --- a/docs/api/trace.span.rst +++ b/docs/api/trace.span.rst @@ -1,5 +1,5 @@ opentelemetry.trace.span -========================== +======================== .. automodule:: opentelemetry.trace.span :members: