From eabee74b9704935412580133f3666f709b931a3b Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 4 Aug 2025 12:05:40 +0200 Subject: [PATCH] Revert "Drop support for opentracing (#2342)" This reverts commit e4dff95d2176a152f385d431848afa71355b3a44. --- .ci/.matrix_framework.yml | 1 + .ci/.matrix_framework_fips.yml | 1 + .ci/.matrix_framework_full.yml | 2 + elasticapm/contrib/opentracing/__init__.py | 43 +++ elasticapm/contrib/opentracing/span.py | 136 ++++++++ elasticapm/contrib/opentracing/tracer.py | 131 ++++++++ setup.cfg | 3 + tests/contrib/opentracing/__init__.py | 29 ++ tests/contrib/opentracing/tests.py | 313 ++++++++++++++++++ tests/requirements/reqs-opentracing-2.0.txt | 2 + .../requirements/reqs-opentracing-newest.txt | 2 + tests/scripts/envs/opentracing.sh | 1 + 12 files changed, 664 insertions(+) create mode 100644 elasticapm/contrib/opentracing/__init__.py create mode 100644 elasticapm/contrib/opentracing/span.py create mode 100644 elasticapm/contrib/opentracing/tracer.py create mode 100644 tests/contrib/opentracing/__init__.py create mode 100644 tests/contrib/opentracing/tests.py create mode 100644 tests/requirements/reqs-opentracing-2.0.txt create mode 100644 tests/requirements/reqs-opentracing-newest.txt create mode 100644 tests/scripts/envs/opentracing.sh diff --git a/.ci/.matrix_framework.yml b/.ci/.matrix_framework.yml index 1cd690c39..8c73d0810 100644 --- a/.ci/.matrix_framework.yml +++ b/.ci/.matrix_framework.yml @@ -12,6 +12,7 @@ FRAMEWORK: - flask-3.0 - jinja2-3 - opentelemetry-newest + - opentracing-newest - twisted-newest - celery-5-flask-2 - celery-5-django-4 diff --git a/.ci/.matrix_framework_fips.yml b/.ci/.matrix_framework_fips.yml index 0c733de80..6bbc9cd3e 100644 --- a/.ci/.matrix_framework_fips.yml +++ b/.ci/.matrix_framework_fips.yml @@ -6,6 +6,7 @@ FRAMEWORK: - flask-3.0 - jinja2-3 - opentelemetry-newest + - opentracing-newest - twisted-newest - celery-5-flask-2 - celery-5-django-5 diff --git a/.ci/.matrix_framework_full.yml b/.ci/.matrix_framework_full.yml index cdabff496..4adb9b25e 100644 --- a/.ci/.matrix_framework_full.yml +++ b/.ci/.matrix_framework_full.yml @@ -30,6 +30,8 @@ FRAMEWORK: - celery-5-django-4 - celery-5-django-5 - opentelemetry-newest + - opentracing-newest + - opentracing-2.0 - twisted-newest - twisted-18 - twisted-17 diff --git a/elasticapm/contrib/opentracing/__init__.py b/elasticapm/contrib/opentracing/__init__.py new file mode 100644 index 000000000..71619ea20 --- /dev/null +++ b/elasticapm/contrib/opentracing/__init__.py @@ -0,0 +1,43 @@ +# BSD 3-Clause License +# +# Copyright (c) 2019, Elasticsearch BV +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import warnings + +from .span import OTSpan # noqa: F401 +from .tracer import Tracer # noqa: F401 + +warnings.warn( + ( + "The OpenTracing bridge is deprecated and will be removed in the next major release. " + "Please migrate to the OpenTelemetry bridge." + ), + DeprecationWarning, +) diff --git a/elasticapm/contrib/opentracing/span.py b/elasticapm/contrib/opentracing/span.py new file mode 100644 index 000000000..6bc00fec5 --- /dev/null +++ b/elasticapm/contrib/opentracing/span.py @@ -0,0 +1,136 @@ +# BSD 3-Clause License +# +# Copyright (c) 2019, Elasticsearch BV +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from opentracing.span import Span as OTSpanBase +from opentracing.span import SpanContext as OTSpanContextBase + +from elasticapm import traces +from elasticapm.utils import get_url_dict +from elasticapm.utils.logging import get_logger + +try: + # opentracing-python 2.1+ + from opentracing import logs as ot_logs + from opentracing import tags +except ImportError: + # opentracing-python <2.1 + from opentracing.ext import tags + + ot_logs = None + + +logger = get_logger("elasticapm.contrib.opentracing") + + +class OTSpan(OTSpanBase): + def __init__(self, tracer, context, elastic_apm_ref) -> None: + super(OTSpan, self).__init__(tracer, context) + self.elastic_apm_ref = elastic_apm_ref + self.is_transaction = isinstance(elastic_apm_ref, traces.Transaction) + self.is_dropped = isinstance(elastic_apm_ref, traces.DroppedSpan) + if not context.span: + context.span = self + + def log_kv(self, key_values, timestamp=None): + exc_type, exc_val, exc_tb = None, None, None + if "python.exception.type" in key_values: + exc_type = key_values["python.exception.type"] + exc_val = key_values.get("python.exception.val") + exc_tb = key_values.get("python.exception.tb") + elif ot_logs and key_values.get(ot_logs.EVENT) == tags.ERROR: + exc_type = key_values[ot_logs.ERROR_KIND] + exc_val = key_values.get(ot_logs.ERROR_OBJECT) + exc_tb = key_values.get(ot_logs.STACK) + else: + logger.debug("Can't handle non-exception type opentracing logs") + if exc_type: + agent = self.tracer._agent + agent.capture_exception(exc_info=(exc_type, exc_val, exc_tb)) + return self + + def set_operation_name(self, operation_name): + self.elastic_apm_ref.name = operation_name + return self + + def set_tag(self, key, value): + if self.is_transaction: + if key == "type": + self.elastic_apm_ref.transaction_type = value + elif key == "result": + self.elastic_apm_ref.result = value + elif key == tags.HTTP_STATUS_CODE: + self.elastic_apm_ref.result = "HTTP {}xx".format(str(value)[0]) + traces.set_context({"status_code": value}, "response") + elif key == "user.id": + traces.set_user_context(user_id=value) + elif key == "user.username": + traces.set_user_context(username=value) + elif key == "user.email": + traces.set_user_context(email=value) + elif key == tags.HTTP_URL: + traces.set_context({"url": get_url_dict(value)}, "request") + elif key == tags.HTTP_METHOD: + traces.set_context({"method": value}, "request") + elif key == tags.COMPONENT: + traces.set_context({"framework": {"name": value}}, "service") + else: + self.elastic_apm_ref.label(**{key: value}) + elif not self.is_dropped: + if key.startswith("db."): + span_context = self.elastic_apm_ref.context or {} + if "db" not in span_context: + span_context["db"] = {} + if key == tags.DATABASE_STATEMENT: + span_context["db"]["statement"] = value + elif key == tags.DATABASE_USER: + span_context["db"]["user"] = value + elif key == tags.DATABASE_TYPE: + span_context["db"]["type"] = value + self.elastic_apm_ref.type = "db." + value + else: + self.elastic_apm_ref.label(**{key: value}) + self.elastic_apm_ref.context = span_context + elif key == tags.SPAN_KIND: + self.elastic_apm_ref.type = value + else: + self.elastic_apm_ref.label(**{key: value}) + return self + + def finish(self, finish_time=None) -> None: + if self.is_transaction: + self.tracer._agent.end_transaction() + elif not self.is_dropped: + self.elastic_apm_ref.transaction.end_span() + + +class OTSpanContext(OTSpanContextBase): + def __init__(self, trace_parent, span=None) -> None: + self.trace_parent = trace_parent + self.span = span diff --git a/elasticapm/contrib/opentracing/tracer.py b/elasticapm/contrib/opentracing/tracer.py new file mode 100644 index 000000000..d331735f6 --- /dev/null +++ b/elasticapm/contrib/opentracing/tracer.py @@ -0,0 +1,131 @@ +# BSD 3-Clause License +# +# Copyright (c) 2019, Elasticsearch BV +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import warnings + +from opentracing import Format, InvalidCarrierException, SpanContextCorruptedException, UnsupportedFormatException +from opentracing.scope_managers import ThreadLocalScopeManager +from opentracing.tracer import ReferenceType +from opentracing.tracer import Tracer as TracerBase + +import elasticapm +from elasticapm import get_client, instrument, traces +from elasticapm.conf import constants +from elasticapm.contrib.opentracing.span import OTSpan, OTSpanContext +from elasticapm.utils import disttracing + + +class Tracer(TracerBase): + def __init__(self, client_instance=None, config=None, scope_manager=None) -> None: + self._agent = client_instance or get_client() or elasticapm.Client(config=config) + if scope_manager and not isinstance(scope_manager, ThreadLocalScopeManager): + warnings.warn( + "Currently, the Elastic APM opentracing bridge only supports the ThreadLocalScopeManager. " + "Usage of other scope managers will lead to unpredictable results." + ) + self._scope_manager = scope_manager or ThreadLocalScopeManager() + if self._agent.config.instrument and self._agent.config.enabled: + instrument() + + def start_active_span( + self, + operation_name, + child_of=None, + references=None, + tags=None, + start_time=None, + ignore_active_span=False, + finish_on_close=True, + ): + ot_span = self.start_span( + operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_span=ignore_active_span, + ) + scope = self._scope_manager.activate(ot_span, finish_on_close) + return scope + + def start_span( + self, operation_name=None, child_of=None, references=None, tags=None, start_time=None, ignore_active_span=False + ): + if isinstance(child_of, OTSpanContext): + parent_context = child_of + elif isinstance(child_of, OTSpan): + parent_context = child_of.context + elif references and references[0].type == ReferenceType.CHILD_OF: + parent_context = references[0].referenced_context + else: + parent_context = None + transaction = traces.execution_context.get_transaction() + if not transaction: + trace_parent = parent_context.trace_parent if parent_context else None + transaction = self._agent.begin_transaction("custom", trace_parent=trace_parent) + transaction.name = operation_name + span_context = OTSpanContext(trace_parent=transaction.trace_parent) + ot_span = OTSpan(self, span_context, transaction) + else: + # to allow setting an explicit parent span, we check if the parent_context is set + # and if it is a span. In all other cases, the parent is found implicitly through the + # execution context. + parent_span_id = ( + parent_context.span.elastic_apm_ref.id + if parent_context and parent_context.span and not parent_context.span.is_transaction + else None + ) + span = transaction._begin_span(operation_name, None, parent_span_id=parent_span_id) + trace_parent = parent_context.trace_parent if parent_context else transaction.trace_parent + span_context = OTSpanContext(trace_parent=trace_parent.copy_from(span_id=span.id)) + ot_span = OTSpan(self, span_context, span) + if tags: + for k, v in tags.items(): + ot_span.set_tag(k, v) + return ot_span + + def extract(self, format, carrier): + if format in (Format.HTTP_HEADERS, Format.TEXT_MAP): + trace_parent = disttracing.TraceParent.from_headers(carrier) + if not trace_parent: + raise SpanContextCorruptedException("could not extract span context from carrier") + return OTSpanContext(trace_parent=trace_parent) + raise UnsupportedFormatException + + def inject(self, span_context, format, carrier): + if format in (Format.HTTP_HEADERS, Format.TEXT_MAP): + if not isinstance(carrier, dict): + raise InvalidCarrierException("carrier for {} format should be dict-like".format(format)) + val = span_context.trace_parent.to_ascii() + carrier[constants.TRACEPARENT_HEADER_NAME] = val + if self._agent.config.use_elastic_traceparent_header: + carrier[constants.TRACEPARENT_LEGACY_HEADER_NAME] = val + return + raise UnsupportedFormatException diff --git a/setup.cfg b/setup.cfg index d3611a899..9ad206732 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,8 @@ tornado = tornado starlette = starlette +opentracing = + opentracing>=2.0.0 sanic = sanic opentelemetry = @@ -79,6 +81,7 @@ markers = gevent eventlet celery + opentracing cassandra psycopg2 mongodb diff --git a/tests/contrib/opentracing/__init__.py b/tests/contrib/opentracing/__init__.py new file mode 100644 index 000000000..7e2b340e6 --- /dev/null +++ b/tests/contrib/opentracing/__init__.py @@ -0,0 +1,29 @@ +# BSD 3-Clause License +# +# Copyright (c) 2019, Elasticsearch BV +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/contrib/opentracing/tests.py b/tests/contrib/opentracing/tests.py new file mode 100644 index 000000000..50970c269 --- /dev/null +++ b/tests/contrib/opentracing/tests.py @@ -0,0 +1,313 @@ +# BSD 3-Clause License +# +# Copyright (c) 2019, Elasticsearch BV +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from datetime import timedelta + +import pytest # isort:skip + +opentracing = pytest.importorskip("opentracing") # isort:skip + +import sys + +import mock +from opentracing import Format + +import elasticapm +from elasticapm.conf import constants +from elasticapm.contrib.opentracing import Tracer +from elasticapm.contrib.opentracing.span import OTSpanContext +from elasticapm.utils.disttracing import TraceParent + +pytestmark = pytest.mark.opentracing + + +try: + from opentracing import logs as ot_logs + from opentracing import tags +except ImportError: + ot_logs = None + + +@pytest.fixture() +def tracer(elasticapm_client): + yield Tracer(client_instance=elasticapm_client) + elasticapm.uninstrument() + + +def test_tracer_with_instantiated_client(elasticapm_client): + tracer = Tracer(client_instance=elasticapm_client) + assert tracer._agent is elasticapm_client + + +def test_tracer_with_config(): + config = {"METRICS_INTERVAL": "0s", "SERVER_URL": "https://example.com/test"} + tracer = Tracer(config=config) + try: + assert tracer._agent.config.metrics_interval == timedelta(seconds=0) + assert tracer._agent.config.server_url == "https://example.com/test" + finally: + tracer._agent.close() + + +def test_tracer_instrument(elasticapm_client): + with mock.patch("elasticapm.contrib.opentracing.tracer.instrument") as mock_instrument: + elasticapm_client.config.instrument = False + Tracer(client_instance=elasticapm_client) + assert mock_instrument.call_count == 0 + + elasticapm_client.config.instrument = True + Tracer(client_instance=elasticapm_client) + assert mock_instrument.call_count == 1 + + +def test_ot_transaction_started(tracer): + with tracer.start_active_span("test") as ot_scope: + ot_scope.span.set_tag("result", "OK") + client = tracer._agent + transaction = client.events[constants.TRANSACTION][0] + assert transaction["type"] == "custom" + assert transaction["name"] == "test" + assert transaction["result"] == "OK" + + +def test_ot_span(tracer): + with tracer.start_active_span("test") as ot_scope_transaction: + with tracer.start_active_span("testspan") as ot_scope_span: + ot_scope_span.span.set_tag("span.kind", "custom") + with tracer.start_active_span("testspan2") as ot_scope_span2: + with tracer.start_active_span("testspan3", child_of=ot_scope_span.span) as ot_scope_span3: + pass + client = tracer._agent + transaction = client.events[constants.TRANSACTION][0] + span1 = client.events[constants.SPAN][2] + span2 = client.events[constants.SPAN][1] + span3 = client.events[constants.SPAN][0] + assert span1["transaction_id"] == span1["parent_id"] == transaction["id"] + assert span1["name"] == "testspan" + + assert span2["transaction_id"] == transaction["id"] + assert span2["parent_id"] == span1["id"] + assert span2["name"] == "testspan2" + + # check that span3 has span1 as parent + assert span3["transaction_id"] == transaction["id"] + assert span3["parent_id"] == span1["id"] + assert span3["name"] == "testspan3" + + +def test_transaction_tags(tracer): + with tracer.start_active_span("test") as ot_scope: + ot_scope.span.set_tag("type", "foo") + ot_scope.span.set_tag("http.status_code", 200) + ot_scope.span.set_tag("http.url", "http://example.com/foo") + ot_scope.span.set_tag("http.method", "GET") + ot_scope.span.set_tag("user.id", 1) + ot_scope.span.set_tag("user.email", "foo@example.com") + ot_scope.span.set_tag("user.username", "foo") + ot_scope.span.set_tag("component", "Django") + ot_scope.span.set_tag("something.else", "foo") + client = tracer._agent + transaction = client.events[constants.TRANSACTION][0] + + assert transaction["type"] == "foo" + assert transaction["result"] == "HTTP 2xx" + assert transaction["context"]["response"]["status_code"] == 200 + assert transaction["context"]["request"]["url"]["full"] == "http://example.com/foo" + assert transaction["context"]["request"]["method"] == "GET" + assert transaction["context"]["user"] == {"id": 1, "email": "foo@example.com", "username": "foo"} + assert transaction["context"]["service"]["framework"]["name"] == "Django" + assert transaction["context"]["tags"] == {"something_else": "foo"} + + +def test_span_tags(tracer): + with tracer.start_active_span("transaction") as ot_scope_t: + with tracer.start_active_span("span") as ot_scope_s: + s = ot_scope_s.span + s.set_tag("db.type", "sql") + s.set_tag("db.statement", "SELECT * FROM foo") + s.set_tag("db.user", "bar") + s.set_tag("db.instance", "baz") + with tracer.start_active_span("span") as ot_scope_s: + s = ot_scope_s.span + s.set_tag("span.kind", "foo") + s.set_tag("something.else", "bar") + client = tracer._agent + span1 = client.events[constants.SPAN][0] + span2 = client.events[constants.SPAN][1] + + assert span1["context"]["db"] == {"type": "sql", "user": "bar", "statement": "SELECT * FROM foo"} + assert span1["type"] == "db.sql" + assert span1["context"]["tags"] == {"db_instance": "baz"} + + assert span2["type"] == "foo" + assert span2["context"]["tags"] == {"something_else": "bar"} + + +@pytest.mark.parametrize("elasticapm_client", [{"transaction_max_spans": 1}], indirect=True) +def test_dropped_spans(tracer): + assert tracer._agent.config.transaction_max_spans == 1 + with tracer.start_active_span("transaction") as ot_scope_t: + with tracer.start_active_span("span") as ot_scope_s: + s = ot_scope_s.span + s.set_tag("db.type", "sql") + with tracer.start_active_span("span") as ot_scope_s: + s = ot_scope_s.span + s.set_tag("db.type", "sql") + client = tracer._agent + spans = client.events[constants.SPAN] + assert len(spans) == 1 + + +def test_error_log(tracer): + with tracer.start_active_span("transaction") as tx_scope: + try: + raise ValueError("oops") + except ValueError: + exc_type, exc_val, exc_tb = sys.exc_info()[:3] + tx_scope.span.log_kv( + {"python.exception.type": exc_type, "python.exception.val": exc_val, "python.exception.tb": exc_tb} + ) + client = tracer._agent + error = client.events[constants.ERROR][0] + + assert error["exception"]["message"] == "ValueError: oops" + + +@pytest.mark.skipif(ot_logs is None, reason="New key names in opentracing-python 2.1") +def test_error_log_ot_21(tracer): + with tracer.start_active_span("transaction") as tx_scope: + try: + raise ValueError("oops") + except ValueError: + exc_type, exc_val, exc_tb = sys.exc_info()[:3] + tx_scope.span.log_kv( + { + ot_logs.EVENT: tags.ERROR, + ot_logs.ERROR_KIND: exc_type, + ot_logs.ERROR_OBJECT: exc_val, + ot_logs.STACK: exc_tb, + } + ) + client = tracer._agent + error = client.events[constants.ERROR][0] + + assert error["exception"]["message"] == "ValueError: oops" + + +def test_error_log_automatic_in_span_context_manager(tracer): + scope = tracer.start_active_span("transaction") + with pytest.raises(ValueError): + with scope.span: + raise ValueError("oops") + + client = tracer._agent + error = client.events[constants.ERROR][0] + + assert error["exception"]["message"] == "ValueError: oops" + + +def test_span_set_bagge_item_noop(tracer): + scope = tracer.start_active_span("transaction") + assert scope.span.set_baggage_item("key", "val") == scope.span + + +def test_tracer_extract_http(tracer): + span_context = tracer.extract( + Format.HTTP_HEADERS, {"elastic-apm-traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"} + ) + + assert span_context.trace_parent.version == 0 + assert span_context.trace_parent.trace_id == "0af7651916cd43dd8448eb211c80319c" + assert span_context.trace_parent.span_id == "b7ad6b7169203331" + + +def test_tracer_extract_map(tracer): + span_context = tracer.extract( + Format.TEXT_MAP, {"elastic-apm-traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"} + ) + + assert span_context.trace_parent.version == 0 + assert span_context.trace_parent.trace_id == "0af7651916cd43dd8448eb211c80319c" + assert span_context.trace_parent.span_id == "b7ad6b7169203331" + + +def test_tracer_extract_binary(tracer): + with pytest.raises(opentracing.UnsupportedFormatException): + tracer.extract(Format.BINARY, b"foo") + + +def test_tracer_extract_corrupted(tracer): + with pytest.raises(opentracing.SpanContextCorruptedException): + tracer.extract(Format.HTTP_HEADERS, {"nothing-to": "see-here"}) + + +@pytest.mark.parametrize( + "elasticapm_client", + [ + pytest.param({"use_elastic_traceparent_header": True}, id="use_elastic_traceparent_header-True"), + pytest.param({"use_elastic_traceparent_header": False}, id="use_elastic_traceparent_header-False"), + ], + indirect=True, +) +def test_tracer_inject_http(tracer): + span_context = OTSpanContext( + trace_parent=TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + ) + carrier = {} + tracer.inject(span_context, Format.HTTP_HEADERS, carrier) + assert carrier[constants.TRACEPARENT_HEADER_NAME] == b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" + if tracer._agent.config.use_elastic_traceparent_header: + assert carrier[constants.TRACEPARENT_LEGACY_HEADER_NAME] == carrier[constants.TRACEPARENT_HEADER_NAME] + + +@pytest.mark.parametrize( + "elasticapm_client", + [ + pytest.param({"use_elastic_traceparent_header": True}, id="use_elastic_traceparent_header-True"), + pytest.param({"use_elastic_traceparent_header": False}, id="use_elastic_traceparent_header-False"), + ], + indirect=True, +) +def test_tracer_inject_map(tracer): + span_context = OTSpanContext( + trace_parent=TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + ) + carrier = {} + tracer.inject(span_context, Format.TEXT_MAP, carrier) + assert carrier[constants.TRACEPARENT_HEADER_NAME] == b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" + if tracer._agent.config.use_elastic_traceparent_header: + assert carrier[constants.TRACEPARENT_LEGACY_HEADER_NAME] == carrier[constants.TRACEPARENT_HEADER_NAME] + + +def test_tracer_inject_binary(tracer): + span_context = OTSpanContext( + trace_parent=TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + ) + with pytest.raises(opentracing.UnsupportedFormatException): + tracer.inject(span_context, Format.BINARY, {}) diff --git a/tests/requirements/reqs-opentracing-2.0.txt b/tests/requirements/reqs-opentracing-2.0.txt new file mode 100644 index 000000000..de859ccbb --- /dev/null +++ b/tests/requirements/reqs-opentracing-2.0.txt @@ -0,0 +1,2 @@ +opentracing>=2.0.0,<2.1.0 +-r reqs-base.txt diff --git a/tests/requirements/reqs-opentracing-newest.txt b/tests/requirements/reqs-opentracing-newest.txt new file mode 100644 index 000000000..b82c2d976 --- /dev/null +++ b/tests/requirements/reqs-opentracing-newest.txt @@ -0,0 +1,2 @@ +opentracing>=2.1.0 +-r reqs-base.txt diff --git a/tests/scripts/envs/opentracing.sh b/tests/scripts/envs/opentracing.sh new file mode 100644 index 000000000..243c0ee96 --- /dev/null +++ b/tests/scripts/envs/opentracing.sh @@ -0,0 +1 @@ +export PYTEST_MARKER="-m opentracing"