diff --git a/.circleci/config.yml b/.circleci/config.yml index 1ed8c455..0fd0a474 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -330,9 +330,9 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette - final_job: requires: - python38 @@ -341,9 +341,9 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette filters: branches: only: master diff --git a/pytest.ini b/pytest.ini index 52835b1d..be615810 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,4 @@ log_cli = 1 log_cli_level = WARN log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %H:%M:%S +pythonpath = src diff --git a/src/instana/collector/helpers/runtime.py b/src/instana/collector/helpers/runtime.py index 2ea68642..8aef48e3 100644 --- a/src/instana/collector/helpers/runtime.py +++ b/src/instana/collector/helpers/runtime.py @@ -2,6 +2,7 @@ # (c) Copyright Instana Inc. 2020 """ Collection helper for the Python runtime """ +import gc import importlib.metadata import os import platform diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index d415dfde..d5e3b77f 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -6,6 +6,7 @@ """ from time import time +from typing import DefaultDict, Any from instana.collector.base import BaseCollector from instana.collector.helpers.runtime import RuntimeHelper @@ -72,7 +73,7 @@ def should_send_snapshot_data(self) -> bool: return True return False - def prepare_payload(self) -> DictionaryOfStan: + def prepare_payload(self) -> DefaultDict[Any, Any]: payload = DictionaryOfStan() payload["spans"] = [] payload["profiles"] = [] diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index a70d032d..25397ec3 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -2,8 +2,8 @@ # (c) Copyright Instana Inc. 2020 -import sys import os +import typing from instana.log import logger from instana.util.ids import header_to_id, header_to_long_id @@ -11,8 +11,15 @@ from instana.w3c_trace_context.traceparent import Traceparent from instana.w3c_trace_context.tracestate import Tracestate +from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + NonRecordingSpan, + set_span_in_context, +) +from opentelemetry.context.context import Context -# The carrier can be a dict or a list. +# The carrier, typed here as CarrierT, can be a dict, a list, or a tuple. # Using the trace header as an example, it can be in the following forms # for extraction: # X-Instana-T @@ -23,6 +30,7 @@ # # For injection, we only support the standard format: # X-Instana-T +CarrierT = typing.TypeVar("CarrierT", typing.Dict, typing.List, typing.Tuple) class BasePropagator(object): @@ -154,7 +162,7 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar correlation = False disable_traceparent = os.environ.get("INSTANA_DISABLE_W3C_TRACE_CORRELATION", "") instana_ancestor = None - ctx = SpanContext() + ctx = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) if level and "correlationType" in level: trace_id, span_id = [None] * 2 correlation = True @@ -166,7 +174,12 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar ctx.correlation_type = None ctx.correlation_id = None - if trace_id and span_id: + if ( + trace_id + and span_id + and trace_id != INVALID_TRACE_ID + and span_id != INVALID_SPAN_ID + ): ctx.trace_id = trace_id[-16:] # only the last 16 chars ctx.span_id = span_id[-16:] # only the last 16 chars ctx.synthetic = synthetic is not None @@ -290,9 +303,22 @@ def extract(self, carrier, disable_w3c_trace_context=False): if traceparent: traceparent = self._tp.validate(traceparent) - ctx = self.__determine_span_context(trace_id, span_id, level, synthetic, traceparent, tracestate, - disable_w3c_trace_context) - + if trace_id is None: + trace_id = INVALID_TRACE_ID + if span_id is None: + span_id = INVALID_SPAN_ID + + span_context = self.__determine_span_context( + trace_id, + span_id, + level, + synthetic, + traceparent, + tracestate, + disable_w3c_trace_context, + ) + ctx = set_span_in_context(NonRecordingSpan(span_context), Context()) return ctx + except Exception: logger.debug("extract error:", exc_info=True) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index ad89cc3b..b8454021 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -23,7 +23,9 @@ from instana.agent.host import HostAgent from instana.agent.test import TestAgent from instana.log import logger +from instana.propagators.base_propagator import CarrierT from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.exceptions import UnsupportedFormatException from instana.propagators.format import Format from instana.propagators.http_propagator import HTTPPropagator from instana.propagators.text_propagator import TextPropagator @@ -86,8 +88,9 @@ def __init__( sampler: Sampler, recorder: StanRecorder, span_processor: Union[HostAgent, TestAgent], - propagators: - Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]], + propagators: Mapping[ + str, Union[BinaryPropagator, HTTPPropagator, TextPropagator] + ], ) -> None: self._tracer_id = generate_id() self._sampler = sampler @@ -239,6 +242,31 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: return span_context + def inject( + self, + span_context: SpanContext, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: CarrierT, + disable_w3c_trace_context: bool = False, + ) -> Optional[CarrierT]: + if format in self._propagators: + return self._propagators[format].inject( + span_context, carrier, disable_w3c_trace_context + ) + + raise UnsupportedFormatException() + + def extract( + self, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: CarrierT, + disable_w3c_trace_context: bool = False, + ) -> Optional[Context]: + if format in self._propagators: + return self._propagators[format].extract(carrier, disable_w3c_trace_context) + + raise UnsupportedFormatException() + # Used by __add_stack re_tracer_frame = re.compile(r"/instana/.*\.py$") diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index a5b33304..89d3c3be 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -1,8 +1,12 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -from ..singletons import agent, tracer, async_tracer, tornado_tracer -from ..log import logger +from typing import Optional, Tuple + +from instana.log import logger +from instana.singletons import agent, tracer, async_tracer, tornado_tracer +from instana.span import InstanaSpan, get_current_span +from instana.tracer import InstanaTracer def extract_custom_headers(tracing_span, headers): @@ -16,14 +20,12 @@ def extract_custom_headers(tracing_span, headers): logger.debug("extract_custom_headers: ", exc_info=True) -def get_active_tracer(): +def get_active_tracer() -> Optional[InstanaTracer]: try: - if tracer.active_span: + # ToDo: Might have to add additional stuff when testing with async and tornado tracer + current_span = get_current_span() + if current_span and current_span.is_recording(): return tracer - elif async_tracer.active_span: - return async_tracer - elif tornado_tracer.active_span: - return tornado_tracer else: return None except Exception: @@ -32,10 +34,13 @@ def get_active_tracer(): return None -def get_tracer_tuple(): +def get_tracer_tuple() -> ( + Tuple[Optional[InstanaTracer], Optional[InstanaSpan], Optional[str]] +): active_tracer = get_active_tracer() + current_span = get_current_span() if active_tracer: - return (active_tracer, active_tracer.active_span, active_tracer.active_span.operation_name) + return (active_tracer, current_span, current_span.name) elif agent.options.allow_exit_as_root: return (tracer, None, None) return (None, None, None) diff --git a/src/instana/w3c_trace_context/traceparent.py b/src/instana/w3c_trace_context/traceparent.py index 3175c6cf..bc39fd3a 100644 --- a/src/instana/w3c_trace_context/traceparent.py +++ b/src/instana/w3c_trace_context/traceparent.py @@ -3,6 +3,7 @@ from ..log import logger import re +from typing import Optional # See https://www.w3.org/TR/trace-context-2/#trace-flags for details on the bitmasks. SAMPLED_BITMASK = 0b1; @@ -45,7 +46,13 @@ def get_traceparent_fields(traceparent): logger.debug("Parsing the traceparent failed: {}".format(err)) return None, None, None, None - def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): + def update_traceparent( + self, + traceparent: Optional[str], + in_trace_id: int, + in_span_id: int, + level: int, + ) -> str: """ This method updates the traceparent header or generates one if there was no traceparent incoming header or it was invalid @@ -56,7 +63,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): :return: the updated traceparent header """ if traceparent is None: # modify the trace_id part only when it was not present at all - trace_id = in_trace_id.zfill(32) + trace_id = ( + in_trace_id.zfill(32) + if not isinstance(in_trace_id, int) + else in_trace_id + ) else: # - We do not need the incoming upstream parent span ID for the header we sent downstream. # - We also do not care about the incoming version: The version field we sent downstream needs to match the @@ -67,12 +78,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): # downstream. _, trace_id, _, _ = self.get_traceparent_fields(traceparent) - parent_id = in_span_id.zfill(16) + parent_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) flags = level & SAMPLED_BITMASK flags = format(flags, '0>2x') - traceparent = "{version}-{traceid}-{parentid}-{flags}".format(version=self.SPECIFICATION_VERSION, - traceid=trace_id, - parentid=parent_id, - flags=flags) + traceparent = f"{self.SPECIFICATION_VERSION}-{trace_id}-{parent_id}-{flags}" return traceparent diff --git a/src/instana/w3c_trace_context/tracestate.py b/src/instana/w3c_trace_context/tracestate.py index b1d066ea..f6eb32cb 100644 --- a/src/instana/w3c_trace_context/tracestate.py +++ b/src/instana/w3c_trace_context/tracestate.py @@ -42,8 +42,10 @@ def update_tracestate(self, tracestate, in_trace_id, in_span_id): :return: tracestate updated """ try: - span_id = in_span_id.zfill(16) # if span_id is shorter than 16 characters we prepend zeros - instana_tracestate = "in={};{}".format(in_trace_id, span_id) + span_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) + instana_tracestate = f"in={in_trace_id};{span_id}" if tracestate is None or tracestate == "": tracestate = instana_tracestate else: diff --git a/tests/conftest.py b/tests/conftest.py index 222df23f..f70d6de5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,8 @@ import sys import pytest +from opentelemetry.context.context import Context +from opentelemetry.trace import set_span_in_context if importlib.util.find_spec('celery'): pytest_plugins = ("celery.contrib.pytest", ) @@ -20,7 +22,6 @@ from instana.span import BaseSpan, InstanaSpan # noqa: E402 from instana.span_context import SpanContext # noqa: E402 - collect_ignore_glob = [ "*autoprofile*", "*clients*", @@ -123,3 +124,8 @@ def span(span_context: SpanContext) -> InstanaSpan: @pytest.fixture def base_span(span: InstanaSpan) -> BaseSpan: return BaseSpan(span, None, "test") + + +@pytest.fixture +def context(span: InstanaSpan) -> Context: + return set_span_in_context(span) diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 89c0817c..cbd8fe4e 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -29,6 +29,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 redis>=3.5.3 requests-mock responses<=0.17.0 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index d4667ec5..bf34cff5 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -29,6 +29,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 redis>=3.5.3 requests-mock responses<=0.17.0 diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index ed419682..185d0398 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -34,6 +34,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 redis>=3.5.3 requests-mock responses<=0.17.0 diff --git a/tests/requirements.txt b/tests/requirements.txt index 9b977dc6..065bd553 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -28,6 +28,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 redis>=3.5.3 requests-mock responses<=0.17.0 diff --git a/tests/test_span.py b/tests/test_span.py index 5f30f9f1..bda0333b 100644 --- a/tests/test_span.py +++ b/tests/test_span.py @@ -44,6 +44,7 @@ def test_span_get_span_context( trace_id: int, span_id: int, ) -> None: + span_name = "test-span" span = InstanaSpan(span_name, span_context) @@ -714,10 +715,9 @@ def test_span_assure_errored_exception(span_context: SpanContext) -> None: assert not span.attributes -def test_get_current_span(span_context) -> None: - # span = get_current_span(span_context) - # assert span - pass +def test_get_current_span(context) -> None: + span = get_current_span(context) + assert isinstance(span, InstanaSpan) def test_get_current_span_INVALID_SPAN() -> None: diff --git a/tests/test_tracer.py b/tests/test_tracer.py index b7fb558f..feae19cc 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,6 +1,5 @@ # (c) Copyright IBM Corp. 2024 -from unittest.mock import patch from opentelemetry.trace import set_span_in_context from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID import pytest