From b48f5fd10d6e4f822dc864120f4573843f52dde1 Mon Sep 17 00:00:00 2001 From: Tobias Michels <66688058+tobmi1@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:36:10 +0200 Subject: [PATCH 01/44] Add test to check if errors are recorded in aio-pika consumer Signed-off-by: Tobias Michels <66688058+tobmi1@users.noreply.github.com> --- tests/clients/test_aio_pika.py | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/clients/test_aio_pika.py b/tests/clients/test_aio_pika.py index 75c1afff..4071e568 100644 --- a/tests/clients/test_aio_pika.py +++ b/tests/clients/test_aio_pika.py @@ -86,6 +86,22 @@ async def consume_message(self, connect_method) -> None: if queue.name in message.body.decode(): break + async def consume_with_exception(self, connect_method) -> None: + connection = await connect_method() + + async def on_message(msg): + raise RuntimeError("Simulated Exception") + + async with connection: + # Creating channel + channel = await connection.channel() + + # Declaring queue + queue = await channel.declare_queue(self.queue_name) + + await queue.consume(on_message) + await asyncio.sleep(1) # Wait to ensure the message is processed + @pytest.mark.parametrize( "params_combination", ["both_args", "both_kwargs", "arg_kwarg"], @@ -184,3 +200,45 @@ def assert_span_info(rabbitmq_span: "ReadableSpan", sort: str) -> None: assert_span_info(rabbitmq_publisher_span, "publish") assert_span_info(rabbitmq_consumer_span, "consume") + + @pytest.mark.parametrize( + "connect_method", + [connect, connect_robust], + ) + def test_consume_with_exception(self, connect_method) -> None: + with tracer.start_as_current_span("test"): + self.loop.run_until_complete(self.publish_message()) + self.loop.run_until_complete(self.consume_with_exception(connect_method)) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + rabbitmq_publisher_span = spans[0] + rabbitmq_consumer_span = spans[1] + test_span = spans[2] + + # Same traceId + assert test_span.t == rabbitmq_publisher_span.t + assert rabbitmq_publisher_span.t == rabbitmq_consumer_span.t + + # Parent relationships + assert rabbitmq_publisher_span.p == test_span.s + assert rabbitmq_consumer_span.p == rabbitmq_publisher_span.s + + # Error logging + assert not rabbitmq_publisher_span.ec + assert rabbitmq_consumer_span.ec == 1 + assert not test_span.ec + + # Span attributes + def assert_span_info(rabbitmq_span: "ReadableSpan", sort: str) -> None: + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == sort + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + assert_span_info(rabbitmq_publisher_span, "publish") + assert_span_info(rabbitmq_consumer_span, "consume") From d412bd78eed7c6812fdb7ef1c0ce4a6ca9a248d6 Mon Sep 17 00:00:00 2001 From: Tobias Michels <66688058+tobmi1@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:37:29 +0200 Subject: [PATCH 02/44] Fix aio-pika instrumentation bug causing the consumer callback to not be included in the trace Signed-off-by: Tobias Michels <66688058+tobmi1@users.noreply.github.com> --- src/instana/instrumentation/aio_pika.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/instana/instrumentation/aio_pika.py b/src/instana/instrumentation/aio_pika.py index 5e3f58d0..ef16dfa9 100644 --- a/src/instana/instrumentation/aio_pika.py +++ b/src/instana/instrumentation/aio_pika.py @@ -100,12 +100,12 @@ async def callback_wrapper( _extract_span_attributes( span, connection, "consume", message.routing_key, message.exchange ) - try: - response = await wrapped(*args, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return response + try: + response = await wrapped(*args, **kwargs) + except Exception as exc: + span.record_exception(exc) + else: + return response wrapped_callback = callback_wrapper(callback) if kwargs.get("callback"): From c245b1c5362f74babed8d13b7a1efd2c7271fcb8 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 8 Sep 2025 16:52:14 +0200 Subject: [PATCH 03/44] fix: fixed reading suppression header from message's headers Signed-off-by: Cagri Yonca --- src/instana/instrumentation/kafka/kafka_python.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/instana/instrumentation/kafka/kafka_python.py b/src/instana/instrumentation/kafka/kafka_python.py index c11e9355..3b1423d3 100644 --- a/src/instana/instrumentation/kafka/kafka_python.py +++ b/src/instana/instrumentation/kafka/kafka_python.py @@ -51,6 +51,9 @@ def trace_kafka_send( # context propagation headers = kwargs.get("headers", []) + if not is_suppressed and ("x_instana_l_s", b"0") in headers: + is_suppressed = True + suppression_header = {"x_instana_l_s": "0" if is_suppressed else "1"} headers.append(suppression_header) @@ -96,10 +99,8 @@ def create_span( ) if not is_suppressed and headers: - for header_name, header_value in headers: - if header_name == "x_instana_l_s" and header_value == b"0": - is_suppressed = True - break + if ("x_instana_l_s", b"0") in headers: + is_suppressed = True if is_suppressed: return From 433d94c0310d73c58d2d40db96e8319be001cdc0 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 9 Sep 2025 16:52:07 +0530 Subject: [PATCH 04/44] fix: `Immutable type, ignoring call to set attribute` on span_context Signed-off-by: Varsha GS --- src/instana/propagators/http_propagator.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/instana/propagators/http_propagator.py b/src/instana/propagators/http_propagator.py index 76ca3114..c6491076 100644 --- a/src/instana/propagators/http_propagator.py +++ b/src/instana/propagators/http_propagator.py @@ -5,6 +5,7 @@ from instana.log import logger from instana.propagators.base_propagator import BasePropagator from instana.util.ids import define_server_timing, hex_id_limited +from instana.span_context import SpanContext from opentelemetry.trace.span import format_span_id @@ -27,7 +28,26 @@ def inject(self, span_context, carrier, disable_w3c_trace_context=False): # Suppression `level` made in the child context or in the parent context # has priority over any non-suppressed `level` setting child_level = int(self.extract_instana_headers(dictionary_carrier)[2] or "1") - span_context.level = min(child_level, span_context.level) + new_level = min(child_level, span_context.level) + + if new_level != span_context.level: + # Create a new span context with the updated level + span_context = SpanContext( + trace_id=span_context.trace_id, + span_id=span_context.span_id, + is_remote=span_context.is_remote, + trace_flags=span_context.trace_flags, + trace_state=span_context.trace_state, + level=new_level, + synthetic=span_context.synthetic, + trace_parent=span_context.trace_parent, + instana_ancestor=span_context.instana_ancestor, + long_trace_id=span_context.long_trace_id, + correlation_type=span_context.correlation_type, + correlation_id=span_context.correlation_id, + traceparent=span_context.traceparent, + tracestate=span_context.tracestate + ) serializable_level = str(span_context.level) From 1c6f5ecd5cd5c20e8c5a19e90667af7205408d81 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 10 Sep 2025 16:42:20 +0530 Subject: [PATCH 05/44] tests: Add testcase to verify suppression Signed-off-by: Varsha GS --- tests/propagators/test_http_propagator.py | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/propagators/test_http_propagator.py b/tests/propagators/test_http_propagator.py index 25b36635..bac0a173 100644 --- a/tests/propagators/test_http_propagator.py +++ b/tests/propagators/test_http_propagator.py @@ -340,3 +340,45 @@ def test_w3c_off_x_instana_l_0( if "tracestate" in carrier_header.keys(): assert "tracestate" in downstream_carrier assert carrier_header["tracestate"] == downstream_carrier["tracestate"] + + def test_suppression_when_child_level_is_lower( + self, + _trace_id: int, + _span_id: int, + ) -> None: + """ + Test that span_context.level is updated when the child level (extracted from carrier) is lower than the current span_context.level. + """ + # Create a span context with level=1 + original_span_context = SpanContext( + trace_id=_trace_id, + span_id=_span_id, + is_remote=False, + level=1, + ) + + # Create a carrier with level=0 (suppression) + carrier_header = {"x-instana-l": "0"} + + # Inject the span context into the carrier + self.hptc.inject(original_span_context, carrier_header) + + # Extract the span context from the carrier to verify the level was updated + extracted_context = self.hptc.extract(carrier_header) + + # Verify that the level is 0 (suppressed) + assert extracted_context.level == 0 + assert extracted_context.suppression + + # Create a new carrier to test the propagation + downstream_carrier = {} + + # Inject the extracted context into the downstream carrier + self.hptc.inject(extracted_context, downstream_carrier) + + # Verify that the downstream carrier has the correct level + assert downstream_carrier.get("X-INSTANA-L") == "0" + + # Verify that no trace or span IDs are injected when suppressed + assert "X-INSTANA-T" not in downstream_carrier + assert "X-INSTANA-S" not in downstream_carrier From 6f6d35c756545374b7274666dbd35aa531c5b132 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 10 Sep 2025 16:26:51 +0200 Subject: [PATCH 06/44] chore(version): Bump version to 3.8.2 Signed-off-by: Cagri Yonca --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 975d3186..adb951cb 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.8.1" +VERSION = "3.8.2" From 51baea85aa24e4c60811c94f2aaf604f36b04cfe Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 11 Sep 2025 14:16:56 +0530 Subject: [PATCH 07/44] fix: suppression propagation in kafka Signed-off-by: Varsha GS --- src/instana/propagators/kafka_propagator.py | 38 +++++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/instana/propagators/kafka_propagator.py b/src/instana/propagators/kafka_propagator.py index 9ba27940..97bae58c 100644 --- a/src/instana/propagators/kafka_propagator.py +++ b/src/instana/propagators/kafka_propagator.py @@ -1,15 +1,12 @@ # (c) Copyright IBM Corp. 2025 -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Dict, Optional from opentelemetry.trace.span import format_span_id from instana.log import logger from instana.propagators.base_propagator import BasePropagator, CarrierT from instana.util.ids import hex_id_limited - -if TYPE_CHECKING: - from instana.span_context import SpanContext - +from instana.span_context import SpanContext class KafkaPropagator(BasePropagator): """ @@ -53,7 +50,7 @@ def extract_carrier_headers(self, carrier: CarrierT) -> Dict[str, Any]: def extract( self, carrier: CarrierT, disable_w3c_trace_context: bool = False - ) -> Optional["SpanContext"]: + ) -> Optional[SpanContext]: """ This method overrides one of the Base classes as with the introduction of W3C trace context for the Kafka requests more extracting steps and @@ -64,7 +61,7 @@ def extract( disable_w3c_trace_context (bool): A flag to disable the W3C trace context. Returns: - Optional["SpanContext"]: The extracted span context or None. + Optional[SpanContext]: The extracted span context or None. """ try: headers = self.extract_carrier_headers(carrier=carrier) @@ -79,7 +76,7 @@ def extract( # Assisted by watsonx Code Assistant def inject( self, - span_context: "SpanContext", + span_context: SpanContext, carrier: CarrierT, disable_w3c_trace_context: bool = True, ) -> None: @@ -103,7 +100,26 @@ def inject( # Suppression `level` made in the child context or in the parent context # has priority over any non-suppressed `level` setting suppression_level = int(self.extract_instana_headers(dictionary_carrier)[2]) - span_context.level = min(suppression_level, span_context.level) + new_level = min(suppression_level, span_context.level) + + if new_level != span_context.level: + # Create a new span context with the updated level + span_context = SpanContext( + trace_id=span_context.trace_id, + span_id=span_context.span_id, + is_remote=span_context.is_remote, + trace_flags=span_context.trace_flags, + trace_state=span_context.trace_state, + level=new_level, + synthetic=span_context.synthetic, + trace_parent=span_context.trace_parent, + instana_ancestor=span_context.instana_ancestor, + long_trace_id=span_context.long_trace_id, + correlation_type=span_context.correlation_type, + correlation_id=span_context.correlation_id, + traceparent=span_context.traceparent, + tracestate=span_context.tracestate + ) def inject_key_value(carrier, key, value): if isinstance(carrier, list): @@ -119,9 +135,9 @@ def inject_key_value(carrier, key, value): inject_key_value( carrier, self.KAFKA_HEADER_KEY_L_S, - str(suppression_level).encode("utf-8"), + str(span_context.level).encode("utf-8"), ) - if suppression_level == 1: + if span_context.level == 1: inject_key_value( carrier, self.KAFKA_HEADER_KEY_T, From 1465089bd4699ec314709197f7f5f21dca744c57 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 22 Sep 2025 13:48:01 +0200 Subject: [PATCH 08/44] chore: Update compatible runtimes and arch for AWS Lambda layer publishing script. Signed-off-by: Paulo Vital --- bin/aws-lambda/build_and_publish_lambda_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/aws-lambda/build_and_publish_lambda_layer.py b/bin/aws-lambda/build_and_publish_lambda_layer.py index 22fd7ad6..bd3de221 100755 --- a/bin/aws-lambda/build_and_publish_lambda_layer.py +++ b/bin/aws-lambda/build_and_publish_lambda_layer.py @@ -170,12 +170,14 @@ "--zip-file", aws_zip_filename, "--compatible-runtimes", - "python3.8", "python3.9", "python3.10", "python3.11", "python3.12", "python3.13", + "--compatible-architectures", + "x86_64", + "arm64", "--region", region, "--profile", From 7c76bdbdb99988afa19ee88ee8f7aebd64077f43 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 22 Sep 2025 13:50:09 +0200 Subject: [PATCH 09/44] chore: Update dev region for AWS Lambda layer publishing script. Signed-off-by: Paulo Vital --- bin/aws-lambda/build_and_publish_lambda_layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/aws-lambda/build_and_publish_lambda_layer.py b/bin/aws-lambda/build_and_publish_lambda_layer.py index bd3de221..e57fe386 100755 --- a/bin/aws-lambda/build_and_publish_lambda_layer.py +++ b/bin/aws-lambda/build_and_publish_lambda_layer.py @@ -107,7 +107,7 @@ ] if dev_mode: - target_regions = ["us-west-1"] + target_regions = ["us-east-1"] LAYER_NAME = "instana-py-dev" else: target_regions = [ From 72b77916d82b3a07fa6a7eff90e132c6bf512efa Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 22 Sep 2025 14:17:30 +0200 Subject: [PATCH 10/44] chore: Print the AWS Lambda layer list as MD. Signed-off-by: Paulo Vital --- bin/aws-lambda/build_and_publish_lambda_layer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/aws-lambda/build_and_publish_lambda_layer.py b/bin/aws-lambda/build_and_publish_lambda_layer.py index e57fe386..0c8dad2d 100755 --- a/bin/aws-lambda/build_and_publish_lambda_layer.py +++ b/bin/aws-lambda/build_and_publish_lambda_layer.py @@ -149,6 +149,7 @@ LAYER_NAME = "instana-python" published = dict() +version = 0 for region in target_regions: print(f"===> Uploading layer to AWS {region} ") @@ -219,5 +220,8 @@ print("===> Published list:") +print(f"AWS Lambda Layer v{version}") +print("| AWS Region | ARN |") +print("| :-- | :-- |") for key in published.keys(): - print(f"{key}\t{published[key]}") + print(f"| {key} | {published[key]} |") From 588958e8774d947ce7c66579cf310d1f8482fd1a Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Sep 2025 11:39:49 +0530 Subject: [PATCH 11/44] fix(aio-pika): implement `_bind_args` method to fetch values from both args and kwargs Signed-off-by: Varsha GS --- src/instana/instrumentation/aio_pika.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/instana/instrumentation/aio_pika.py b/src/instana/instrumentation/aio_pika.py index ef16dfa9..2dcb6ad3 100644 --- a/src/instana/instrumentation/aio_pika.py +++ b/src/instana/instrumentation/aio_pika.py @@ -43,18 +43,26 @@ async def publish_with_instana( ) -> Optional["ConfirmationFrameType"]: if tracing_is_off(): return await wrapped(*args, **kwargs) - + tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None + def _bind_args( + message: Type["AbstractMessage"], + routing_key: str, + *args: object, + **kwargs: object, + ) -> Tuple[object, ...]: + return (message, routing_key, args, kwargs) + + (message, routing_key, args, kwargs) = _bind_args( + *args, **kwargs + ) + with tracer.start_as_current_span( "rabbitmq", span_context=parent_context ) as span: connection = instance.channel._connection - message = kwargs["message"] if kwargs.get("message") else args[0] - routing_key = ( - kwargs["routing_key"] if kwargs.get("routing_key") else args[1] - ) _extract_span_attributes( span, connection, "publish", routing_key, instance.name @@ -66,6 +74,9 @@ async def publish_with_instana( message.properties.headers, disable_w3c_trace_context=True, ) + + args = (message, routing_key) + args + try: response = await wrapped(*args, **kwargs) except Exception as exc: From c28a94ea719cc085eea798a2c334aed49f07c235 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 24 Sep 2025 14:45:58 +0530 Subject: [PATCH 12/44] test(aio-pika): verify publish works with an empty `routing_key` Signed-off-by: Varsha GS --- src/instana/instrumentation/aio_pika.py | 2 +- tests/clients/test_aio_pika.py | 55 +++++++++---------------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/instana/instrumentation/aio_pika.py b/src/instana/instrumentation/aio_pika.py index 2dcb6ad3..a47e09f7 100644 --- a/src/instana/instrumentation/aio_pika.py +++ b/src/instana/instrumentation/aio_pika.py @@ -43,7 +43,7 @@ async def publish_with_instana( ) -> Optional["ConfirmationFrameType"]: if tracing_is_off(): return await wrapped(*args, **kwargs) - + tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None diff --git a/tests/clients/test_aio_pika.py b/tests/clients/test_aio_pika.py index 4071e568..20e97618 100644 --- a/tests/clients/test_aio_pika.py +++ b/tests/clients/test_aio_pika.py @@ -56,6 +56,9 @@ async def publish_message(self, params_combination: str = "both_args") -> None: elif params_combination == "arg_kwarg": args = (message,) kwargs = {"routing_key": queue_name} + elif params_combination == "arg_kwarg_empty_key": + args = (message,) + kwargs = {"routing_key": ""} else: # params_combination == "both_args" args = (message, queue_name) @@ -102,6 +105,15 @@ async def on_message(msg): await queue.consume(on_message) await asyncio.sleep(1) # Wait to ensure the message is processed + def assert_span_info(self, rabbitmq_span: "ReadableSpan", sort: str, key: str = "test.queue") -> None: + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == sort + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == key + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + @pytest.mark.parametrize( "params_combination", ["both_args", "both_kwargs", "arg_kwarg"], @@ -127,13 +139,8 @@ def test_basic_publish(self, params_combination) -> None: assert not rabbitmq_span.ec # Span attributes - assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" - assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" - assert rabbitmq_span.data["rabbitmq"]["address"] - assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" - assert rabbitmq_span.stack - assert isinstance(rabbitmq_span.stack, list) - assert len(rabbitmq_span.stack) > 0 + key = "" if params_combination == "arg_kwarg_empty_key" else self.queue_name + self.assert_span_info(rabbitmq_span, "publish", key) def test_basic_publish_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True @@ -151,13 +158,7 @@ def test_basic_publish_as_root_exit_span(self) -> None: assert not rabbitmq_span.ec # Span attributes - assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" - assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" - assert rabbitmq_span.data["rabbitmq"]["address"] - assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" - assert rabbitmq_span.stack - assert isinstance(rabbitmq_span.stack, list) - assert len(rabbitmq_span.stack) > 0 + self.assert_span_info(rabbitmq_span, "publish") @pytest.mark.parametrize( "connect_method", @@ -189,17 +190,8 @@ def test_basic_consume(self, connect_method) -> None: assert not test_span.ec # Span attributes - def assert_span_info(rabbitmq_span: "ReadableSpan", sort: str) -> None: - assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" - assert rabbitmq_span.data["rabbitmq"]["sort"] == sort - assert rabbitmq_span.data["rabbitmq"]["address"] - assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" - assert rabbitmq_span.stack - assert isinstance(rabbitmq_span.stack, list) - assert len(rabbitmq_span.stack) > 0 - - assert_span_info(rabbitmq_publisher_span, "publish") - assert_span_info(rabbitmq_consumer_span, "consume") + self.assert_span_info(rabbitmq_publisher_span, "publish") + self.assert_span_info(rabbitmq_consumer_span, "consume") @pytest.mark.parametrize( "connect_method", @@ -231,14 +223,5 @@ def test_consume_with_exception(self, connect_method) -> None: assert not test_span.ec # Span attributes - def assert_span_info(rabbitmq_span: "ReadableSpan", sort: str) -> None: - assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" - assert rabbitmq_span.data["rabbitmq"]["sort"] == sort - assert rabbitmq_span.data["rabbitmq"]["address"] - assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" - assert rabbitmq_span.stack - assert isinstance(rabbitmq_span.stack, list) - assert len(rabbitmq_span.stack) > 0 - - assert_span_info(rabbitmq_publisher_span, "publish") - assert_span_info(rabbitmq_consumer_span, "consume") + self.assert_span_info(rabbitmq_publisher_span, "publish") + self.assert_span_info(rabbitmq_consumer_span, "consume") From 0167611c97fb5ba11e26142191f21782391545c4 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 17 Sep 2025 16:35:19 +0200 Subject: [PATCH 13/44] ci: Add support to test Python 3.14.0rc3. Signed-off-by: Paulo Vital --- .tekton/github-pr-pipeline.yaml.part | 2 +- .tekton/pipeline.yaml | 4 ++-- .tekton/python-tracer-prepuller.yaml | 2 +- Dockerfile-py3140 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.tekton/github-pr-pipeline.yaml.part b/.tekton/github-pr-pipeline.yaml.part index e7c15930..2e0aac50 100644 --- a/.tekton/github-pr-pipeline.yaml.part +++ b/.tekton/github-pr-pipeline.yaml.part @@ -28,7 +28,7 @@ spec: default: public.ecr.aws/docker/library/python:3.13-bookworm - name: py-314-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.14.0rc2 + default: public.ecr.aws/docker/library/python:3.14.0rc3 workspaces: - name: python-tracer-ci-pipeline-pvc tasks: diff --git a/.tekton/pipeline.yaml b/.tekton/pipeline.yaml index 14cb96d4..a87ff532 100644 --- a/.tekton/pipeline.yaml +++ b/.tekton/pipeline.yaml @@ -26,7 +26,7 @@ spec: default: public.ecr.aws/docker/library/python:3.13-bookworm - name: py-314-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.14.0rc2 + default: public.ecr.aws/docker/library/python:3.14.0rc3 workspaces: - name: python-tracer-ci-pipeline-pvc tasks: @@ -110,7 +110,7 @@ spec: - clone params: - name: py-version - value: 3.14.0rc2 + value: 3.14.0rc3 taskRef: name: python-tracer-unittest-python-next-task workspaces: diff --git a/.tekton/python-tracer-prepuller.yaml b/.tekton/python-tracer-prepuller.yaml index 76b0609a..b6a6f8a0 100644 --- a/.tekton/python-tracer-prepuller.yaml +++ b/.tekton/python-tracer-prepuller.yaml @@ -59,7 +59,7 @@ spec: image: public.ecr.aws/docker/library/python:3.13-bookworm command: ["sh", "-c", "'true'"] - name: prepuller-314 - image: public.ecr.aws/docker/library/python:3.14.0rc2 + image: public.ecr.aws/docker/library/python:3.14.0rc3 command: ["sh", "-c", "'true'"] # Use the pause container to ensure the Pod goes into a `Running` phase diff --git a/Dockerfile-py3140 b/Dockerfile-py3140 index a8aa2331..9a39e30d 100644 --- a/Dockerfile-py3140 +++ b/Dockerfile-py3140 @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/python:3.14.0b2 +FROM public.ecr.aws/docker/library/python:3.14.0rc3 RUN apt-get update \ && apt-get install -y --no-install-recommends \ From a311e04e21c4311d07992cc70c16a19da5f1051d Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 17 Sep 2025 16:43:20 +0200 Subject: [PATCH 14/44] chore: remove unnecessary files. Signed-off-by: Paulo Vital --- Dockerfile-py3140 | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 Dockerfile-py3140 diff --git a/Dockerfile-py3140 b/Dockerfile-py3140 deleted file mode 100644 index 9a39e30d..00000000 --- a/Dockerfile-py3140 +++ /dev/null @@ -1,21 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.14.0rc3 - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential python3-dev \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -ENV WORKDIR_=/root/base - -WORKDIR $WORKDIR_ -COPY ./tests/requirements-minimal.txt . -COPY ./tests/requirements-pre314.txt . - -ENV VIRTUAL_ENV="$WORKDIR_/venv" -RUN python -m venv $VIRTUAL_ENV - -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -RUN python -m pip install --upgrade pip \ - && python -m pip install -r requirements-pre314.txt From 8eae90ec651a2d7cab03b54cbfbb17ac22bfe8cc Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Fri, 26 Sep 2025 10:42:23 +0200 Subject: [PATCH 15/44] chore(version): Bump version to 3.8.3 Signed-off-by: Paulo Vital --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index adb951cb..034f31c6 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.8.2" +VERSION = "3.8.3" From f02e3b7d6daeb9a3bae02cdce6dc06bc01d74c8f Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 20 Aug 2025 14:12:57 +0200 Subject: [PATCH 16/44] ci: Update Kafka container. Moving to use the `ubuntu/kafka` container image as it has support to `amd64(x86_64)`, `arm64`, `ppc64le`, and `s390x`. Signed-off-by: Paulo Vital --- .circleci/config.yml | 22 ++++++++++------------ .tekton/task.yaml | 25 ++++++++++--------------- docker-compose.yml | 20 +++++++++++--------- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5fef25d9..d4451306 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -244,18 +244,16 @@ jobs: - store-pytest-results - store-coverage-report - py312kafka: + py313kafka: docker: - - image: public.ecr.aws/docker/library/python:3.12 - - image: public.ecr.aws/bitnami/kafka:3.9.0 + - image: public.ecr.aws/docker/library/python:3.13 + - image: public.ecr.aws/ubuntu/zookeeper:latest + environment: + TZ: UTC + - image: public.ecr.aws/ubuntu/kafka:latest environment: - KAFKA_CFG_NODE_ID: 0 - KAFKA_CFG_PROCESS_ROLES: controller,broker - KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@localhost:9093 - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,EXTERNAL://localhost:9094 + TZ: UTC + ZOOKEEPER_HOST: localhost working_directory: ~/repo steps: - checkout @@ -309,7 +307,7 @@ workflows: - py39cassandra - py39gevent - py312aws - - py312kafka + - py313kafka - autowrapt: matrix: parameters: @@ -322,5 +320,5 @@ workflows: - py39cassandra - py39gevent - py312aws - - py312kafka + - py313kafka - autowrapt diff --git a/.tekton/task.yaml b/.tekton/task.yaml index e5d79c92..3f3d98ec 100644 --- a/.tekton/task.yaml +++ b/.tekton/task.yaml @@ -166,23 +166,18 @@ metadata: name: python-tracer-unittest-kafka-task spec: sidecars: + - name: zookeeper + image: public.ecr.aws/ubuntu/zookeeper:latest + env: + - name: TZ + value: "UTC" - name: kafka - image: public.ecr.aws/bitnami/kafka:3.9.0 + image: public.ecr.aws/ubuntu/kafka:latest env: - - name: KAFKA_CFG_NODE_ID - value: "0" - - name: KAFKA_CFG_PROCESS_ROLES - value: "controller,broker" - - name: KAFKA_CFG_LISTENERS - value: "PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094" - - name: KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP - value: "CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT" - - name: KAFKA_CFG_CONTROLLER_QUORUM_VOTERS - value: "0@kafka:9093" - - name: KAFKA_CFG_CONTROLLER_LISTENER_NAMES - value: "CONTROLLER" - - name: KAFKA_CFG_ADVERTISED_LISTENERS - value: "PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094" + - name: TZ + value: "UTC" + - name: ZOOKEEPER_HOST + value: zookeeper params: - name: imageDigest type: string diff --git a/docker-compose.yml b/docker-compose.yml index 45393b76..35dea903 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,16 +61,18 @@ services: - "8681:8681" - "8682:8682" + # Sidecar container for Kafka + zookeeper: + image: public.ecr.aws/ubuntu/zookeeper:latest + ports: + - 2181:2181 + environment: + - TZ=UTC + kafka: - image: public.ecr.aws/bitnami/kafka:latest + image: public.ecr.aws/ubuntu/kafka:latest # works on amd64, arm64, ppc64le and s390x ports: - '9092:9092' - - '9094:9094' environment: - - KAFKA_CFG_NODE_ID=0 - - KAFKA_CFG_PROCESS_ROLES=controller,broker - - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 + - TZ=UTC + - ZOOKEEPER_HOST=zookeeper From 2ab738479f5953d8665f15db67ffb7e4c88fc840 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 2 Oct 2025 15:32:25 +0200 Subject: [PATCH 17/44] ci: Fix Kafka connection issues to ubuntu/kafka Signed-off-by: Cagri Yonca --- .circleci/config.yml | 25 +++++++++++++++++++++++-- .tekton/task.yaml | 34 +++++++++++++++++++++++++++++++--- docker-compose.yml | 37 +++++++++++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d4451306..1bba6674 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -247,13 +247,34 @@ jobs: py313kafka: docker: - image: public.ecr.aws/docker/library/python:3.13 - - image: public.ecr.aws/ubuntu/zookeeper:latest + - image: public.ecr.aws/ubuntu/zookeeper:3.1-22.04_edge environment: TZ: UTC - - image: public.ecr.aws/ubuntu/kafka:latest + - image: public.ecr.aws/ubuntu/kafka:3.1-22.04_edge environment: TZ: UTC ZOOKEEPER_HOST: localhost + ZOOKEEPER_PORT: 2181 + command: + - /opt/kafka/config/server.properties + - --override + - listeners=INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9094 + - --override + - advertised.listeners=INTERNAL://localhost:9093,EXTERNAL://localhost:9094 + - --override + - listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + - --override + - inter.broker.listener.name=INTERNAL + - --override + - broker.id=1 + - --override + - offsets.topic.replication.factor=1 + - --override + - transaction.state.log.replication.factor=1 + - --override + - transaction.state.log.min.isr=1 + - --override + - auto.create.topics.enable=true working_directory: ~/repo steps: - checkout diff --git a/.tekton/task.yaml b/.tekton/task.yaml index 3f3d98ec..0a9a6d05 100644 --- a/.tekton/task.yaml +++ b/.tekton/task.yaml @@ -167,17 +167,45 @@ metadata: spec: sidecars: - name: zookeeper - image: public.ecr.aws/ubuntu/zookeeper:latest + image: public.ecr.aws/ubuntu/zookeeper:3.1-22.04_edge + ports: + - containerPort: 9093 env: - name: TZ value: "UTC" - name: kafka - image: public.ecr.aws/ubuntu/kafka:latest + image: public.ecr.aws/ubuntu/kafka:3.1-22.04_edge env: - name: TZ value: "UTC" - name: ZOOKEEPER_HOST - value: zookeeper + value: localhost + - name: ZOOKEEPER_PORT + value: "2181" + ports: + - containerPort: 9093 + - containerPort: 9094 + command: + - /opt/kafka/bin/kafka-server-start.sh + - /opt/kafka/config/server.properties + - --override + - listeners=INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9094 + - --override + - advertised.listeners=INTERNAL://localhost:9093,EXTERNAL://localhost:9094 + - --override + - listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + - --override + - inter.broker.listener.name=INTERNAL + - --override + - broker.id=1 + - --override + - offsets.topic.replication.factor=1 + - --override + - transaction.state.log.replication.factor=1 + - --override + - transaction.state.log.min.isr=1 + - --override + - auto.create.topics.enable=true params: - name: imageDigest type: string diff --git a/docker-compose.yml b/docker-compose.yml index 35dea903..299806a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,16 +63,37 @@ services: # Sidecar container for Kafka zookeeper: - image: public.ecr.aws/ubuntu/zookeeper:latest - ports: - - 2181:2181 - environment: - - TZ=UTC + image: public.ecr.aws/ubuntu/zookeeper:3.1-22.04_edge + ports: ["2181:2181"] + environment: [ "TZ=UTC" ] kafka: - image: public.ecr.aws/ubuntu/kafka:latest # works on amd64, arm64, ppc64le and s390x - ports: - - '9092:9092' + image: public.ecr.aws/ubuntu/kafka:3.1-22.04_edge + depends_on: [zookeeper] + ports: + - "9094:9094" + - "9093:9093" environment: - TZ=UTC - ZOOKEEPER_HOST=zookeeper + - ZOOKEEPER_PORT=2181 + command: + - /opt/kafka/config/server.properties + - --override + - listeners=INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9094 + - --override + - advertised.listeners=INTERNAL://kafka:9093,EXTERNAL://127.0.0.1:9094 + - --override + - listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + - --override + - inter.broker.listener.name=INTERNAL + - --override + - broker.id=1 + - --override + - offsets.topic.replication.factor=1 + - --override + - transaction.state.log.replication.factor=1 + - --override + - transaction.state.log.min.isr=1 + - --override + - auto.create.topics.enable=true From 02219a3aaf8e38d64cbc2da2693deadba59defd2 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 7 Oct 2025 07:12:29 +0200 Subject: [PATCH 18/44] chore(ci): Enable CircleCi automatic workflow reruns. Added the `max_auto_reruns` config to reduce the impact of temporary workflow failures due to transient issues. The automatic workflow reruns function similarly to manually selecting `Rerun workflow from failed` in the CircleCI web app. More info at https://circleci.com/docs/guides/orchestrate/automatic-reruns/ Signed-off-by: Paulo Vital --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1bba6674..a06d8287 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -319,6 +319,7 @@ jobs: workflows: tests: + max_auto_reruns: 2 jobs: - python3x: matrix: From a9c7a9179be0ee1b652cf0d87b2fbf7fff216f64 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 8 Oct 2025 11:42:30 +0530 Subject: [PATCH 19/44] currency: Add Cassandra and newly supported libraries Signed-off-by: Varsha GS --- .tekton/.currency/resources/table.json | 80 +++++++++++++++----------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/.tekton/.currency/resources/table.json b/.tekton/.currency/resources/table.json index 4de524a9..238526f8 100644 --- a/.tekton/.currency/resources/table.json +++ b/.tekton/.currency/resources/table.json @@ -8,9 +8,10 @@ "Cloud Native": "No" }, { - "Package name": "Celery", - "Support Policy": "45-days", - "Beta version": "No", + "Package name": "WSGI", + "Support Policy": "0-day", + "Beta version": "Yes", + "Last Supported Version": "1.0.1", "Cloud Native": "No" }, { @@ -56,124 +57,133 @@ "Cloud Native": "No" }, { - "Package name": "Webapp2", - "Support Policy": "On demand", + "Package name": "Aiohttp", + "Support Policy": "45-days", "Beta version": "No", - "Last Supported Version": "2.5.2", "Cloud Native": "No" }, { - "Package name": "WSGI", - "Support Policy": "0-day", - "Beta version": "Yes", - "Last Supported Version": "1.0.1", + "Package name": "Httpx", + "Support Policy": "45-days", + "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "Aiohttp", + "Package name": "Requests", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "Asynqp", - "Support Policy": "Deprecated", + "Package name": "Urllib3", + "Support Policy": "45-days", "Beta version": "No", - "Last Supported Version": "0.6", "Cloud Native": "No" }, { - "Package name": "Boto3", + "Package name": "Grpcio", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Google-cloud-pubsub", + "Package name": "Cassandra-driver", "Support Policy": "45-days", "Beta version": "No", - "Cloud Native": "Yes" + "Cloud Native": "No" }, { - "Package name": "Google-cloud-storage", + "Package name": "Mysqlclient", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Grpcio", + "Package name": "PyMySQL", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Mysqlclient", + "Package name": "Pymongo", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Pika", + "Package name": "Psycopg2", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "PyMySQL", + "Package name": "Redis", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Pymongo", + "Package name": "SQLAlchemy", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "Yes" }, { - "Package name": "Psycopg2", + "Package name": "Aioamqp", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "Redis", + "Package name": "Aio-pika", "Support Policy": "45-days", "Beta version": "No", - "Cloud Native": "Yes" + "Cloud Native": "No" }, { - "Package name": "Requests", + "Package name": "Confluent-kafka", "Support Policy": "45-days", "Beta version": "No", - "Cloud Native": "Yes" + "Cloud Native": "No" }, { - "Package name": "SQLAlchemy", + "Package name": "Kafka-python-ng", "Support Policy": "45-days", "Beta version": "No", - "Cloud Native": "Yes" + "Cloud Native": "No" }, { - "Package name": "Urllib3", + "Package name": "Pika", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "Spyne", + "Package name": "Boto3", + "Support Policy": "45-days", + "Beta version": "No", + "Cloud Native": "Yes" + }, + { + "Package name": "Google-cloud-pubsub", "Support Policy": "45-days", "Beta version": "No", - "Cloud Native": "No" + "Cloud Native": "Yes" }, { - "Package name": "Aio-pika", + "Package name": "Google-cloud-storage", "Support Policy": "45-days", "Beta version": "No", + "Cloud Native": "Yes" + }, + { + "Package name": "Gevent", + "Support Policy": "On demand", + "Beta version": "No", "Cloud Native": "No" }, { - "Package name": "Aioamqp", + "Package name": "Celery", "Support Policy": "45-days", "Beta version": "No", "Cloud Native": "No" From ccffdb505e4baf319845e666cb7468b9734dada9 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 8 Oct 2025 12:15:36 +0530 Subject: [PATCH 20/44] currency: Add new libraries to report - fix warnings Signed-off-by: Varsha GS --- .tekton/.currency/docs/report.md | 48 ++++++++++---------- .tekton/.currency/scripts/generate_report.py | 45 +++++++++--------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/.tekton/.currency/docs/report.md b/.tekton/.currency/docs/report.md index c3a8fa22..a739efe1 100644 --- a/.tekton/.currency/docs/report.md +++ b/.tekton/.currency/docs/report.md @@ -3,31 +3,33 @@ | Package name | Support Policy | Beta version | Last Supported Version | Latest version | Up-to-date | Release date | Latest Version Published At | Days behind | Cloud Native | |:---------------------|:-----------------|:---------------|:-------------------------|:-----------------|:-------------|:---------------|:------------------------------|:--------------|:---------------| | ASGI | 45-days | No | 3.0 | 3.0 | Yes | 2019-03-04 | 2019-03-04 | 0 day/s | No | -| Celery | 45-days | No | 5.5.3 | 5.5.3 | Yes | 2025-06-01 | 2025-06-01 | 0 day/s | No | -| Django | 45-days | No | 5.2.3 | 5.2.3 | Yes | 2025-06-10 | 2025-06-10 | 0 day/s | No | -| FastAPI | 45-days | No | 0.115.12 | 0.115.12 | Yes | 2025-03-23 | 2025-03-23 | 0 day/s | No | -| Flask | 45-days | No | 3.1.1 | 3.1.1 | Yes | 2025-05-13 | 2025-05-13 | 0 day/s | No | +| WSGI | 0-day | Yes | 1.0.1 | 1.0.1 | Yes | 2010-09-26 | 2010-09-26 | 0 day/s | No | +| Django | 45-days | No | 5.2.7 | 5.2.7 | Yes | 2025-10-01 | 2025-10-01 | 0 day/s | No | +| FastAPI | 45-days | No | 0.118.0 | 0.118.0 | Yes | 2025-09-29 | 2025-09-29 | 0 day/s | No | +| Flask | 45-days | No | 3.1.2 | 3.1.2 | Yes | 2025-08-19 | 2025-08-19 | 0 day/s | No | | Pyramid | 45-days | No | 2.0.2 | 2.0.2 | Yes | 2023-08-25 | 2023-08-25 | 0 day/s | No | | Sanic | On demand | No | 25.3.0 | 25.3.0 | Yes | 2025-03-31 | 2025-03-31 | 0 day/s | No | -| Starlette | 45-days | No | 0.47.0 | 0.47.0 | Yes | 2025-05-29 | 2025-05-29 | 0 day/s | No | -| Tornado | 45-days | No | 6.5.1 | 6.5.1 | Yes | 2025-05-22 | 2025-05-22 | 0 day/s | No | -| Webapp2 | On demand | No | 2.5.2 | 2.5.2 | Yes | 2012-09-28 | 2012-09-28 | 0 day/s | No | -| WSGI | 0-day | Yes | 1.0.1 | 1.0.1 | Yes | 2010-09-26 | 2010-09-26 | 0 day/s | No | -| Aiohttp | 45-days | No | 3.12.13 | 3.12.13 | Yes | 2025-06-14 | 2025-06-14 | 0 day/s | No | -| Asynqp | Deprecated | No | 0.6 | 0.6 | Yes | 2019-01-20 | 2019-01-20 | 0 day/s | No | -| Boto3 | 45-days | No | 1.38.36 | 1.38.36 | Yes | 2025-06-12 | 2025-06-12 | 0 day/s | Yes | -| Google-cloud-pubsub | 45-days | No | 2.30.0 | 2.30.0 | Yes | 2025-06-09 | 2025-06-09 | 0 day/s | Yes | -| Google-cloud-storage | 45-days | No | 3.1.0 | 3.1.0 | Yes | 2025-02-28 | 2025-02-28 | 0 day/s | Yes | -| Grpcio | 45-days | No | 1.73.0 | 1.73.0 | Yes | 2025-06-09 | 2025-06-09 | 0 day/s | Yes | +| Starlette | 45-days | No | 0.48.0 | 0.48.0 | Yes | 2025-09-13 | 2025-09-13 | 0 day/s | No | +| Tornado | 45-days | No | 6.5.2 | 6.5.2 | Yes | 2025-08-08 | 2025-08-08 | 0 day/s | No | +| Aiohttp | 45-days | No | 3.13.0 | 3.13.0 | Yes | 2025-10-06 | 2025-10-06 | 0 day/s | No | +| Httpx | 45-days | No | 0.28.1 | 0.28.1 | Yes | 2024-12-06 | 2024-12-06 | 0 day/s | No | +| Requests | 45-days | No | 2.32.5 | 2.32.5 | Yes | 2025-08-18 | 2025-08-18 | 0 day/s | No | +| Urllib3 | 45-days | No | 2.5.0 | 2.5.0 | Yes | 2025-06-18 | 2025-06-18 | 0 day/s | No | +| Grpcio | 45-days | No | 1.75.1 | 1.75.1 | Yes | 2025-09-26 | 2025-09-26 | 0 day/s | Yes | +| Cassandra-driver | 45-days | No | 3.29.2 | 3.29.2 | Yes | 2024-09-10 | 2024-09-10 | 0 day/s | No | | Mysqlclient | 45-days | No | 2.2.7 | 2.2.7 | Yes | 2025-01-10 | 2025-01-10 | 0 day/s | Yes | -| Pika | 45-days | No | 1.3.2 | 1.3.2 | Yes | 2023-05-05 | 2023-05-05 | 0 day/s | No | -| PyMySQL | 45-days | No | 1.1.1 | 1.1.1 | Yes | 2024-05-21 | 2024-05-21 | 0 day/s | Yes | -| Pymongo | 45-days | No | 4.13.1 | 4.13.1 | Yes | 2025-06-11 | 2025-06-11 | 0 day/s | Yes | +| PyMySQL | 45-days | No | 1.1.2 | 1.1.2 | Yes | 2025-08-24 | 2025-08-24 | 0 day/s | Yes | +| Pymongo | 45-days | No | 4.15.3 | 4.15.3 | Yes | 2025-10-07 | 2025-10-07 | 0 day/s | Yes | | Psycopg2 | 45-days | No | 2.9.10 | 2.9.10 | Yes | 2024-10-16 | 2024-10-16 | 0 day/s | No | -| Redis | 45-days | No | 6.2.0 | 6.2.0 | Yes | 2025-05-28 | 2025-05-28 | 0 day/s | Yes | -| Requests | 45-days | No | 2.32.4 | 2.32.4 | Yes | 2025-06-09 | 2025-06-09 | 0 day/s | Yes | -| SQLAlchemy | 45-days | No | 2.0.41 | 2.0.41 | Yes | 2025-05-14 | 2025-05-14 | 0 day/s | Yes | -| Urllib3 | 45-days | No | 2.4.0 | 2.4.0 | Yes | 2025-04-10 | 2025-04-10 | 0 day/s | No | -| Spyne | 45-days | No | 2.14.0 | 2.14.0 | Yes | 2022-02-03 | 2022-02-03 | 0 day/s | No | -| Aio-pika | 45-days | No | 9.5.5 | 9.5.5 | Yes | 2025-02-26 | 2025-02-26 | 0 day/s | No | +| Redis | 45-days | No | 6.4.0 | 6.4.0 | Yes | 2025-08-07 | 2025-08-07 | 0 day/s | Yes | +| SQLAlchemy | 45-days | No | 2.0.43 | 2.0.43 | Yes | 2025-08-11 | 2025-08-11 | 0 day/s | Yes | | Aioamqp | 45-days | No | 0.15.0 | 0.15.0 | Yes | 2022-04-05 | 2022-04-05 | 0 day/s | No | +| Aio-pika | 45-days | No | 9.5.7 | 9.5.7 | Yes | 2025-08-05 | 2025-08-05 | 0 day/s | No | +| Confluent-kafka | 45-days | No | 2.11.1 | 2.11.1 | Yes | 2025-08-18 | 2025-08-18 | 0 day/s | No | +| Kafka-python-ng | 45-days | No | 2.2.3 | 2.2.3 | Yes | 2024-10-02 | 2024-10-02 | 0 day/s | No | +| Pika | 45-days | No | 1.3.2 | 1.3.2 | Yes | 2023-05-05 | 2023-05-05 | 0 day/s | No | +| Boto3 | 45-days | No | 1.40.47 | 1.40.47 | Yes | 2025-10-07 | 2025-10-07 | 0 day/s | Yes | +| Google-cloud-pubsub | 45-days | No | 2.31.1 | 2.31.1 | Yes | 2025-07-28 | 2025-07-28 | 0 day/s | Yes | +| Google-cloud-storage | 45-days | No | 3.4.0 | 3.4.0 | Yes | 2025-09-15 | 2025-09-15 | 0 day/s | Yes | +| Gevent | On demand | No | 25.9.1 | 25.9.1 | Yes | 2025-09-17 | 2025-09-17 | 0 day/s | No | +| Celery | 45-days | No | 5.5.3 | 5.5.3 | Yes | 2025-06-01 | 2025-06-01 | 0 day/s | No | diff --git a/.tekton/.currency/scripts/generate_report.py b/.tekton/.currency/scripts/generate_report.py index 0d4ca056..64055b9c 100644 --- a/.tekton/.currency/scripts/generate_report.py +++ b/.tekton/.currency/scripts/generate_report.py @@ -31,7 +31,7 @@ def get_upstream_version(dependency, last_supported_version): last_supported_version_release_date = "Not found" if dependency in SPEC_MAP: # webscrape info from official website - version_pattern = "(\d+\.\d+\.?\d*)" + version_pattern = r"(\d+\.\d+\.?\d*)" latest_version_release_date = "" url = SPEC_MAP[dependency] @@ -181,17 +181,17 @@ def process_taskrun_logs( f"Retrieving container logs from the successful taskrun pod {pod_name} of taskrun {taskrun_name}.." ) if task_name == "python-tracer-unittest-gevent-starlette-task": - match = re.search("Successfully installed .* (starlette-[^\s]+)", logs) - tekton_ci_output += f"{match[1]}\n" - elif task_name == "python-tracer-unittest-googlecloud-task": - match = re.search( - "Successfully installed .* (google-cloud-storage-[^\s]+)", logs - ) + match = re.search(r"Successfully installed .*(gevent-[^\s]+) .* (starlette-[^\s]+)", logs) + tekton_ci_output += f"{match[1]}\n{match[2]}\n" + elif task_name == "python-tracer-unittest-kafka-task": + match = re.search(r"Successfully installed .*(confluent-kafka-[^\s]+) .* (kafka-python-ng-[^\s]+)", logs) + tekton_ci_output += f"{match[1]}\n{match[2]}\n" + elif task_name == "python-tracer-unittest-cassandra-task": + match = re.search(r"Successfully installed .*(cassandra-driver-[^\s]+)", logs) tekton_ci_output += f"{match[1]}\n" elif task_name == "python-tracer-unittest-default-task": - for line in logs.splitlines(): - if "Successfully installed" in line: - tekton_ci_output += line + lines = re.findall(r"^Successfully installed .*", logs, re.M) + tekton_ci_output += "\n".join(lines) break else: print( @@ -202,36 +202,39 @@ def process_taskrun_logs( def get_tekton_ci_output(): """Get the latest successful scheduled tekton pipeline output""" + # # To run locally # config.load_kube_config() + + ## To run inside the tekton kubernetes cluster config.load_incluster_config() namespace = "default" core_v1_client = client.CoreV1Api() - task_name = "python-tracer-unittest-gevent-starlette-task" taskrun_filter = lambda tr: tr["status"]["conditions"][0]["type"] == "Succeeded" # noqa: E731 + + task_name = "python-tracer-unittest-gevent-starlette-task" starlette_taskruns = get_taskruns(namespace, task_name, taskrun_filter) tekton_ci_output = process_taskrun_logs( starlette_taskruns, core_v1_client, namespace, task_name, "" ) - task_name = "python-tracer-unittest-googlecloud-task" - taskrun_filter = ( # noqa: E731 - lambda tr: tr["metadata"]["name"].endswith("unittest-googlecloud-0") - and tr["status"]["conditions"][0]["type"] == "Succeeded" + task_name = "python-tracer-unittest-kafka-task" + kafka_taskruns = get_taskruns(namespace, task_name, taskrun_filter) + + tekton_ci_output = process_taskrun_logs( + kafka_taskruns, core_v1_client, namespace, task_name, tekton_ci_output ) - googlecloud_taskruns = get_taskruns(namespace, task_name, taskrun_filter) + + task_name = "python-tracer-unittest-cassandra-task" + cassandra_taskruns = get_taskruns(namespace, task_name, taskrun_filter) tekton_ci_output = process_taskrun_logs( - googlecloud_taskruns, core_v1_client, namespace, task_name, tekton_ci_output + cassandra_taskruns, core_v1_client, namespace, task_name, tekton_ci_output ) task_name = "python-tracer-unittest-default-task" - taskrun_filter = ( # noqa: E731 - lambda tr: tr["metadata"]["name"].endswith("unittest-default-3") - and tr["status"]["conditions"][0]["type"] == "Succeeded" - ) default_taskruns = get_taskruns(namespace, task_name, taskrun_filter) tekton_ci_output = process_taskrun_logs( From 8fd76d8a0640fa230891a85432c7d0f5246492cb Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 8 Oct 2025 12:24:30 +0530 Subject: [PATCH 21/44] currency: Enhance `get_tekton_ci_output()` Signed-off-by: Varsha GS --- .tekton/.currency/scripts/generate_report.py | 57 +++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/.tekton/.currency/scripts/generate_report.py b/.tekton/.currency/scripts/generate_report.py index 64055b9c..9ae08d11 100644 --- a/.tekton/.currency/scripts/generate_report.py +++ b/.tekton/.currency/scripts/generate_report.py @@ -202,44 +202,37 @@ def process_taskrun_logs( def get_tekton_ci_output(): """Get the latest successful scheduled tekton pipeline output""" - # # To run locally - # config.load_kube_config() - - ## To run inside the tekton kubernetes cluster - config.load_incluster_config() + try: + config.load_incluster_config() + print("Using in-cluster Kubernetes configuration...") + except config.config_exception.ConfigException: + # Fall back to local config if running locally and not inside cluster + config.load_kube_config() + print("Using local Kubernetes configuration...") namespace = "default" core_v1_client = client.CoreV1Api() taskrun_filter = lambda tr: tr["status"]["conditions"][0]["type"] == "Succeeded" # noqa: E731 - task_name = "python-tracer-unittest-gevent-starlette-task" - starlette_taskruns = get_taskruns(namespace, task_name, taskrun_filter) - - tekton_ci_output = process_taskrun_logs( - starlette_taskruns, core_v1_client, namespace, task_name, "" - ) - - task_name = "python-tracer-unittest-kafka-task" - kafka_taskruns = get_taskruns(namespace, task_name, taskrun_filter) - - tekton_ci_output = process_taskrun_logs( - kafka_taskruns, core_v1_client, namespace, task_name, tekton_ci_output - ) - - task_name = "python-tracer-unittest-cassandra-task" - cassandra_taskruns = get_taskruns(namespace, task_name, taskrun_filter) - - tekton_ci_output = process_taskrun_logs( - cassandra_taskruns, core_v1_client, namespace, task_name, tekton_ci_output - ) - - task_name = "python-tracer-unittest-default-task" - default_taskruns = get_taskruns(namespace, task_name, taskrun_filter) - - tekton_ci_output = process_taskrun_logs( - default_taskruns, core_v1_client, namespace, task_name, tekton_ci_output - ) + tasks = [ + "python-tracer-unittest-gevent-starlette-task", + "python-tracer-unittest-kafka-task", + "python-tracer-unittest-cassandra-task", + "python-tracer-unittest-default-task" + ] + + tekton_ci_output = "" + + for task_name in tasks: + try: + taskruns = get_taskruns(namespace, task_name, taskrun_filter) + + tekton_ci_output = process_taskrun_logs( + taskruns, core_v1_client, namespace, task_name, tekton_ci_output + ) + except Exception as exc: + print(f"Error processing task {task_name}: {str(exc)}") return tekton_ci_output From 081c01f5d7ebe3b71000ec8406d550351189ffd7 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 8 Oct 2025 12:25:45 +0530 Subject: [PATCH 22/44] ci(tekton): Run only pipelines/tasks required for currency Signed-off-by: Varsha GS --- .tekton/pipeline.yaml | 44 ---------------------------- .tekton/scheduled-eventlistener.yaml | 2 +- 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/.tekton/pipeline.yaml b/.tekton/pipeline.yaml index a87ff532..efbd6fe5 100644 --- a/.tekton/pipeline.yaml +++ b/.tekton/pipeline.yaml @@ -6,27 +6,12 @@ spec: params: - name: revision type: string - - name: py-38-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.8-bookworm - - name: py-39-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.9-bookworm - - name: py-310-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.10-bookworm - - name: py-311-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.11-bookworm - name: py-312-imageDigest type: string default: public.ecr.aws/docker/library/python:3.12-bookworm - name: py-313-imageDigest type: string default: public.ecr.aws/docker/library/python:3.13-bookworm - - name: py-314-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.14.0rc3 workspaces: - name: python-tracer-ci-pipeline-pvc tasks: @@ -48,13 +33,7 @@ spec: params: - name: imageDigest value: - - $(params.py-38-imageDigest) - - $(params.py-39-imageDigest) - - $(params.py-310-imageDigest) - - $(params.py-311-imageDigest) - - $(params.py-312-imageDigest) - $(params.py-313-imageDigest) - # - $(params.py-314-imageDigest) taskRef: name: python-tracer-unittest-default-task workspaces: @@ -82,17 +61,6 @@ spec: workspaces: - name: task-pvc workspace: python-tracer-ci-pipeline-pvc - - name: unittest-aws - runAfter: - - clone - params: - - name: imageDigest - value: $(params.py-313-imageDigest) - taskRef: - name: python-tracer-unittest-aws-task - workspaces: - - name: task-pvc - workspace: python-tracer-ci-pipeline-pvc - name: unittest-kafka runAfter: - clone @@ -104,15 +72,3 @@ spec: workspaces: - name: task-pvc workspace: python-tracer-ci-pipeline-pvc - - name: unittest-python-next - displayName: "Python next $(params.imageDigest)" - runAfter: - - clone - params: - - name: py-version - value: 3.14.0rc3 - taskRef: - name: python-tracer-unittest-python-next-task - workspaces: - - name: task-pvc - workspace: python-tracer-ci-pipeline-pvc diff --git a/.tekton/scheduled-eventlistener.yaml b/.tekton/scheduled-eventlistener.yaml index 5fdc3129..f9b8e2a6 100644 --- a/.tekton/scheduled-eventlistener.yaml +++ b/.tekton/scheduled-eventlistener.yaml @@ -25,7 +25,7 @@ spec: - name: git-commit-sha value: $(tt.params.git-commit-sha) pipelineRef: - name: github-pr-python-tracer-ci-pipeline + name: python-tracer-ci-pipeline workspaces: - name: python-tracer-ci-pipeline-pvc volumeClaimTemplate: From 9183bcbaf94ee2d2543db4f1016c56484d505475 Mon Sep 17 00:00:00 2001 From: minatooni Date: Fri, 26 Sep 2025 11:51:51 +0900 Subject: [PATCH 23/44] fix: tracing fastapi app Signed-off-by: minatooni --- src/instana/instrumentation/fastapi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/instana/instrumentation/fastapi.py b/src/instana/instrumentation/fastapi.py index b2e9b018..68b19f6a 100644 --- a/src/instana/instrumentation/fastapi.py +++ b/src/instana/instrumentation/fastapi.py @@ -71,6 +71,10 @@ def init_with_instana( kwargs["middleware"] = [Middleware(InstanaASGIMiddleware)] elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) + elif isinstance(middleware, tuple): + kwargs["middleware"] = (*middleware, Middleware(InstanaASGIMiddleware)) + else: + logger.warning("Unsupported FastAPI middleware sequence type.") exception_handlers = kwargs.get("exception_handlers") if exception_handlers is None: From 548a72be2d9c52512d22c0c832c86661f4d67edd Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 9 Oct 2025 10:08:12 +0530 Subject: [PATCH 24/44] fix: command used to run the python process - adapt to legacy systems like ibm i Signed-off-by: Varsha GS --- src/instana/fsm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/fsm.py b/src/instana/fsm.py index 7355a0ab..f9473907 100644 --- a/src/instana/fsm.py +++ b/src/instana/fsm.py @@ -119,7 +119,7 @@ def announce_sensor(self, e: Any) -> bool: # rely on ps rather than adding a dependency on something like # psutil which requires dev packages, gcc etc... proc = subprocess.Popen( - ["ps", "-p", str(pid), "-o", "command"], stdout=subprocess.PIPE + ["ps", "-p", str(pid), "-o", "args"], stdout=subprocess.PIPE ) (out, _) = proc.communicate() parts = out.split(b"\n") From 0f2cc575e4ef6b1d1789f2e40af9f22784b4fe03 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 7 Oct 2025 17:23:12 +0200 Subject: [PATCH 25/44] chore: Update README.md file. Signed-off-by: Paulo Vital --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 514530c7..20e08ff5 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,13 @@ Any feedback is welcome. Happy Python visibility. ## Installation -Instana remotely instruments your Python web servers automatically via [Instana AutoTrace™️]. To configure which Python processes this applies to, see the [configuration page]. +You can use automatic installation or manual installation as described in the following sections: -## Manual Installation +### Automatic installation + +Instana remotely instruments your Python applications automatically by [Instana AutoTrace webhook] in Kubernetes and Red Hat OpenShift clusters. However, if you prefer to install the package manually, see [Manual Installation](#manual-installation) as follows. + +### Manual Installation If you wish to instrument your applications manually, you can install the package with the following into the `virtualenv`, `pipenv`, or container (hosted on [PyPI]): @@ -27,7 +31,7 @@ or to alternatively update an existing installation: pip install -U instana -### Activating Without Code Changes +#### Activating Without Code Changes The Instana package can then be activated _without any code changes required_ by setting the following environment variable for your Python application: @@ -35,7 +39,7 @@ The Instana package can then be activated _without any code changes required_ by This will cause the Instana Python package to instrument your Python application automatically. Once it finds the Instana host agent, it will report Python metrics and distributed traces. -### Activating via Import +#### Activating With Code Changes Alternatively, if you prefer the manual method, import the `instana` package inside of your Python application: @@ -57,11 +61,11 @@ Want to instrument other languages? See our [Node.js], [Go], [Ruby] instrumenta [Instana]: https://www.instana.com/ "IBM Instana Observability" -[Instana AutoTrace™️]: https://www.ibm.com/docs/en/instana-observability/current?topic=kubernetes-instana-autotrace-webhook "Instana AutoTrace" +[Instana AutoTrace webhook]: https://www.ibm.com/docs/en/instana-observability/current?topic=kubernetes-instana-autotrace-webhook "Instana AutoTrace webhook" [configuration page]: https://www.ibm.com/docs/en/instana-observability/current?topic=package-python-configuration-configuring-instana#general "Instana Python package configuration" [PyPI]: https://pypi.python.org/pypi/instana "Instana package at PyPI" [installation document]: https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-python-instana-python-package#installation-methods "Instana Python package installation methods" -[documentation portal]: https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-python-instana-python-package "Instana Python package documentation" +[documentation portal]: https://ibm.biz/monitoring-python "Monitoring Python - IBM documentation" [Node.js]: https://github.com/instana/nodejs "Instana Node.JS Tracer" [Go]: https://github.com/instana/golang-sensor "Instana Go Tracer" [Ruby]: https://github.com/instana/ruby-sensor "Instana Ruby Tracer" From 83996ff0c288a124a4eea608f8560eae7bfc5931 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 7 Oct 2025 17:25:13 +0200 Subject: [PATCH 26/44] feat: Add support to Python 3.14.0 ... and drop support to Python 3.8. Signed-off-by: Paulo Vital --- .circleci/config.yml | 43 +++------------------------- .tekton/github-pr-pipeline.yaml.part | 7 ++--- .tekton/pipeline.yaml | 1 + .tekton/python-tracer-prepuller.yaml | 5 +--- Dockerfile | 2 +- pyproject.toml | 8 ++++-- src/instana/autoprofile/profiler.py | 7 +++-- 7 files changed, 18 insertions(+), 55 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a06d8287..83007df7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,8 +19,8 @@ commands: CHANGED_FILES=$(git diff --name-only origin/main...HEAD) # Check if any relevant files changed - echo "$CHANGED_FILES" | grep -q -E "^(src/|tests/|tests_aws/|.circleci/)" || { - echo "No changes in src/, tests/, tests_aws/, or .circleci directories. Skipping tests." + echo "$CHANGED_FILES" | grep -q -E "^(src/|tests/|tests_autowrapt/|tests_aws/|.circleci/|pyproject.toml)" || { + echo "No changes in src/, tests/, tests_autowrapt/, tests_aws/, .circleci directories or pyproject.toml file. Skipping tests." circleci step halt } @@ -161,38 +161,6 @@ jobs: - store-pytest-results - store-coverage-report - python314: - docker: - - image: ghcr.io/pvital/pvital-py3.14.0:latest - - image: public.ecr.aws/docker/library/postgres:16.2-bookworm - environment: - POSTGRES_USER: root - POSTGRES_PASSWORD: passw0rd - POSTGRES_DB: instana_test_db - - image: public.ecr.aws/docker/library/mariadb:11.3.2 - environment: - MYSQL_ROOT_PASSWORD: passw0rd - MYSQL_DATABASE: instana_test_db - - image: public.ecr.aws/docker/library/redis:7.2.4-bookworm - - image: public.ecr.aws/docker/library/rabbitmq:3.13.0 - - image: public.ecr.aws/docker/library/mongo:7.0.6 - - image: quay.io/thekevjames/gcloud-pubsub-emulator:latest - environment: - PUBSUB_EMULATOR_HOST: 0.0.0.0:8681 - PUBSUB_PROJECT1: test-project,test-topic - working_directory: ~/repo - steps: - - checkout - - check-if-tests-needed - - run: | - cp -a /root/base/venv ./venv - . venv/bin/activate - pip install 'wheel==0.45.1' - pip install -r requirements.txt - - run-tests-with-coverage-report - - store-pytest-results - - store-coverage-report - py39cassandra: docker: - image: public.ecr.aws/docker/library/python:3.9 @@ -324,8 +292,7 @@ workflows: - python3x: matrix: parameters: - py-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - - python314 + py-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - py39cassandra - py39gevent - py312aws @@ -333,12 +300,10 @@ workflows: - autowrapt: matrix: parameters: - py-version: ["3.11", "3.12", "3.13"] + py-version: ["3.11", "3.12", "3.13", "3.14"] - final_job: requires: - python3x - # Uncomment the following when giving real support to 3.14 - # - python314 - py39cassandra - py39gevent - py312aws diff --git a/.tekton/github-pr-pipeline.yaml.part b/.tekton/github-pr-pipeline.yaml.part index 2e0aac50..5a4782c1 100644 --- a/.tekton/github-pr-pipeline.yaml.part +++ b/.tekton/github-pr-pipeline.yaml.part @@ -8,9 +8,6 @@ spec: type: string - name: git-commit-sha type: string - - name: py-38-imageDigest - type: string - default: public.ecr.aws/docker/library/python:3.8-bookworm - name: py-39-imageDigest type: string default: public.ecr.aws/docker/library/python:3.9-bookworm @@ -28,7 +25,7 @@ spec: default: public.ecr.aws/docker/library/python:3.13-bookworm - name: py-314-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.14.0rc3 + default: public.ecr.aws/docker/library/python:3.14-bookworm workspaces: - name: python-tracer-ci-pipeline-pvc tasks: @@ -51,7 +48,7 @@ spec: - unittest-gevent-starlette - unittest-aws - unittest-kafka - - unittest-python-next +# - unittest-python-next taskRef: kind: Task name: github-set-status diff --git a/.tekton/pipeline.yaml b/.tekton/pipeline.yaml index efbd6fe5..ba2fbef0 100644 --- a/.tekton/pipeline.yaml +++ b/.tekton/pipeline.yaml @@ -34,6 +34,7 @@ spec: - name: imageDigest value: - $(params.py-313-imageDigest) + - $(params.py-314-imageDigest) taskRef: name: python-tracer-unittest-default-task workspaces: diff --git a/.tekton/python-tracer-prepuller.yaml b/.tekton/python-tracer-prepuller.yaml index b6a6f8a0..0ef3ec41 100644 --- a/.tekton/python-tracer-prepuller.yaml +++ b/.tekton/python-tracer-prepuller.yaml @@ -40,9 +40,6 @@ spec: - name: prepuller-kafka image: public.ecr.aws/bitnami/kafka:3.9.0 command: ["sh", "-c", "'true'"] - - name: prepuller-38 - image: public.ecr.aws/docker/library/python:3.8-bookworm - command: ["sh", "-c", "'true'"] - name: prepuller-39 image: public.ecr.aws/docker/library/python:3.9-bookworm command: ["sh", "-c", "'true'"] @@ -59,7 +56,7 @@ spec: image: public.ecr.aws/docker/library/python:3.13-bookworm command: ["sh", "-c", "'true'"] - name: prepuller-314 - image: public.ecr.aws/docker/library/python:3.14.0rc3 + image: public.ecr.aws/docker/library/python:3.14-bookworm command: ["sh", "-c", "'true'"] # Use the pause container to ensure the Pod goes into a `Running` phase diff --git a/Dockerfile b/Dockerfile index a193d6d1..ba04c9c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Development Container -FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm +FROM public.ecr.aws/docker/library/python:3.14-slim RUN apt-get -y -qq update && \ apt-get -y -qq upgrade && \ diff --git a/pyproject.toml b/pyproject.toml index bcd86863..5edc3a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dynamic = [ ] description = "Python Distributed Tracing & Metrics Sensor for Instana." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = "MIT" keywords = [ "performance", @@ -31,12 +31,12 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Topic :: System :: Monitoring", @@ -69,7 +69,7 @@ dev = [ ] [project.urls] -Documentation = "https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-python-instana-python-package" +Documentation = "https://ibm.biz/monitoring-python" Issues = "https://github.com/instana/python-sensor/issues" Source = "https://github.com/instana/python-sensor" @@ -80,6 +80,8 @@ path = "src/instana/version.py" include = [ "/src", "/tests", + "/tests_autowrapt", + "/tests_aws", ] [tool.hatch.build.targets.wheel] diff --git a/src/instana/autoprofile/profiler.py b/src/instana/autoprofile/profiler.py index 2e685a0e..dc417c46 100644 --- a/src/instana/autoprofile/profiler.py +++ b/src/instana/autoprofile/profiler.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from types import FrameType + from instana.agent.host import HostAgent @@ -52,11 +53,11 @@ def start(self, **kwargs: Dict[str, Any]) -> None: return try: - if not min_version(3, 8): - raise Exception("Supported Python versions 3.8 or higher.") + if not min_version(3, 9): + raise EnvironmentError("Supported Python versions: 3.9 or higher.") if platform.python_implementation() != "CPython": - raise Exception("Supported Python interpreter is CPython.") + raise EnvironmentError("Supported Python interpreter: CPython.") if self.profiler_destroyed: logger.warning("Destroyed profiler cannot be started.") From 7cab540af4b9fea8d694b2c771458b8828eefe76 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 7 Oct 2025 17:32:59 +0200 Subject: [PATCH 27/44] chore(ci): Update to trixie container images... when possible, as redis doesn't have it. Signed-off-by: Paulo Vital --- .circleci/config.yml | 2 +- .tekton/.currency/currency-tasks.yaml | 2 +- .tekton/github-pr-pipeline.yaml.part | 12 ++++++------ .tekton/pipeline.yaml | 7 +++++-- .tekton/python-tracer-prepuller.yaml | 14 +++++++------- .tekton/task.yaml | 4 ++-- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 83007df7..b55277ca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,7 +135,7 @@ jobs: type: string docker: - image: public.ecr.aws/docker/library/python:<> - - image: public.ecr.aws/docker/library/postgres:16.2-bookworm + - image: public.ecr.aws/docker/library/postgres:16.10-trixie environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd diff --git a/.tekton/.currency/currency-tasks.yaml b/.tekton/.currency/currency-tasks.yaml index c35a97d2..7f5ead15 100644 --- a/.tekton/.currency/currency-tasks.yaml +++ b/.tekton/.currency/currency-tasks.yaml @@ -32,7 +32,7 @@ spec: mountPath: /workspace steps: - name: generate-currency-report - image: public.ecr.aws/docker/library/python:3.12-bookworm + image: public.ecr.aws/docker/library/python:3.12-trixie script: | #!/usr/bin/env bash cd /workspace/python-sensor/.tekton/.currency diff --git a/.tekton/github-pr-pipeline.yaml.part b/.tekton/github-pr-pipeline.yaml.part index 5a4782c1..db2319ab 100644 --- a/.tekton/github-pr-pipeline.yaml.part +++ b/.tekton/github-pr-pipeline.yaml.part @@ -10,22 +10,22 @@ spec: type: string - name: py-39-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.9-bookworm + default: public.ecr.aws/docker/library/python:3.9-trixie - name: py-310-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.10-bookworm + default: public.ecr.aws/docker/library/python:3.10-trixie - name: py-311-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.11-bookworm + default: public.ecr.aws/docker/library/python:3.11-trixie - name: py-312-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.12-bookworm + default: public.ecr.aws/docker/library/python:3.12-trixie - name: py-313-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.13-bookworm + default: public.ecr.aws/docker/library/python:3.13-trixie - name: py-314-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.14-bookworm + default: public.ecr.aws/docker/library/python:3.14-trixie workspaces: - name: python-tracer-ci-pipeline-pvc tasks: diff --git a/.tekton/pipeline.yaml b/.tekton/pipeline.yaml index ba2fbef0..a74ef6be 100644 --- a/.tekton/pipeline.yaml +++ b/.tekton/pipeline.yaml @@ -8,10 +8,13 @@ spec: type: string - name: py-312-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.12-bookworm + default: public.ecr.aws/docker/library/python:3.12-trixie - name: py-313-imageDigest type: string - default: public.ecr.aws/docker/library/python:3.13-bookworm + default: public.ecr.aws/docker/library/python:3.13-trixie + - name: py-314-imageDigest + type: string + default: public.ecr.aws/docker/library/python:3.14-trixie workspaces: - name: python-tracer-ci-pipeline-pvc tasks: diff --git a/.tekton/python-tracer-prepuller.yaml b/.tekton/python-tracer-prepuller.yaml index 0ef3ec41..3d711dab 100644 --- a/.tekton/python-tracer-prepuller.yaml +++ b/.tekton/python-tracer-prepuller.yaml @@ -35,28 +35,28 @@ spec: image: public.ecr.aws/docker/library/mariadb:11.3.2 command: ["sh", "-c", "'true'"] - name: prepuller-postgres - image: public.ecr.aws/docker/library/postgres:16.2-bookworm + image: public.ecr.aws/docker/library/postgres:16.10-trixie command: ["sh", "-c", "'true'"] - name: prepuller-kafka image: public.ecr.aws/bitnami/kafka:3.9.0 command: ["sh", "-c", "'true'"] - name: prepuller-39 - image: public.ecr.aws/docker/library/python:3.9-bookworm + image: public.ecr.aws/docker/library/python:3.9-trixie command: ["sh", "-c", "'true'"] - name: prepuller-310 - image: public.ecr.aws/docker/library/python:3.10-bookworm + image: public.ecr.aws/docker/library/python:3.10-trixie command: ["sh", "-c", "'true'"] - name: prepuller-311 - image: public.ecr.aws/docker/library/python:3.11-bookworm + image: public.ecr.aws/docker/library/python:3.11-trixie command: ["sh", "-c", "'true'"] - name: prepuller-312 - image: public.ecr.aws/docker/library/python:3.12-bookworm + image: public.ecr.aws/docker/library/python:3.12-trixie command: ["sh", "-c", "'true'"] - name: prepuller-313 - image: public.ecr.aws/docker/library/python:3.13-bookworm + image: public.ecr.aws/docker/library/python:3.13-trixie command: ["sh", "-c", "'true'"] - name: prepuller-314 - image: public.ecr.aws/docker/library/python:3.14-bookworm + image: public.ecr.aws/docker/library/python:3.14-trixie command: ["sh", "-c", "'true'"] # Use the pause container to ensure the Pod goes into a `Running` phase diff --git a/.tekton/task.yaml b/.tekton/task.yaml index 0a9a6d05..f6b21a05 100644 --- a/.tekton/task.yaml +++ b/.tekton/task.yaml @@ -104,7 +104,7 @@ spec: - name: mongo image: public.ecr.aws/docker/library/mongo:7.0.6 - name: postgres - image: public.ecr.aws/docker/library/postgres:16.2-bookworm + image: public.ecr.aws/docker/library/postgres:16.10-trixie env: - name: POSTGRES_USER value: root @@ -248,7 +248,7 @@ spec: - name: mongo image: public.ecr.aws/docker/library/mongo:7.0.6 - name: postgres - image: public.ecr.aws/docker/library/postgres:16.2-bookworm + image: public.ecr.aws/docker/library/postgres:16.10-trixie env: - name: POSTGRES_USER value: root From 1a05aba2de05d7d800596059ab4ec8b8a4d039b7 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 7 Oct 2025 17:34:23 +0200 Subject: [PATCH 28/44] chore(version): Bump version to 3.9.0 Signed-off-by: Paulo Vital --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 034f31c6..4884162b 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.8.3" +VERSION = "3.9.0" From 0bde1d8d323f5488563e8653c9bd87da25d470e7 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 15 Oct 2025 15:31:24 +0200 Subject: [PATCH 29/44] fix: TypeError in agent announcement process - Add type check in fsm.py to ensure payload is a dictionary before passing to set_from - Add defensive check in host.py to verify required keys exist in announce response - Prevents "'bool' object is not subscriptable" error in confluent_kafka tests Signed-off-by: Paulo Vital --- src/instana/agent/host.py | 13 +++++++++---- src/instana/fsm.py | 2 +- tests/agent/test_host.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/instana/agent/host.py b/src/instana/agent/host.py index ad39440c..9ecc74ca 100644 --- a/src/instana/agent/host.py +++ b/src/instana/agent/host.py @@ -138,10 +138,15 @@ def set_from( @return: None """ self.options.set_from(res_data) - self.announce_data = AnnounceData( - pid=res_data["pid"], - agentUuid=res_data["agentUuid"], - ) + + # Ensure required keys are present + if "pid" in res_data and "agentUuid" in res_data: + self.announce_data = AnnounceData( + pid=res_data["pid"], + agentUuid=res_data["agentUuid"], + ) + else: + logger.debug(f"Missing required keys in announce response: {res_data}") def get_from_structure(self) -> Dict[str, str]: """ diff --git a/src/instana/fsm.py b/src/instana/fsm.py index f9473907..c4145a5f 100644 --- a/src/instana/fsm.py +++ b/src/instana/fsm.py @@ -148,7 +148,7 @@ def announce_sensor(self, e: Any) -> bool: payload = self.agent.announce(d) - if not payload: + if not payload or not isinstance(payload, dict): logger.debug("Cannot announce sensor. Scheduling retry.") self.schedule_retry( self.announce_sensor, e, f"{self.THREAD_NAME}: announce" diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py index 058c676c..613d4478 100644 --- a/tests/agent/test_host.py +++ b/tests/agent/test_host.py @@ -5,7 +5,7 @@ import json import logging import os -from typing import Generator +from typing import Any, Dict, Generator from unittest.mock import Mock import pytest @@ -717,3 +717,36 @@ def test_is_service_or_endpoint_ignored(self) -> None: # don't ignore other services assert not self.agent._HostAgent__is_endpoint_ignored("service3") assert not self.agent._HostAgent__is_endpoint_ignored("service3") + + @pytest.mark.parametrize( + "input_data", + [ + { + "agentUuid": "test-uuid", + }, + { + "pid": 1234, + }, + { + "extraHeaders": ["value-3"], + }, + ], + ids=["missing_pid", "missing_agent_uuid", "missing_both_required_keys"], + ) + def test_set_from_missing_required_keys( + self, input_data: Dict[str, Any], caplog: pytest.LogCaptureFixture + ) -> None: + """Test set_from when required keys are missing in res_data.""" + agent = HostAgent() + caplog.set_level(logging.DEBUG, logger="instana") + + res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + } + res_data.update(input_data) + + agent.set_from(res_data) + + assert agent.announce_data is None + assert "Missing required keys in announce response" in caplog.messages[-1] + assert str(res_data) in caplog.messages[-1] From ebab26eba510cf97b704e4676e51a4e626b3dc7e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 24 Oct 2025 09:58:34 +0530 Subject: [PATCH 30/44] chore(version): Bump version to `3.9.1` Signed-off-by: Varsha GS --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 4884162b..9c3e1cdd 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.9.0" +VERSION = "3.9.1" From e1641c03f0f87bd118fbce5fe9b658fbfe97f198 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 28 Oct 2025 14:12:06 +0530 Subject: [PATCH 31/44] chore: remove `setuptools` from project dependency - dependency on `pkg_resources` gone after `wrapt-2.0.0` Signed-off-by: Varsha GS --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5edc3a6f..c934a36c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ dependencies = [ "opentelemetry-semantic-conventions>=0.48b0", "typing_extensions>=4.12.2", "pyyaml>=6.0.2", - "setuptools>=69.0.0; python_version >= \"3.12\"", "psutil>=5.9.0; sys_platform == \"win32\"", ] From e0f17a59b431755684d2f56442fedd3449cfaeaf Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 28 Oct 2025 16:37:25 +0100 Subject: [PATCH 32/44] fix: IndexError in the `confluent_kafka_python.py` Fixed IndexError in `confluent_kafka_python.py` by handling both positional and keyword arguments for the topic parameter in the `trace_kafka_produce` function. The issue occurred when the topic was passed as a keyword argument, resulting in an empty args tuple and causing an IndexError when trying to access `args[0]`. The solution: 1. Modified the `trace_kafka_produce` function to get the topic from either `args` or `kwargs` 2. Added safety checks to handle edge cases 3. Added two new test methods to verify the fix works with different argument patterns: - `test_trace_confluent_kafka_produce_with_keyword_topic` - `test_trace_confluent_kafka_produce_with_keyword_args` This fix ensures that the Kafka instrumentation works correctly regardless of how the `produce` method is called, improving the robustness of the Python sensor. Signed-off-by: Paulo Vital --- .../kafka/confluent_kafka_python.py | 17 ++-- tests/clients/kafka/test_confluent_kafka.py | 80 ++++++++++++++++--- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/instana/instrumentation/kafka/confluent_kafka_python.py b/src/instana/instrumentation/kafka/confluent_kafka_python.py index e5d991d2..f2f327f1 100644 --- a/src/instana/instrumentation/kafka/confluent_kafka_python.py +++ b/src/instana/instrumentation/kafka/confluent_kafka_python.py @@ -14,11 +14,8 @@ from instana.log import logger from instana.propagators.format import Format from instana.singletons import get_tracer - from instana.util.traceutils import ( - get_tracer_tuple, - tracing_is_off, - ) from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple, tracing_is_off consumer_token = None consumer_span = contextvars.ContextVar("confluent_kafka_consumer_span") @@ -69,16 +66,20 @@ def trace_kafka_produce( tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None + + # Get the topic from either args or kwargs + topic = args[0] if args else kwargs.get("topic", "") + is_suppressed = tracer.exporter._HostAgent__is_endpoint_ignored( "kafka", "produce", - args[0], + topic, ) with tracer.start_as_current_span( "kafka-producer", span_context=parent_context, kind=SpanKind.PRODUCER ) as span: - span.set_attribute("kafka.service", args[0]) + span.set_attribute("kafka.service", topic) span.set_attribute("kafka.access", "produce") # context propagation @@ -89,6 +90,10 @@ def trace_kafka_produce( # dictionary. To maintain compatibility with the headers for the # Kafka Python library, we will use a list of tuples. headers = args[6] if len(args) > 6 else kwargs.get("headers", []) + + # Initialize headers if it's None + if headers is None: + headers = [] suppression_header = {"x_instana_l_s": "0" if is_suppressed else "1"} headers.append(suppression_header) diff --git a/tests/clients/kafka/test_confluent_kafka.py b/tests/clients/kafka/test_confluent_kafka.py index 61f31bce..a5c9b334 100644 --- a/tests/clients/kafka/test_confluent_kafka.py +++ b/tests/clients/kafka/test_confluent_kafka.py @@ -5,30 +5,26 @@ from typing import Generator import pytest -from confluent_kafka import ( - Consumer, - KafkaException, - Producer, -) +from confluent_kafka import Consumer, KafkaException, Producer from confluent_kafka.admin import AdminClient, NewTopic -from mock import patch, Mock +from mock import Mock, patch from opentelemetry.trace import SpanKind from opentelemetry.trace.span import format_span_id from instana.configurator import config -from instana.options import StandardOptions -from instana.singletons import agent, tracer -from instana.util.config import parse_ignored_endpoints_from_yaml -from tests.helpers import get_first_span_by_filter, testenv from instana.instrumentation.kafka import confluent_kafka_python from instana.instrumentation.kafka.confluent_kafka_python import ( clear_context, - save_consumer_span_into_context, close_consumer_span, - trace_kafka_close, consumer_span, + save_consumer_span_into_context, + trace_kafka_close, ) +from instana.options import StandardOptions +from instana.singletons import agent, tracer from instana.span.span import InstanaSpan +from instana.util.config import parse_ignored_endpoints_from_yaml +from tests.helpers import get_first_span_by_filter, testenv class TestConfluentKafka: @@ -120,6 +116,66 @@ def test_trace_confluent_kafka_produce(self) -> None: assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] assert kafka_span.data["kafka"]["access"] == "produce" + def test_trace_confluent_kafka_produce_with_keyword_topic(self) -> None: + """Test that tracing works when topic is passed as a keyword argument.""" + with tracer.start_as_current_span("test"): + # Pass topic as a keyword argument + self.producer.produce(topic=testenv["kafka_topic"], value=b"raw_bytes") + self.producer.flush(timeout=10) + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + kafka_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == kafka_span.t + + # Parent relationships + assert kafka_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not kafka_span.ec + + assert kafka_span.n == "kafka" + assert kafka_span.k == SpanKind.CLIENT + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + assert kafka_span.data["kafka"]["access"] == "produce" + + def test_trace_confluent_kafka_produce_with_keyword_args(self) -> None: + """Test that tracing works when both topic and headers are passed as keyword arguments.""" + with tracer.start_as_current_span("test"): + # Pass both topic and headers as keyword arguments + self.producer.produce( + topic=testenv["kafka_topic"], + value=b"raw_bytes", + headers=[("custom-header", b"header-value")], + ) + self.producer.flush(timeout=10) + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + kafka_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == kafka_span.t + + # Parent relationships + assert kafka_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not kafka_span.ec + + assert kafka_span.n == "kafka" + assert kafka_span.k == SpanKind.CLIENT + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + assert kafka_span.data["kafka"]["access"] == "produce" + def test_trace_confluent_kafka_consume(self) -> None: agent.options.set_trace_configurations() # Produce some events From b4263fcda375c056569daa3692a9a5f3c1eb1cfa Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 28 Oct 2025 16:49:16 +0100 Subject: [PATCH 33/44] fix: IndexError in the `kafka_python.py` Fixed potential IndexError in `kafka_python.py` by handling both positional and keyword arguments for the topic parameter in the `trace_kafka_send` function. The issue is similar to the one fixed in `confluent_kafka_python.py`, where an IndexError could occur when the topic was passed as a keyword argument, resulting in an empty `args` tuple. The solution: 1. Modified the `trace_kafka_send` function to get the topic from either `args` or `kwargs` 2. Added safety checks to handle edge cases 3. Added two new test methods to verify the fix works with different argument patterns: - `test_trace_kafka_python_send_with_keyword_topic` - `test_trace_kafka_python_send_with_keyword_args` This fix ensures that the Kafka instrumentation works correctly regardless of how the `send` method is called, improving the robustness of the Python sensor. Signed-off-by: Paulo Vital --- .../instrumentation/kafka/kafka_python.py | 13 ++-- tests/clients/kafka/test_kafka_python.py | 75 +++++++++++++++++-- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/instana/instrumentation/kafka/kafka_python.py b/src/instana/instrumentation/kafka/kafka_python.py index 3b1423d3..307b7d52 100644 --- a/src/instana/instrumentation/kafka/kafka_python.py +++ b/src/instana/instrumentation/kafka/kafka_python.py @@ -14,11 +14,8 @@ from instana.log import logger from instana.propagators.format import Format from instana.singletons import get_tracer - from instana.util.traceutils import ( - get_tracer_tuple, - tracing_is_off, - ) from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple, tracing_is_off if TYPE_CHECKING: from kafka.producer.future import FutureRecordMetadata @@ -38,15 +35,19 @@ def trace_kafka_send( tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None + + # Get the topic from either args or kwargs + topic = args[0] if args else kwargs.get("topic", "") + is_suppressed = tracer.exporter._HostAgent__is_endpoint_ignored( "kafka", "send", - args[0], + topic, ) with tracer.start_as_current_span( "kafka-producer", span_context=parent_context, kind=SpanKind.PRODUCER ) as span: - span.set_attribute("kafka.service", args[0]) + span.set_attribute("kafka.service", topic) span.set_attribute("kafka.access", "send") # context propagation diff --git a/tests/clients/kafka/test_kafka_python.py b/tests/clients/kafka/test_kafka_python.py index eb3723e3..a1d0ccbb 100644 --- a/tests/clients/kafka/test_kafka_python.py +++ b/tests/clients/kafka/test_kafka_python.py @@ -12,19 +12,18 @@ from opentelemetry.trace.span import format_span_id from instana.configurator import config -from instana.options import StandardOptions -from instana.singletons import agent, tracer -from instana.util.config import parse_ignored_endpoints_from_yaml -from tests.helpers import get_first_span_by_filter, testenv - from instana.instrumentation.kafka import kafka_python from instana.instrumentation.kafka.kafka_python import ( clear_context, - save_consumer_span_into_context, close_consumer_span, consumer_span, + save_consumer_span_into_context, ) +from instana.options import StandardOptions +from instana.singletons import agent, tracer from instana.span.span import InstanaSpan +from instana.util.config import parse_ignored_endpoints_from_yaml +from tests.helpers import get_first_span_by_filter, testenv class TestKafkaPython: @@ -122,6 +121,70 @@ def test_trace_kafka_python_send(self) -> None: assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] assert kafka_span.data["kafka"]["access"] == "send" + def test_trace_kafka_python_send_with_keyword_topic(self) -> None: + """Test that tracing works when topic is passed as a keyword argument.""" + with tracer.start_as_current_span("test"): + # Pass topic as a keyword argument + future = self.producer.send( + topic=testenv["kafka_topic"], value=b"raw_bytes" + ) + + _ = future.get(timeout=10) # noqa: F841 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + kafka_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == kafka_span.t + + # Parent relationships + assert kafka_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not kafka_span.ec + + assert kafka_span.n == "kafka" + assert kafka_span.k == SpanKind.CLIENT + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + assert kafka_span.data["kafka"]["access"] == "send" + + def test_trace_kafka_python_send_with_keyword_args(self) -> None: + """Test that tracing works when both topic and headers are passed as keyword arguments.""" + with tracer.start_as_current_span("test"): + # Pass both topic and headers as keyword arguments + future = self.producer.send( + topic=testenv["kafka_topic"], + value=b"raw_bytes", + headers=[("custom-header", b"header-value")], + ) + + _ = future.get(timeout=10) # noqa: F841 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + kafka_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == kafka_span.t + + # Parent relationships + assert kafka_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not kafka_span.ec + + assert kafka_span.n == "kafka" + assert kafka_span.k == SpanKind.CLIENT + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + assert kafka_span.data["kafka"]["access"] == "send" + def test_trace_kafka_python_consume(self) -> None: # Produce some events self.producer.send(testenv["kafka_topic"], b"raw_bytes1") From 4121d8d621d794dccd922fe4ad7de98f79905e66 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 29 Oct 2025 11:42:28 +0100 Subject: [PATCH 34/44] chore(version): Bump version to `3.9.2` Signed-off-by: Paulo Vital --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 9c3e1cdd..1419967c 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.9.1" +VERSION = "3.9.2" From 5bd20fe640b395761b40949b50386a304f5a5d0f Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 16:23:18 +0530 Subject: [PATCH 35/44] wsgi: Ensure span stays active throughout the response iteration Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 105 ++++++++++++++++++---------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5ab7a2f7..5f039c15 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -5,7 +5,7 @@ Instana WSGI Middleware """ -from typing import Dict, Any, Callable, List, Tuple, Optional +from typing import Dict, Any, Callable, List, Tuple, Optional, Iterable, TYPE_CHECKING from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace @@ -15,6 +15,8 @@ from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers +if TYPE_CHECKING: + from instana.span.span import InstanaSpan class InstanaWSGIMiddleware(object): """Instana WSGI middleware""" @@ -25,15 +27,41 @@ def __init__(self, app: object) -> None: def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ + # Extract context and start span + span_context = tracer.extract(Format.HTTP_HEADERS, env) + span = tracer.start_span("wsgi", span_context=span_context) + + # Attach context - this makes the span current + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + + # Extract custom headers from request + extract_custom_headers(span, env, format=True) + + # Set request attributes + if "PATH_INFO" in env: + span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + def new_start_response( status: str, headers: List[Tuple[object, ...]], exc_info: Optional[Exception] = None, ) -> object: """Modified start response with additional headers.""" - extract_custom_headers(self.span, headers) + extract_custom_headers(span, headers) - tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) + tracer.inject(span.context, Format.HTTP_HEADERS, headers) headers_str = [ (header[0], str(header[1])) @@ -41,39 +69,44 @@ def new_start_response( else header for header in headers ] - res = start_response(status, headers_str, exc_info) + # Set status code attribute sc = status.split(" ")[0] if 500 <= int(sc): - self.span.mark_as_errored() - - self.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) - if self.span and self.span.is_recording(): - self.span.end() - if self.token: - context.detach(self.token) - return res - - span_context = tracer.extract(Format.HTTP_HEADERS, env) - self.span = tracer.start_span("wsgi", span_context=span_context) - - ctx = trace.set_span_in_context(self.span) - self.token = context.attach(ctx) - - extract_custom_headers(self.span, env, format=True) - - if "PATH_INFO" in env: - self.span.set_attribute("http.path", env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - self.span.set_attribute("http.params", scrubbed_params) - if "REQUEST_METHOD" in env: - self.span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) - if "HTTP_HOST" in env: - self.span.set_attribute("http.host", env["HTTP_HOST"]) - - return self.app(environ, new_start_response) + span.mark_as_errored() + + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) + + return start_response(status, headers_str, exc_info) + + try: + iterable = self.app(environ, new_start_response) + + # Wrap the iterable to ensure span ends after iteration completes + return _end_span_after_iterating(iterable, span, token) + + except Exception as exc: + # If exception occurs before iteration completes, end span and detach token + if span and span.is_recording(): + span.record_exception(exc) + span.end() + if token: + context.detach(token) + raise exc + + +def _end_span_after_iterating( + iterable: Iterable[object], span: "InstanaSpan", token: object +) -> Iterable[object]: + try: + yield from iterable + finally: + # Ensure iterable cleanup (important for generators) + if hasattr(iterable, "close"): + iterable.close() + + # End span and detach token after iteration completes + if span and span.is_recording(): + span.end() + if token: + context.detach(token) From fea91b7170dbc7f59624094630815f99facb466d Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 16:43:32 +0530 Subject: [PATCH 36/44] chore(wsgi): move setting request attributes to a separate method Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5f039c15..ea020495 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -39,19 +39,7 @@ def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: extract_custom_headers(span, env, format=True) # Set request attributes - if "PATH_INFO" in env: - span.set_attribute("http.path", env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - if "REQUEST_METHOD" in env: - span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) - if "HTTP_HOST" in env: - span.set_attribute("http.host", env["HTTP_HOST"]) + _set_request_attributes(span, env) def new_start_response( status: str, @@ -110,3 +98,18 @@ def _end_span_after_iterating( span.end() if token: context.detach(token) + +def _set_request_attributes(span: "InstanaSpan", env: Dict[str, Any]) -> None: + if "PATH_INFO" in env: + span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + span.set_attribute(SpanAttributes.HTTP_HOST, env["HTTP_HOST"]) From 6bc03da7cf5b099fc6565cb391398607c2fb7312 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 13 Nov 2025 17:03:02 +0530 Subject: [PATCH 37/44] chore(version): Bump version to 3.9.3 Signed-off-by: Varsha GS --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 1419967c..6db3016f 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.9.2" +VERSION = "3.9.3" From 9369fddf4e70006a9aac85ca05cbad871ca2fd4e Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 17 Nov 2025 15:23:34 +0100 Subject: [PATCH 38/44] fix: Response class doesn't have exception attribute. Fixes: #819 Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pyramid.py | 39 ++++++------ tests/apps/pyramid/pyramid_app/app.py | 18 +++++- tests/frameworks/test_pyramid.py | 83 ++++++++++++++++++++++---- 3 files changed, 107 insertions(+), 33 deletions(-) diff --git a/src/instana/instrumentation/pyramid.py b/src/instana/instrumentation/pyramid.py index 6faed9db..a16f4d88 100644 --- a/src/instana/instrumentation/pyramid.py +++ b/src/instana/instrumentation/pyramid.py @@ -1,28 +1,28 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union + + import wrapt + from opentelemetry.semconv.trace import SpanAttributes + from pyramid.config import Configurator from pyramid.httpexceptions import HTTPException from pyramid.path import caller_package from pyramid.settings import aslist from pyramid.tweens import EXCVIEW - from pyramid.config import Configurator - from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple - import wrapt - - from opentelemetry.semconv.trace import SpanAttributes - from opentelemetry.trace import SpanKind from instana.log import logger - from instana.singletons import tracer, agent + from instana.propagators.format import Format + from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers - from instana.propagators.format import Format if TYPE_CHECKING: + from pyramid.registry import Registry from pyramid.request import Request from pyramid.response import Response - from pyramid.registry import Registry class InstanaTweenFactory(object): """A factory that provides Instana instrumentation tween for Pyramid apps""" @@ -32,11 +32,11 @@ def __init__( ) -> None: self.handler = handler - def __call__(self, request: "Request") -> "Response": + def __call__(self, request: "Request") -> Optional["Response"]: ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) with tracer.start_as_current_span("wsgi", span_context=ctx) as span: - span.set_attribute("http.host", request.host) + span.set_attribute(SpanAttributes.HTTP_HOST, request.host) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) span.set_attribute(SpanAttributes.HTTP_URL, request.path) @@ -57,9 +57,7 @@ def __call__(self, request: "Request") -> "Response": span.set_attribute( "http.path_tpl", request.matched_route.pattern ) - extract_custom_headers(span, response.headers) - tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) except HTTPException as e: response = e @@ -69,7 +67,6 @@ def __call__(self, request: "Request") -> "Response": except BaseException as e: span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) span.record_exception(e) - logger.debug( "Pyramid InstanaTweenFactory BaseException: ", exc_info=True ) @@ -78,16 +75,18 @@ def __call__(self, request: "Request") -> "Response": span.set_attribute( SpanAttributes.HTTP_STATUS_CODE, response.status_int ) - - if 500 <= response.status_int: - if response.exception: - span.record_exception(response.exception) - span.assure_errored() - + if response.status_code >= 500: + handle_exception(span, response) return response INSTANA_TWEEN = __name__ + ".InstanaTweenFactory" + def handle_exception(span, response: Union["Response", HTTPException]) -> None: + if isinstance(response, HTTPException): + span.record_exception(response.exception) + else: + span.record_exception(response.body) + # implicit tween ordering def includeme(config: Configurator) -> None: logger.debug("Instrumenting pyramid") diff --git a/tests/apps/pyramid/pyramid_app/app.py b/tests/apps/pyramid/pyramid_app/app.py index 867b2e7c..88763bbc 100644 --- a/tests/apps/pyramid/pyramid_app/app.py +++ b/tests/apps/pyramid/pyramid_app/app.py @@ -1,12 +1,12 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -from wsgiref.simple_server import make_server -from pyramid.config import Configurator import logging +from wsgiref.simple_server import make_server -from pyramid.response import Response import pyramid.httpexceptions as exc +from pyramid.config import Configurator +from pyramid.response import Response from tests.helpers import testenv @@ -25,6 +25,10 @@ def please_fail(request): raise exc.HTTPInternalServerError("internal error") +def fail_with_http_exception(request): + raise exc.HTTPException("bad request") + + def tableflip(request): raise BaseException("fake exception") @@ -39,6 +43,10 @@ def hello_user(request): return Response(f"Hello {user}!") +def return_error_response(request): + return Response("Error", status=500) + + app = None settings = { "pyramid.tweens": "tests.apps.pyramid.pyramid_utils.tweens.timing_tween_factory", @@ -48,12 +56,16 @@ def hello_user(request): config.add_view(hello_world, route_name="hello") config.add_route("fail", "/500") config.add_view(please_fail, route_name="fail") + config.add_route("fail_with_http_exception", "/fail_with_http_exception") + config.add_view(fail_with_http_exception, route_name="fail_with_http_exception") config.add_route("crash", "/exception") config.add_view(tableflip, route_name="crash") config.add_route("response_headers", "/response_headers") config.add_view(response_headers, route_name="response_headers") config.add_route("hello_user", "/hello_user/{user}") config.add_view(hello_user, route_name="hello_user") + config.add_route(name="return_error_response", pattern="/return_error_response") + config.add_view(return_error_response, route_name="return_error_response") app = config.make_wsgi_app() pyramid_server = make_server("127.0.0.1", testenv["pyramid_port"], app) diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index f2a8a640..72f934c5 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -1,15 +1,16 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +from typing import Generator + import pytest import urllib3 -from typing import Generator +import tests.apps.pyramid.pyramid_app # noqa: F401 +from instana.singletons import agent, tracer +from instana.span.span import get_current_span from instana.util.ids import hex_id -import tests.apps.pyramid.pyramid_app from tests.helpers import testenv -from instana.singletons import tracer, agent -from instana.span.span import get_current_span class TestPyramid: @@ -77,7 +78,9 @@ def test_get_request(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 200 @@ -161,7 +164,9 @@ def test_500(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/500" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 500 @@ -178,6 +183,56 @@ def test_500(self) -> None: assert type(urllib3_span.stack) is list assert len(urllib3_span.stack) > 1 + def test_return_error_response(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/return_error_response" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + pyramid_span = spans[0] + + assert response.status == 500 + + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) + assert pyramid_span.data["http"]["url"] == "/return_error_response" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 500 + assert pyramid_span.data["http"]["error"] == "b'Error'" + assert pyramid_span.data["http"]["path_tpl"] == "/return_error_response" + + assert pyramid_span.ec == 1 + + def test_fail_with_http_exception(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/fail_with_http_exception" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + pyramid_span = spans[0] + + assert response.status == 520 + + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) + assert pyramid_span.data["http"]["url"] == "/fail_with_http_exception" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 520 + assert pyramid_span.data["http"]["error"] == "bad request" + assert pyramid_span.data["http"]["path_tpl"] == "/fail_with_http_exception" + + assert pyramid_span.ec == 1 + def test_exception(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request( @@ -211,7 +266,9 @@ def test_exception(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/exception" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 500 @@ -270,7 +327,9 @@ def test_response_header_capture(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/response_headers" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 200 @@ -341,7 +400,9 @@ def test_request_header_capture(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 200 @@ -419,7 +480,9 @@ def test_scrub_secret_path_template(self) -> None: # wsgi assert pyramid_span.n == "wsgi" - assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["pyramid_port"] + ) assert pyramid_span.data["http"]["url"] == "/hello_user/oswald" assert pyramid_span.data["http"]["method"] == "GET" assert pyramid_span.data["http"]["status"] == 200 From 6b4f6e4c71e67b32329156bd9084f21c45757579 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 20 Nov 2025 11:07:04 +0530 Subject: [PATCH 39/44] `uwsgi`: Remove postfork hooks Signed-off-by: Varsha GS --- src/instana/__init__.py | 1 - src/instana/hooks/hook_uwsgi.py | 54 --------------------------------- 2 files changed, 55 deletions(-) delete mode 100644 src/instana/hooks/hook_uwsgi.py diff --git a/src/instana/__init__.py b/src/instana/__init__.py index f9511537..5d66246e 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -213,7 +213,6 @@ def boot_agent() -> None: # Hooks from instana.hooks import ( hook_gunicorn, # noqa: F401 - hook_uwsgi, # noqa: F401 ) diff --git a/src/instana/hooks/hook_uwsgi.py b/src/instana/hooks/hook_uwsgi.py deleted file mode 100644 index 1995ffeb..00000000 --- a/src/instana/hooks/hook_uwsgi.py +++ /dev/null @@ -1,54 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2019 - -""" -The uwsgi and uwsgidecorators packages are added automatically to the Python environment -when running under uWSGI. Here we attempt to detect the presence of these packages and -then use the appropriate hooks. -""" - -try: - from instana.log import logger - from instana.singletons import agent - - import uwsgi - - logger.debug( - f"uWSGI options: {uwsgi.opt}", - ) - - opt_master = uwsgi.opt.get("master", False) - opt_lazy_apps = uwsgi.opt.get("lazy-apps", False) - - if not uwsgi.opt.get("enable-threads", False) and not uwsgi.opt.get( - "gevent", False - ): - logger.warning( - "Required: Neither uWSGI threads or gevent is enabled. " - + "Please enable by using the uWSGI --enable-threads or --gevent option." - ) - - if opt_master and not opt_lazy_apps: - # --master is supplied in uWSGI options (otherwise uwsgidecorators package won't be available) - # When --lazy-apps is True, this postfork hook isn't needed - import uwsgidecorators - - @uwsgidecorators.postfork - def uwsgi_handle_fork() -> None: - """This is our uWSGI hook to detect and act when worker processes are forked off.""" - logger.debug("Handling uWSGI fork...") - agent.handle_fork() - - logger.debug("Applied uWSGI hooks") - else: - logger.debug( - f"uWSGI --master={opt_master} --lazy-apps={opt_lazy_apps}: postfork hooks not applied" - ) - -except ImportError: - logger.debug( - "uwsgi hooks: decorators not available: likely not running under uWSGI" - ) - -except AttributeError: - logger.debug("uwsgi hooks: Running under uWSGI but decorators not available") From 3c1b5ac2805bdbf98e2e426ca157bd90d138bb81 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 24 Nov 2025 10:47:50 +0100 Subject: [PATCH 40/44] chore: use get_tracer() for all tracer access Signed-off-by: Cagri Yonca --- src/instana/instrumentation/aio_pika.py | 11 +- src/instana/instrumentation/aiohttp/server.py | 3 +- src/instana/instrumentation/asgi.py | 9 +- src/instana/instrumentation/aws/boto3.py | 5 +- src/instana/instrumentation/aws/dynamodb.py | 4 +- src/instana/instrumentation/aws/s3.py | 10 +- src/instana/instrumentation/celery.py | 23 +-- .../instrumentation/django/middleware.py | 9 +- .../instrumentation/google/cloud/pubsub.py | 3 +- src/instana/instrumentation/pika.py | 6 +- src/instana/instrumentation/pyramid.py | 3 +- src/instana/instrumentation/sanic.py | 4 +- src/instana/instrumentation/spyne.py | 26 ++- src/instana/instrumentation/wsgi.py | 5 +- src/instana/util/traceutils.py | 7 +- tests/apps/app_django.py | 4 +- tests/apps/grpc_server/stan_client.py | 44 ++--- tests/clients/boto3/README.md | 3 +- tests/clients/boto3/test_boto3_dynamodb.py | 26 +-- tests/clients/boto3/test_boto3_lambda.py | 46 +++-- tests/clients/boto3/test_boto3_s3.py | 61 +++---- .../boto3/test_boto3_secretsmanager.py | 46 +++-- tests/clients/boto3/test_boto3_ses.py | 46 +++-- tests/clients/boto3/test_boto3_sqs.py | 16 +- tests/clients/kafka/test_confluent_kafka.py | 28 ++-- tests/clients/kafka/test_kafka_python.py | 26 +-- tests/clients/test_aio_pika.py | 17 +- tests/clients/test_aioamqp.py | 14 +- tests/clients/test_cassandra-driver.py | 16 +- tests/clients/test_couchbase.py | 79 ++++----- tests/clients/test_google-cloud-pubsub.py | 13 +- tests/clients/test_google-cloud-storage.py | 74 +++++---- tests/clients/test_httpx.py | 19 ++- tests/clients/test_logging.py | 33 ++-- tests/clients/test_mysqlclient.py | 24 +-- tests/clients/test_pep0249.py | 23 +-- tests/clients/test_pika.py | 12 +- tests/clients/test_psycopg2.py | 26 +-- tests/clients/test_pymongo.py | 20 ++- tests/clients/test_pymysql.py | 29 ++-- tests/clients/test_redis.py | 29 ++-- tests/clients/test_sqlalchemy.py | 13 +- tests/clients/test_urllib3.py | 66 ++++---- tests/collector/test_utils.py | 16 +- tests/frameworks/test_aiohttp_client.py | 35 ++-- tests/frameworks/test_aiohttp_server.py | 38 +++-- tests/frameworks/test_asyncio.py | 14 +- tests/frameworks/test_celery.py | 14 +- tests/frameworks/test_django.py | 42 +++-- tests/frameworks/test_fastapi.py | 50 +++--- tests/frameworks/test_fastapi_middleware.py | 10 +- tests/frameworks/test_flask.py | 131 +++++++++------ tests/frameworks/test_gevent.py | 88 ++++++---- tests/frameworks/test_grpcio.py | 24 +-- tests/frameworks/test_pyramid.py | 24 +-- tests/frameworks/test_sanic.py | 35 ++-- tests/frameworks/test_spyne.py | 38 +++-- tests/frameworks/test_starlette.py | 20 ++- tests/frameworks/test_starlette_middleware.py | 10 +- tests/frameworks/test_tornado_client.py | 116 ++++++++----- tests/frameworks/test_tornado_server.py | 125 +++++++++----- tests/frameworks/test_wsgi.py | 140 +++++++++------- tests/helpers.py | 5 +- tests/util/test_traceutils.py | 157 +++++++++--------- 64 files changed, 1229 insertions(+), 884 deletions(-) diff --git a/src/instana/instrumentation/aio_pika.py b/src/instana/instrumentation/aio_pika.py index a47e09f7..db6b7586 100644 --- a/src/instana/instrumentation/aio_pika.py +++ b/src/instana/instrumentation/aio_pika.py @@ -1,7 +1,7 @@ # (c) Copyright IBM Corp. 2025 try: - import aio_pika + import aio_pika # noqa: F401 import wrapt from typing import ( TYPE_CHECKING, @@ -16,7 +16,7 @@ from instana.log import logger from instana.propagators.format import Format from instana.util.traceutils import get_tracer_tuple, tracing_is_off - from instana.singletons import tracer + from instana.singletons import get_tracer if TYPE_CHECKING: from instana.span.span import InstanaSpan @@ -54,10 +54,8 @@ def _bind_args( **kwargs: object, ) -> Tuple[object, ...]: return (message, routing_key, args, kwargs) - - (message, routing_key, args, kwargs) = _bind_args( - *args, **kwargs - ) + + (message, routing_key, args, kwargs) = _bind_args(*args, **kwargs) with tracer.start_as_current_span( "rabbitmq", span_context=parent_context @@ -102,6 +100,7 @@ async def callback_wrapper( kwargs: Dict[str, Any], ) -> Callable[[Type["AbstractMessage"]], Any]: message = args[0] + tracer = get_tracer() parent_context = tracer.extract( Format.HTTP_HEADERS, message.headers, disable_w3c_trace_context=True ) diff --git a/src/instana/instrumentation/aiohttp/server.py b/src/instana/instrumentation/aiohttp/server.py index ff22ae6b..1cb04b38 100644 --- a/src/instana/instrumentation/aiohttp/server.py +++ b/src/instana/instrumentation/aiohttp/server.py @@ -9,7 +9,7 @@ from instana.log import logger from instana.propagators.format import Format -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers @@ -29,6 +29,7 @@ async def stan_middleware( handler: Callable[..., object], ) -> Awaitable["aiohttp.web.Response"]: try: + tracer = get_tracer() span_context = tracer.extract(Format.HTTP_HEADERS, request.headers) span: "InstanaSpan" = tracer.start_span( "aiohttp-server", span_context=span_context diff --git a/src/instana/instrumentation/asgi.py b/src/instana/instrumentation/asgi.py index 775c4f50..a2df2cce 100644 --- a/src/instana/instrumentation/asgi.py +++ b/src/instana/instrumentation/asgi.py @@ -8,11 +8,10 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind from instana.log import logger from instana.propagators.format import Format -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers @@ -66,6 +65,7 @@ async def __call__( send: Callable[[Dict[str, Any]], Awaitable[None]], ) -> None: request_context = None + tracer = get_tracer() if scope["type"] not in ("http", "websocket"): return await self.app(scope, receive, send) @@ -104,11 +104,14 @@ async def send_wrapper(response: Dict[str, Any]) -> Awaitable[None]: if status_code: if 500 <= int(status_code): current_span.mark_as_errored() - current_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + current_span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, status_code + ) headers = response.get("headers") if headers: extract_custom_headers(current_span, headers) + tracer = get_tracer() tracer.inject(current_span.context, Format.BINARY, headers) except Exception: logger.debug("ASGI send_wrapper error: ", exc_info=True) diff --git a/src/instana/instrumentation/aws/boto3.py b/src/instana/instrumentation/aws/boto3.py index 3350a1ac..a41c7b87 100644 --- a/src/instana/instrumentation/aws/boto3.py +++ b/src/instana/instrumentation/aws/boto3.py @@ -1,4 +1,6 @@ # (c) Copyright IBM Corp. 2025 + + try: from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Tuple, Type @@ -19,7 +21,7 @@ from instana.log import logger from instana.propagators.format import Format - from instana.singletons import tracer + from instana.singletons import get_tracer from instana.span.span import get_current_span from instana.util.traceutils import ( extract_custom_headers, @@ -34,6 +36,7 @@ def lambda_inject_context(payload: Dict[str, Any], span: "InstanaSpan") -> None: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.invoke """ try: + tracer = get_tracer() invoke_payload = payload.get("Payload", {}) if not isinstance(invoke_payload, dict): diff --git a/src/instana/instrumentation/aws/dynamodb.py b/src/instana/instrumentation/aws/dynamodb.py index ef9fe251..bb1e15d2 100644 --- a/src/instana/instrumentation/aws/dynamodb.py +++ b/src/instana/instrumentation/aws/dynamodb.py @@ -1,12 +1,13 @@ # (c) Copyright IBM Corp. 2025 + from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type if TYPE_CHECKING: from botocore.client import BaseClient from instana.log import logger -from instana.singletons import tracer +from instana.singletons import get_tracer from instana.span_context import SpanContext @@ -17,6 +18,7 @@ def create_dynamodb_span( kwargs: Dict[str, Any], parent_context: SpanContext, ) -> None: + tracer = get_tracer() with tracer.start_as_current_span("dynamodb", span_context=parent_context) as span: try: span.set_attribute("dynamodb.op", args[0]) diff --git a/src/instana/instrumentation/aws/s3.py b/src/instana/instrumentation/aws/s3.py index d13b8bff..78ac17fe 100644 --- a/src/instana/instrumentation/aws/s3.py +++ b/src/instana/instrumentation/aws/s3.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + try: from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type @@ -11,7 +12,7 @@ import wrapt from instana.log import logger - from instana.singletons import tracer + from instana.singletons import get_tracer from instana.util.traceutils import ( get_tracer_tuple, tracing_is_off, @@ -31,6 +32,7 @@ def create_s3_span( kwargs: Dict[str, Any], parent_context: SpanContext, ) -> None: + tracer = get_tracer() with tracer.start_as_current_span("s3", span_context=parent_context) as span: try: span.set_attribute("s3.op", args[0]) @@ -66,7 +68,8 @@ def collect_s3_injected_attributes( span.set_attribute("s3.bucket", args[1]) except Exception: logger.debug( - f"collect_s3_injected_attributes collect error: {wrapped.__name__}", exc_info=True + f"collect_s3_injected_attributes collect error: {wrapped.__name__}", + exc_info=True, ) try: @@ -74,7 +77,8 @@ def collect_s3_injected_attributes( except Exception as exc: span.record_exception(exc) logger.debug( - f"collect_s3_injected_attributes error: {wrapped.__name__}", exc_info=True + f"collect_s3_injected_attributes error: {wrapped.__name__}", + exc_info=True, ) raise diff --git a/src/instana/instrumentation/celery.py b/src/instana/instrumentation/celery.py index c69131aa..16589175 100644 --- a/src/instana/instrumentation/celery.py +++ b/src/instana/instrumentation/celery.py @@ -2,20 +2,20 @@ # (c) Copyright Instana Inc. 2020 -import contextvars -from typing import Any, Dict, Tuple -from instana.log import logger -from instana.propagators.format import Format -from instana.singletons import tracer -from instana.span.span import InstanaSpan -from instana.util.traceutils import get_tracer_tuple -from opentelemetry import trace, context - try: - import celery + import celery # noqa: F401 + import contextvars + from typing import Any, Dict, Tuple + from urllib import parse + from celery import registry, signals + from opentelemetry import context, trace - from urllib import parse + from instana.log import logger + from instana.propagators.format import Format + from instana.singletons import get_tracer + from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple client_token: Dict[str, Any] = {} worker_token: Dict[str, Any] = {} @@ -67,6 +67,7 @@ def task_prerun( ) -> None: try: ctx = None + tracer = get_tracer() task = kwargs.get("sender", None) task_id = kwargs.get("task_id", None) diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index 5e5b8419..c73d30e4 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2018 + try: import sys @@ -11,7 +12,7 @@ from typing import TYPE_CHECKING, Dict, Any, Callable, Optional, List, Tuple, Type from instana.log import logger - from instana.singletons import agent, tracer + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format @@ -55,6 +56,7 @@ def __init__( def process_request(self, request: Type["HttpRequest"]) -> None: try: + tracer = get_tracer() env = request.META span_context = tracer.extract(Format.HTTP_HEADERS, env) @@ -81,7 +83,9 @@ def process_request(self, request: Type["HttpRequest"]) -> None: ) request.span.set_attribute("http.params", scrubbed_params) if "HTTP_HOST" in env: - request.span.set_attribute(SpanAttributes.HTTP_HOST, env["HTTP_HOST"]) + request.span.set_attribute( + SpanAttributes.HTTP_HOST, env["HTTP_HOST"] + ) except Exception: logger.debug("Django middleware @ process_request", exc_info=True) @@ -118,6 +122,7 @@ def process_response( extract_custom_headers( request.span, response.headers, format=False ) + tracer = get_tracer() tracer.inject(request.span.context, Format.HTTP_HEADERS, response) except Exception: logger.debug("Instana middleware @ process_response", exc_info=True) diff --git a/src/instana/instrumentation/google/cloud/pubsub.py b/src/instana/instrumentation/google/cloud/pubsub.py index fe4b5424..d2275c83 100644 --- a/src/instana/instrumentation/google/cloud/pubsub.py +++ b/src/instana/instrumentation/google/cloud/pubsub.py @@ -8,7 +8,7 @@ from instana.log import logger from instana.propagators.format import Format -from instana.singletons import tracer +from instana.singletons import get_tracer from instana.util.traceutils import get_tracer_tuple, tracing_is_off if TYPE_CHECKING: @@ -98,6 +98,7 @@ def subscribe_with_instana( def callback_with_instana(message): if message.attributes: + tracer = get_tracer() parent_context = tracer.extract( Format.TEXT_MAP, message.attributes, disable_w3c_trace_context=True ) diff --git a/src/instana/instrumentation/pika.py b/src/instana/instrumentation/pika.py index 9be66182..5fe0736f 100644 --- a/src/instana/instrumentation/pika.py +++ b/src/instana/instrumentation/pika.py @@ -2,6 +2,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 + try: import types from typing import ( @@ -20,7 +21,7 @@ from instana.log import logger from instana.propagators.format import Format - from instana.singletons import tracer + from instana.singletons import get_tracer from instana.util.traceutils import get_tracer_tuple, tracing_is_off if TYPE_CHECKING: @@ -142,6 +143,7 @@ def _cb_wrapper( properties: pika.BasicProperties, body: str, ) -> None: + tracer = get_tracer() parent_context = tracer.extract( Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True ) @@ -189,6 +191,7 @@ def _cb_wrapper( properties: pika.BasicProperties, body: str, ) -> None: + tracer = get_tracer() parent_context = tracer.extract( Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True ) @@ -230,6 +233,7 @@ def _bind_args( (queue, args, kwargs) = _bind_args(*args, **kwargs) def _consume(gen: Iterator[object]) -> object: + tracer = get_tracer() for yielded in gen: # Bypass the delivery created due to inactivity timeout if not yielded or not any(yielded): diff --git a/src/instana/instrumentation/pyramid.py b/src/instana/instrumentation/pyramid.py index a16f4d88..46f1c78e 100644 --- a/src/instana/instrumentation/pyramid.py +++ b/src/instana/instrumentation/pyramid.py @@ -15,7 +15,7 @@ from instana.log import logger from instana.propagators.format import Format - from instana.singletons import agent, tracer + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers @@ -33,6 +33,7 @@ def __init__( self.handler = handler def __call__(self, request: "Request") -> Optional["Response"]: + tracer = get_tracer() ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) with tracer.start_as_current_span("wsgi", span_context=ctx) as span: diff --git a/src/instana/instrumentation/sanic.py b/src/instana/instrumentation/sanic.py index c3c1cac5..8d0537ad 100644 --- a/src/instana/instrumentation/sanic.py +++ b/src/instana/instrumentation/sanic.py @@ -23,7 +23,7 @@ from opentelemetry import context, trace from opentelemetry.semconv.trace import SpanAttributes - from instana.singletons import tracer, agent + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format @@ -44,6 +44,7 @@ def init_with_instana( @app.middleware("request") def request_with_instana(request: Request) -> None: try: + tracer = get_tracer() if "http" not in request.scheme: return @@ -99,6 +100,7 @@ def exception_with_instana(request: Request, exception: Exception) -> None: @app.middleware("response") def response_with_instana(request: Request, response: HTTPResponse) -> None: try: + tracer = get_tracer() if not hasattr(request.ctx, "span"): # pragma: no cover return span = request.ctx.span diff --git a/src/instana/instrumentation/spyne.py b/src/instana/instrumentation/spyne.py index bfb4c83d..6b6055e5 100644 --- a/src/instana/instrumentation/spyne.py +++ b/src/instana/instrumentation/spyne.py @@ -1,14 +1,23 @@ # (c) Copyright IBM Corp. 2025 try: - import spyne + import spyne # noqa: F401 import wrapt - from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple, Iterable, Type, Optional + from typing import ( + TYPE_CHECKING, + Dict, + Any, + Callable, + Tuple, + Iterable, + Type, + Optional, + ) from types import SimpleNamespace from instana.log import logger - from instana.singletons import agent, tracer + from instana.singletons import agent, get_tracer from instana.propagators.format import Format from instana.util.secrets import strip_secrets_from_query @@ -32,13 +41,14 @@ def set_span_attributes(span: "InstanaSpan", headers: Dict[str, Any]) -> None: if "SERVER_PORT" in headers: span.set_attribute("rpc.port", headers["SERVER_PORT"]) - def record_error(span: "InstanaSpan", response_string: str, error: Optional[Type[Exception]]) -> None: + def record_error( + span: "InstanaSpan", response_string: str, error: Optional[Type[Exception]] + ) -> None: resp_code = int(response_string.split()[0]) if 500 <= resp_code: span.record_exception(error) - @wrapt.patch_function_wrapper("spyne.server.wsgi", "WsgiApplication.handle_error") def handle_error_with_instana( wrapped: Callable[..., Iterable[object]], @@ -47,6 +57,7 @@ def handle_error_with_instana( kwargs: Dict[str, Any], ) -> Iterable[object]: ctx = args[0] + tracer = get_tracer() # span created inside process_request() will be handled by finalize() method if ctx.udc and ctx.udc.span: @@ -55,7 +66,9 @@ def handle_error_with_instana( headers = ctx.transport.req_env span_context = tracer.extract(Format.HTTP_HEADERS, headers) - with tracer.start_as_current_span("rpc-server", span_context=span_context) as span: + with tracer.start_as_current_span( + "rpc-server", span_context=span_context + ) as span: set_span_attributes(span, headers) response_headers = ctx.transport.resp_headers @@ -96,6 +109,7 @@ def process_request_with_instana( kwargs: Dict[str, Any], ) -> None: ctx = args[0] + tracer = get_tracer() headers = ctx.transport.req_env span_context = tracer.extract(Format.HTTP_HEADERS, headers) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index ea020495..63798e89 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -11,13 +11,14 @@ from opentelemetry import context, trace from instana.propagators.format import Format -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers if TYPE_CHECKING: from instana.span.span import InstanaSpan + class InstanaWSGIMiddleware(object): """Instana WSGI middleware""" @@ -26,6 +27,7 @@ def __init__(self, app: object) -> None: def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ + tracer = get_tracer() # Extract context and start span span_context = tracer.extract(Format.HTTP_HEADERS, env) @@ -99,6 +101,7 @@ def _end_span_after_iterating( if token: context.detach(token) + def _set_request_attributes(span: "InstanaSpan", env: Dict[str, Any]) -> None: if "PATH_INFO" in env: span.set_attribute("http.path", env["PATH_INFO"]) diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index d0a3af23..6ea17bff 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 + from typing import ( Optional, Tuple, @@ -13,7 +14,7 @@ ) from instana.log import logger -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span if TYPE_CHECKING: @@ -68,7 +69,7 @@ def get_active_tracer() -> Optional["InstanaTracer"]: if current_span: # asyncio Spans are used as NonRecording Spans solely for context propagation if current_span.is_recording() or current_span.name == "asyncio": - return tracer + return get_tracer() return None return None except Exception: @@ -90,7 +91,7 @@ def get_tracer_tuple() -> ( if active_tracer: return (active_tracer, current_span, current_span.name) elif agent.options.allow_exit_as_root: - return (tracer, None, None) + return (get_tracer(), None, None) return (None, None, None) diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 5e7227ac..545abc96 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -4,6 +4,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2018 + import os import sys import time @@ -17,7 +18,7 @@ from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind -from instana.singletons import tracer +from instana.singletons import get_tracer filepath, extension = os.path.splitext(__file__) os.environ["DJANGO_SETTINGS_MODULE"] = os.path.basename(filepath) @@ -110,6 +111,7 @@ def not_found(request): def complex(request): + tracer = get_tracer() with tracer.start_as_current_span("asteroid") as pspan: pspan.set_attribute("component", "Python simple example app") pspan.set_attribute("span.kind", SpanKind.CLIENT) diff --git a/tests/apps/grpc_server/stan_client.py b/tests/apps/grpc_server/stan_client.py index 450d62eb..4b10355b 100644 --- a/tests/apps/grpc_server/stan_client.py +++ b/tests/apps/grpc_server/stan_client.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 + import time import random @@ -8,7 +9,7 @@ import stan_pb2 import stan_pb2_grpc -from instana.singletons import tracer +from instana.singletons import get_tracer testenv = dict() testenv["grpc_port"] = 10814 @@ -17,14 +18,14 @@ def generate_questions(): - """ Used in the streaming grpc tests """ + """Used in the streaming grpc tests""" questions = [ stan_pb2.QuestionRequest(question="Are you there?"), stan_pb2.QuestionRequest(question="What time is it?"), stan_pb2.QuestionRequest(question="Where in the world is Waldo?"), stan_pb2.QuestionRequest(question="What did one campfire say to the other?"), stan_pb2.QuestionRequest(question="Is cereal soup?"), - stan_pb2.QuestionRequest(question="What is always coming, but never arrives?") + stan_pb2.QuestionRequest(question="What is always coming, but never arrives?"), ] for q in questions: yield q @@ -36,20 +37,25 @@ def generate_questions(): # The grpc client apparently needs a second to connect and initialize time.sleep(1) -with tracer.start_active_span('http-server') as scope: - scope.span.set_tag('http.url', 'https://localhost:8080/grpc-client') - scope.span.set_tag('http.method', 'GET') - scope.span.set_tag('span.kind', 'entry') - response = server_stub.OneQuestionOneResponse(stan_pb2.QuestionRequest(question="Are you there?")) - -with tracer.start_active_span('http-server') as scope: - scope.span.set_tag('http.url', 'https://localhost:8080/grpc-server-streaming') - scope.span.set_tag('http.method', 'GET') - scope.span.set_tag('span.kind', 'entry') - responses = server_stub.OneQuestionManyResponses(stan_pb2.QuestionRequest(question="Are you there?")) - -with tracer.start_active_span('http-server') as scope: - scope.span.set_tag('http.url', 'https://localhost:8080/grpc-client-streaming') - scope.span.set_tag('http.method', 'GET') - scope.span.set_tag('span.kind', 'entry') +tracer = get_tracer() +with tracer.start_active_span("http-server") as scope: + scope.span.set_tag("http.url", "https://localhost:8080/grpc-client") + scope.span.set_tag("http.method", "GET") + scope.span.set_tag("span.kind", "entry") + response = server_stub.OneQuestionOneResponse( + stan_pb2.QuestionRequest(question="Are you there?") + ) + +with tracer.start_active_span("http-server") as scope: + scope.span.set_tag("http.url", "https://localhost:8080/grpc-server-streaming") + scope.span.set_tag("http.method", "GET") + scope.span.set_tag("span.kind", "entry") + responses = server_stub.OneQuestionManyResponses( + stan_pb2.QuestionRequest(question="Are you there?") + ) + +with tracer.start_active_span("http-server") as scope: + scope.span.set_tag("http.url", "https://localhost:8080/grpc-client-streaming") + scope.span.set_tag("http.method", "GET") + scope.span.set_tag("span.kind", "entry") response = server_stub.ManyQuestionsOneResponse(generate_questions()) diff --git a/tests/clients/boto3/README.md b/tests/clients/boto3/README.md index 33c9a199..00e551e4 100644 --- a/tests/clients/boto3/README.md +++ b/tests/clients/boto3/README.md @@ -10,9 +10,10 @@ from opentelemetry.trace import SpanKind from moto import mock_aws import tests.apps.flask_app from tests.helpers import testenv -from instana.singletons import tracer +from instana.singletons import get_tracer http_client = urllib3.PoolManager() +tracer = get_tracer() @mock_aws def test_app_boto3_sqs(): diff --git a/tests/clients/boto3/test_boto3_dynamodb.py b/tests/clients/boto3/test_boto3_dynamodb.py index 55f09df6..fad69d66 100644 --- a/tests/clients/boto3/test_boto3_dynamodb.py +++ b/tests/clients/boto3/test_boto3_dynamodb.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os from typing import Generator @@ -9,14 +10,15 @@ from moto import mock_aws from instana.options import StandardOptions -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter class TestDynamoDB: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() @@ -37,7 +39,7 @@ def test_vanilla_create_table(self) -> None: assert result["TableNames"][0] == "dynamodb-table" def test_dynamodb_create_table(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.create_table( TableName="dynamodb-table", KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], @@ -73,7 +75,7 @@ def test_ignore_dynamodb(self) -> None: os.environ["INSTANA_IGNORE_ENDPOINTS"] = "dynamodb" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.create_table( TableName="dynamodb-table", KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], @@ -97,7 +99,7 @@ def test_ignore_create_table(self) -> None: os.environ["INSTANA_IGNORE_ENDPOINTS"] = "dynamodb:createtable" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.create_table( TableName="dynamodb-table", KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], @@ -144,7 +146,7 @@ def test_dynamodb_create_table_as_root_exit_span(self) -> None: assert dynamodb_span.data["dynamodb"]["table"] == "dynamodb-table" def test_dynamodb_list_tables(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.dynamodb.list_tables() assert len(result["TableNames"]) == 0 @@ -177,7 +179,7 @@ def test_dynamodb_put_item(self) -> None: AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.put_item( TableName="dynamodb-table", Item={"id": {"S": "1"}, "name": {"S": "John"}}, @@ -216,7 +218,7 @@ def test_dynamodb_scan(self) -> None: TableName="dynamodb-table", Item=test_item, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.dynamodb.scan(TableName="dynamodb-table") assert result["Items"] == [test_item] @@ -255,7 +257,7 @@ def test_dynamodb_get_item(self) -> None: TableName="dynamodb-table", Item=test_item, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.dynamodb.get_item( TableName="dynamodb-table", Key={"id": {"S": "1"}} ) @@ -296,7 +298,7 @@ def test_dynamodb_update_item(self) -> None: TableName="dynamodb-table", Item=test_item, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.update_item( TableName="dynamodb-table", Key={"id": {"S": "1"}}, # Specify the key @@ -339,7 +341,7 @@ def test_dynamodb_delete_item(self) -> None: TableName="dynamodb-table", Item=test_item, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.delete_item( TableName="dynamodb-table", Key={"id": {"S": "1"}} ) @@ -380,7 +382,7 @@ def test_dynamodb_query_item(self) -> None: self.dynamodb.put_item( TableName="dynamodb-table", Item={"id": {"S": "2"}, "name": {"S": "Jack"}} ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.dynamodb.query( TableName="dynamodb-table", KeyConditionExpression="id = :pk_val", diff --git a/tests/clients/boto3/test_boto3_lambda.py b/tests/clients/boto3/test_boto3_lambda.py index 78117804..6800dab5 100644 --- a/tests/clients/boto3/test_boto3_lambda.py +++ b/tests/clients/boto3/test_boto3_lambda.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import pytest import json from typing import Generator import boto3 from moto import mock_aws -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter @@ -16,7 +17,8 @@ class TestLambda: def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws(config={"lambda": {"use_docker": False}}) self.mock.start() @@ -29,7 +31,7 @@ def _resource(self) -> Generator[None, None, None]: agent.options.allow_exit_as_root = False def test_lambda_invoke(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.aws_lambda.invoke( FunctionName=self.function_name, Payload=json.dumps({"message": "success"}), @@ -43,11 +45,15 @@ def test_lambda_invoke(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -114,7 +120,7 @@ def add_custom_header_before_call(params, **kwargs): "before-call.lambda.Invoke", add_custom_header_before_call ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.aws_lambda.invoke( FunctionName=self.function_name, Payload=json.dumps({"message": "success"}), @@ -128,11 +134,15 @@ def add_custom_header_before_call(params, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -178,7 +188,7 @@ def add_custom_header_before_sign(request, **kwargs): "before-sign.lambda.Invoke", add_custom_header_before_sign ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.aws_lambda.invoke( FunctionName=self.function_name, Payload=json.dumps({"message": "success"}), @@ -192,11 +202,15 @@ def add_custom_header_before_sign(request, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -242,7 +256,7 @@ def modify_after_call_args(parsed, **kwargs): # Register the function to an event event_system.register("after-call.lambda.Invoke", modify_after_call_args) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.aws_lambda.invoke( FunctionName=self.function_name, Payload=json.dumps({"message": "success"}), @@ -256,11 +270,15 @@ def modify_after_call_args(parsed, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span diff --git a/tests/clients/boto3/test_boto3_s3.py b/tests/clients/boto3/test_boto3_s3.py index d20b51cd..b0c23ea2 100644 --- a/tests/clients/boto3/test_boto3_s3.py +++ b/tests/clients/boto3/test_boto3_s3.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os from io import BytesIO @@ -9,7 +10,7 @@ from typing import Generator from moto import mock_aws -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) @@ -24,7 +25,8 @@ class TestS3: def setup_class(cls) -> None: cls.bucket_name = "aws_bucket_name" cls.object_name = "aws_key_name" - cls.recorder = tracer.span_processor + cls.tracer = get_tracer() + cls.recorder = cls.tracer.span_processor cls.mock = mock_aws() @pytest.fixture(autouse=True) @@ -47,7 +49,7 @@ def test_vanilla_create_bucket(self) -> None: assert result["Buckets"][0]["Name"] == self.bucket_name def test_s3_create_bucket(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket=self.bucket_name) result = self.s3.list_buckets() @@ -93,7 +95,7 @@ def test_s3_create_bucket_as_root_exit_span(self) -> None: assert s3_span.data["s3"]["bucket"] == self.bucket_name def test_s3_list_buckets(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.s3.list_buckets() assert len(result["Buckets"]) == 0 @@ -121,13 +123,15 @@ def test_s3_list_buckets(self) -> None: def test_s3_vanilla_upload_file(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) - result = self.s3.upload_file(upload_filename, self.bucket_name, self.object_name) + result = self.s3.upload_file( + upload_filename, self.bucket_name, self.object_name + ) assert not result def test_s3_upload_file(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.s3.upload_file(upload_filename, self.bucket_name, self.object_name) spans = self.recorder.queued_spans() @@ -153,7 +157,7 @@ def test_s3_upload_file(self) -> None: def test_s3_upload_file_obj(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with open(upload_filename, "rb") as fd: self.s3.upload_fileobj(fd, self.bucket_name, self.object_name) @@ -181,8 +185,10 @@ def test_s3_download_file(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) self.s3.upload_file(upload_filename, self.bucket_name, self.object_name) - with tracer.start_as_current_span("test"): - self.s3.download_file(self.bucket_name, self.object_name, download_target_filename) + with self.tracer.start_as_current_span("test"): + self.s3.download_file( + self.bucket_name, self.object_name, download_target_filename + ) spans = self.recorder.queued_spans() assert len(spans) == 2 @@ -208,7 +214,7 @@ def test_s3_download_file_obj(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) self.s3.upload_file(upload_filename, self.bucket_name, self.object_name) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with open(download_target_filename, "wb") as fd: self.s3.download_fileobj(self.bucket_name, self.object_name, fd) @@ -235,7 +241,7 @@ def test_s3_download_file_obj(self) -> None: def test_s3_list_obj(self) -> None: self.s3.create_bucket(Bucket=self.bucket_name) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.s3.list_objects(Bucket=self.bucket_name) spans = self.recorder.queued_spans() @@ -263,42 +269,41 @@ def test_s3_resource_bucket_upload_fileobj(self) -> None: Verify boto3.resource().Bucket().upload_fileobj() works correctly with BytesIO objects """ test_data = b"somedata" - + # Create a bucket using the client first self.s3.create_bucket(Bucket=self.bucket_name) - - s3_resource = boto3.resource( - "s3", - region_name="us-east-1" - ) + + s3_resource = boto3.resource("s3", region_name="us-east-1") bucket = s3_resource.Bucket(name=self.bucket_name) - - with tracer.start_as_current_span("test"): + + with self.tracer.start_as_current_span("test"): bucket.upload_fileobj(BytesIO(test_data), self.object_name) - + # Verify the upload was successful by retrieving the object response = bucket.Object(self.object_name).get() file_content = response["Body"].read() - + # Assert the content matches what we uploaded assert file_content == test_data - + # Verify the spans were created correctly spans = self.recorder.queued_spans() assert len(spans) >= 2 - + filter = lambda span: span.n == "sdk" # noqa: E731 test_span = get_first_span_by_filter(spans, filter) assert test_span - - filter = lambda span: span.n == "s3" and span.data["s3"]["op"] == "UploadFileObj" # noqa: E731 + + def filter(span): + return span.n == "s3" and span.data["s3"]["op"] == "UploadFileObj" # noqa: E731 + s3_span = get_first_span_by_filter(spans, filter) assert s3_span - + assert s3_span.t == test_span.t assert s3_span.p == test_span.s - + assert not test_span.ec assert not s3_span.ec - + assert s3_span.data["s3"]["bucket"] == self.bucket_name diff --git a/tests/clients/boto3/test_boto3_secretsmanager.py b/tests/clients/boto3/test_boto3_secretsmanager.py index e8a715fc..7f5896ff 100644 --- a/tests/clients/boto3/test_boto3_secretsmanager.py +++ b/tests/clients/boto3/test_boto3_secretsmanager.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os import boto3 import pytest from typing import Generator from moto import mock_aws -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) @@ -18,7 +19,8 @@ class TestSecretsManager: def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() @@ -43,7 +45,7 @@ def test_get_secret_value(self) -> None: assert response["Name"] == secret_id - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) assert result["Name"] == secret_id @@ -51,11 +53,15 @@ def test_get_secret_value(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -147,7 +153,7 @@ def add_custom_header_before_call(params, **kwargs): "before-call.secrets-manager.GetSecretValue", add_custom_header_before_call ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) assert result["Name"] == secret_id @@ -155,11 +161,15 @@ def add_custom_header_before_call(params, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -219,7 +229,7 @@ def add_custom_header_before_sign(request, **kwargs): "before-sign.secrets-manager.GetSecretValue", add_custom_header_before_sign ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) assert result["Name"] == secret_id @@ -227,11 +237,15 @@ def add_custom_header_before_sign(request, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -293,7 +307,7 @@ def modify_after_call_args(parsed, **kwargs): "after-call.secrets-manager.GetSecretValue", modify_after_call_args ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) assert result["Name"] == secret_id @@ -301,11 +315,15 @@ def modify_after_call_args(parsed, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span diff --git a/tests/clients/boto3/test_boto3_ses.py b/tests/clients/boto3/test_boto3_ses.py index afea6b0e..00352b96 100644 --- a/tests/clients/boto3/test_boto3_ses.py +++ b/tests/clients/boto3/test_boto3_ses.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os import boto3 import pytest from typing import Generator from moto import mock_aws -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) @@ -18,7 +19,8 @@ class TestSes: def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() @@ -34,7 +36,7 @@ def test_vanilla_verify_email(self) -> None: assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 def test_verify_email(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.ses.verify_email_identity( EmailAddress="pglombardo+instana299@tuta.io" ) @@ -44,11 +46,15 @@ def test_verify_email(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -119,7 +125,7 @@ def add_custom_header_before_call(params, **kwargs): "before-call.ses.VerifyEmailIdentity", add_custom_header_before_call ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.ses.verify_email_identity( EmailAddress="pglombardo+instana299@tuta.io" ) @@ -129,11 +135,15 @@ def add_custom_header_before_call(params, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -182,7 +192,7 @@ def add_custom_header_before_sign(request, **kwargs): "before-sign.ses.VerifyEmailIdentity", add_custom_header_before_sign ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.ses.verify_email_identity( EmailAddress="pglombardo+instana299@tuta.io" ) @@ -192,11 +202,15 @@ def add_custom_header_before_sign(request, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span @@ -247,7 +261,7 @@ def modify_after_call_args(parsed, **kwargs): "after-call.ses.VerifyEmailIdentity", modify_after_call_args ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): result = self.ses.verify_email_identity( EmailAddress="pglombardo+instana299@tuta.io" ) @@ -257,11 +271,15 @@ def modify_after_call_args(parsed, **kwargs): spans = self.recorder.queued_spans() assert len(spans) == 2 - filter = lambda span: span.n == "sdk" + def filter(span): + return span.n == "sdk" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "boto3" + def filter(span): + return span.n == "boto3" + boto_span = get_first_span_by_filter(spans, filter) assert boto_span diff --git a/tests/clients/boto3/test_boto3_sqs.py b/tests/clients/boto3/test_boto3_sqs.py index ec0c5578..cc6821c8 100644 --- a/tests/clients/boto3/test_boto3_sqs.py +++ b/tests/clients/boto3/test_boto3_sqs.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os import boto3 import pytest @@ -10,7 +11,7 @@ from moto import mock_aws import tests.apps.flask_app # noqa: F401 -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_filter, get_first_span_by_name, testenv pwd = os.path.dirname(os.path.abspath(__file__)) @@ -21,7 +22,8 @@ class TestSqs: def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() @@ -49,7 +51,7 @@ def test_send_message(self) -> None: assert response["QueueUrl"] queue_url = response["QueueUrl"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -166,7 +168,7 @@ def test_send_message_as_root_exit_span(self) -> None: ) def test_app_boto3_sqs(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.http_client.request("GET", testenv["flask_server"] + "/boto3/sqs") spans = self.recorder.queued_spans() @@ -238,7 +240,7 @@ def add_custom_header_before_call(params, **kwargs): ) queue_url = response["QueueUrl"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -329,7 +331,7 @@ def add_custom_header_before_sign(request, **kwargs): ) queue_url = response["QueueUrl"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -420,7 +422,7 @@ def modify_after_call_args(parsed, **kwargs): event_system.register("after-call.sqs.SendMessage", modify_after_call_args) queue_url = response["QueueUrl"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, diff --git a/tests/clients/kafka/test_confluent_kafka.py b/tests/clients/kafka/test_confluent_kafka.py index a5c9b334..36538566 100644 --- a/tests/clients/kafka/test_confluent_kafka.py +++ b/tests/clients/kafka/test_confluent_kafka.py @@ -1,5 +1,6 @@ # (c) Copyright IBM Corp. 2025 + import os import time from typing import Generator @@ -21,7 +22,7 @@ trace_kafka_close, ) from instana.options import StandardOptions -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import InstanaSpan from instana.util.config import parse_ignored_endpoints_from_yaml from tests.helpers import get_first_span_by_filter, testenv @@ -33,7 +34,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # Kafka admin client @@ -91,7 +93,7 @@ def _resource(self) -> Generator[None, None, None]: time.sleep(3) def test_trace_confluent_kafka_produce(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.producer.produce(testenv["kafka_topic"], b"raw_bytes") self.producer.flush(timeout=10) @@ -118,7 +120,7 @@ def test_trace_confluent_kafka_produce(self) -> None: def test_trace_confluent_kafka_produce_with_keyword_topic(self) -> None: """Test that tracing works when topic is passed as a keyword argument.""" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): # Pass topic as a keyword argument self.producer.produce(topic=testenv["kafka_topic"], value=b"raw_bytes") self.producer.flush(timeout=10) @@ -146,7 +148,7 @@ def test_trace_confluent_kafka_produce_with_keyword_topic(self) -> None: def test_trace_confluent_kafka_produce_with_keyword_args(self) -> None: """Test that tracing works when both topic and headers are passed as keyword arguments.""" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): # Pass both topic and headers as keyword arguments self.producer.produce( topic=testenv["kafka_topic"], @@ -190,7 +192,7 @@ def test_trace_confluent_kafka_consume(self) -> None: consumer = Consumer(consumer_config) consumer.subscribe([testenv["kafka_topic"]]) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): msgs = consumer.consume(num_messages=1, timeout=60) # noqa: F841 consumer.close() @@ -212,7 +214,7 @@ def test_trace_confluent_kafka_poll(self) -> None: consumer = Consumer(consumer_config) consumer.subscribe([testenv["kafka_topic"]]) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): msg = consumer.poll(timeout=3) # noqa: F841 consumer.close() @@ -250,7 +252,7 @@ def test_trace_confluent_kafka_error(self) -> None: consumer = Consumer(consumer_config) consumer.subscribe(["inexistent_kafka_topic"]) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): consumer.consume(-10) consumer.close() @@ -283,7 +285,7 @@ def test_trace_confluent_kafka_error(self) -> None: @patch.dict(os.environ, {"INSTANA_IGNORE_ENDPOINTS": "kafka"}) def test_ignore_confluent_kafka(self) -> None: agent.options.set_trace_configurations() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.producer.produce(testenv["kafka_topic"], b"raw_bytes") self.producer.flush(timeout=10) @@ -296,7 +298,7 @@ def test_ignore_confluent_kafka(self) -> None: @patch.dict(os.environ, {"INSTANA_IGNORE_ENDPOINTS": "kafka:produce"}) def test_ignore_confluent_kafka_producer(self) -> None: agent.options.set_trace_configurations() - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Produce some events self.producer.produce(testenv["kafka_topic"], b"raw_bytes1") self.producer.produce(testenv["kafka_topic"], b"raw_bytes2") @@ -327,7 +329,7 @@ def test_ignore_confluent_kafka_consumer(self) -> None: self.producer.produce(testenv["kafka_topic"], b"raw_bytes2") self.producer.flush() - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Consume the events consumer_config = self.kafka_config.copy() consumer_config["group.id"] = "my-group" @@ -360,7 +362,7 @@ def test_ignore_confluent_specific_topic(self) -> None: ] ) - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Produce some events self.producer.produce(testenv["kafka_topic"], b"raw_bytes1") self.producer.produce(testenv["kafka_topic"] + "_1", b"raw_bytes1") @@ -401,7 +403,7 @@ def test_ignore_confluent_specific_topic_with_config_file(self) -> None: "tests/util/test_configuration-1.yaml" ) - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Produce some events self.producer.produce(testenv["kafka_topic"], b"raw_bytes1") self.producer.flush() diff --git a/tests/clients/kafka/test_kafka_python.py b/tests/clients/kafka/test_kafka_python.py index a1d0ccbb..99cb8f2d 100644 --- a/tests/clients/kafka/test_kafka_python.py +++ b/tests/clients/kafka/test_kafka_python.py @@ -1,5 +1,6 @@ # (c) Copyright IBM Corp. 2025 + import os from typing import Generator @@ -20,7 +21,7 @@ save_consumer_span_into_context, ) from instana.options import StandardOptions -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import InstanaSpan from instana.util.config import parse_ignored_endpoints_from_yaml from tests.helpers import get_first_span_by_filter, testenv @@ -32,7 +33,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # Kafka admin client @@ -95,7 +97,7 @@ def _resource(self) -> Generator[None, None, None]: self.kafka_client.close() def test_trace_kafka_python_send(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): future = self.producer.send(testenv["kafka_topic"], b"raw_bytes") _ = future.get(timeout=10) # noqa: F841 @@ -123,7 +125,7 @@ def test_trace_kafka_python_send(self) -> None: def test_trace_kafka_python_send_with_keyword_topic(self) -> None: """Test that tracing works when topic is passed as a keyword argument.""" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): # Pass topic as a keyword argument future = self.producer.send( topic=testenv["kafka_topic"], value=b"raw_bytes" @@ -154,7 +156,7 @@ def test_trace_kafka_python_send_with_keyword_topic(self) -> None: def test_trace_kafka_python_send_with_keyword_args(self) -> None: """Test that tracing works when both topic and headers are passed as keyword arguments.""" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): # Pass both topic and headers as keyword arguments future = self.producer.send( topic=testenv["kafka_topic"], @@ -200,7 +202,7 @@ def test_trace_kafka_python_consume(self) -> None: consumer_timeout_ms=1000, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): for msg in consumer: if msg is None: break @@ -250,7 +252,7 @@ def test_trace_kafka_python_poll(self) -> None: consumer_timeout_ms=1000, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): msg = consumer.poll(timeout_ms=3000) # noqa: F841 consumer.close() @@ -292,7 +294,7 @@ def test_trace_kafka_python_error(self) -> None: consumer_timeout_ms=1000, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): consumer._client = None try: @@ -342,7 +344,7 @@ def consume_from_topic(self, topic_name: str) -> None: enable_auto_commit=False, consumer_timeout_ms=1000, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): for msg in consumer: if msg is None: break @@ -352,7 +354,7 @@ def consume_from_topic(self, topic_name: str) -> None: @patch.dict(os.environ, {"INSTANA_IGNORE_ENDPOINTS": "kafka"}) def test_ignore_kafka(self) -> None: agent.options.set_trace_configurations() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.producer.send(testenv["kafka_topic"], b"raw_bytes") self.producer.flush() @@ -365,7 +367,7 @@ def test_ignore_kafka(self) -> None: @patch.dict(os.environ, {"INSTANA_IGNORE_ENDPOINTS": "kafka:send"}) def test_ignore_kafka_producer(self) -> None: agent.options.set_trace_configurations() - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Produce some events self.producer.send(testenv["kafka_topic"], b"raw_bytes1") self.producer.send(testenv["kafka_topic"], b"raw_bytes2") @@ -414,7 +416,7 @@ def test_ignore_kafka_consumer(self) -> None: ) def test_ignore_specific_topic(self) -> None: agent.options.set_trace_configurations() - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): # Produce some events self.producer.send(testenv["kafka_topic"], b"raw_bytes1") self.producer.send(testenv["kafka_topic"] + "_1", b"raw_bytes1") diff --git a/tests/clients/test_aio_pika.py b/tests/clients/test_aio_pika.py index 20e97618..6d2102c6 100644 --- a/tests/clients/test_aio_pika.py +++ b/tests/clients/test_aio_pika.py @@ -5,7 +5,7 @@ import asyncio from aio_pika import Message, connect, connect_robust -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer if TYPE_CHECKING: from instana.span.readable_span import ReadableSpan @@ -16,7 +16,8 @@ class TestAioPika: def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.loop = asyncio.new_event_loop() @@ -58,7 +59,7 @@ async def publish_message(self, params_combination: str = "both_args") -> None: kwargs = {"routing_key": queue_name} elif params_combination == "arg_kwarg_empty_key": args = (message,) - kwargs = {"routing_key": ""} + kwargs = {"routing_key": ""} else: # params_combination == "both_args" args = (message, queue_name) @@ -105,7 +106,9 @@ async def on_message(msg): await queue.consume(on_message) await asyncio.sleep(1) # Wait to ensure the message is processed - def assert_span_info(self, rabbitmq_span: "ReadableSpan", sort: str, key: str = "test.queue") -> None: + def assert_span_info( + self, rabbitmq_span: "ReadableSpan", sort: str, key: str = "test.queue" + ) -> None: assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" assert rabbitmq_span.data["rabbitmq"]["sort"] == sort assert rabbitmq_span.data["rabbitmq"]["address"] @@ -119,7 +122,7 @@ def assert_span_info(self, rabbitmq_span: "ReadableSpan", sort: str, key: str = ["both_args", "both_kwargs", "arg_kwarg"], ) def test_basic_publish(self, params_combination) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.loop.run_until_complete(self.publish_message(params_combination)) spans = self.recorder.queued_spans() @@ -165,7 +168,7 @@ def test_basic_publish_as_root_exit_span(self) -> None: [connect, connect_robust], ) def test_basic_consume(self, connect_method) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.loop.run_until_complete(self.publish_message()) self.loop.run_until_complete(self.consume_message(connect_method)) @@ -198,7 +201,7 @@ def test_basic_consume(self, connect_method) -> None: [connect, connect_robust], ) def test_consume_with_exception(self, connect_method) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.loop.run_until_complete(self.publish_message()) self.loop.run_until_complete(self.consume_with_exception(connect_method)) diff --git a/tests/clients/test_aioamqp.py b/tests/clients/test_aioamqp.py index 7afa04b9..960190b0 100644 --- a/tests/clients/test_aioamqp.py +++ b/tests/clients/test_aioamqp.py @@ -1,10 +1,13 @@ +# (c) Copyright IBM Corp. 2025 + + import asyncio from typing import Any, Generator import aioamqp import pytest -from instana.singletons import tracer +from instana.singletons import get_tracer from tests.helpers import testenv from aioamqp.properties import Properties from aioamqp.envelope import Envelope @@ -16,7 +19,8 @@ class TestAioamqp: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.loop = asyncio.new_event_loop() @@ -59,7 +63,7 @@ async def callback( envelope: Envelope, properties: Properties, ) -> None: - with tracer.start_as_current_span("callback-span"): + with self.tracer.start_as_current_span("callback-span"): await channel.basic_client_ack(delivery_tag=envelope.delivery_tag) _, protocol = await aioamqp.connect( @@ -70,7 +74,7 @@ async def callback( await channel.basic_consume(callback, queue_name="message_queue", no_ack=False) def test_basic_publish(self) -> None: - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): self.loop.run_until_complete(self.publish_message()) spans = self.recorder.queued_spans() @@ -90,7 +94,7 @@ def test_basic_publish(self) -> None: assert not test_span.p def test_basic_consumer(self) -> None: - with tracer.start_as_current_span("test-span"): + with self.tracer.start_as_current_span("test-span"): self.loop.run_until_complete(self.publish_message()) self.loop.run_until_complete(self.consume_message()) diff --git a/tests/clients/test_cassandra-driver.py b/tests/clients/test_cassandra-driver.py index 07945259..b433b578 100644 --- a/tests/clients/test_cassandra-driver.py +++ b/tests/clients/test_cassandra-driver.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import random import time from typing import Generator @@ -10,7 +11,7 @@ from cassandra.cluster import Cluster, ResultSet from cassandra.query import SimpleStatement -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from tests.helpers import get_first_span_by_name, testenv cluster = Cluster([testenv["cassandra_host"]], load_balancing_policy=None) @@ -35,7 +36,8 @@ class TestCassandra: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield agent.options.allow_exit_as_root = False @@ -66,7 +68,7 @@ def test_untraced_execute_error(self) -> None: def test_execute(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = session.execute("SELECT name, age, email FROM users") assert isinstance(res, ResultSet) @@ -125,7 +127,7 @@ def test_execute_as_root_exit_span(self) -> None: def test_execute_async(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = session.execute_async("SELECT name, age, email FROM users").result() assert isinstance(res, ResultSet) @@ -158,7 +160,7 @@ def test_execute_async(self) -> None: def test_simple_statement(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): query = SimpleStatement( "SELECT name, age, email FROM users", is_idempotent=True ) @@ -196,7 +198,7 @@ def test_execute_error(self) -> None: res = None try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = session.execute("Not a real query") except Exception: pass @@ -232,7 +234,7 @@ def test_execute_error(self) -> None: def test_prepared_statement(self) -> None: prepared = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): prepared = session.prepare( "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" ) diff --git a/tests/clients/test_couchbase.py b/tests/clients/test_couchbase.py index 9064fb06..d941b656 100644 --- a/tests/clients/test_couchbase.py +++ b/tests/clients/test_couchbase.py @@ -1,14 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os + import time from typing import Generator from unittest.mock import patch import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter from couchbase.admin import Admin @@ -43,7 +43,8 @@ class TestStandardCouchDB: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.cluster = Cluster("couchbase://%s" % testenv["couchdb_host"]) self.bucket = Bucket( "couchbase://%s/travel-sample" % testenv["couchdb_host"], @@ -62,7 +63,7 @@ def test_vanilla_get(self) -> None: def test_upsert(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.upsert("test_upsert", 1) assert res @@ -122,7 +123,7 @@ def test_upsert_multi(self) -> None: kvs["first_test_upsert_multi"] = 1 kvs["second_test_upsert_multi"] = 1 - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.upsert_multi(kvs) assert res @@ -159,7 +160,7 @@ def test_insert_new(self) -> None: except NotFoundError: pass - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert_new", 1) assert res @@ -196,7 +197,7 @@ def test_insert_existing(self) -> None: pass try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert", 1) except KeyExistsError: pass @@ -242,7 +243,7 @@ def test_insert_multi(self) -> None: except NotFoundError: pass - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.insert_multi(kvs) assert res @@ -279,7 +280,7 @@ def test_replace(self) -> None: except KeyExistsError: pass - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) assert res @@ -317,7 +318,7 @@ def test_replace_non_existent(self) -> None: pass try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) except NotFoundError: pass @@ -360,7 +361,7 @@ def test_replace_multi(self) -> None: self.bucket.upsert("first_test_replace_multi", "one") self.bucket.upsert("second_test_replace_multi", "two") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.replace_multi(kvs) assert res @@ -394,7 +395,7 @@ def test_append(self) -> None: self.bucket.upsert("test_append", "one") res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.append("test_append", "two") assert res @@ -433,7 +434,7 @@ def test_append_multi(self) -> None: self.bucket.upsert("first_test_append_multi", "one") self.bucket.upsert("second_test_append_multi", "two") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.append_multi(kvs) assert res @@ -467,7 +468,7 @@ def test_prepend(self) -> None: self.bucket.upsert("test_prepend", "one") res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.prepend("test_prepend", "two") assert res @@ -506,7 +507,7 @@ def test_prepend_multi(self) -> None: self.bucket.upsert("first_test_prepend_multi", "one") self.bucket.upsert("second_test_prepend_multi", "two") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.prepend_multi(kvs) assert res @@ -539,7 +540,7 @@ def test_prepend_multi(self) -> None: def test_get(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.get("test-key") assert res @@ -572,7 +573,7 @@ def test_rget(self) -> None: res = None try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.rget("test-key", replica_index=None) except CouchbaseTransientError: pass @@ -613,7 +614,7 @@ def test_get_not_found(self) -> None: pass try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.get("test_get_not_found") except NotFoundError: pass @@ -652,7 +653,7 @@ def test_get_multi(self) -> None: self.bucket.upsert("first_test_get_multi", "one") self.bucket.upsert("second_test_get_multi", "two") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.get_multi( ["first_test_get_multi", "second_test_get_multi"] ) @@ -688,7 +689,7 @@ def test_touch(self) -> None: res = None self.bucket.upsert("test_touch", 1) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.touch("test_touch") assert res @@ -723,7 +724,7 @@ def test_touch_multi(self) -> None: self.bucket.upsert("first_test_touch_multi", "one") self.bucket.upsert("second_test_touch_multi", "two") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.touch_multi( ["first_test_touch_multi", "second_test_touch_multi"] ) @@ -759,7 +760,7 @@ def test_lock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) assert rv assert rv.success @@ -817,7 +818,7 @@ def test_lock_unlock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) assert rv assert rv.success @@ -878,7 +879,7 @@ def test_lock_unlock_muilti(self) -> None: keys_to_lock = ("test_lock_unlock_multi_1", "test_lock_unlock_multi_2") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): rv = self.bucket.lock_multi(keys_to_lock, ttl=5) assert rv assert rv["test_lock_unlock_multi_1"].success @@ -940,7 +941,7 @@ def test_remove(self) -> None: res = None self.bucket.upsert("test_remove", 1) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.remove("test_remove") assert res @@ -976,7 +977,7 @@ def test_remove_multi(self) -> None: keys_to_remove = ("test_remove_multi_1", "test_remove_multi_2") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.remove_multi(keys_to_remove) assert res @@ -1010,7 +1011,7 @@ def test_counter(self) -> None: res = None self.bucket.upsert("test_counter", 1) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.counter("test_counter", delta=10) assert res @@ -1044,7 +1045,7 @@ def test_counter_multi(self) -> None: self.bucket.upsert("first_test_counter", 1) self.bucket.upsert("second_test_counter", 1) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.counter_multi( ("first_test_counter", "second_test_counter") ) @@ -1087,7 +1088,7 @@ def test_mutate_in(self) -> None: }, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.mutate_in( "king_arthur", SD.array_addunique("interests", "Cats"), @@ -1131,7 +1132,7 @@ def test_lookup_in(self) -> None: }, ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.lookup_in( "king_arthur", SD.get("email"), SD.get("interests") ) @@ -1165,7 +1166,7 @@ def test_lookup_in(self) -> None: def test_stats(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.stats() assert res @@ -1196,7 +1197,7 @@ def test_stats(self) -> None: def test_ping(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.ping() assert res @@ -1227,7 +1228,7 @@ def test_ping(self) -> None: def test_diagnostics(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.diagnostics() assert res @@ -1259,7 +1260,7 @@ def test_observe(self) -> None: res = None self.bucket.upsert("test_observe", 1) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.observe("test_observe") assert res @@ -1295,7 +1296,7 @@ def test_observe_multi(self) -> None: keys_to_observe = ("test_observe_multi_1", "test_observe_multi_2") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.observe_multi(keys_to_observe) assert res @@ -1328,14 +1329,14 @@ def test_observe_multi(self) -> None: def test_query_with_instana_tracing_off(self) -> None: res = None - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.couchbase_inst.tracing_is_off", return_value=True ): res = self.bucket.n1ql_query("SELECT 1") assert res def test_query_with_instana_exception(self) -> None: - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.couchbase_inst.collect_attributes", side_effect=Exception("test-error"), ): @@ -1349,7 +1350,7 @@ def test_query_with_instana_exception(self) -> None: def test_raw_n1ql_query(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.n1ql_query("SELECT 1") assert res @@ -1381,7 +1382,7 @@ def test_raw_n1ql_query(self) -> None: def test_n1ql_query(self) -> None: res = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.bucket.n1ql_query( N1QLQuery( 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"' diff --git a/tests/clients/test_google-cloud-pubsub.py b/tests/clients/test_google-cloud-pubsub.py index db262e70..98168bda 100644 --- a/tests/clients/test_google-cloud-pubsub.py +++ b/tests/clients/test_google-cloud-pubsub.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 + import os import threading import time @@ -12,7 +13,7 @@ from google.cloud.pubsub_v1.publisher import exceptions from opentelemetry.trace import SpanKind -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.test_utils import _TraceContextMixin @@ -25,7 +26,8 @@ class TestPubSubPublish(_TraceContextMixin): @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.project_id = "test-project" @@ -44,7 +46,7 @@ def _resource(self) -> Generator[None, None, None]: def test_publish(self) -> None: # publish a single message - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): future = self.publisher.publish( self.topic_path, b"Test Message", origin="instana" ) @@ -114,10 +116,11 @@ class TestPubSubSubscribe(_TraceContextMixin): def setup_class(cls) -> None: cls.publisher = PublisherClient() cls.subscriber = SubscriberClient() + cls.tracer = get_tracer() @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.project_id = "test-project" @@ -155,7 +158,7 @@ def _resource(self) -> Generator[None, None, None]: ) def test_subscribe(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): # Publish a message future = self.publisher.publish( self.topic_path, b"Test Message to PubSub", origin="instana" diff --git a/tests/clients/test_google-cloud-storage.py b/tests/clients/test_google-cloud-storage.py index 51b560ba..23af7dc7 100644 --- a/tests/clients/test_google-cloud-storage.py +++ b/tests/clients/test_google-cloud-storage.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import sys from typing import Generator import json @@ -8,7 +9,7 @@ import requests import io -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.test_utils import _TraceContextMixin from opentelemetry.trace import SpanKind @@ -24,7 +25,8 @@ class TestGoogleCloudStorage(_TraceContextMixin): @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield agent.options.allow_exit_as_root = False @@ -40,7 +42,7 @@ def test_buckets_list(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): buckets = client.list_buckets() for _ in buckets: pass @@ -106,7 +108,7 @@ def test_buckets_insert(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.create_bucket("test bucket") spans = self.recorder.queued_spans() @@ -139,7 +141,7 @@ def test_buckets_get(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.get_bucket("test bucket") spans = self.recorder.queued_spans() @@ -172,7 +174,7 @@ def test_buckets_patch(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").patch() spans = self.recorder.queued_spans() @@ -204,7 +206,7 @@ def test_buckets_update(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").update() spans = self.recorder.queued_spans() @@ -236,7 +238,7 @@ def test_buckets_get_iam_policy(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").get_iam_policy() spans = self.recorder.queued_spans() @@ -268,7 +270,7 @@ def test_buckets_set_iam_policy(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").set_iam_policy(iam.Policy()) spans = self.recorder.queued_spans() @@ -301,7 +303,7 @@ def test_buckets_test_iam_permissions(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").test_iam_permissions("test-permission") spans = self.recorder.queued_spans() @@ -341,7 +343,7 @@ def test_buckets_lock_retention_policy(self, mock_requests: Mock) -> None: bucket = client.bucket("test bucket") bucket.reload() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): bucket.lock_retention_policy() spans = self.recorder.queued_spans() @@ -371,7 +373,7 @@ def test_buckets_delete(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").delete() spans = self.recorder.queued_spans() @@ -403,7 +405,7 @@ def test_objects_compose(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("dest object").compose( [ storage.blob.Blob("object 1", "test bucket"), @@ -446,7 +448,7 @@ def test_objects_copy(self, mock_requests: Mock) -> None: ) bucket = client.bucket("src bucket") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): bucket.copy_blob( bucket.blob("src object"), client.bucket("dest bucket"), @@ -483,7 +485,7 @@ def test_objects_delete(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").delete() spans = self.recorder.queued_spans() @@ -516,7 +518,7 @@ def test_objects_attrs(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").exists() spans = self.recorder.queued_spans() @@ -553,7 +555,7 @@ def test_objects_get(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").download_to_file( io.BytesIO(), raw_download=True ) @@ -588,7 +590,7 @@ def test_objects_insert(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").upload_from_string( "CONTENT" ) @@ -623,7 +625,7 @@ def test_objects_list(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): blobs = client.bucket("test bucket").list_blobs() for _ in blobs: @@ -658,7 +660,7 @@ def test_objects_patch(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").patch() spans = self.recorder.queued_spans() @@ -698,7 +700,7 @@ def test_objects_rewrite(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("dest bucket").blob("dest object").rewrite( client.bucket("src bucket").blob("src object") ) @@ -735,7 +737,7 @@ def test_objects_update(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").update() spans = self.recorder.queued_spans() @@ -769,7 +771,7 @@ def test_default_acls_list(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").default_object_acl.get_entities() spans = self.recorder.queued_spans() @@ -802,7 +804,7 @@ def test_object_acls_list(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").acl.get_entities() spans = self.recorder.queued_spans() @@ -836,7 +838,7 @@ def test_object_hmac_keys_create(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.create_hmac_key("test@example.com") spans = self.recorder.queued_spans() @@ -866,7 +868,7 @@ def test_object_hmac_keys_delete(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): key = storage.hmac_key.HMACKeyMetadata(client, access_id="test key") key.state = storage.hmac_key.HMACKeyMetadata.INACTIVE_STATE key.delete() @@ -902,7 +904,7 @@ def test_object_hmac_keys_get(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): storage.hmac_key.HMACKeyMetadata(client, access_id="test key").exists() spans = self.recorder.queued_spans() @@ -936,7 +938,7 @@ def test_object_hmac_keys_list(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): keys = client.list_hmac_keys() for _ in keys: @@ -972,7 +974,7 @@ def test_object_hmac_keys_update(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): storage.hmac_key.HMACKeyMetadata(client, access_id="test key").update() spans = self.recorder.queued_spans() @@ -1009,7 +1011,7 @@ def test_object_get_service_account_email(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): client.get_service_account_email() spans = self.recorder.queued_spans() @@ -1044,7 +1046,7 @@ def test_batch_operation(self, mock_requests: Mock) -> None: ) bucket = client.bucket("test-bucket") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with client.batch(): for obj in ["obj1", "obj2"]: bucket.delete_blob(obj) @@ -1062,7 +1064,7 @@ def test_execute_with_instana_without_tags(self, mock_requests: Mock) -> None: client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.google.cloud.storage._collect_attributes", return_value=None, ): @@ -1075,7 +1077,7 @@ def test_execute_with_instana_tracing_is_off(self) -> None: client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.google.cloud.storage.tracing_is_off", return_value=True, ): @@ -1094,7 +1096,7 @@ def test_download_with_instana_tracing_is_off(self, mock_requests: Mock) -> None client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.google.cloud.storage.tracing_is_off", return_value=True, ): @@ -1118,7 +1120,7 @@ def test_upload_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.google.cloud.storage.tracing_is_off", return_value=True, ): @@ -1142,7 +1144,7 @@ def test_finish_batch_operation_tracing_is_off(self, mock_requests: Mock) -> Non ) bucket = client.bucket("test-bucket") - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.google.cloud.storage.tracing_is_off", return_value=True, ): diff --git a/tests/clients/test_httpx.py b/tests/clients/test_httpx.py index db14c892..bfc15389 100644 --- a/tests/clients/test_httpx.py +++ b/tests/clients/test_httpx.py @@ -1,13 +1,13 @@ # (c) Copyright IBM Corp. 2025 + import pytest import httpx from typing import Generator import asyncio -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id -import tests.apps.flask_app from tests.helpers import testenv @@ -17,7 +17,8 @@ class TestHttpxClients: def setup_class(cls) -> None: cls.client = httpx.Client() cls.host = "127.0.0.1" - cls.recorder = tracer.span_processor + cls.tracer = get_tracer() + cls.recorder = cls.tracer.span_processor def teardown_class(cls) -> None: cls.client.close() @@ -73,7 +74,7 @@ def execute_request( def test_get_request(self, request_mode) -> None: path = "/" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request(request_mode, path) spans = self.recorder.queued_spans() @@ -185,7 +186,7 @@ def test_get_request_as_root_exit_span(self, request_mode) -> None: def test_get_request_with_query(self, request_mode) -> None: path = "/" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request( request_mode, path + "?user=instana&pass=itsasecret" ) @@ -231,7 +232,7 @@ def test_get_request_with_query(self, request_mode) -> None: def test_post_request(self, request_mode) -> None: path = "/notfound" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request(request_mode, path, request_method="POST") spans = self.recorder.queued_spans() @@ -274,7 +275,7 @@ def test_post_request(self, request_mode) -> None: def test_5xx_request(self, request_mode) -> None: path = "/500" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request(request_mode, path) spans = self.recorder.queued_spans() @@ -335,7 +336,7 @@ def test_response_header_capture(self, request_mode) -> None: agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] path = "/response_headers" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request(request_mode, path) spans = self.recorder.queued_spans() @@ -392,7 +393,7 @@ def test_request_header_capture(self, request_mode) -> None: "X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too", } - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): res = self.execute_request(request_mode, path, headers=request_headers) spans = self.recorder.queued_spans() diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 0fa5d2dc..dcd4a487 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import logging from typing import Generator from unittest.mock import patch @@ -8,7 +9,7 @@ import pytest from opentelemetry.trace import SpanKind -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.runtime import get_runtime_env_info @@ -18,7 +19,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.logger = logging.getLogger("unit test") yield @@ -28,7 +30,7 @@ def _resource(self) -> Generator[None, None, None]: def test_no_span(self) -> None: self.logger.setLevel(logging.INFO) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.info("info message") spans = self.recorder.queued_spans() @@ -36,7 +38,7 @@ def test_no_span(self) -> None: assert len(spans) == 1 def test_extra_span(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() @@ -46,7 +48,7 @@ def test_extra_span(self) -> None: assert spans[0].data["log"].get("message") == "foo bar" def test_log_with_tuple(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("foo %s", ("bar",)) spans = self.recorder.queued_spans() @@ -56,7 +58,7 @@ def test_log_with_tuple(self) -> None: assert spans[0].data["log"].get("message") == "foo ('bar',)" def test_log_with_dict(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("foo %s", {"bar": 18}) spans = self.recorder.queued_spans() @@ -66,7 +68,7 @@ def test_log_with_dict(self) -> None: assert spans[0].data["log"].get("message") == "foo {'bar': 18}" def test_parameters(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: a = 42 b = 0 @@ -98,7 +100,7 @@ def test_root_exit_span(self) -> None: assert spans[0].data["log"].get("message") == "foo bar" def test_exception(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with patch( "instana.span.span.InstanaSpan.add_event", side_effect=Exception("mocked error"), @@ -121,7 +123,7 @@ def test_log_caller(self, caplog: pytest.LogCaptureFixture) -> None: def log_custom_warning(): self.logger.warning("foo %s", "bar") - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): log_custom_warning() assert caplog.records[-1].funcName == "log_custom_warning" @@ -156,7 +158,7 @@ def log_custom_warning(): def main(): log_custom_warning() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): main() assert caplog.records[-1].funcName == expected_caller_name @@ -174,7 +176,8 @@ class TestLoggingDisabling: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: # Setup - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.logger = logging.getLogger("unit test") @@ -188,7 +191,7 @@ def _resource(self) -> Generator[None, None, None]: agent.options.allow_exit_as_root = False def test_logging_enabled(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("test message") spans = self.recorder.queued_spans() @@ -200,7 +203,7 @@ def test_logging_disabled(self) -> None: # Disable logging spans agent.options.disabled_spans = ["logging"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("test message") spans = self.recorder.queued_spans() @@ -214,7 +217,7 @@ def test_logging_disabled_via_env_var(self, monkeypatch): original_options = agent.options agent.options = type(original_options)() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("test message") spans = self.recorder.queued_spans() @@ -232,7 +235,7 @@ def test_logging_disabled_via_yaml(self) -> None: tracing_config = {"disable": [{"logging": True}]} agent.options.set_tracing(tracing_config) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.logger.warning("test message") spans = self.recorder.queued_spans() diff --git a/tests/clients/test_mysqlclient.py b/tests/clients/test_mysqlclient.py index 069a4edd..231e449c 100644 --- a/tests/clients/test_mysqlclient.py +++ b/tests/clients/test_mysqlclient.py @@ -1,11 +1,12 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import sys import MySQLdb import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from tests.helpers import testenv @@ -42,9 +43,10 @@ def _resource(self): setup_cursor.close() self.cursor = self.db.cursor() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() - tracer.cur_ctx = None + self.tracer.cur_ctx = None yield if self.cursor and self.cursor.connection.open: self.cursor.close() @@ -62,7 +64,7 @@ def test_vanilla_query(self): assert len(spans) == 0 def test_basic_query(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() @@ -110,7 +112,7 @@ def test_basic_query_as_root_exit_span(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_insert(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( """INSERT INTO users(name, email) VALUES(%s, %s)""", ("beaker", "beaker@muppets.com"), @@ -140,7 +142,7 @@ def test_basic_insert(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_executemany(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.executemany( "INSERT INTO users(name, email) VALUES(%s, %s)", [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], @@ -171,7 +173,7 @@ def test_executemany(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_call_proc(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): callproc_result = self.cursor.callproc("test_proc", ("beaker",)) assert isinstance(callproc_result, tuple) @@ -197,7 +199,7 @@ def test_call_proc(self): def test_error_capture(self): affected_rows = None try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass @@ -227,7 +229,7 @@ def test_error_capture(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_cursor_ctx_mgr(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") @@ -252,7 +254,7 @@ def test_connect_cursor_ctx_mgr(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_ctx_mgr(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") @@ -276,7 +278,7 @@ def test_connect_ctx_mgr(self): assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_cursor_ctx_mgr(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 2bd0c6e2..ff78bf99 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -1,3 +1,6 @@ +# (c) Copyright IBM Corp. 2025 + + import logging from typing import Generator from unittest.mock import patch @@ -10,9 +13,8 @@ ConnectionWrapper, CursorWrapper, ) -from instana.singletons import tracer +from instana.singletons import get_tracer from instana.span.span import InstanaSpan -from opentelemetry.trace import SpanKind from pytest import LogCaptureFixture from tests.helpers import testenv @@ -21,6 +23,7 @@ class TestCursorWrapper: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: + self.tracer = get_tracer() self.connect_params = [ "db", { @@ -111,7 +114,7 @@ def test_cursor_wrapper_default(self) -> None: def test_collect_kvs(self) -> None: self.reset_table() - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: sample_sql = """ select * from tests; """ @@ -124,7 +127,7 @@ def test_collect_kvs(self) -> None: def test_collect_kvs_error(self, caplog: LogCaptureFixture) -> None: self.reset_table() - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: connect_params = "sample" sample_wrapper = CursorWrapper( self.test_cursor, @@ -143,7 +146,7 @@ def test_enter(self) -> None: def test_execute_with_tracing_off(self) -> None: self.reset_table() - with tracer.start_as_current_span("sqlalchemy"): + with self.tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_params = (2, "sample-name", "sample-email@mail.com") self.test_wrapper.execute(sample_sql, sample_params) @@ -154,7 +157,7 @@ def test_execute_with_tracing_off(self) -> None: def test_execute_with_tracing(self) -> None: self.reset_table() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_params = (3, "sample-name", "sample-email@mail.com") self.test_wrapper.execute(sample_sql, sample_params) @@ -176,7 +179,7 @@ def test_execute_with_tracing(self) -> None: def test_executemany_with_tracing_off(self) -> None: self.reset_table() - with tracer.start_as_current_span("sqlalchemy"): + with self.tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_seq_of_params = [ (4, "sample-name-3", "sample-email-3@mail.com"), @@ -191,7 +194,7 @@ def test_executemany_with_tracing_off(self) -> None: def test_executemany_with_tracing(self) -> None: self.reset_table() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_seq_of_params = [ (6, "sample-name-3", "sample-email-3@mail.com"), @@ -217,7 +220,7 @@ def test_executemany_with_tracing(self) -> None: def test_callproc_with_tracing_off(self) -> None: self.reset_table() self.reset_procedure() - with tracer.start_as_current_span("sqlalchemy"): + with self.tracer.start_as_current_span("sqlalchemy"): sample_proc_name = "call insert_user(%s, %s, %s);" sample_params = (8, "sample-name-8", "sample-email-8@mail.com") self.test_wrapper.callproc(sample_proc_name, sample_params) @@ -230,7 +233,7 @@ def test_callproc_with_tracing_off(self) -> None: def test_callproc_with_tracing(self) -> None: self.reset_table() self.reset_procedure() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): sample_proc_name = "call insert_user(%s, %s, %s);" sample_params = (9, "sample-name-9", "sample-email-9@mail.com") self.test_wrapper.callproc(sample_proc_name, sample_params) diff --git a/tests/clients/test_pika.py b/tests/clients/test_pika.py index d01d58d9..7abb2991 100644 --- a/tests/clients/test_pika.py +++ b/tests/clients/test_pika.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 + import threading import time from typing import Generator, Optional @@ -13,7 +14,7 @@ import pytest from opentelemetry.trace.span import format_span_id -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id @@ -31,7 +32,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.connection = self._create_connection() @@ -342,7 +344,7 @@ def _create_obj(self) -> pika.channel.Channel: def test_basic_publish(self, send_method, _unused) -> None: self.obj._set_state(self.obj.OPEN) - with tracer.start_as_current_span("testing"): + with self.tracer.start_as_current_span("testing"): self.obj.basic_publish("test.exchange", "test.queue", "Hello!") spans = self.recorder.queued_spans() @@ -432,7 +434,7 @@ def test_basic_publish_as_root_exit_span(self, send_method, _unused) -> None: def test_basic_publish_with_headers(self, send_method, _unused) -> None: self.obj._set_state(self.obj.OPEN) - with tracer.start_as_current_span("testing"): + with self.tracer.start_as_current_span("testing"): self.obj.basic_publish( "test.exchange", "test.queue", @@ -471,7 +473,7 @@ def test_basic_publish_tracing_off(self, send_method, _unused, mocker) -> None: self.obj._set_state(self.obj.OPEN) - with tracer.start_as_current_span("testing"): + with self.tracer.start_as_current_span("testing"): self.obj.basic_publish("test.exchange", "test.queue", "Hello!") spans = self.recorder.queued_spans() diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index 17b88bb4..d7b80291 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import logging import pytest from typing import Generator from instana.instrumentation.psycopg2 import register_json_with_instana from tests.helpers import testenv -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer import psycopg2 import psycopg2.extras @@ -19,6 +20,7 @@ class TestPsycoPG2: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: + self.tracer = get_tracer() kwargs = { "host": testenv["postgresql_host"], "port": testenv["postgresql_port"], @@ -52,9 +54,9 @@ def _resource(self) -> Generator[None, None, None]: self.db.commit() self.cursor = self.db.cursor() - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() - tracer.cur_ctx = None + self.tracer.cur_ctx = None yield if self.cursor and not self.cursor.connection.closed: self.cursor.close() @@ -82,7 +84,7 @@ def test_vanilla_query(self) -> None: assert len(spans) == 0 def test_basic_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() @@ -134,7 +136,7 @@ def test_basic_query_as_root_exit_span(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_basic_insert(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.cursor.execute( """INSERT INTO users(name, email) VALUES(%s, %s)""", ("beaker", "beaker@muppets.com"), @@ -165,7 +167,7 @@ def test_basic_insert(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_executemany(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.cursor.executemany( "INSERT INTO users(name, email) VALUES(%s, %s)", [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], @@ -198,7 +200,7 @@ def test_executemany(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_call_proc(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): callproc_result = self.cursor.callproc("test_proc", ("beaker",)) assert isinstance(callproc_result, tuple) @@ -224,7 +226,7 @@ def test_call_proc(self) -> None: def test_error_capture(self) -> None: affected_rows = result = None try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from blah""") affected_rows = self.cursor.rowcount self.cursor.fetchone() @@ -302,7 +304,7 @@ def test_register_type(self) -> None: ext.register_type(ext.UUIDARRAY, self.cursor) def test_connect_cursor_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") @@ -331,7 +333,7 @@ def test_connect_cursor_ctx_mgr(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_connect_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") @@ -360,7 +362,7 @@ def test_connect_ctx_mgr(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_cursor_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") @@ -389,7 +391,7 @@ def test_cursor_ctx_mgr(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_deprecated_parameter_database(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() diff --git a/tests/clients/test_pymongo.py b/tests/clients/test_pymongo.py index 251f0b40..aab0686d 100644 --- a/tests/clients/test_pymongo.py +++ b/tests/clients/test_pymongo.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import json import logging from typing import Generator @@ -9,7 +10,7 @@ import pymongo import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.helpers import testenv @@ -26,14 +27,15 @@ def _resource(self) -> Generator[None, None, None]: password=testenv["mongodb_pw"], ) self.client.test.records.delete_many(filter={}) - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield self.client.close() agent.options.allow_exit_as_root = False def test_successful_find_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.find_one({"type": "string"}) current_span = get_current_span() assert not current_span.is_recording() @@ -86,7 +88,7 @@ def test_successful_find_query_as_root_span(self) -> None: assert not db_span.data["mongo"]["json"] def test_successful_insert_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.insert_one({"type": "string"}) current_span = get_current_span() assert not current_span.is_recording() @@ -113,7 +115,7 @@ def test_successful_insert_query(self) -> None: assert not db_span.data["mongo"]["filter"] def test_successful_update_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.update_one( {"type": "string"}, {"$set": {"type": "int"}} ) @@ -151,7 +153,7 @@ def test_successful_update_query(self) -> None: } in payload def test_successful_delete_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.delete_one(filter={"type": "string"}) current_span = get_current_span() assert not current_span.is_recording() @@ -182,7 +184,7 @@ def test_successful_delete_query(self) -> None: assert {"q": {"type": "string"}, "limit": 1} in payload def test_successful_aggregate_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.count_documents({"type": "string"}) current_span = get_current_span() assert not current_span.is_recording() @@ -219,7 +221,7 @@ def test_successful_map_reduce_query(self) -> None: mapper = "function () { this.tags.forEach(function(z) { emit(z, 1); }); }" reducer = "function (key, values) { return len(values); }" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.map_reduce( bson.code.Code(mapper), bson.code.Code(reducer), @@ -258,7 +260,7 @@ def test_successful_map_reduce_query(self) -> None: assert payload["reduce"], {"$code": reducer} == db_span.data["mongo"]["json"] def test_successful_mutiple_queries(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.test.records.bulk_write( [ pymongo.InsertOne({"type": "string"}), diff --git a/tests/clients/test_pymysql.py b/tests/clients/test_pymysql.py index 8e4793d5..22af80a4 100644 --- a/tests/clients/test_pymysql.py +++ b/tests/clients/test_pymysql.py @@ -1,14 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time + import pytest import pymysql from typing import Generator from tests.helpers import testenv -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer class TestPyMySQL: @@ -42,9 +42,10 @@ def _resource(self) -> Generator[None, None, None]: setup_cursor.execute(s) self.cursor = self.db.cursor() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() - tracer.cur_ctx = None + self.tracer.cur_ctx = None yield if self.cursor and self.cursor.connection.open: self.cursor.close() @@ -62,7 +63,7 @@ def test_vanilla_query(self) -> None: assert len(spans) == 0 def test_basic_query(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() @@ -110,7 +111,7 @@ def test_basic_query_as_root_exit_span(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_query_with_params(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users where id=1""") result = self.cursor.fetchone() @@ -136,7 +137,7 @@ def test_query_with_params(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_insert(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( """INSERT INTO users(name, email) VALUES(%s, %s)""", ("beaker", "beaker@muppets.com"), @@ -167,7 +168,7 @@ def test_basic_insert(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_executemany(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.executemany( "INSERT INTO users(name, email) VALUES(%s, %s)", [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], @@ -198,7 +199,7 @@ def test_executemany(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_call_proc(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): callproc_result = self.cursor.callproc("test_proc", ("beaker",)) assert isinstance(callproc_result, tuple) @@ -224,7 +225,7 @@ def test_call_proc(self) -> None: def test_error_capture(self) -> None: affected_rows = None try: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass @@ -254,7 +255,7 @@ def test_error_capture(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_cursor_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") @@ -279,7 +280,7 @@ def test_connect_cursor_ctx_mgr(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") @@ -303,7 +304,7 @@ def test_connect_ctx_mgr(self) -> None: assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_cursor_ctx_mgr(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") @@ -329,7 +330,7 @@ def test_cursor_ctx_mgr(self) -> None: def test_deprecated_parameter_db(self) -> None: """test_deprecated_parameter_db""" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() diff --git a/tests/clients/test_redis.py b/tests/clients/test_redis.py index 78883e85..7096ce0a 100644 --- a/tests/clients/test_redis.py +++ b/tests/clients/test_redis.py @@ -11,7 +11,7 @@ import redis from instana.options import StandardOptions -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.helpers import testenv @@ -20,7 +20,8 @@ class TestRedis: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.client = redis.Redis(host=testenv["redis_host"], db=testenv["redis_db"]) yield @@ -30,7 +31,7 @@ def _resource(self) -> Generator[None, None, None]: def test_set_get(self) -> None: result = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.set("fooy", "barY") result = self.client.get("foox") @@ -197,7 +198,7 @@ def test_set_get_as_root_span(self) -> None: def test_set_incr_get(self) -> None: result = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("counter", "10") self.client.incr("counter") result = self.client.get("counter") @@ -284,7 +285,7 @@ def test_set_incr_get(self) -> None: def test_old_redis_client(self) -> None: result = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.set("fooy", "barY") result = self.client.get("foox") @@ -372,7 +373,7 @@ def test_old_redis_client(self) -> None: def test_pipelined_requests(self) -> None: result = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): pipe = self.client.pipeline() pipe.set("foox", "barX") pipe.set("fooy", "barY") @@ -424,20 +425,20 @@ def test_pipelined_requests(self) -> None: ) @patch("instana.span.span.InstanaSpan.record_exception") def test_execute_command_with_instana_exception(self, mock_record_func, _) -> None: - with tracer.start_as_current_span("test"), pytest.raises( + with self.tracer.start_as_current_span("test"), pytest.raises( Exception, match="test-error" ): self.client.set("counter", "10") mock_record_func.assert_called() def test_execute_comand_with_instana_tracing_off(self) -> None: - with tracer.start_as_current_span("redis"): + with self.tracer.start_as_current_span("redis"): response = self.client.set("counter", "10") assert response def test_execute_with_instana_tracing_off(self) -> None: result = None - with tracer.start_as_current_span("redis"): + with self.tracer.start_as_current_span("redis"): pipe = self.client.pipeline() pipe.set("foox", "barX") pipe.set("fooy", "barY") @@ -449,7 +450,7 @@ def test_execute_with_instana_exception( self, caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.DEBUG, logger="instana") - with tracer.start_as_current_span("test"), patch( + with self.tracer.start_as_current_span("test"), patch( "instana.instrumentation.redis.collect_attributes", side_effect=Exception("test-error"), ): @@ -466,7 +467,7 @@ def test_ignore_redis( os.environ["INSTANA_IGNORE_ENDPOINTS"] = "redis" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.get("foox") @@ -480,7 +481,7 @@ def test_ignore_redis_single_command(self) -> None: os.environ["INSTANA_IGNORE_ENDPOINTS"] = "redis:set" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.get("foox") @@ -501,7 +502,7 @@ def test_ignore_redis_single_command(self) -> None: def test_ignore_redis_multiple_commands(self) -> None: os.environ["INSTANA_IGNORE_ENDPOINTS"] = "redis:set,get" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.get("foox") @@ -518,7 +519,7 @@ def test_ignore_redis_multiple_commands(self) -> None: def test_ignore_redis_with_another_instrumentation(self) -> None: os.environ["INSTANA_IGNORE_ENDPOINTS"] = "redis:set;something_else:something" agent.options = StandardOptions() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.client.set("foox", "barX") self.client.get("foox") diff --git a/tests/clients/test_sqlalchemy.py b/tests/clients/test_sqlalchemy.py index 9ace784c..3d4866b2 100644 --- a/tests/clients/test_sqlalchemy.py +++ b/tests/clients/test_sqlalchemy.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Generator import pytest @@ -8,7 +9,7 @@ from sqlalchemy.exc import OperationalError from sqlalchemy.orm import declarative_base, sessionmaker -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.helpers import testenv @@ -39,6 +40,7 @@ def __repr__(self) -> None: @pytest.fixture(scope="class") def db_setup() -> None: + tracer = get_tracer() with tracer.start_as_current_span("metadata") as span: Base.metadata.create_all(engine) span.end() @@ -63,7 +65,8 @@ class TestSQLAlchemy: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.session = Session() yield @@ -72,7 +75,7 @@ def _resource(self) -> Generator[None, None, None]: agent.options.allow_exit_as_root = False def test_session_add(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): self.session.add(stan_user) self.session.commit() @@ -148,7 +151,7 @@ def test_session_add_as_root_exit_span(self) -> None: assert len(sql_span.stack) > 0 def test_transaction(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with engine.begin() as connection: connection.execute(text("select 1")) connection.execute( @@ -212,7 +215,7 @@ def test_transaction(self) -> None: assert len(sql_span1.stack) > 0 def test_error_logging(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: self.session.execute(text("htVwGrCwVThisIsInvalidSQLaw4ijXd88")) # self.session.commit() diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index 6c5fc318..0a595721 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import logging import sys from multiprocessing.pool import ThreadPool @@ -15,7 +16,7 @@ extract_custom_headers, collect_response, ) -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer import tests.apps.flask_app # noqa: F401 from tests.helpers import testenv @@ -29,10 +30,11 @@ class TestUrllib3: @pytest.fixture(autouse=True) def _setup(self) -> Generator[None, None, None]: """SetUp and TearDown""" + self.tracer = get_tracer() # setup # Clear all spans before a test run self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield # teardown @@ -89,7 +91,7 @@ def make_request(u=None) -> int: assert len(spans) == 16 def test_get_request(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() @@ -138,7 +140,7 @@ def test_get_request(self): def test_get_request_https(self): request_url = "https://jsonplaceholder.typicode.com:443/todos/1" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", request_url) spans = self.recorder.queued_spans() @@ -182,7 +184,7 @@ def test_get_request_as_root_exit_span(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert urllib3_span.t == wsgi_span.t @@ -216,7 +218,7 @@ def test_get_request_as_root_exit_span(self): assert len(urllib3_span.stack) > 1 def test_get_request_with_query(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/?one=1&two=2") spans = self.recorder.queued_spans() @@ -228,7 +230,7 @@ def test_get_request_with_query(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -266,7 +268,7 @@ def test_get_request_with_query(self): assert len(urllib3_span.stack) > 1 def test_get_request_with_alt_query(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request( "GET", testenv["flask_server"] + "/", fields={"one": "1", "two": 2} ) @@ -280,7 +282,7 @@ def test_get_request_with_alt_query(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -318,7 +320,7 @@ def test_get_request_with_alt_query(self): assert len(urllib3_span.stack) > 1 def test_put_request(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("PUT", testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() @@ -330,7 +332,7 @@ def test_put_request(self): assert r assert r.status == 404 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -367,7 +369,7 @@ def test_put_request(self): assert len(urllib3_span.stack) > 1 def test_301_redirect(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/301") spans = self.recorder.queued_spans() @@ -381,7 +383,7 @@ def test_301_redirect(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId traceId = test_span.t @@ -443,7 +445,7 @@ def test_301_redirect(self): assert len(urllib3_span2.stack) > 1 def test_302_redirect(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/302") spans = self.recorder.queued_spans() @@ -457,7 +459,7 @@ def test_302_redirect(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId traceId = test_span.t @@ -519,7 +521,7 @@ def test_302_redirect(self): assert len(urllib3_span2.stack) > 1 def test_5xx_request(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/504") spans = self.recorder.queued_spans() @@ -531,7 +533,7 @@ def test_5xx_request(self): assert r assert r.status == 504 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId traceId = test_span.t @@ -569,7 +571,7 @@ def test_5xx_request(self): assert len(urllib3_span.stack) > 1 def test_exception_logging(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: r = self.http.request("GET", testenv["flask_server"] + "/exception") except Exception: @@ -597,7 +599,7 @@ def test_exception_logging(self): assert r assert r.status == 500 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId traceId = test_span.t @@ -641,7 +643,7 @@ def test_exception_logging(self): def test_client_error(self): r = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: r = self.http.request( "GET", @@ -683,7 +685,7 @@ def test_client_error(self): def test_requests_pkg_get(self): self.recorder.clear_spans() - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = requests.get(testenv["flask_server"] + "/", timeout=2) spans = self.recorder.queued_spans() @@ -695,7 +697,7 @@ def test_requests_pkg_get(self): assert r assert r.status_code == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -735,7 +737,7 @@ def test_requests_pkg_get_with_custom_headers(self): my_custom_headers = dict() my_custom_headers["X-PGL-1"] = "1" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = requests.get( testenv["flask_server"] + "/", timeout=2, headers=my_custom_headers ) @@ -749,7 +751,7 @@ def test_requests_pkg_get_with_custom_headers(self): assert r assert r.status_code == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -786,7 +788,7 @@ def test_requests_pkg_get_with_custom_headers(self): assert len(urllib3_span.stack) > 1 def test_requests_pkg_put(self): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = requests.put(testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() @@ -797,7 +799,7 @@ def test_requests_pkg_put(self): test_span = spans[2] assert r.status_code == 404 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -837,7 +839,7 @@ def test_response_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request("GET", testenv["flask_server"] + "/response_headers") spans = self.recorder.queued_spans() @@ -849,7 +851,7 @@ def test_response_header_capture(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -903,7 +905,7 @@ def test_request_header_capture(self): "X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too", } - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): r = self.http.request( "GET", testenv["flask_server"] + "/", headers=request_headers ) @@ -917,7 +919,7 @@ def test_request_header_capture(self): assert r assert r.status == 200 - # assert not tracer.active_span + # assert not self.tracer.active_span # Same traceId assert test_span.t == urllib3_span.t @@ -996,7 +998,7 @@ def test_collect_kvs_exception( def test_internal_span_creation_with_url_in_hostname(self) -> None: internal_url = "https://com.instana.example.com/api/test" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: self.http.request("GET", internal_url, retries=False, timeout=1) except Exception: @@ -1015,7 +1017,7 @@ def test_internal_span_creation_with_url_in_hostname(self) -> None: def test_internal_span_creation_with_url_in_path(self) -> None: internal_url_path = "https://example.com/com.instana/api/test" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: self.http.request("GET", internal_url_path, retries=False, timeout=1) except Exception: diff --git a/tests/collector/test_utils.py b/tests/collector/test_utils.py index 373e3149..6d233934 100644 --- a/tests/collector/test_utils.py +++ b/tests/collector/test_utils.py @@ -1,12 +1,13 @@ -import time +# (c) Copyright IBM Corp. 2025 + + import pytest from typing import Generator from instana.collector.utils import format_span -from instana.singletons import tracer +from instana.singletons import get_tracer from instana.span.registered_span import RegisteredSpan -from instana.span.span import InstanaSpan, get_current_span +from instana.span.span import get_current_span from opentelemetry.trace.span import format_span_id -from opentelemetry.trace import SpanKind from instana.span_context import SpanContext @@ -14,19 +15,20 @@ class TestUtils: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.span_context = None yield def test_format_span(self, span_context: SpanContext) -> None: self.span_context = span_context - with tracer.start_as_current_span( + with self.tracer.start_as_current_span( name="span1", span_context=self.span_context ) as pspan: expected_trace_id = format_span_id(pspan.context.trace_id) expected_span_id = format_span_id(pspan.context.span_id) assert get_current_span() is pspan - with tracer.start_as_current_span(name="span2") as cspan: + with self.tracer.start_as_current_span(name="span2") as cspan: assert get_current_span() is cspan assert cspan.parent_id == pspan.context.span_id span_list = [ diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index 3a2b29ea..39659dcb 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Any, Dict, Generator, Optional import aiohttp import asyncio import pytest -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id import tests.apps.flask_app # noqa: F401 @@ -34,7 +35,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -47,7 +49,7 @@ def _resource(self) -> Generator[None, None, None]: def test_client_get(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") @@ -136,7 +138,7 @@ async def test(): def test_client_get_301(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/301") @@ -186,7 +188,7 @@ async def test(): def test_client_get_405(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/405") @@ -232,7 +234,7 @@ async def test(): def test_client_get_500(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/500") @@ -279,7 +281,7 @@ async def test(): def test_client_get_504(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/504") @@ -326,7 +328,7 @@ async def test(): def test_client_get_with_params_to_scrub(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, testenv["flask_server"], params={"secret": "yeah"} @@ -378,7 +380,7 @@ def test_client_response_header_capture(self) -> None: agent.options.extra_http_headers = ["X-Capture-This"] async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, testenv["flask_server"] + "/response_headers" @@ -409,7 +411,10 @@ async def test(): assert aiohttp_span.n == "aiohttp-client" assert aiohttp_span.data["http"]["status"] == 200 - assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/response_headers" + assert ( + aiohttp_span.data["http"]["url"] + == testenv["flask_server"] + "/response_headers" + ) assert aiohttp_span.data["http"]["method"] == "GET" assert aiohttp_span.stack assert isinstance(aiohttp_span.stack, list) @@ -431,14 +436,14 @@ async def test(): def test_client_error(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, "http://doesnotexist:10/") response = None try: response = self.loop.run_until_complete(test()) - except: + except Exception: pass spans = self.recorder.queued_spans() @@ -476,7 +481,7 @@ def test_client_get_tracing_off(self, mocker) -> None: ) async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") @@ -492,7 +497,7 @@ async def test(): def test_client_get_provided_tracing_config(self, mocker) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession(trace_configs=[]) as session: return await self.fetch(session, testenv["flask_server"] + "/") @@ -511,7 +516,7 @@ def test_client_request_header_capture(self) -> None: } async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, testenv["flask_server"] + "/", headers=request_headers diff --git a/tests/frameworks/test_aiohttp_server.py b/tests/frameworks/test_aiohttp_server.py index 6c2ca672..86781c6b 100644 --- a/tests/frameworks/test_aiohttp_server.py +++ b/tests/frameworks/test_aiohttp_server.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import asyncio from typing import Generator import aiohttp import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id from tests.helpers import testenv @@ -27,8 +28,10 @@ def _resource(self) -> Generator[None, None, None]: # Load test server application import tests.apps.aiohttp_app # noqa: F401 + self.tracer = get_tracer() + # Clear all spans before a test run - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -38,7 +41,7 @@ def _resource(self) -> Generator[None, None, None]: def test_server_get(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/") @@ -87,7 +90,7 @@ async def test(): def test_server_get_204(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/204") @@ -138,7 +141,7 @@ def test_server_synthetic_request(self): async def test(): headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, testenv["aiohttp_server"] + "/", headers=headers @@ -160,7 +163,7 @@ async def test(): def test_server_get_with_params_to_scrub(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, @@ -211,7 +214,7 @@ def test_server_request_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list agent.options.extra_http_headers = [ @@ -280,7 +283,7 @@ def test_server_response_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list agent.options.extra_http_headers = [ @@ -289,8 +292,7 @@ async def test(): ] return await self.fetch( - session, - testenv["aiohttp_server"] + "/response_headers" + session, testenv["aiohttp_server"] + "/response_headers" ) response = self.loop.run_until_complete(test()) @@ -318,7 +320,10 @@ async def test(): assert aioserver_span.n == "aiohttp-server" assert aioserver_span.data["http"]["status"] == 200 - assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/response_headers" + assert ( + aioserver_span.data["http"]["url"] + == f"{testenv['aiohttp_server']}/response_headers" + ) assert aioserver_span.data["http"]["method"] == "GET" assert not aioserver_span.stack @@ -340,7 +345,7 @@ async def test(): def test_server_get_401(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/401") @@ -384,7 +389,7 @@ async def test(): def test_server_get_500(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/500") @@ -428,7 +433,7 @@ async def test(): def test_server_get_exception(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch( session, testenv["aiohttp_server"] + "/exception" @@ -490,8 +495,9 @@ def _resource(self) -> Generator[None, None, None]: # Load test server application import tests.apps.aiohttp_app2 # noqa: F401 + self.tracer = get_tracer() # Clear all spans before a test run - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -501,7 +507,7 @@ def _resource(self) -> Generator[None, None, None]: def test_server_get(self): async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/") diff --git a/tests/frameworks/test_asyncio.py b/tests/frameworks/test_asyncio.py index 5a3fbe61..a5017e46 100644 --- a/tests/frameworks/test_asyncio.py +++ b/tests/frameworks/test_asyncio.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import asyncio from typing import Any, Dict, Generator, Optional @@ -9,7 +10,7 @@ import tests.apps.flask_app # noqa: F401 from instana.configurator import config -from instana.singletons import tracer +from instana.singletons import get_tracer from tests.helpers import testenv @@ -32,7 +33,8 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -53,7 +55,7 @@ async def run_later(msg="Hello"): return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) @@ -82,7 +84,7 @@ async def run_later(msg="Hello"): return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) @@ -105,7 +107,7 @@ async def run_later(msg="Hello"): return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) @@ -134,7 +136,7 @@ async def run_later(msg="Hello"): return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) diff --git a/tests/frameworks/test_celery.py b/tests/frameworks/test_celery.py index 126f0368..90bd4318 100644 --- a/tests/frameworks/test_celery.py +++ b/tests/frameworks/test_celery.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import time from typing import Generator, List @@ -12,7 +13,7 @@ import celery.contrib.testing.worker import pytest -from instana.singletons import tracer +from instana.singletons import get_tracer from instana.span.span import InstanaSpan from tests.helpers import get_first_span_by_filter @@ -48,7 +49,8 @@ def filter_out_ping_tasks( class TestCelery: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield @@ -57,7 +59,7 @@ def test_apply_async( celery_app: celery.app.base.Celery, celery_worker: celery.contrib.testing.worker.TestWorkController, ) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): _ = add.apply_async(args=(4, 5)) # Wait for jobs to finish @@ -110,7 +112,7 @@ def test_delay( celery_app: celery.app.base.Celery, celery_worker: celery.contrib.testing.worker.TestWorkController, ) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): _ = add.delay(4, 5) # Wait for jobs to finish @@ -163,7 +165,7 @@ def test_send_task( celery_app: celery.app.base.Celery, celery_worker: celery.contrib.testing.worker.TestWorkController, ) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): _ = celery_app.send_task("tests.frameworks.test_celery.add", (1, 2)) # Wait for jobs to finish @@ -216,7 +218,7 @@ def test_error_reporting( celery_app: celery.app.base.Celery, celery_worker: celery.contrib.testing.worker.TestWorkController, ) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): _ = will_raise_error.apply_async() # Wait for jobs to finish diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index ab1712dc..91e85715 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os import urllib3 @@ -11,7 +12,7 @@ from instana.util.ids import hex_id from tests.apps.app_django import INSTALLED_APPS -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from tests.helpers import ( fail_with_message_and_span_dump, get_first_span_by_filter, @@ -27,7 +28,8 @@ class TestDjango(StaticLiveServerTestCase): def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor # clear all spans before a test run self.recorder.clear_spans() yield @@ -35,7 +37,7 @@ def _resource(self) -> Generator[None, None, None]: os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "" def test_basic_request(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", self.live_server_url + "/", fields={"test": 1} ) @@ -91,7 +93,7 @@ def test_basic_request(self) -> None: def test_synthetic_request(self) -> None: headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", self.live_server_url + "/", headers=headers ) @@ -113,7 +115,7 @@ def test_synthetic_request(self) -> None: assert test_span.sy is None def test_request_with_error(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", self.live_server_url + "/cause_error") assert response @@ -127,15 +129,21 @@ def test_request_with_error(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + def filter(span): + return span.n == "sdk" and span.data["sdk"]["name"] == "test" + test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == "urllib3" + def filter(span): + return span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, filter) assert urllib3_span - filter = lambda span: span.n == "django" + def filter(span): + return span.n == "django" + django_span = get_first_span_by_filter(spans, filter) assert django_span @@ -174,7 +182,7 @@ def test_request_with_error(self) -> None: assert django_span.stack is None def test_request_with_not_found(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", self.live_server_url + "/not_found") assert response @@ -188,7 +196,9 @@ def test_request_with_not_found(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == "django" + def filter(span): + return span.n == "django" + django_span = get_first_span_by_filter(spans, filter) assert django_span @@ -196,7 +206,7 @@ def test_request_with_not_found(self) -> None: assert 404 == django_span.data["http"]["status"] def test_request_with_not_found_no_route(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", self.live_server_url + "/no_route") assert response @@ -210,7 +220,9 @@ def test_request_with_not_found_no_route(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == "django" + def filter(span): + return span.n == "django" + django_span = get_first_span_by_filter(spans, filter) assert django_span assert django_span.data["http"]["path_tpl"] is None @@ -218,7 +230,7 @@ def test_request_with_not_found_no_route(self) -> None: assert 404 == django_span.data["http"]["status"] def test_complex_request(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", self.live_server_url + "/complex") assert response @@ -283,7 +295,7 @@ def test_request_header_capture(self) -> None: request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", self.live_server_url + "/", headers=request_headers ) @@ -329,7 +341,7 @@ def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", self.live_server_url + "/response_with_headers" ) diff --git a/tests/frameworks/test_fastapi.py b/tests/frameworks/test_fastapi.py index 97943c21..80213971 100644 --- a/tests/frameworks/test_fastapi.py +++ b/tests/frameworks/test_fastapi.py @@ -1,11 +1,12 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Generator from fastapi.testclient import TestClient import pytest -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id from tests.apps.fastapi_app.app import fastapi_server @@ -21,7 +22,8 @@ def _resource(self) -> Generator[None, None, None]: self.client = TestClient(fastapi_server) # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # Hack together a manual custom headers list; We'll use this in tests @@ -52,7 +54,7 @@ def test_vanilla_get(self) -> None: def test_basic_get(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -86,7 +88,7 @@ def test_basic_get(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -103,7 +105,7 @@ def test_basic_get(self) -> None: def test_400(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -137,7 +139,7 @@ def test_400(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -154,7 +156,7 @@ def test_400(self) -> None: def test_500(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -188,7 +190,7 @@ def test_500(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -204,7 +206,7 @@ def test_500(self) -> None: def test_path_templates(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -238,7 +240,7 @@ def test_path_templates(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -254,7 +256,7 @@ def test_path_templates(self) -> None: def test_secret_scrubbing(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -288,7 +290,7 @@ def test_secret_scrubbing(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -304,7 +306,7 @@ def test_secret_scrubbing(self) -> None: assert asgi_span.data["http"]["params"] == "secret=" def test_synthetic_request(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -339,7 +341,7 @@ def test_synthetic_request(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -358,14 +360,14 @@ def test_synthetic_request(self) -> None: assert not test_span.sy def test_request_header_capture(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() headers = { "X-INSTANA-T": hex_id(span_context.trace_id), "X-INSTANA-S": hex_id(span_context.span_id), - "X-Capture-This": "this", + "X-Capture-This": "this", "X-Capture-That": "that", } result = self.client.get("/", headers=headers) @@ -394,9 +396,9 @@ def test_request_header_capture(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) - assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) + assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" assert not asgi_span.ec @@ -415,10 +417,10 @@ def test_request_header_capture(self) -> None: assert asgi_span.data["http"]["header"]["X-Capture-That"] == "that" def test_response_header_capture(self) -> None: - # The background FastAPI server is pre-configured with custom headers + # The background FastAPI server is pre-configured with custom headers # to capture. - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -452,7 +454,7 @@ def test_response_header_capture(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" @@ -473,7 +475,7 @@ def test_response_header_capture(self) -> None: assert asgi_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" def test_non_async_simple(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -536,7 +538,7 @@ def test_non_async_simple(self) -> None: assert not asgi_span2.data["http"]["params"] def test_non_async_threadpool(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -570,7 +572,7 @@ def test_non_async_threadpool(self) -> None: assert test_span.t == asgi_span.t assert test_span.s == asgi_span.p - + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" diff --git a/tests/frameworks/test_fastapi_middleware.py b/tests/frameworks/test_fastapi_middleware.py index 23f83b86..8dd0c4cd 100644 --- a/tests/frameworks/test_fastapi_middleware.py +++ b/tests/frameworks/test_fastapi_middleware.py @@ -1,11 +1,11 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging + from typing import Generator import pytest -from instana.singletons import tracer +from instana.singletons import get_tracer from fastapi.testclient import TestClient from instana.util.ids import hex_id @@ -23,9 +23,11 @@ def _resource(self) -> Generator[None, None, None]: # setup # We are using the TestClient from FastAPI to make it easier. from tests.apps.fastapi_app.app2 import fastapi_server + self.client = TestClient(fastapi_server) # Clear all spans before a test run. - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield del fastapi_server @@ -49,7 +51,7 @@ def test_vanilla_get(self) -> None: def test_basic_get(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index d3a5f10e..6b453d2a 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import unittest import urllib3 import flask @@ -8,7 +9,7 @@ from instana.util.ids import hex_id -if hasattr(flask.signals, 'signals_available'): +if hasattr(flask.signals, "signals_available"): from flask.signals import signals_available else: # Beginning from 2.3.0 as stated in the notes @@ -19,33 +20,32 @@ from opentelemetry.trace import SpanKind -import tests.apps.flask_app -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from tests.helpers import testenv class TestFlask(unittest.TestCase): - def setUp(self) -> None: - """ Clear all spans before a test run """ + """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() def tearDown(self) -> None: - """ Do nothing for now """ + """Do nothing for now""" return None def test_vanilla_requests(self) -> None: - r = self.http.request('GET', testenv["flask_server"] + '/') + r = self.http.request("GET", testenv["flask_server"] + "/") assert r.status == 200 spans = self.recorder.queued_spans() assert len(spans) == 1 def test_get_request(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() @@ -118,7 +118,7 @@ def test_get_request(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_get_request_with_query_params(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["flask_server"] + "/" + "?key1=val1&key2=val2" ) @@ -194,8 +194,10 @@ def test_get_request_with_query_params(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_get_request_with_suppression(self) -> None: - headers = {'X-INSTANA-L':'0'} - response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) + headers = {"X-INSTANA-L": "0"} + response = self.http.urlopen( + "GET", testenv["flask_server"] + "/", headers=headers + ) spans = self.recorder.queued_spans() @@ -214,11 +216,14 @@ def test_get_request_with_suppression(self) -> None: def test_get_request_with_suppression_and_w3c(self) -> None: """Incoming Level 0 Plus W3C Trace Context Specification Headers""" headers = { - 'X-INSTANA-L':'0', - 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'tracestate': 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7'} + "X-INSTANA-L": "0", + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + "tracestate": "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", + } - response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) + response = self.http.urlopen( + "GET", testenv["flask_server"] + "/", headers=headers + ) spans = self.recorder.queued_spans() @@ -228,7 +233,9 @@ def test_get_request_with_suppression_and_w3c(self) -> None: assert not response.headers.get("X-INSTANA-S", None) assert response.headers.get("traceparent", None) is not None - assert response.headers["traceparent"].startswith("00-0af7651916cd43dd8448eb211c80319c") + assert response.headers["traceparent"].startswith( + "00-0af7651916cd43dd8448eb211c80319c" + ) assert response.headers["traceparent"][-1] == "0" # The tracestate has to be present assert response.headers.get("tracestate", None) is not None @@ -240,12 +247,10 @@ def test_get_request_with_suppression_and_w3c(self) -> None: assert spans == [] def test_synthetic_request(self) -> None: - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/', headers=headers) + with self.tracer.start_as_current_span("test"): + _ = self.http.request("GET", testenv["flask_server"] + "/", headers=headers) spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -259,8 +264,8 @@ def test_synthetic_request(self) -> None: assert test_span.sy is None def test_render_template(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/render') + with self.tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["flask_server"] + "/render") spans = self.recorder.queued_spans() assert len(spans) == 4 @@ -339,8 +344,10 @@ def test_render_template(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_render_template_string(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/render_string') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/render_string" + ) spans = self.recorder.queued_spans() assert len(spans) == 4 @@ -422,8 +429,10 @@ def test_render_template_string(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_301(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/301', redirect=False) + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/301", redirect=False + ) spans = self.recorder.queued_spans() @@ -463,8 +472,8 @@ def test_301(self) -> None: # Error logging assert test_span.ec is None - assert None == urllib3_span.ec - assert None == wsgi_span.ec + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi assert "wsgi" == wsgi_span.n @@ -491,8 +500,8 @@ def test_301(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_custom_404(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/custom-404') + with self.tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["flask_server"] + "/custom-404") spans = self.recorder.queued_spans() @@ -532,8 +541,8 @@ def test_custom_404(self) -> None: # Error logging assert test_span.ec is None - assert None == urllib3_span.ec - assert None == wsgi_span.ec + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi assert "wsgi" == wsgi_span.n @@ -562,8 +571,10 @@ def test_custom_404(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_404(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/11111111111') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/11111111111" + ) spans = self.recorder.queued_spans() @@ -603,8 +614,8 @@ def test_404(self) -> None: # Error logging assert test_span.ec is None - assert None == urllib3_span.ec - assert None == wsgi_span.ec + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi assert "wsgi" == wsgi_span.n @@ -633,8 +644,8 @@ def test_404(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_500(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/500') + with self.tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["flask_server"] + "/500") spans = self.recorder.queued_spans() @@ -705,8 +716,10 @@ def test_render_error(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/render_error') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/render_error" + ) spans = self.recorder.queued_spans() @@ -774,7 +787,8 @@ def test_render_error(self) -> None: assert "urllib3" == urllib3_span.n assert 500 == urllib3_span.data["http"]["status"] assert ( - testenv["flask_server"] + "/render_error" == urllib3_span.data["http"]["url"] + testenv["flask_server"] + "/render_error" + == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None @@ -788,8 +802,8 @@ def test_exception(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/exception') + with self.tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["flask_server"] + "/exception") spans = self.recorder.queued_spans() @@ -840,7 +854,9 @@ def test_exception(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 500 == urllib3_span.data["http"]["status"] - assert testenv["flask_server"] + "/exception" == urllib3_span.data["http"]["url"] + assert ( + testenv["flask_server"] + "/exception" == urllib3_span.data["http"]["url"] + ) assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -850,8 +866,10 @@ def test_exception(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_custom_exception_with_log(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/exception-invalid-usage') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/exception-invalid-usage" + ) spans = self.recorder.queued_spans() @@ -932,8 +950,10 @@ def test_custom_exception_with_log(self) -> None: assert wsgi_span.data["http"]["path_tpl"] is None def test_path_templates(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/users/Ricky/sayhello') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/users/Ricky/sayhello" + ) spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -1012,7 +1032,7 @@ def test_request_header_capture(self) -> None: "X-Capture-That-Too": "that too", } - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["flask_server"] + "/", headers=request_headers ) @@ -1049,14 +1069,15 @@ def test_request_header_capture(self) -> None: agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["flask_server"] + '/response_headers') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/response_headers" + ) spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -1135,7 +1156,7 @@ def test_response_header_capture(self) -> None: agent.options.extra_http_headers = original_extra_http_headers def test_request_started_exception(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): with patch( "instana.singletons.tracer.extract", side_effect=Exception("mocked error"), diff --git a/tests/frameworks/test_gevent.py b/tests/frameworks/test_gevent.py index 31847024..cc02131a 100644 --- a/tests/frameworks/test_gevent.py +++ b/tests/frameworks/test_gevent.py @@ -1,29 +1,34 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import os -import pytest +from typing import Generator -import urllib3 import gevent +import pytest +import urllib3 from gevent.pool import Group -from typing import Generator - -import tests.apps.flask_app -from instana.singletons import tracer -from tests.helpers import testenv, get_spans_by_filter, filter_test_span +import tests.apps.flask_app # noqa: F401 +from instana.singletons import get_tracer +from tests.helpers import filter_test_span, get_spans_by_filter, testenv # Skip the tests if the environment variable `GEVENT_TEST` is not set -pytestmark = pytest.mark.skipif(not os.environ.get("GEVENT_TEST"), reason="GEVENT_TEST not set") +pytestmark = pytest.mark.skipif( + not os.environ.get("GEVENT_TEST"), reason="GEVENT_TEST not set" +) class TestGEvent: @classmethod def setup_class(cls) -> None: """Setup that runs once before all tests in the class""" - cls.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["flask_port"], maxsize=20) - cls.recorder = tracer.span_processor + cls.http = urllib3.HTTPConnectionPool( + "127.0.0.1", port=testenv["flask_port"], maxsize=20 + ) + cls.tracer = get_tracer() + cls.recorder = cls.tracer.span_processor @pytest.fixture(autouse=True) def setUp(self) -> Generator[None, None, None]: @@ -32,11 +37,11 @@ def setUp(self) -> Generator[None, None, None]: def make_http_call(self, n=None): """Helper function to make HTTP calls""" - return self.http.request('GET', testenv["flask_server"] + '/') + return self.http.request("GET", testenv["flask_server"] + "/") def spawn_calls(self): """Helper function to spawn multiple HTTP calls""" - with tracer.start_as_current_span('spawn_calls'): + with self.tracer.start_as_current_span("spawn_calls"): jobs = [] jobs.append(gevent.spawn(self.make_http_call)) jobs.append(gevent.spawn(self.make_http_call)) @@ -47,47 +52,56 @@ def spawn_imap_unordered(self): """Helper function to test imap_unordered""" igroup = Group() result = [] - with tracer.start_as_current_span('test'): + with self.tracer.start_as_current_span("test"): for i in igroup.imap_unordered(self.make_http_call, range(3)): result.append(i) def launch_gevent_chain(self): """Helper function to launch a chain of gevent calls""" - with tracer.start_as_current_span('test'): + with self.tracer.start_as_current_span("test"): gevent.spawn(self.spawn_calls).join() def test_spawning(self): gevent.spawn(self.launch_gevent_chain) gevent.sleep(2) - + spans = self.recorder.queued_spans() - + assert len(spans) == 8 - + test_spans = get_spans_by_filter(spans, filter_test_span) assert test_spans assert len(test_spans) == 1 - + test_span = test_spans[0] - - span_filter = lambda span: span.n == "sdk" \ - and span.data['sdk']['name'] == 'spawn_calls' and span.p == test_span.s + + def span_filter(span): + return ( + span.n == "sdk" + and span.data["sdk"]["name"] == "spawn_calls" + and span.p == test_span.s + ) + spawn_spans = get_spans_by_filter(spans, span_filter) assert spawn_spans assert len(spawn_spans) == 1 - + spawn_span = spawn_spans[0] - - span_filter = lambda span: span.n == "urllib3" + + def span_filter(span): + return span.n == "urllib3" + urllib3_spans = get_spans_by_filter(spans, span_filter) - + for urllib3_span in urllib3_spans: # spans should all have the same test span parent assert urllib3_span.t == spawn_span.t assert urllib3_span.p == spawn_span.s - + # find the wsgi span generated from this urllib3 request - span_filter = lambda span: span.n == "wsgi" and span.p == urllib3_span.s + def span_filter(span): + return span.n == "wsgi" and span.p == urllib3_span.s + wsgi_spans = get_spans_by_filter(spans, span_filter) assert wsgi_spans is not None assert len(wsgi_spans) == 1 @@ -95,27 +109,31 @@ def test_spawning(self): def test_imap_unordered(self): gevent.spawn(self.spawn_imap_unordered) gevent.sleep(2) - + spans = self.recorder.queued_spans() assert len(spans) == 7 - + test_spans = get_spans_by_filter(spans, filter_test_span) assert test_spans is not None assert len(test_spans) == 1 - + test_span = test_spans[0] - - span_filter = lambda span: span.n == "urllib3" + + def span_filter(span): + return span.n == "urllib3" + urllib3_spans = get_spans_by_filter(spans, span_filter) assert len(urllib3_spans) == 3 - + for urllib3_span in urllib3_spans: # spans should all have the same test span parent assert urllib3_span.t == test_span.t assert urllib3_span.p == test_span.s - + # find the wsgi span generated from this urllib3 request - span_filter = lambda span: span.n == "wsgi" and span.p == urllib3_span.s + def span_filter(span): + return span.n == "wsgi" and span.p == urllib3_span.s + wsgi_spans = get_spans_by_filter(spans, span_filter) assert wsgi_spans is not None assert len(wsgi_spans) == 1 diff --git a/tests/frameworks/test_grpcio.py b/tests/frameworks/test_grpcio.py index 0638f64a..2b716e1a 100644 --- a/tests/frameworks/test_grpcio.py +++ b/tests/frameworks/test_grpcio.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import time import random from typing import Generator @@ -15,7 +16,7 @@ import tests.apps.grpc_server.stan_pb2_grpc as stan_pb2_grpc from tests.helpers import testenv, get_first_span_by_name -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span @@ -23,7 +24,8 @@ class TestGRPCIO: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() self.channel = grpc.insecure_channel(testenv["grpc_server"]) self.server_stub = stan_pb2_grpc.StanStub(self.channel) @@ -71,7 +73,7 @@ def test_vanilla_request_via_with_call(self) -> None: ) def test_unary_one_to_one(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.server_stub.OneQuestionOneResponse( stan_pb2.QuestionRequest(question="Are you there?") ) @@ -134,7 +136,7 @@ def test_unary_one_to_one(self) -> None: assert test_span.data["sdk"]["name"] == "test" def test_streaming_many_to_one(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.server_stub.ManyQuestionsOneResponse( self.generate_questions() ) @@ -195,7 +197,7 @@ def test_streaming_many_to_one(self) -> None: assert test_span.data["sdk"]["name"] == "test" def test_streaming_one_to_many(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): responses = self.server_stub.OneQuestionManyResponses( stan_pb2.QuestionRequest(question="Are you there?") ) @@ -259,7 +261,7 @@ def test_streaming_one_to_many(self) -> None: assert test_span.data["sdk"]["name"] == "test" def test_streaming_many_to_many(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): responses = self.server_stub.ManyQuestionsManyReponses( self.generate_questions() ) @@ -323,7 +325,7 @@ def test_streaming_many_to_many(self) -> None: assert test_span.data["sdk"]["name"] == "test" def test_unary_one_to_one_with_call(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.server_stub.OneQuestionOneResponse.with_call( stan_pb2.QuestionRequest(question="Are you there?") ) @@ -386,7 +388,7 @@ def test_unary_one_to_one_with_call(self) -> None: assert test_span.data["sdk"]["name"] == "test" def test_streaming_many_to_one_with_call(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.server_stub.ManyQuestionsOneResponse.with_call( self.generate_questions() ) @@ -456,7 +458,7 @@ def process_response(future): == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" ) - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): future = self.server_stub.OneQuestionOneResponse.future( stan_pb2.QuestionRequest(question="Are you there?") ) @@ -520,7 +522,7 @@ def process_response(future): assert result.was_answered assert result.answer == "Ok" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): future = self.server_stub.ManyQuestionsOneResponse.future( self.generate_questions() ) @@ -582,7 +584,7 @@ def process_response(future): def test_server_error(self) -> None: response = None - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: response = self.server_stub.OneQuestionOneErrorResponse( stan_pb2.QuestionRequest(question="Do u error?") diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index 72f934c5..3839e9e4 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -1,13 +1,14 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Generator import pytest import urllib3 import tests.apps.pyramid.pyramid_app # noqa: F401 -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span from instana.util.ids import hex_id from tests.helpers import testenv @@ -18,7 +19,8 @@ class TestPyramid: def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() def test_vanilla_requests(self) -> None: @@ -29,7 +31,7 @@ def test_vanilla_requests(self) -> None: assert len(spans) == 1 def test_get_request(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["pyramid_server"] + "/") spans = self.recorder.queued_spans() @@ -100,7 +102,7 @@ def test_get_request(self) -> None: def test_synthetic_request(self) -> None: headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/", headers=headers ) @@ -119,7 +121,7 @@ def test_synthetic_request(self) -> None: assert not test_span.sy def test_500(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["pyramid_server"] + "/500") spans = self.recorder.queued_spans() @@ -184,7 +186,7 @@ def test_500(self) -> None: assert len(urllib3_span.stack) > 1 def test_return_error_response(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/return_error_response" ) @@ -209,7 +211,7 @@ def test_return_error_response(self) -> None: assert pyramid_span.ec == 1 def test_fail_with_http_exception(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/fail_with_http_exception" ) @@ -234,7 +236,7 @@ def test_fail_with_http_exception(self) -> None: assert pyramid_span.ec == 1 def test_exception(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/exception" ) @@ -292,7 +294,7 @@ def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/response_headers" ) @@ -366,7 +368,7 @@ def test_request_header_capture(self) -> None: "X-Capture-That-Too": "that too", } - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/", headers=request_headers ) @@ -428,7 +430,7 @@ def test_request_header_capture(self) -> None: agent.options.extra_http_headers = original_extra_http_headers def test_scrub_secret_path_template(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/hello_user/oswald?secret=sshhh" ) diff --git a/tests/frameworks/test_sanic.py b/tests/frameworks/test_sanic.py index 7aa08e21..c938937c 100644 --- a/tests/frameworks/test_sanic.py +++ b/tests/frameworks/test_sanic.py @@ -1,13 +1,18 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 + import pytest from typing import Generator from sanic_testing.testing import SanicTestClient -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.util.ids import hex_id -from tests.helpers import get_first_span_by_filter, get_first_span_by_name, filter_test_span +from tests.helpers import ( + get_first_span_by_filter, + get_first_span_by_name, + filter_test_span, +) from tests.test_utils import _TraceContextMixin from tests.apps.sanic_app.server import app @@ -17,6 +22,7 @@ class TestSanic(_TraceContextMixin): def setup_class(cls) -> None: cls.client = SanicTestClient(app, port=1337, host="127.0.0.1") cls.endpoint = f"{cls.client.host}:{cls.client.port}" + cls.tracer = get_tracer() # Hack together a manual custom headers list; We'll use this in tests agent.options.extra_http_headers = [ @@ -31,7 +37,8 @@ def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" # setup # Clear all spans before a test run - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() def test_vanilla_get(self) -> None: @@ -49,7 +56,7 @@ def test_vanilla_get(self) -> None: def test_basic_get(self) -> None: path = "/" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 200 @@ -100,7 +107,7 @@ def test_basic_get(self) -> None: def test_404(self) -> None: path = "/foo/not_an_int" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 404 @@ -151,7 +158,7 @@ def test_404(self) -> None: def test_sanic_exception(self) -> None: path = "/wrong" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 400 @@ -202,7 +209,7 @@ def test_sanic_exception(self) -> None: def test_500_instana_exception(self) -> None: path = "/instana_exception" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 500 @@ -253,7 +260,7 @@ def test_500_instana_exception(self) -> None: def test_500(self) -> None: path = "/test_request_args" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 500 @@ -304,7 +311,7 @@ def test_500(self) -> None: def test_path_templates(self) -> None: path = "/foo/1" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 200 @@ -355,8 +362,8 @@ def test_path_templates(self) -> None: def test_secret_scrubbing(self) -> None: path = "/" - with tracer.start_as_current_span("test"): - request, response = self.client.get(path+"?secret=shhh") + with self.tracer.start_as_current_span("test"): + request, response = self.client.get(path + "?secret=shhh") assert response.status_code == 200 @@ -406,7 +413,7 @@ def test_secret_scrubbing(self) -> None: def test_synthetic_request(self) -> None: path = "/" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): headers = { "X-INSTANA-SYNTHETIC": "1", } @@ -464,7 +471,7 @@ def test_synthetic_request(self) -> None: def test_request_header_capture(self) -> None: path = "/" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): headers = { "X-Capture-This": "this", "X-Capture-That": "that", @@ -515,7 +522,7 @@ def test_request_header_capture(self) -> None: def test_response_header_capture(self) -> None: path = "/response_headers" - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): request, response = self.client.get(path) assert response.status_code == 200 diff --git a/tests/frameworks/test_spyne.py b/tests/frameworks/test_spyne.py index 999b9b6d..4f9b60b2 100644 --- a/tests/frameworks/test_spyne.py +++ b/tests/frameworks/test_spyne.py @@ -5,9 +5,9 @@ import pytest from typing import Generator -from tests.apps import spyne_app from tests.helpers import testenv -from instana.singletons import agent, tracer +from tests.apps import spyne_app # noqa: F401 +from instana.singletons import get_tracer from instana.span.span import get_current_span from instana.util.ids import hex_id @@ -17,7 +17,8 @@ class TestSpyne: def _resource(self) -> Generator[None, None, None]: """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() time.sleep(0.1) @@ -30,7 +31,7 @@ def test_vanilla_requests(self) -> None: assert response.status == 200 def test_get_request(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["spyne_server"] + "/hello") spans = self.recorder.queued_spans() @@ -86,8 +87,11 @@ def test_get_request(self) -> None: assert spyne_span.stack is None def test_secret_scrubbing(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request("GET", testenv["spyne_server"] + "/say_hello?name=World×=4&secret=sshhh") + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", + testenv["spyne_server"] + "/say_hello?name=World×=4&secret=sshhh", + ) spans = self.recorder.queued_spans() @@ -137,21 +141,24 @@ def test_secret_scrubbing(self) -> None: assert spyne_span.n == "rpc-server" assert spyne_span.data["rpc"]["host"] == "127.0.0.1" assert spyne_span.data["rpc"]["call"] == "/say_hello" - assert spyne_span.data["rpc"]["params"] == "name=World×=4&secret=" + assert ( + spyne_span.data["rpc"]["params"] == "name=World×=4&secret=" + ) assert spyne_span.data["rpc"]["port"] == str(testenv["spyne_port"]) assert spyne_span.data["rpc"]["error"] is None assert spyne_span.stack is None def test_custom_404(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request("GET", testenv["spyne_server"] + "/custom_404?user_id=9876") + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["spyne_server"] + "/custom_404?user_id=9876" + ) spans = self.recorder.queued_spans() assert len(spans) == 4 assert get_current_span().is_recording() is False - log_span = spans[0] spyne_span = spans[1] urllib3_span = spans[2] test_span = spans[3] @@ -214,7 +221,7 @@ def test_custom_404(self) -> None: assert len(urllib3_span.stack) > 1 def test_404(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["spyne_server"] + "/11111") spans = self.recorder.queued_spans() @@ -274,16 +281,14 @@ def test_404(self) -> None: assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 404 - assert ( - testenv["spyne_server"] + "/11111" == urllib3_span.data["http"]["url"] - ) + assert testenv["spyne_server"] + "/11111" == urllib3_span.data["http"]["url"] assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list assert len(urllib3_span.stack) > 1 def test_500(self) -> None: - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["spyne_server"] + "/exception") spans = self.recorder.queued_spans() @@ -291,7 +296,6 @@ def test_500(self) -> None: assert len(spans) == 4 assert get_current_span().is_recording() is False - log_span = spans[0] spyne_span = spans[1] urllib3_span = spans[2] test_span = spans[3] @@ -335,6 +339,6 @@ def test_500(self) -> None: assert spyne_span.n == "rpc-server" assert spyne_span.data["rpc"]["host"] == "127.0.0.1" assert spyne_span.data["rpc"]["call"] == "/exception" - assert spyne_span.data["rpc"]["port"] == str(testenv["spyne_port"]) + assert spyne_span.data["rpc"]["port"] == str(testenv["spyne_port"]) assert spyne_span.data["rpc"]["error"] assert spyne_span.stack is None diff --git a/tests/frameworks/test_starlette.py b/tests/frameworks/test_starlette.py index d44f39d8..6da96a6c 100644 --- a/tests/frameworks/test_starlette.py +++ b/tests/frameworks/test_starlette.py @@ -1,10 +1,11 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Generator import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from starlette.testclient import TestClient from instana.util.ids import hex_id @@ -19,15 +20,16 @@ def _resource(self) -> Generator[None, None, None]: # setup # We are using the TestClient from Starlette to make it easier. self.client = TestClient(starlette_server) + self.tracer = get_tracer() # Configure to capture custom headers agent.options.extra_http_headers = [ "X-Capture-This", "X-Capture-That", "X-Capture-This-Too", - "X-Capture-That-Too" + "X-Capture-That-Too", ] # Clear all spans before a test run. - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() def test_vanilla_get(self) -> None: @@ -49,7 +51,7 @@ def test_vanilla_get(self) -> None: def test_basic_get(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -98,7 +100,7 @@ def test_basic_get(self) -> None: def test_path_templates(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -147,7 +149,7 @@ def test_path_templates(self) -> None: def test_secret_scrubbing(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -195,7 +197,7 @@ def test_secret_scrubbing(self) -> None: assert asgi_span.data["http"]["params"] == "secret=" def test_synthetic_request(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -247,7 +249,7 @@ def test_synthetic_request(self) -> None: assert not test_span.sy def test_request_header_capture(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -302,7 +304,7 @@ def test_request_header_capture(self) -> None: assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] def test_response_header_capture(self) -> None: - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() diff --git a/tests/frameworks/test_starlette_middleware.py b/tests/frameworks/test_starlette_middleware.py index 5e35c376..e14a2426 100644 --- a/tests/frameworks/test_starlette_middleware.py +++ b/tests/frameworks/test_starlette_middleware.py @@ -1,10 +1,11 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + from typing import Generator import pytest -from instana.singletons import agent, tracer +from instana.singletons import get_tracer from starlette.testclient import TestClient from instana.util.ids import hex_id @@ -22,9 +23,10 @@ def _resource(self) -> Generator[None, None, None]: """SetUp and TearDown""" # setup # We are using the TestClient from Starlette to make it easier. + self.tracer = get_tracer() self.client = TestClient(starlette_server) # Clear all spans before a test run. - self.recorder = tracer.span_processor + self.recorder = self.tracer.span_processor self.recorder.clear_spans() yield @@ -47,7 +49,7 @@ def test_vanilla_get(self) -> None: def test_basic_get(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() @@ -96,7 +98,7 @@ def test_basic_get(self) -> None: def test_basic_get_500(self) -> None: result = None - with tracer.start_as_current_span("test") as span: + with self.tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. span_context = span.get_span_context() diff --git a/tests/frameworks/test_tornado_client.py b/tests/frameworks/test_tornado_client.py index 2c93afdc..a038728f 100644 --- a/tests/frameworks/test_tornado_client.py +++ b/tests/frameworks/test_tornado_client.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import time import asyncio import pytest @@ -8,19 +9,19 @@ import tornado from tornado.httpclient import AsyncHTTPClient -from instana.singletons import tracer, agent +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span - +import tests.apps.tornado_server # noqa: F401 from instana.util.ids import hex_id -import tests.apps.tornado_server from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter -class TestTornadoClient: +class TestTornadoClient: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Clear all spans before a test run """ - self.recorder = tracer.span_processor + """Clear all spans before a test run""" + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -34,7 +35,7 @@ def _resource(self) -> Generator[None, None, None]: def test_get(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/") response = tornado.ioloop.IOLoop.current().run_sync(test) @@ -87,14 +88,16 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_post(self) -> None: async def test(): - with tracer.start_as_current_span("test"): - return await self.http_client.fetch(testenv["tornado_server"] + "/", method="POST", body='asdf') + with self.tracer.start_as_current_span("test"): + return await self.http_client.fetch( + testenv["tornado_server"] + "/", method="POST", body="asdf" + ) response = tornado.ioloop.IOLoop.current().run_sync(test) assert isinstance(response, tornado.httpclient.HTTPResponse) @@ -142,13 +145,13 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_301(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/301") response = tornado.ioloop.IOLoop.current().run_sync(test) @@ -164,13 +167,30 @@ async def test(): client301_span = spans[3] test_span = spans[4] - filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 + def filter(span): + return span.n == "tornado-server" and span.data["http"]["status"] == 301 + server301_span = get_first_span_by_filter(spans, filter) - filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 200 + + def filter(span): + return span.n == "tornado-server" and span.data["http"]["status"] == 200 + server_span = get_first_span_by_filter(spans, filter) - filter = lambda span: span.n == "tornado-client" and span.data["http"]["url"] == testenv["tornado_server"] + "/" + + def filter(span): + return ( + span.n == "tornado-client" + and span.data["http"]["url"] == testenv["tornado_server"] + "/" + ) + client_span = get_first_span_by_filter(spans, filter) - filter = lambda span: span.n == "tornado-client" and span.data["http"]["url"] == testenv["tornado_server"] + "/301" + + def filter(span): + return ( + span.n == "tornado-client" + and span.data["http"]["url"] == testenv["tornado_server"] + "/301" + ) + client301_span = get_first_span_by_filter(spans, filter) test_span = get_first_span_by_name(spans, "sdk") @@ -227,15 +247,17 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_405(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: - return await self.http_client.fetch(testenv["tornado_server"] + "/405") + return await self.http_client.fetch( + testenv["tornado_server"] + "/405" + ) except tornado.httpclient.HTTPClientError as e: return e.response @@ -285,15 +307,17 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_500(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: - return await self.http_client.fetch(testenv["tornado_server"] + "/500") + return await self.http_client.fetch( + testenv["tornado_server"] + "/500" + ) except tornado.httpclient.HTTPClientError as e: return e.response @@ -343,15 +367,17 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_504(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): try: - return await self.http_client.fetch(testenv["tornado_server"] + "/504") + return await self.http_client.fetch( + testenv["tornado_server"] + "/504" + ) except tornado.httpclient.HTTPClientError as e: return e.response @@ -401,14 +427,16 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_with_params_to_scrub(self) -> None: async def test(): - with tracer.start_as_current_span("test"): - return await self.http_client.fetch(testenv["tornado_server"] + "/?secret=yeah") + with self.tracer.start_as_current_span("test"): + return await self.http_client.fetch( + testenv["tornado_server"] + "/?secret=yeah" + ) response = tornado.ioloop.IOLoop.current().run_sync(test) assert isinstance(response, tornado.httpclient.HTTPResponse) @@ -440,13 +468,13 @@ async def test(): assert server_span.n == "tornado-server" assert server_span.data["http"]["status"] == 200 assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] - assert 'secret=' == server_span.data["http"]["params"] + assert "secret=" == server_span.data["http"]["params"] assert server_span.data["http"]["method"] == "GET" assert client_span.n == "tornado-client" assert client_span.data["http"]["status"] == 200 assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] - assert 'secret=' == client_span.data["http"]["params"] + assert "secret=" == client_span.data["http"]["params"] assert client_span.data["http"]["method"] == "GET" assert client_span.stack assert type(client_span.stack) is list @@ -457,7 +485,7 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @@ -472,8 +500,10 @@ def test_request_header_capture(self) -> None: } async def test(): - with tracer.start_as_current_span("test"): - return await self.http_client.fetch(testenv["tornado_server"] + "/", headers=request_headers) + with self.tracer.start_as_current_span("test"): + return await self.http_client.fetch( + testenv["tornado_server"] + "/", headers=request_headers + ) response = tornado.ioloop.IOLoop.current().run_sync(test) assert isinstance(response, tornado.httpclient.HTTPResponse) @@ -522,7 +552,7 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @@ -539,8 +569,10 @@ def test_response_header_capture(self) -> None: agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] async def test(): - with tracer.start_as_current_span("test"): - return await self.http_client.fetch(testenv["tornado_server"] + "/response_headers") + with self.tracer.start_as_current_span("test"): + return await self.http_client.fetch( + testenv["tornado_server"] + "/response_headers" + ) response = tornado.ioloop.IOLoop.current().run_sync(test) assert isinstance(response, tornado.httpclient.HTTPResponse) @@ -572,13 +604,19 @@ async def test(): assert server_span.n == "tornado-server" assert server_span.data["http"]["status"] == 200 - assert testenv["tornado_server"] + "/response_headers" == server_span.data["http"]["url"] + assert ( + testenv["tornado_server"] + "/response_headers" + == server_span.data["http"]["url"] + ) assert not server_span.data["http"]["params"] assert server_span.data["http"]["method"] == "GET" assert client_span.n == "tornado-client" assert client_span.data["http"]["status"] == 200 - assert testenv["tornado_server"] + "/response_headers" == client_span.data["http"]["url"] + assert ( + testenv["tornado_server"] + "/response_headers" + == client_span.data["http"]["url"] + ) assert client_span.data["http"]["method"] == "GET" assert client_span.stack assert type(client_span.stack) is list @@ -589,7 +627,7 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" diff --git a/tests/frameworks/test_tornado_server.py b/tests/frameworks/test_tornado_server.py index 2287fcc4..f7e13388 100644 --- a/tests/frameworks/test_tornado_server.py +++ b/tests/frameworks/test_tornado_server.py @@ -1,20 +1,20 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import pytest -from typing import Generator import asyncio +from typing import Generator + import aiohttp +import pytest import tornado from tornado.httpclient import AsyncHTTPClient -from instana.util.ids import hex_id -import tests.apps.tornado_server - -from instana.singletons import tracer, agent -from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter +import tests.apps.tornado_server # noqa: F401 +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span +from instana.util.ids import hex_id +from tests.helpers import get_first_span_by_filter, get_first_span_by_name, testenv class TestTornadoServer: @@ -27,15 +27,18 @@ async def fetch(self, session, url, headers=None, params=None): async def post(self, session, url, headers=None): try: - async with session.post(url, headers=headers, data={"hello": "post"}) as response: + async with session.post( + url, headers=headers, data={"hello": "post"} + ) as response: return response except aiohttp.web_exceptions.HTTPException: pass @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Clear all spans before a test run """ - self.recorder = tracer.span_processor + """Clear all spans before a test run""" + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -49,7 +52,7 @@ def _resource(self) -> Generator[None, None, None]: def test_get(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["tornado_server"] + "/") @@ -105,13 +108,13 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_post(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.post(session, testenv["tornado_server"] + "/") @@ -166,19 +169,19 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_synthetic_request(self) -> None: async def test(): - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["tornado_server"] + "/", headers=headers) + return await self.fetch( + session, testenv["tornado_server"] + "/", headers=headers + ) tornado.ioloop.IOLoop.current().run_sync(test) @@ -195,7 +198,7 @@ async def test(): def test_get_301(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["tornado_server"] + "/301") @@ -204,9 +207,14 @@ async def test(): spans = self.recorder.queued_spans() assert len(spans) == 4 - filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 + def filter(span): + return span.n == "tornado-server" and span.data["http"]["status"] == 301 + tornado_301_span = get_first_span_by_filter(spans, filter) - filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 200 + + def filter(span): + return span.n == "tornado-server" and span.data["http"]["status"] == 200 + tornado_span = get_first_span_by_filter(spans, filter) aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") @@ -241,7 +249,9 @@ async def test(): assert not tornado_span.ec assert tornado_301_span.data["http"]["status"] == 301 - assert testenv["tornado_server"] + "/301" == tornado_301_span.data["http"]["url"] + assert ( + testenv["tornado_server"] + "/301" == tornado_301_span.data["http"]["url"] + ) assert not tornado_span.data["http"]["params"] assert tornado_301_span.data["http"]["method"] == "GET" assert not tornado_301_span.stack @@ -263,14 +273,14 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @pytest.mark.skip("Non deterministic (flaky) testcase") def test_get_405(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["tornado_server"] + "/405") @@ -325,14 +335,14 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @pytest.mark.skip("Non deterministic (flaky) testcase") def test_get_500(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["tornado_server"] + "/500") @@ -378,7 +388,7 @@ async def test(): assert aiohttp_span.data["http"]["status"] == 500 assert testenv["tornado_server"] + "/500" == aiohttp_span.data["http"]["url"] assert aiohttp_span.data["http"]["method"] == "GET" - assert 'Internal Server Error' == aiohttp_span.data["http"]["error"] + assert "Internal Server Error" == aiohttp_span.data["http"]["error"] assert aiohttp_span.stack assert isinstance(aiohttp_span.stack, list) assert len(aiohttp_span.stack) > 1 @@ -388,14 +398,14 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @pytest.mark.skip("Non deterministic (flaky) testcase") def test_get_504(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["tornado_server"] + "/504") @@ -441,7 +451,7 @@ async def test(): assert aiohttp_span.data["http"]["status"] == 504 assert testenv["tornado_server"] + "/504" == aiohttp_span.data["http"]["url"] assert aiohttp_span.data["http"]["method"] == "GET" - assert 'Gateway Timeout' == aiohttp_span.data["http"]["error"] + assert "Gateway Timeout" == aiohttp_span.data["http"]["error"] assert aiohttp_span.stack assert isinstance(aiohttp_span.stack, list) assert len(aiohttp_span.stack) > 1 @@ -451,15 +461,17 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_get_with_params_to_scrub(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["tornado_server"], params={"secret": "yeah"}) + return await self.fetch( + session, testenv["tornado_server"], params={"secret": "yeah"} + ) response = tornado.ioloop.IOLoop.current().run_sync(test) @@ -513,23 +525,31 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" def test_request_header_capture(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom request headers list - agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + ] request_headers = { "X-Capture-This": "this", - "X-Capture-That": "that" + "X-Capture-That": "that", } - return await self.fetch(session, testenv["tornado_server"], headers=request_headers, params={"secret": "iloveyou"}) + return await self.fetch( + session, + testenv["tornado_server"], + headers=request_headers, + params={"secret": "iloveyou"}, + ) response = tornado.ioloop.IOLoop.current().run_sync(test) @@ -583,7 +603,7 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" @@ -594,12 +614,19 @@ async def test(): def test_response_header_capture(self) -> None: async def test(): - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom response headers list - agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + agent.options.extra_http_headers = [ + "X-Capture-This-Too", + "X-Capture-That-Too", + ] - return await self.fetch(session, testenv["tornado_server"] + "/response_headers", params={"secret": "itsasecret"}) + return await self.fetch( + session, + testenv["tornado_server"] + "/response_headers", + params={"secret": "itsasecret"}, + ) response = tornado.ioloop.IOLoop.current().run_sync(test) @@ -635,13 +662,19 @@ async def test(): assert not tornado_span.ec assert tornado_span.data["http"]["status"] == 200 - assert testenv["tornado_server"] + "/response_headers" == tornado_span.data["http"]["url"] + assert ( + testenv["tornado_server"] + "/response_headers" + == tornado_span.data["http"]["url"] + ) assert tornado_span.data["http"]["params"] == "secret=" assert tornado_span.data["http"]["method"] == "GET" assert not tornado_span.stack assert aiohttp_span.data["http"]["status"] == 200 - assert testenv["tornado_server"] + "/response_headers" == aiohttp_span.data["http"]["url"] + assert ( + testenv["tornado_server"] + "/response_headers" + == aiohttp_span.data["http"]["url"] + ) assert aiohttp_span.data["http"]["method"] == "GET" assert aiohttp_span.data["http"]["params"] == "secret=" assert aiohttp_span.stack @@ -653,7 +686,7 @@ async def test(): assert "X-INSTANA-S" in response.headers assert response.headers["X-INSTANA-S"] == hex_id(tornado_span.s) assert "X-INSTANA-L" in response.headers - assert response.headers["X-INSTANA-L"] == '1' + assert response.headers["X-INSTANA-L"] == "1" assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 882c5cfd..056a4c01 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -1,29 +1,31 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 + import time import urllib3 import pytest from typing import Generator from instana.util.ids import hex_id -from tests.apps import bottle_app from tests.helpers import testenv -from instana.singletons import agent, tracer +from tests.apps import bottle_app # noqa: F401 +from instana.singletons import agent, get_tracer from instana.span.span import get_current_span class TestWSGI: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Clear all spans before a test run """ + """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.span_processor + self.tracer = get_tracer() + self.recorder = self.tracer.span_processor self.recorder.clear_spans() time.sleep(0.1) def test_vanilla_requests(self) -> None: - response = self.http.request('GET', testenv["wsgi_server"] + '/') + response = self.http.request("GET", testenv["wsgi_server"] + "/") spans = self.recorder.queued_spans() assert 1 == len(spans) @@ -31,8 +33,8 @@ def test_vanilla_requests(self) -> None: assert response.status == 200 def test_get_request(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/') + with self.tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["wsgi_server"] + "/") spans = self.recorder.queued_spans() @@ -46,20 +48,20 @@ def test_get_request(self) -> None: assert response assert 200 == response.status - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value # Same traceId assert test_span.t == urllib3_span.t @@ -80,19 +82,19 @@ def test_get_request(self) -> None: # wsgi assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] - assert '/' == wsgi_span.data["http"]["path"] - assert 'GET' == wsgi_span.data["http"]["method"] + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/" == wsgi_span.data["http"]["path"] + assert "GET" == wsgi_span.data["http"]["method"] assert "200" == wsgi_span.data["http"]["status"] assert wsgi_span.data["http"]["error"] is None assert wsgi_span.stack is None def test_synthetic_request(self) -> None: - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) + headers = {"X-INSTANA-SYNTHETIC": "1"} + with self.tracer.start_as_current_span("test"): + _ = self.http.request("GET", testenv["wsgi_server"] + "/", headers=headers) spans = self.recorder.queued_spans() @@ -108,8 +110,10 @@ def test_synthetic_request(self) -> None: assert test_span.sy is None def test_secret_scrubbing(self) -> None: - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/?secret=shhh') + with self.tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["wsgi_server"] + "/?secret=shhh" + ) spans = self.recorder.queued_spans() @@ -123,20 +127,20 @@ def test_secret_scrubbing(self) -> None: assert response assert 200 == response.status - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value # Same traceId assert test_span.t == urllib3_span.t @@ -153,20 +157,24 @@ def test_secret_scrubbing(self) -> None: # wsgi assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] - assert '/' == wsgi_span.data["http"]["path"] - assert 'secret=' == wsgi_span.data["http"]["params"] - assert 'GET' == wsgi_span.data["http"]["method"] + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/" == wsgi_span.data["http"]["path"] + assert "secret=" == wsgi_span.data["http"]["params"] + assert "GET" == wsgi_span.data["http"]["method"] assert "200" == wsgi_span.data["http"]["status"] assert wsgi_span.data["http"]["error"] is None assert wsgi_span.stack is None def test_with_incoming_context(self) -> None: request_headers = dict() - request_headers['X-INSTANA-T'] = '0000000000000001' - request_headers['X-INSTANA-S'] = '0000000000000001' + request_headers["X-INSTANA-T"] = "0000000000000001" + request_headers["X-INSTANA-S"] = "0000000000000001" - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) + response = self.http.request( + "GET", testenv["wsgi_server"] + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -181,27 +189,29 @@ def test_with_incoming_context(self) -> None: assert wsgi_span.t == 1 assert wsgi_span.p == 1 - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() - request_headers['X-InSTANa-T'] = '0000000000000001' - request_headers['X-instana-S'] = '0000000000000001' + request_headers["X-InSTANa-T"] = "0000000000000001" + request_headers["X-instana-S"] = "0000000000000001" - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) + response = self.http.request( + "GET", testenv["wsgi_server"] + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -216,27 +226,27 @@ def test_with_incoming_mixed_case_context(self) -> None: assert wsgi_span.t == 1 assert wsgi_span.p == 1 - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["wsgi_server"] + "/response_headers" ) @@ -271,7 +281,9 @@ def test_response_header_capture(self) -> None: # wsgi assert wsgi_span.n == "wsgi" - assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["wsgi_port"]) + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) assert wsgi_span.data["http"]["path"] == "/response_headers" assert wsgi_span.data["http"]["method"] == "GET" assert wsgi_span.data["http"]["status"] == "200" @@ -294,7 +306,7 @@ def test_request_header_capture(self) -> None: "X-Capture-That-Too": "that too", } - with tracer.start_as_current_span("test"): + with self.tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["wsgi_server"] + "/", headers=request_headers ) @@ -328,7 +340,9 @@ def test_request_header_capture(self) -> None: # wsgi assert wsgi_span.n == "wsgi" - assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["wsgi_port"]) + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) assert wsgi_span.data["http"]["path"] == "/" assert wsgi_span.data["http"]["method"] == "GET" assert wsgi_span.data["http"]["status"] == "200" diff --git a/tests/helpers.py b/tests/helpers.py index f7c2efc4..07cd94e0 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2018 + import os import pytest @@ -114,6 +115,7 @@ def filter_test_span(span): """ return span.n == "sdk" and span.data["sdk"]["name"] == "test" + def get_first_span_by_name(spans, name): """ Get the first span in that has a span.n value of @@ -168,12 +170,13 @@ def launch_traced_request(url): import requests from instana.log import logger - from instana.singletons import tracer + from instana.singletons import get_tracer logger.warn( "Launching request with a root SDK span name of 'launch_traced_request'" ) + tracer = get_tracer() with tracer.start_as_current_span("launch_traced_request"): response = requests.get(url) diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py index 3cfc87c0..462ed1b8 100644 --- a/tests/util/test_traceutils.py +++ b/tests/util/test_traceutils.py @@ -1,8 +1,10 @@ # (c) Copyright IBM Corp. 2024 + +from typing import Generator import pytest -from instana.singletons import agent, tracer +from instana.singletons import agent, get_tracer from instana.tracer import InstanaTracer from instana.util.traceutils import ( extract_custom_headers, @@ -12,86 +14,93 @@ ) -@pytest.mark.parametrize( - "custom_headers, format", - [ - ( - { - "X-Capture-This-Too": "this too", - "X-Capture-That-Too": "that too", - }, - False, - ), - ( - { - "HTTP_X_CAPTURE_THIS_TOO": "this too", - "HTTP_X_CAPTURE_THAT_TOO": "that too", - }, - True, - ), - ( - [("X-CAPTURE-THIS-TOO", "this too"), ("x-capture-that-too", "that too")], - False, - ), - ( - [ - (b"X-Capture-This-Too", b"this too"), - (b"X-Capture-That-Too", b"that too"), - ], - False, - ), - ( - [ - ("HTTP_X_CAPTURE_THIS_TOO", "this too"), - ("HTTP_X_CAPTURE_THAT_TOO", "that too"), - ], - True, - ), - ], -) -def test_extract_custom_headers(span, custom_headers, format) -> None: - agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] - extract_custom_headers(span, custom_headers, format=format) - assert len(span.attributes) == 2 - assert span.attributes["http.header.X-Capture-This-Too"] == "this too" - assert span.attributes["http.header.X-Capture-That-Too"] == "that too" - - -def test_get_activate_tracer(mocker) -> None: - assert not get_active_tracer() +class TestTraceutils: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.tracer = get_tracer() - with tracer.start_as_current_span("test"): - response = get_active_tracer() - assert isinstance(response, InstanaTracer) - assert response == tracer - with mocker.patch( - "instana.span.span.InstanaSpan.is_recording", return_value=False - ): - assert not get_active_tracer() + @pytest.mark.parametrize( + "custom_headers, format", + [ + ( + { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + }, + False, + ), + ( + { + "HTTP_X_CAPTURE_THIS_TOO": "this too", + "HTTP_X_CAPTURE_THAT_TOO": "that too", + }, + True, + ), + ( + [ + ("X-CAPTURE-THIS-TOO", "this too"), + ("x-capture-that-too", "that too"), + ], + False, + ), + ( + [ + (b"X-Capture-This-Too", b"this too"), + (b"X-Capture-That-Too", b"that too"), + ], + False, + ), + ( + [ + ("HTTP_X_CAPTURE_THIS_TOO", "this too"), + ("HTTP_X_CAPTURE_THAT_TOO", "that too"), + ], + True, + ), + ], + ) + def test_extract_custom_headers(self, span, custom_headers, format) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + extract_custom_headers(span, custom_headers, format=format) + assert len(span.attributes) == 2 + assert span.attributes["http.header.X-Capture-This-Too"] == "this too" + assert span.attributes["http.header.X-Capture-That-Too"] == "that too" + def test_get_activate_tracer(self, mocker) -> None: + assert not get_active_tracer() -def test_get_tracer_tuple() -> None: - response = get_tracer_tuple() - assert response == (None, None, None) + with self.tracer.start_as_current_span("test"): + response = get_active_tracer() + assert isinstance(response, InstanaTracer) + assert response == self.tracer + with mocker.patch( + "instana.span.span.InstanaSpan.is_recording", return_value=False + ): + assert not get_active_tracer() - agent.options.allow_exit_as_root = True - response = get_tracer_tuple() - assert response == (tracer, None, None) - agent.options.allow_exit_as_root = False + def test_get_tracer_tuple( + self, + ) -> None: + response = get_tracer_tuple() + assert response == (None, None, None) - with tracer.start_as_current_span("test") as span: + agent.options.allow_exit_as_root = True response = get_tracer_tuple() - assert response == (tracer, span, span.name) + assert response == (self.tracer, None, None) + agent.options.allow_exit_as_root = False + with self.tracer.start_as_current_span("test") as span: + response = get_tracer_tuple() + assert response == (self.tracer, span, span.name) -def test_tracing_is_off() -> None: - response = tracing_is_off() - assert response - with tracer.start_as_current_span("test"): + def test_tracing_is_off(self) -> None: response = tracing_is_off() - assert not response + assert response + with self.tracer.start_as_current_span("test"): + response = tracing_is_off() + assert not response - agent.options.allow_exit_as_root = True - response = tracing_is_off() - assert not response - agent.options.allow_exit_as_root = False + agent.options.allow_exit_as_root = True + response = tracing_is_off() + assert not response + agent.options.allow_exit_as_root = False From e387b88f3d63fe9f6af39b7657476cc746ecef87 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 24 Nov 2025 10:48:07 +0100 Subject: [PATCH 41/44] chore: add type hinting to tornado instrumentation Signed-off-by: Cagri Yonca --- src/instana/instrumentation/tornado/client.py | 41 ++++++---- src/instana/instrumentation/tornado/server.py | 77 +++++++++++++------ 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/instana/instrumentation/tornado/client.py b/src/instana/instrumentation/tornado/client.py index 134c7f7e..33a4dc51 100644 --- a/src/instana/instrumentation/tornado/client.py +++ b/src/instana/instrumentation/tornado/client.py @@ -1,30 +1,42 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 + try: import tornado import wrapt import functools - from typing import TYPE_CHECKING, Dict, Any + from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple + + if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from asyncio import Future + from tornado.httpclient import AsyncHTTPClient from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger - from instana.singletons import agent, tracer + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format from instana.span.span import get_current_span - - @wrapt.patch_function_wrapper('tornado.httpclient', 'AsyncHTTPClient.fetch') - def fetch_with_instana(wrapped, instance, argv, kwargs): + @wrapt.patch_function_wrapper("tornado.httpclient", "AsyncHTTPClient.fetch") + def fetch_with_instana( + wrapped: Callable[..., object], + instance: "AsyncHTTPClient", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> "Future": try: parent_span = get_current_span() # If we're not tracing, just return - if (not parent_span.is_recording()) or (parent_span.name == "tornado-client"): + if (not parent_span.is_recording()) or ( + parent_span.name == "tornado-client" + ): return wrapped(*argv, **kwargs) request = argv[0] @@ -35,14 +47,14 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): request = tornado.httpclient.HTTPRequest(url=request, **kwargs) new_kwargs = {} - for param in ('callback', 'raise_error'): + for param in ("callback", "raise_error"): # if not in instead and pop if param in kwargs: new_kwargs[param] = kwargs.pop(param) kwargs = new_kwargs parent_context = parent_span.get_span_context() if parent_span else None - + tracer = get_tracer() span = tracer.start_span("tornado-client", span_context=parent_context) extract_custom_headers(span, request.headers) @@ -50,10 +62,11 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): tracer.inject(span.context, Format.HTTP_HEADERS, request.headers) # Query param scrubbing - parts = request.url.split('?') + parts = request.url.split("?") if len(parts) > 1: - cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, - agent.options.secrets_list) + cleaned_qp = strip_secrets_from_query( + parts[1], agent.options.secrets_matcher, agent.options.secrets_list + ) span.set_attribute("http.params", cleaned_qp) span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) @@ -69,8 +82,7 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): except Exception: logger.debug("Tornado fetch_with_instana: ", exc_info=True) - - def finish_tracing(future, span): + def finish_tracing(future: "Future", span: "InstanaSpan") -> None: try: response = future.result() span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.code) @@ -84,7 +96,6 @@ def finish_tracing(future, span): if span.is_recording(): span.end() - logger.debug("Instrumenting tornado client") except ImportError: pass diff --git a/src/instana/instrumentation/tornado/server.py b/src/instana/instrumentation/tornado/server.py index 82266961..dd902998 100644 --- a/src/instana/instrumentation/tornado/server.py +++ b/src/instana/instrumentation/tornado/server.py @@ -1,38 +1,53 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 try: import tornado + from typing import TYPE_CHECKING, Callable, Tuple, Dict, Any, Coroutine, Optional import wrapt + if TYPE_CHECKING: + from tornado.web import RequestHandler + from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger - from instana.singletons import agent, tracer + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format - - - @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler._execute') - def execute_with_instana(wrapped, instance, argv, kwargs): + @wrapt.patch_function_wrapper("tornado.web", "RequestHandler._execute") + def execute_with_instana( + wrapped: Callable[..., object], + instance: "RequestHandler", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> Coroutine: try: span_context = None - if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__: - span_context = tracer.extract(Format.HTTP_HEADERS, - instance.request.headers.__dict__['_dict']) + tracer = get_tracer() + if ( + hasattr(instance.request.headers, "__dict__") + and "_dict" in instance.request.headers.__dict__ + ): + span_context = tracer.extract( + Format.HTTP_HEADERS, instance.request.headers.__dict__["_dict"] + ) span = tracer.start_span("tornado-server", span_context=span_context) # Query param scrubbing if instance.request.query is not None and len(instance.request.query) > 0: - cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher, - agent.options.secrets_list) + cleaned_qp = strip_secrets_from_query( + instance.request.query, + agent.options.secrets_matcher, + agent.options.secrets_list, + ) span.set_attribute("http.params", cleaned_qp) - + url = f"{instance.request.protocol}://{instance.request.host}{instance.request.path}" span.set_attribute(SpanAttributes.HTTP_URL, url) span.set_attribute(SpanAttributes.HTTP_METHOD, instance.request.method) @@ -52,20 +67,29 @@ def execute_with_instana(wrapped, instance, argv, kwargs): except Exception: logger.debug("tornado execute", exc_info=True) - - @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.set_default_headers') - def set_default_headers_with_instana(wrapped, instance, argv, kwargs): - if not hasattr(instance.request, '_instana'): + @wrapt.patch_function_wrapper("tornado.web", "RequestHandler.set_default_headers") + def set_default_headers_with_instana( + wrapped: Callable[..., object], + instance: "RequestHandler", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> Optional[Coroutine]: + if not hasattr(instance.request, "_instana"): return wrapped(*argv, **kwargs) span = instance.request._instana + tracer = get_tracer() tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers) - - @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.on_finish') - def on_finish_with_instana(wrapped, instance, argv, kwargs): + @wrapt.patch_function_wrapper("tornado.web", "RequestHandler.on_finish") + def on_finish_with_instana( + wrapped: Callable[..., object], + instance: "RequestHandler", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> Coroutine: try: - if not hasattr(instance.request, '_instana'): + if not hasattr(instance.request, "_instana"): return wrapped(*argv, **kwargs) span = instance.request._instana @@ -86,11 +110,15 @@ def on_finish_with_instana(wrapped, instance, argv, kwargs): except Exception: logger.debug("tornado on_finish", exc_info=True) - - @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.log_exception') - def log_exception_with_instana(wrapped, instance, argv, kwargs): + @wrapt.patch_function_wrapper("tornado.web", "RequestHandler.log_exception") + def log_exception_with_instana( + wrapped: Callable[..., object], + instance: "RequestHandler", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> Coroutine: try: - if not hasattr(instance.request, '_instana'): + if not hasattr(instance.request, "_instana"): return wrapped(*argv, **kwargs) if not isinstance(argv[1], tornado.web.HTTPError): @@ -101,7 +129,6 @@ def log_exception_with_instana(wrapped, instance, argv, kwargs): except Exception: logger.debug("tornado log_exception", exc_info=True) - logger.debug("Instrumenting tornado server") except ImportError: pass From aff617e5515cd6fb9755352bb27b29a54c8c7700 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 24 Nov 2025 10:48:13 +0100 Subject: [PATCH 42/44] sonarqube: Fix duplicated code blocks for grpcio and flask (vanilla and with_blinker) instrumentations Signed-off-by: Cagri Yonca --- src/instana/instrumentation/flask/common.py | 118 ++++++- src/instana/instrumentation/flask/vanilla.py | 107 +----- .../instrumentation/flask/with_blinker.py | 101 +----- src/instana/instrumentation/grpcio.py | 312 ++++++------------ 4 files changed, 228 insertions(+), 410 deletions(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index a0e6f6fb..f544eca0 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -1,25 +1,31 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 -import wrapt -import flask +import re from importlib.metadata import version -from typing import Callable, Tuple, Dict, Any, TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Type, Union +import flask +import wrapt +from opentelemetry import context, trace from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger -from instana.singletons import tracer from instana.propagators.format import Format - +from instana.singletons import agent, get_tracer +from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import extract_custom_headers if TYPE_CHECKING: - from werkzeug.exceptions import HTTPException from flask.typing import ResponseReturnValue from jinja2.environment import Template + from werkzeug.exceptions import HTTPException -@wrapt.patch_function_wrapper('flask', 'templating._render') +path_tpl_re = re.compile("<.*>") + + +@wrapt.patch_function_wrapper("flask", "templating._render") def render_with_instana( wrapped: Callable[..., str], instance: object, @@ -32,6 +38,7 @@ def render_with_instana( parent_span = flask.g.span parent_context = parent_span.get_span_context() + tracer = get_tracer() with tracer.start_as_current_span("render", span_context=parent_context) as span: try: @@ -50,7 +57,7 @@ def render_with_instana( raise -@wrapt.patch_function_wrapper('flask', 'Flask.handle_user_exception') +@wrapt.patch_function_wrapper("flask", "Flask.handle_user_exception") def handle_user_exception_with_instana( wrapped: Callable[..., Union["HTTPException", "ResponseReturnValue"]], instance: flask.app.Flask, @@ -70,7 +77,7 @@ def handle_user_exception_with_instana( if isinstance(response, tuple): status_code = response[1] else: - if hasattr(response, 'code'): + if hasattr(response, "code"): status_code = response.code else: status_code = response.status_code @@ -80,12 +87,99 @@ def handle_user_exception_with_instana( span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, int(status_code)) - if hasattr(response, 'headers'): + if hasattr(response, "headers"): + tracer = get_tracer() tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) if span and span.is_recording(): span.end() flask.g.span = None - except: + except Exception: logger.debug("handle_user_exception_with_instana:", exc_info=True) return response + + +def create_span(): + env = flask.request.environ + tracer = get_tracer() + span_context = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("wsgi", span_context=span_context) + flask.g.span = span + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + flask.g.token = token + + extract_custom_headers(span, env, format=True) + + span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) + if "PATH_INFO" in env: + span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + + if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( + flask.request.url_rule.rule + ): + path_tpl = flask.request.url_rule.rule.replace("<", "{") + path_tpl = path_tpl.replace(">", "}") + span.set_attribute("http.path_tpl", path_tpl) + + +def inject_span( + response: flask.wrappers.Response, + error_message: str, + set_flask_g_none: bool = False, +): + span = None + try: + # If we're not tracing, just return + if not hasattr(flask.g, "span"): + return response + + span = flask.g.span + if span: + if 500 <= response.status_code: + span.mark_as_errored() + + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) + ) + extract_custom_headers(span, response.headers, format=False) + tracer = get_tracer() + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + except Exception: + logger.debug(error_message, exc_info=True) + finally: + if span and span.is_recording(): + span.end() + if set_flask_g_none: + flask.g.span = None + + +def teardown_request_with_instana(*argv: Union[Exception, Type[Exception]]) -> None: + """ + In the case of exceptions, after_request_with_instana isn't called + so we capture those cases here. + """ + if hasattr(flask.g, "span") and flask.g.span: + if len(argv) > 0 and argv[0]: + span = flask.g.span + span.record_exception(argv[0]) + if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + if flask.g.span.is_recording(): + flask.g.span.end() + flask.g.span = None + + if hasattr(flask.g, "token") and flask.g.token: + context.detach(flask.g.token) + flask.g.token = None diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index fed13f16..b2d14cfb 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -1,58 +1,24 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 -import re +from typing import Callable, Dict, Tuple + import flask import wrapt -from typing import Callable, Tuple, Dict, Type, Union - -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry import context, trace +from instana.instrumentation.flask.common import ( + create_span, + inject_span, + teardown_request_with_instana, +) from instana.log import logger -from instana.singletons import agent, tracer -from instana.util.secrets import strip_secrets_from_query -from instana.util.traceutils import extract_custom_headers -from instana.propagators.format import Format - -path_tpl_re = re.compile('<.*>') def before_request_with_instana() -> None: try: - env = flask.request.environ - span_context = tracer.extract(Format.HTTP_HEADERS, env) - - span = tracer.start_span("wsgi", span_context=span_context) - flask.g.span = span - - ctx = trace.set_span_in_context(span) - token = context.attach(ctx) - flask.g.token = token - - extract_custom_headers(span, env, format=True) - - span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) - if "PATH_INFO" in env: - span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - if "HTTP_HOST" in env: - span.set_attribute("http.host", env["HTTP_HOST"]) - - if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( - flask.request.url_rule.rule - ): - path_tpl = flask.request.url_rule.rule.replace("<", "{") - path_tpl = path_tpl.replace(">", "}") - span.set_attribute("http.path_tpl", path_tpl) - except: + create_span() + except Exception: logger.debug("Flask before_request", exc_info=True) return None @@ -61,62 +27,21 @@ def before_request_with_instana() -> None: def after_request_with_instana( response: flask.wrappers.Response, ) -> flask.wrappers.Response: - span = None - try: - # If we're not tracing, just return - if not hasattr(flask.g, "span"): - return response - - span = flask.g.span - if span: - - if 500 <= response.status_code: - span.mark_as_errored() - - span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) - ) - extract_custom_headers(span, response.headers, format=False) - - tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - except: - logger.debug("Flask after_request", exc_info=True) - finally: - if span and span.is_recording(): - span.end() - flask.g.span = None + inject_span(response, "Flask after_request", set_flask_g_none=True) return response -def teardown_request_with_instana(*argv: Union[Exception, Type[Exception]]) -> None: - """ - In the case of exceptions, after_request_with_instana isn't called - so we capture those cases here. - """ - if hasattr(flask.g, "span") and flask.g.span: - if len(argv) > 0 and argv[0]: - span = flask.g.span - span.record_exception(argv[0]) - if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) - if flask.g.span.is_recording(): - flask.g.span.end() - flask.g.span = None - - if hasattr(flask.g, "token") and flask.g.token: - context.detach(flask.g.token) - flask.g.token = None - - -@wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') +@wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") def full_dispatch_request_with_instana( wrapped: Callable[..., flask.wrappers.Response], instance: flask.app.Flask, argv: Tuple, kwargs: Dict, ) -> flask.wrappers.Response: - if not hasattr(instance, '_stan_wuz_here'): - logger.debug("Flask(vanilla): Applying flask before/after instrumentation funcs") + if not hasattr(instance, "_stan_wuz_here"): + logger.debug( + "Flask(vanilla): Applying flask before/after instrumentation funcs" + ) setattr(instance, "_stan_wuz_here", True) instance.before_request(before_request_with_instana) instance.after_request(after_request_with_instana) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index df3af703..1792ee42 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -1,88 +1,33 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 -import re -import wrapt -from typing import Any, Tuple, Dict, Callable +from typing import Any, Callable, Dict, Tuple +import flask +import wrapt +from flask import got_request_exception, request_finished, request_started from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry import context, trace +from instana.instrumentation.flask.common import ( + create_span, + inject_span, + teardown_request_with_instana, +) from instana.log import logger -from instana.util.secrets import strip_secrets_from_query -from instana.singletons import agent, tracer -from instana.util.traceutils import extract_custom_headers -from instana.propagators.format import Format - -import flask -from flask import request_started, request_finished, got_request_exception - -path_tpl_re = re.compile("<.*>") def request_started_with_instana(sender: flask.app.Flask, **extra: Any) -> None: try: - env = flask.request.environ - - span_context = tracer.extract(Format.HTTP_HEADERS, env) - - span = tracer.start_span("wsgi", span_context=span_context) - flask.g.span = span - - ctx = trace.set_span_in_context(span) - token = context.attach(ctx) - flask.g.token = token - - extract_custom_headers(span, env, format=True) - - span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) - if "PATH_INFO" in env: - span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) - if "QUERY_STRING" in env and len(env["QUERY_STRING"]): - scrubbed_params = strip_secrets_from_query( - env["QUERY_STRING"], - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - if "HTTP_HOST" in env: - span.set_attribute("http.host", env["HTTP_HOST"]) - - if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( - flask.request.url_rule.rule - ): - path_tpl = flask.request.url_rule.rule.replace("<", "{") - path_tpl = path_tpl.replace(">", "}") - span.set_attribute("http.path_tpl", path_tpl) - except: + create_span() + except Exception: logger.debug("Flask request_started_with_instana", exc_info=True) def request_finished_with_instana( sender: flask.app.Flask, response: flask.wrappers.Response, **extra: Any ) -> None: - span = None - try: - if not hasattr(flask.g, "span"): - return - - span = flask.g.span - if span: - if 500 <= response.status_code: - span.mark_as_errored() - - span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) - ) - extract_custom_headers(span, response.headers, format=False) - - tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - except Exception: - logger.debug("Flask request_finished_with_instana", exc_info=True) - finally: - if span and span.is_recording(): - span.end() + inject_span(response, "Flask request_finished_with_instana") def log_exception_with_instana( @@ -102,26 +47,6 @@ def log_exception_with_instana( span.end() -def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: - """ - In the case of exceptions, request_finished_with_instana isn't called - so we capture those cases here. - """ - if hasattr(flask.g, "span") and flask.g.span: - if len(argv) > 0 and argv[0]: - span = flask.g.span - span.record_exception(argv[0]) - if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) - if flask.g.span.is_recording(): - flask.g.span.end() - flask.g.span = None - - if hasattr(flask.g, "token") and flask.g.token: - context.detach(flask.g.token) - flask.g.token = None - - @wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") def full_dispatch_request_with_instana( wrapped: Callable[..., flask.wrappers.Response], diff --git a/src/instana/instrumentation/grpcio.py b/src/instana/instrumentation/grpcio.py index ec73faa0..3fce14fc 100644 --- a/src/instana/instrumentation/grpcio.py +++ b/src/instana/instrumentation/grpcio.py @@ -1,20 +1,26 @@ -# (c) Copyright IBM Corp. 2021 +# (c) Copyright IBM Corp. 2021, 2025 # (c) Copyright Instana Inc. 2019 + try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union + import grpc from grpc._channel import ( - _UnaryUnaryMultiCallable, + _StreamStreamMultiCallable, _StreamUnaryMultiCallable, _UnaryStreamMultiCallable, - _StreamStreamMultiCallable, + _UnaryUnaryMultiCallable, ) + if TYPE_CHECKING: + from grpc._server import _Server + import wrapt from instana.log import logger - from instana.singletons import tracer from instana.propagators.format import Format + from instana.singletons import get_tracer from instana.span.span import get_current_span SUPPORTED_TYPES = [ @@ -50,9 +56,21 @@ def collect_attributes(span, instance, argv, kwargs): logger.debug("grpc.collect_attributes non-fatal error", exc_info=True) return span - @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.with_call") - def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): + def create_span( + wrapped: Callable[..., object], + instance: Union[ + _UnaryUnaryMultiCallable, + _StreamUnaryMultiCallable, + _UnaryStreamMultiCallable, + _StreamStreamMultiCallable, + ], + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + call_type: str, + record_exception: bool = True, + ) -> object: parent_span = get_current_span() + tracer = get_tracer() # If we're not tracing, just return if not parent_span.is_recording(): @@ -61,7 +79,7 @@ def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( - "rpc-client", span_context=parent_context + "rpc-client", span_context=parent_context, record_exception=record_exception ) as span: try: if "metadata" not in kwargs: @@ -74,7 +92,7 @@ def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): disable_w3c_trace_context=True, ) collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "unary") + span.set_attribute("rpc.call_type", call_type) rv = wrapped(*argv, **kwargs) except Exception as exc: @@ -82,236 +100,92 @@ def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): else: return rv - @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.future") - def unary_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "unary") + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.with_call") + def unary_unary_with_call_with_instana( + wrapped: Callable[..., object], + instance: _UnaryUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="unary") - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.future") + def unary_unary_future_with_instana( + wrapped: Callable[..., object], + instance: _UnaryUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="unary") @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.__call__") - def unary_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context, record_exception=False - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "unary") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def unary_unary_call_with_instana( + wrapped: Callable[..., object], + instance: _UnaryUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span( + wrapped, instance, argv, kwargs, call_type="unary", record_exception=False + ) @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.__call__") - def stream_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "stream") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def stream_unary_call_with_instana( + wrapped: Callable[..., object], + instance: _StreamUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="stream") @wrapt.patch_function_wrapper( "grpc._channel", "_StreamUnaryMultiCallable.with_call" ) - def stream_unary_with_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "stream") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def stream_unary_with_call_with_instana( + wrapped: Callable[..., object], + instance: _StreamUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="stream") @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.future") - def stream_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "stream") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def stream_unary_future_with_instana( + wrapped: Callable[..., object], + instance: _StreamUnaryMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="stream") @wrapt.patch_function_wrapper("grpc._channel", "_UnaryStreamMultiCallable.__call__") - def unary_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "stream") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def unary_stream_call_with_instana( + wrapped: Callable[..., object], + instance: _UnaryStreamMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="stream") @wrapt.patch_function_wrapper( "grpc._channel", "_StreamStreamMultiCallable.__call__" ) - def stream_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - - # If we're not tracing, just return - if not parent_span.is_recording(): - return wrapped(*argv, **kwargs) - - parent_context = parent_span.get_span_context() if parent_span else None - - with tracer.start_as_current_span( - "rpc-client", span_context=parent_context - ) as span: - try: - if "metadata" not in kwargs: - kwargs["metadata"] = [] - - kwargs["metadata"] = tracer.inject( - span.context, - Format.BINARY, - kwargs["metadata"], - disable_w3c_trace_context=True, - ) - collect_attributes(span, instance, argv, kwargs) - span.set_attribute("rpc.call_type", "stream") - - rv = wrapped(*argv, **kwargs) - except Exception as exc: - span.record_exception(exc) - else: - return rv + def stream_stream_call_with_instana( + wrapped: Callable[..., object], + instance: _StreamStreamMultiCallable, + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + return create_span(wrapped, instance, argv, kwargs, call_type="stream") @wrapt.patch_function_wrapper("grpc._server", "_call_behavior") - def call_behavior_with_instana(wrapped, instance, argv, kwargs): + def call_behavior_with_instana( + wrapped: Callable[..., object], + instance: "_Server", + argv: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + tracer = get_tracer() # Prep any incoming context headers metadata = argv[0].invocation_metadata metadata_dict = {} From 3a8603f4802ad355a715839da73ecd2acde65416 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 25 Nov 2025 12:35:11 +0100 Subject: [PATCH 43/44] fix: do not create spans when confluent-kafka.poll() response is empty Signed-off-by: Cagri Yonca --- .../kafka/confluent_kafka_python.py | 3 +- tests/clients/kafka/test_confluent_kafka.py | 303 +++++++++++++++++- 2 files changed, 304 insertions(+), 2 deletions(-) diff --git a/src/instana/instrumentation/kafka/confluent_kafka_python.py b/src/instana/instrumentation/kafka/confluent_kafka_python.py index f2f327f1..d406f7c1 100644 --- a/src/instana/instrumentation/kafka/confluent_kafka_python.py +++ b/src/instana/instrumentation/kafka/confluent_kafka_python.py @@ -251,7 +251,8 @@ def trace_kafka_poll( try: res = wrapped(*args, **kwargs) - create_span("poll", res.topic(), res.headers()) + if res: + create_span("poll", res.topic(), res.headers()) return res except Exception as exc: exception = exc diff --git a/tests/clients/kafka/test_confluent_kafka.py b/tests/clients/kafka/test_confluent_kafka.py index 36538566..bc1d85b7 100644 --- a/tests/clients/kafka/test_confluent_kafka.py +++ b/tests/clients/kafka/test_confluent_kafka.py @@ -2,8 +2,9 @@ import os +import threading import time -from typing import Generator +from typing import Generator, List import pytest from confluent_kafka import Consumer, KafkaException, Producer @@ -775,3 +776,303 @@ def test_trace_kafka_close_exception_handling(self, span: "InstanaSpan") -> None # Verify span was ended assert not span.is_recording() + + def test_confluent_kafka_poll_returns_none(self) -> None: + consumer_config = self.kafka_config.copy() + consumer_config["group.id"] = "test-empty-poll-group" + consumer_config["auto.offset.reset"] = "earliest" + + consumer = Consumer(consumer_config) + consumer.subscribe([testenv["kafka_topic"] + "_3"]) + + with self.tracer.start_as_current_span("test"): + msg = consumer.poll(timeout=0.1) + + assert msg is None + + consumer.close() + + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + test_span = spans[0] + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" + + def test_confluent_kafka_poll_returns_none_with_context_cleanup(self) -> None: + consumer_config = self.kafka_config.copy() + consumer_config["group.id"] = "test-context-cleanup-group" + consumer_config["auto.offset.reset"] = "earliest" + + consumer = Consumer(consumer_config) + consumer.subscribe([testenv["kafka_topic"] + "_3"]) + + # Consume any existing messages to ensure topic is empty + while True: + msg = consumer.poll(timeout=0.5) + if msg is None: + break + + # Clear any spans created during cleanup + self.recorder.clear_spans() + + with self.tracer.start_as_current_span("test"): + for _ in range(3): + msg = consumer.poll(timeout=0.1) + assert msg is None + + consumer.close() + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + test_span = spans[0] + assert test_span.n == "sdk" + + def test_confluent_kafka_poll_none_then_message(self) -> None: + # First, create a temporary consumer to clean up any existing messages + cleanup_config = self.kafka_config.copy() + cleanup_config["group.id"] = "test-none-then-message-cleanup" + cleanup_config["auto.offset.reset"] = "earliest" + + cleanup_consumer = Consumer(cleanup_config) + cleanup_consumer.subscribe([testenv["kafka_topic"] + "_3"]) + + # Consume any existing messages + while True: + msg = cleanup_consumer.poll(timeout=0.5) + if msg is None: + break + + cleanup_consumer.close() + + # Clear any spans created during cleanup + self.recorder.clear_spans() + + # Now run the actual test with a fresh consumer + consumer_config = self.kafka_config.copy() + consumer_config["group.id"] = "test-none-then-message-group" + consumer_config["auto.offset.reset"] = "earliest" + + consumer = Consumer(consumer_config) + consumer.subscribe([testenv["kafka_topic"] + "_3"]) + + with self.tracer.start_as_current_span("test"): + msg1 = consumer.poll(timeout=0.1) + assert msg1 is None + + self.producer.produce(testenv["kafka_topic"] + "_3", b"test_message") + self.producer.flush(timeout=10) + + msg2 = consumer.poll(timeout=5) + assert msg2 is not None + assert msg2.value() == b"test_message" + + consumer.close() + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + kafka_span = get_first_span_by_filter( + spans, + lambda span: span.n == "kafka" and span.data["kafka"]["access"] == "poll", + ) + assert kafka_span is not None + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + "_3" + + kafka_span = get_first_span_by_filter( + spans, + lambda span: span.n == "kafka" + and span.data["kafka"]["access"] == "produce", + ) + assert kafka_span is not None + assert kafka_span.data["kafka"]["service"] == testenv["kafka_topic"] + "_3" + + def test_confluent_kafka_poll_multithreaded_context_isolation(self) -> None: + agent.options.allow_exit_as_root = True + agent.options.set_trace_configurations() + + # Produce messages to multiple topics + num_threads = 3 + messages_per_topic = 2 + + for i in range(num_threads): + topic = f"{testenv['kafka_topic']}_thread_{i}" + # Create topic + try: + self.kafka_client.create_topics( + [NewTopic(topic, num_partitions=1, replication_factor=1)] + ) + except KafkaException: + pass + + # Produce messages + for j in range(messages_per_topic): + self.producer.produce(topic, f"message_{j}".encode()) + + self.producer.flush(timeout=10) + time.sleep(1) # Allow messages to be available + + # Track results from each thread + thread_results: List[dict] = [] + thread_errors: List[Exception] = [] + lock = threading.Lock() + + def consume_from_topic(thread_id: int) -> None: + try: + topic = f"{testenv['kafka_topic']}_thread_{thread_id}" + consumer_config = self.kafka_config.copy() + consumer_config["group.id"] = f"test-multithread-group-{thread_id}" + consumer_config["auto.offset.reset"] = "earliest" + + consumer = Consumer(consumer_config) + consumer.subscribe([topic]) + + messages_consumed = 0 + none_polls = 0 + max_polls = 10 + + with self.tracer.start_as_current_span(f"thread-{thread_id}"): + for _ in range(max_polls): + msg = consumer.poll(timeout=1.0) + + if msg is None: + none_polls += 1 + _ = consumer_span.get(None) + else: + if msg.error(): + continue + messages_consumed += 1 + + assert msg.topic() == topic + + if messages_consumed >= messages_per_topic: + break + + consumer.close() + + with lock: + thread_results.append( + { + "thread_id": thread_id, + "topic": topic, + "messages_consumed": messages_consumed, + "none_polls": none_polls, + "success": True, + } + ) + + except Exception as e: + with lock: + thread_errors.append(e) + thread_results.append( + {"thread_id": thread_id, "success": False, "error": str(e)} + ) + + threads = [] + for i in range(num_threads): + thread = threading.Thread(target=consume_from_topic, args=(i,)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join(timeout=30) + + assert len(thread_errors) == 0, f"Errors in threads: {thread_errors}" + + assert len(thread_results) == num_threads + for result in thread_results: + assert result[ + "success" + ], f"Thread {result['thread_id']} failed: {result.get('error')}" + assert ( + result["messages_consumed"] == messages_per_topic + ), f"Thread {result['thread_id']} consumed {result['messages_consumed']} messages, expected {messages_per_topic}" + + spans = self.recorder.queued_spans() + + expected_min_spans = num_threads * (1 + messages_per_topic * 2) + assert ( + len(spans) >= expected_min_spans + ), f"Expected at least {expected_min_spans} spans, got {len(spans)}" + + for i in range(num_threads): + topic = f"{testenv['kafka_topic']}_thread_{i}" + + poll_spans = [ + s + for s in spans + if s.n == "kafka" + and s.data.get("kafka", {}).get("access") == "poll" + and s.data.get("kafka", {}).get("service") == topic + ] + + assert ( + len(poll_spans) >= 1 + ), f"Expected poll spans for topic {topic}, got {len(poll_spans)}" + + topics_to_delete = [ + f"{testenv['kafka_topic']}_thread_{i}" for i in range(num_threads) + ] + self.kafka_client.delete_topics(topics_to_delete) + time.sleep(1) + + def test_confluent_kafka_poll_multithreaded_with_none_returns(self) -> None: + num_threads = 5 + + thread_errors: List[Exception] = [] + lock = threading.Lock() + + def poll_empty_topic(thread_id: int) -> None: + try: + consumer_config = self.kafka_config.copy() + consumer_config["group.id"] = f"test-empty-poll-{thread_id}" + consumer_config["auto.offset.reset"] = "earliest" + + consumer = Consumer(consumer_config) + consumer.subscribe([testenv["kafka_topic"] + "_3"]) + + # Consume any existing messages to ensure topic is empty + while True: + msg = consumer.poll(timeout=0.5) + if msg is None: + break + + with self.tracer.start_as_current_span( + f"empty-poll-thread-{thread_id}" + ): + for _ in range(5): + msg = consumer.poll(timeout=0.1) + assert msg is None, "Expected None from empty topic" + + time.sleep(0.01) + + consumer.close() + + except Exception as e: + with lock: + thread_errors.append(e) + + threads = [] + for i in range(num_threads): + thread = threading.Thread(target=poll_empty_topic, args=(i,)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join(timeout=10) + + assert ( + len(thread_errors) == 0 + ), f"Context errors in threads: {[str(e) for e in thread_errors]}" + + spans = self.recorder.queued_spans() + + test_spans = [s for s in spans if s.n == "sdk"] + assert ( + len(test_spans) == num_threads + ), f"Expected {num_threads} test spans, got {len(test_spans)}" + + kafka_spans = [s for s in spans if s.n == "kafka"] + assert ( + len(kafka_spans) == 0 + ), f"Expected no kafka spans for None polls, got {len(kafka_spans)}" From 11c553fa3fedd814e40008a6d521e694932a315a Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Fri, 28 Nov 2025 11:00:14 +0100 Subject: [PATCH 44/44] chore(version): Bump version to 3.9.4 Signed-off-by: Cagri Yonca --- src/instana/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/version.py b/src/instana/version.py index 6db3016f..35303cec 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "3.9.3" +VERSION = "3.9.4"