diff --git a/src/instana/helpers.py b/src/instana/helpers.py index d5ddecaf..bf8c5c3d 100644 --- a/src/instana/helpers.py +++ b/src/instana/helpers.py @@ -1,12 +1,6 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2018 -import os -from string import Template - -from instana import eum_api_key as global_eum_api_key -from .singletons import tracer -from instana.log import logger # Usage: # @@ -26,7 +20,7 @@ def eum_snippet(trace_id=None, eum_api_key=None, meta=None): @return string """ - return '' + return "" def eum_test_snippet(trace_id=None, eum_api_key=None, meta=None): @@ -40,4 +34,4 @@ def eum_test_snippet(trace_id=None, eum_api_key=None, meta=None): @return string """ - return '' + return "" diff --git a/src/instana/instrumentation/aio_pika.py b/src/instana/instrumentation/aio_pika.py index a47e09f7..7f0aa89a 100644 --- a/src/instana/instrumentation/aio_pika.py +++ b/src/instana/instrumentation/aio_pika.py @@ -1,29 +1,31 @@ # (c) Copyright IBM Corp. 2025 + try: - import aio_pika - import wrapt from typing import ( TYPE_CHECKING, - Dict, Any, Callable, + Dict, + Optional, Tuple, Type, - Optional, ) + import wrapt + 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 + from instana.util.traceutils import get_tracer_tuple if TYPE_CHECKING: - from instana.span.span import InstanaSpan + from aio_pika.abc import AbstractMessage, ConsumerTag from aio_pika.exchange import Exchange - from aiormq.abc import ConfirmationFrameType - from aio_pika.abc import ConsumerTag, AbstractMessage from aio_pika.queue import Queue, QueueIterator + from aiormq.abc import ConfirmationFrameType + + from instana.span.span import InstanaSpan def _extract_span_attributes( span: "InstanaSpan", connection, sort: str, routing_key: str, exchange: str @@ -41,10 +43,10 @@ async def publish_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> Optional["ConfirmationFrameType"]: - if tracing_is_off(): + tracer, parent_span, _ = get_tracer_tuple() + if not tracer: 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( @@ -54,10 +56,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 @@ -91,6 +91,10 @@ async def consume_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> "ConsumerTag": + tracer = get_tracer() + if not tracer: + return await wrapped(*args, **kwargs) + connection = instance.channel._connection callback = kwargs["callback"] if kwargs.get("callback") else args[0] diff --git a/src/instana/instrumentation/aioamqp.py b/src/instana/instrumentation/aioamqp.py index 01efdc4a..f8d0ff9b 100644 --- a/src/instana/instrumentation/aioamqp.py +++ b/src/instana/instrumentation/aioamqp.py @@ -1,14 +1,14 @@ # (c) Copyright IBM Corp. 2025 try: - import aioamqp from typing import Any, Callable, Dict, Tuple + import aioamqp import wrapt from opentelemetry.trace.status import StatusCode from instana.log import logger - from instana.util.traceutils import get_tracer_tuple, tracing_is_off + from instana.util.traceutils import get_tracer_tuple @wrapt.patch_function_wrapper("aioamqp.channel", "Channel.basic_publish") async def basic_publish_with_instana( @@ -17,10 +17,10 @@ async def basic_publish_with_instana( argv: Tuple[object, Tuple[object, ...]], kwargs: Dict[str, Any], ) -> object: - if tracing_is_off(): + tracer, parent_span, _ = get_tracer_tuple() + if not tracer: return await wrapped(*argv, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( "aioamqp-publisher", span_context=parent_context @@ -57,11 +57,11 @@ async def basic_consume_with_instana( argv: Tuple[object, Tuple[object, ...]], kwargs: Dict[str, Any], ) -> object: - if tracing_is_off(): + tracer, parent_span, _ = get_tracer_tuple() + if not tracer: return await wrapped(*argv, **kwargs) callback = argv[0] - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None @wrapt.decorator diff --git a/src/instana/instrumentation/aiohttp/client.py b/src/instana/instrumentation/aiohttp/client.py index 667c2620..316ebc52 100644 --- a/src/instana/instrumentation/aiohttp/client.py +++ b/src/instana/instrumentation/aiohttp/client.py @@ -2,36 +2,38 @@ # (c) Copyright Instana Inc. 2019 -from types import SimpleNamespace -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple -import wrapt - -from opentelemetry.semconv.trace import SpanAttributes - -from instana.log import logger -from instana.propagators.format import Format -from instana.singletons import agent -from instana.util.secrets import strip_secrets_from_query -from instana.util.traceutils import get_tracer_tuple, tracing_is_off, extract_custom_headers - try: + from types import SimpleNamespace + from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple + import aiohttp + import wrapt + from opentelemetry.semconv.trace import SpanAttributes + + from instana.log import logger + from instana.propagators.format import Format + from instana.singletons import agent + from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import ( + extract_custom_headers, + get_tracer_tuple, + ) if TYPE_CHECKING: from aiohttp.client import ClientSession - from instana.span.span import InstanaSpan + from instana.span.span import InstanaSpan async def stan_request_start( session: "ClientSession", trace_config_ctx: SimpleNamespace, params ) -> Awaitable[None]: try: + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: trace_config_ctx.span_context = None return - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None span = tracer.start_span("aiohttp-client", span_context=parent_context) diff --git a/src/instana/instrumentation/asyncio.py b/src/instana/instrumentation/asyncio.py index 070dfe85..e95f89ed 100644 --- a/src/instana/instrumentation/asyncio.py +++ b/src/instana/instrumentation/asyncio.py @@ -2,20 +2,22 @@ # (c) Copyright Instana Inc. 2019 -import time -from contextlib import contextmanager -from typing import Any, Callable, Dict, Iterator, Tuple +try: + import time + from contextlib import contextmanager + from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Tuple -import wrapt -from opentelemetry.trace import use_span -from opentelemetry.trace.status import StatusCode + import wrapt + from opentelemetry.trace import use_span + from opentelemetry.trace.status import StatusCode -from instana.configurator import config -from instana.log import logger -from instana.span.span import InstanaSpan -from instana.util.traceutils import get_tracer_tuple, tracing_is_off + from instana.configurator import config + from instana.log import logger + from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple -try: + if TYPE_CHECKING: + from instana.tracer import InstanaTracer import asyncio @wrapt.patch_function_wrapper("asyncio", "ensure_future") @@ -25,13 +27,11 @@ def ensure_future_with_instana( argv: Tuple[object, Tuple[object, ...]], kwargs: Dict[str, Any], ) -> object: - if ( - not config["asyncio_task_context_propagation"]["enabled"] - or tracing_is_off() - ): + tracer, parent_span, _ = get_tracer_tuple() + if not config["asyncio_task_context_propagation"]["enabled"] or not tracer: return wrapped(*argv, **kwargs) - with _start_as_current_async_span() as span: + with _start_as_current_async_span(tracer, parent_span) as span: try: span.set_status(StatusCode.OK) return wrapped(*argv, **kwargs) @@ -47,13 +47,11 @@ def create_task_with_instana( argv: Tuple[object, Tuple[object, ...]], kwargs: Dict[str, Any], ) -> object: - if ( - not config["asyncio_task_context_propagation"]["enabled"] - or tracing_is_off() - ): + tracer, parent_span, _ = get_tracer_tuple() + if not config["asyncio_task_context_propagation"]["enabled"] or not tracer: return wrapped(*argv, **kwargs) - with _start_as_current_async_span() as span: + with _start_as_current_async_span(tracer, parent_span) as span: try: span.set_status(StatusCode.OK) return wrapped(*argv, **kwargs) @@ -61,12 +59,14 @@ def create_task_with_instana( logger.debug(f"asyncio create_task_with_instana error: {exc}") @contextmanager - def _start_as_current_async_span() -> Iterator[InstanaSpan]: + def _start_as_current_async_span( + tracer: "InstanaTracer", + parent_span: "InstanaSpan", + ) -> Iterator[InstanaSpan]: """ Creates and yield a special InstanaSpan to only propagate the Asyncio context. """ - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None _time = time.time_ns() diff --git a/src/instana/instrumentation/aws/boto3.py b/src/instana/instrumentation/aws/boto3.py index 3350a1ac..2d7d36f9 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 @@ -12,6 +14,7 @@ from botocore.client import BaseClient from instana.span.span import InstanaSpan + from instana.tracer import InstanaTracer import json @@ -19,15 +22,16 @@ from instana.log import logger from instana.propagators.format import Format - from instana.singletons import tracer - from instana.span.span import get_current_span from instana.util.traceutils import ( extract_custom_headers, get_tracer_tuple, - tracing_is_off, ) - def lambda_inject_context(payload: Dict[str, Any], span: "InstanaSpan") -> None: + def lambda_inject_context( + tracer: "InstanaTracer", + payload: Dict[str, Any], + span: "InstanaSpan", + ) -> None: """ When boto3 lambda client 'Invoke' is called, we want to inject the tracing context. boto3/botocore has specific requirements: @@ -51,9 +55,9 @@ def emit_add_auth_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> Callable[..., None]: - current_span = get_current_span() - if not tracing_is_off() and current_span and current_span.is_recording(): - extract_custom_headers(current_span, args[0].headers) + _, parent_span, _ = get_tracer_tuple() + if parent_span: + extract_custom_headers(parent_span, args[0].headers) return wrapped(*args, **kwargs) @wrapt.patch_function_wrapper("botocore.client", "BaseClient._make_api_call") @@ -63,18 +67,19 @@ def make_api_call_with_instana( args: Sequence[Dict[str, Any]], kwargs: Dict[str, Any], ) -> Dict[str, Any]: + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None if instance.meta.service_model.service_name == "dynamodb": - create_dynamodb_span(wrapped, instance, args, kwargs, parent_context) + create_dynamodb_span( + wrapped, instance, args, kwargs, parent_context, tracer + ) elif instance.meta.service_model.service_name == "s3": - create_s3_span(wrapped, instance, args, kwargs, parent_context) + create_s3_span(wrapped, instance, args, kwargs, parent_context, tracer) else: with tracer.start_as_current_span( "boto3", span_context=parent_context @@ -98,7 +103,7 @@ def make_api_call_with_instana( # Inject context when invoking lambdas if "lambda" in instance._endpoint.host and operation == "Invoke": - lambda_inject_context(payload, span) + lambda_inject_context(tracer, payload, span) try: result = wrapped(*args, **kwargs) diff --git a/src/instana/instrumentation/aws/dynamodb.py b/src/instana/instrumentation/aws/dynamodb.py index ef9fe251..6ab7cda9 100644 --- a/src/instana/instrumentation/aws/dynamodb.py +++ b/src/instana/instrumentation/aws/dynamodb.py @@ -1,31 +1,39 @@ # (c) Copyright IBM Corp. 2025 -from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type -if TYPE_CHECKING: - from botocore.client import BaseClient +try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type -from instana.log import logger -from instana.singletons import tracer -from instana.span_context import SpanContext + if TYPE_CHECKING: + from botocore.client import BaseClient + from instana.tracer import InstanaTracer + from instana.log import logger + from instana.span_context import SpanContext -def create_dynamodb_span( - wrapped: Callable[..., Dict[str, Any]], - instance: Type["BaseClient"], - args: Sequence[Dict[str, Any]], - kwargs: Dict[str, Any], - parent_context: SpanContext, -) -> None: - with tracer.start_as_current_span("dynamodb", span_context=parent_context) as span: - try: - span.set_attribute("dynamodb.op", args[0]) - span.set_attribute("dynamodb.region", instance._client_config.region_name) - if "TableName" in args[1].keys(): - span.set_attribute("dynamodb.table", args[1]["TableName"]) - except Exception as exc: - span.record_exception(exc) - logger.debug("create_dynamodb_span: collect error", exc_info=True) + def create_dynamodb_span( + wrapped: Callable[..., Dict[str, Any]], + instance: Type["BaseClient"], + args: Sequence[Dict[str, Any]], + kwargs: Dict[str, Any], + parent_context: SpanContext, + tracer: "InstanaTracer", + ) -> None: + with tracer.start_as_current_span( + "dynamodb", span_context=parent_context + ) as span: + try: + span.set_attribute("dynamodb.op", args[0]) + span.set_attribute( + "dynamodb.region", instance._client_config.region_name + ) + if "TableName" in args[1].keys(): + span.set_attribute("dynamodb.table", args[1]["TableName"]) + except Exception as exc: + span.record_exception(exc) + logger.debug("create_dynamodb_span: collect error", exc_info=True) + logger.debug("Instrumenting DynamoDB") -logger.debug("Instrumenting DynamoDB") +except ImportError: + pass diff --git a/src/instana/instrumentation/aws/lambda_inst.py b/src/instana/instrumentation/aws/lambda_inst.py index 62cabcc4..5721065b 100644 --- a/src/instana/instrumentation/aws/lambda_inst.py +++ b/src/instana/instrumentation/aws/lambda_inst.py @@ -5,12 +5,11 @@ Instrumentation for AWS Lambda functions """ -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple - -if TYPE_CHECKING: - from instana.agent.aws_lambda import AWSLambdaAgent - try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple + + if TYPE_CHECKING: + from instana.agent.aws_lambda import AWSLambdaAgent import sys import traceback diff --git a/src/instana/instrumentation/aws/s3.py b/src/instana/instrumentation/aws/s3.py index d13b8bff..c1107fa4 100644 --- a/src/instana/instrumentation/aws/s3.py +++ b/src/instana/instrumentation/aws/s3.py @@ -8,13 +8,14 @@ if TYPE_CHECKING: from botocore.client import BaseClient + + from instana.tracer import InstanaTracer + import wrapt from instana.log import logger - from instana.singletons import tracer from instana.util.traceutils import ( get_tracer_tuple, - tracing_is_off, ) operations = { @@ -30,6 +31,7 @@ def create_s3_span( args: Sequence[Dict[str, Any]], kwargs: Dict[str, Any], parent_context: SpanContext, + tracer: "InstanaTracer", ) -> None: with tracer.start_as_current_span("s3", span_context=parent_context) as span: try: @@ -46,12 +48,11 @@ def collect_s3_injected_attributes( args: Sequence[object], kwargs: Dict[str, Any], ) -> Callable[..., object]: + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span("s3", span_context=parent_context) as span: @@ -66,7 +67,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 +76,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/cassandra.py b/src/instana/instrumentation/cassandra.py index 2ad9d768..8feaca11 100644 --- a/src/instana/instrumentation/cassandra.py +++ b/src/instana/instrumentation/cassandra.py @@ -14,7 +14,7 @@ import wrapt from instana.log import logger - from instana.util.traceutils import get_tracer_tuple, tracing_is_off + from instana.util.traceutils import get_tracer_tuple if TYPE_CHECKING: from cassandra.cluster import ResponseFuture, Session @@ -73,11 +73,11 @@ def request_init_with_instana( fn: "ResponseFuture", ) -> None: tracer, parent_span, _ = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None - - if tracing_is_off(): + if not tracer: return + parent_context = parent_span.get_span_context() if parent_span else None + attributes = {} if isinstance(fn.query, cassandra.query.SimpleStatement): attributes["cassandra.query"] = fn.query.query_string diff --git a/src/instana/instrumentation/celery.py b/src/instana/instrumentation/celery.py index c69131aa..9329a6f9 100644 --- a/src/instana/instrumentation/celery.py +++ b/src/instana/instrumentation/celery.py @@ -2,20 +2,19 @@ # (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 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] = {} @@ -66,6 +65,10 @@ def task_prerun( **kwargs: Dict[str, Any], ) -> None: try: + tracer = get_tracer() + if not tracer: + return + ctx = None task = kwargs.get("sender", None) @@ -144,40 +147,42 @@ def before_task_publish( ) -> None: try: tracer, parent_span, _ = get_tracer_tuple() + if not tracer: + return + parent_context = parent_span.get_span_context() if parent_span else None - if tracer: - body = kwargs["body"] - headers = kwargs["headers"] - task_name = kwargs["sender"] - task = registry.tasks.get(task_name) - task_id = _get_task_id(headers, body) - - span = tracer.start_span("celery-client", span_context=parent_context) - span.set_attribute("task", task_name) - span.set_attribute("task_id", task_id) - add_broker_attributes(span, task.app.conf["broker_url"]) - - # Context propagation - context_headers = {} - tracer.inject( - span.context, - Format.HTTP_HEADERS, - context_headers, - disable_w3c_trace_context=True, - ) + body = kwargs["body"] + headers = kwargs["headers"] + task_name = kwargs["sender"] + task = registry.tasks.get(task_name) + task_id = _get_task_id(headers, body) - # Fix for broken header propagation - # https://github.com/celery/celery/issues/4875 - task_headers = kwargs.get("headers") or {} - task_headers.setdefault("headers", {}) - task_headers["headers"].update(context_headers) - kwargs["headers"] = task_headers - - ctx = trace.set_span_in_context(span) - token = context.attach(ctx) - client_token["token"] = token - client_span.set(span) + span = tracer.start_span("celery-client", span_context=parent_context) + span.set_attribute("task", task_name) + span.set_attribute("task_id", task_id) + add_broker_attributes(span, task.app.conf["broker_url"]) + + # Context propagation + context_headers = {} + tracer.inject( + span.context, + Format.HTTP_HEADERS, + context_headers, + disable_w3c_trace_context=True, + ) + + # Fix for broken header propagation + # https://github.com/celery/celery/issues/4875 + task_headers = kwargs.get("headers") or {} + task_headers.setdefault("headers", {}) + task_headers["headers"].update(context_headers) + kwargs["headers"] = task_headers + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + client_token["token"] = token + client_span.set(span) except Exception: logger.debug("celery-client before_task_publish: ", exc_info=True) diff --git a/src/instana/instrumentation/couchbase.py b/src/instana/instrumentation/couchbase.py index d9678230..c29aabfc 100644 --- a/src/instana/instrumentation/couchbase.py +++ b/src/instana/instrumentation/couchbase.py @@ -8,6 +8,7 @@ try: import couchbase + from instana.log import logger if not ( @@ -17,15 +18,14 @@ logger.debug("Instana supports 2.3.4 <= couchbase_versions < 3.0.0. Skipping.") raise ImportError - from couchbase.bucket import Bucket - from couchbase.n1ql import N1QLQuery - from typing import Any, Callable, Dict, Tuple, Union import wrapt + from couchbase.bucket import Bucket + from couchbase.n1ql import N1QLQuery from instana.span.span import InstanaSpan - from instana.util.traceutils import get_tracer_tuple, tracing_is_off + from instana.util.traceutils import get_tracer_tuple # List of operations to instrument # incr, incr_multi, decr, decr_multi, retrieve_in are wrappers around operations above @@ -94,12 +94,12 @@ def wrapper( kwargs: Dict[str, Any], ) -> object: tracer, parent_span, _ = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None - # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( "couchbase", span_context=parent_context ) as span: @@ -120,12 +120,12 @@ def query_with_instana( kwargs: Dict[str, Any], ) -> object: tracer, parent_span, _ = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None - # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( "couchbase", span_context=parent_context ) as span: diff --git a/src/instana/instrumentation/fastapi.py b/src/instana/instrumentation/fastapi.py index 68b19f6a..6a4fc089 100644 --- a/src/instana/instrumentation/fastapi.py +++ b/src/instana/instrumentation/fastapi.py @@ -6,24 +6,22 @@ https://fastapi.tiangolo.com/ """ -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple - try: import os import signal + from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple import fastapi import wrapt from fastapi import HTTPException from fastapi.exception_handlers import http_exception_handler + from opentelemetry.semconv.trace import SpanAttributes from starlette.middleware import Middleware from instana.instrumentation.asgi import InstanaASGIMiddleware from instana.log import logger + from instana.span.span import get_current_span from instana.util.gunicorn import running_in_gunicorn - from instana.util.traceutils import get_tracer_tuple - - from opentelemetry.semconv.trace import SpanAttributes if TYPE_CHECKING: from starlette.requests import Request @@ -48,12 +46,14 @@ async def instana_exception_handler( to the default exception handler. """ try: - _, span, _ = get_tracer_tuple() + current_span = get_current_span() - if span: + if current_span: if hasattr(exc, "detail") and 500 <= exc.status_code: - span.set_attribute("http.error", exc.detail) - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, exc.status_code) + current_span.set_attribute("http.error", exc.detail) + current_span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, exc.status_code + ) except Exception: logger.debug("FastAPI instana_exception_handler: ", exc_info=True) @@ -72,7 +72,7 @@ def init_with_instana( elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) elif isinstance(middleware, tuple): - kwargs["middleware"] = (*middleware, Middleware(InstanaASGIMiddleware)) + kwargs["middleware"] = (*middleware, Middleware(InstanaASGIMiddleware)) else: logger.warning("Unsupported FastAPI middleware sequence type.") diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index a0e6f6fb..abbed447 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -10,8 +10,9 @@ 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 get_tracer +from instana.util.traceutils import get_tracer_tuple if TYPE_CHECKING: @@ -19,15 +20,18 @@ from flask.typing import ResponseReturnValue from jinja2.environment import Template -@wrapt.patch_function_wrapper('flask', 'templating._render') + +@wrapt.patch_function_wrapper("flask", "templating._render") def render_with_instana( wrapped: Callable[..., str], instance: object, argv: Tuple[flask.app.Flask, "Template", Dict[str, Any]], kwargs: Dict[str, Any], ) -> str: + tracer, parent_span, _ = get_tracer_tuple() + # If we're not tracing, just return - if not (hasattr(flask, "g") and hasattr(flask.g, "span")): + if not (hasattr(flask, "g") and hasattr(flask.g, "span")) and not tracer: return wrapped(*argv, **kwargs) parent_span = flask.g.span @@ -50,7 +54,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, @@ -60,6 +64,12 @@ def handle_user_exception_with_instana( # Call original and then try to do post processing response = wrapped(*argv, **kwargs) + tracer = get_tracer() + + # return early if we're not tracing + if not tracer: + return response + try: exc = argv[0] @@ -70,7 +80,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 +90,12 @@ 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.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 diff --git a/src/instana/instrumentation/google/cloud/pubsub.py b/src/instana/instrumentation/google/cloud/pubsub.py index fe4b5424..60b10f60 100644 --- a/src/instana/instrumentation/google/cloud/pubsub.py +++ b/src/instana/instrumentation/google/cloud/pubsub.py @@ -2,19 +2,19 @@ # (c) Copyright Instana Inc. 2021 -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple +try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple -import wrapt + import wrapt -from instana.log import logger -from instana.propagators.format import Format -from instana.singletons import tracer -from instana.util.traceutils import get_tracer_tuple, tracing_is_off + 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 -if TYPE_CHECKING: - from instana.span.span import InstanaSpan + if TYPE_CHECKING: + from instana.span.span import InstanaSpan -try: from google.cloud import pubsub_v1 def _set_publisher_attributes( @@ -49,11 +49,11 @@ def publish_with_instana( """References: - PublisherClient.publish(topic_path, messages, metadata) """ + tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( @@ -95,6 +95,10 @@ def subscribe_with_instana( - SubscriberClient.subscribe(subscription_path, callback) - callback(message) is called from the subscription future """ + tracer = get_tracer() + # return early if we're not tracing + if not tracer: + return def callback_with_instana(message): if message.attributes: diff --git a/src/instana/instrumentation/google/cloud/storage.py b/src/instana/instrumentation/google/cloud/storage.py index a1ccb6d9..00d7e9f4 100644 --- a/src/instana/instrumentation/google/cloud/storage.py +++ b/src/instana/instrumentation/google/cloud/storage.py @@ -2,17 +2,17 @@ # (c) Copyright Instana Inc. 2020 -import wrapt -import re - -from typing import Any, Callable, Dict, Tuple, Union -from instana.log import logger -from instana.instrumentation.google.cloud.collectors import _storage_api -from instana.util.traceutils import get_tracer_tuple, tracing_is_off - try: + import re + from typing import Any, Callable, Dict, Tuple, Union + + import wrapt from google.cloud import storage + from instana.instrumentation.google.cloud.collectors import _storage_api + from instana.log import logger + from instana.util.traceutils import get_tracer_tuple + logger.debug("Instrumenting google-cloud-storage") def _collect_attributes( @@ -60,12 +60,13 @@ def execute_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer, parent_span, _ = get_tracer_tuple() + # batch requests are traced with finish_batch_with_instana() # also return early if we're not tracing - if isinstance(instance, storage.Batch) or tracing_is_off(): + if isinstance(instance, storage.Batch) or not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span("gcs", span_context=parent_context) as span: @@ -91,11 +92,11 @@ def download_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span("gcs", span_context=parent_context) as span: @@ -127,11 +128,11 @@ def upload_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span("gcs", span_context=parent_context) as span: @@ -152,11 +153,11 @@ def finish_batch_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span("gcs", span_context=parent_context) as span: diff --git a/src/instana/instrumentation/grpcio.py b/src/instana/instrumentation/grpcio.py index ec73faa0..2c262926 100644 --- a/src/instana/instrumentation/grpcio.py +++ b/src/instana/instrumentation/grpcio.py @@ -1,21 +1,21 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 + try: import grpc + import wrapt from grpc._channel import ( - _UnaryUnaryMultiCallable, + _StreamStreamMultiCallable, _StreamUnaryMultiCallable, _UnaryStreamMultiCallable, - _StreamStreamMultiCallable, + _UnaryUnaryMultiCallable, ) - import wrapt - from instana.log import logger - from instana.singletons import tracer from instana.propagators.format import Format - from instana.span.span import get_current_span + from instana.singletons import get_tracer + from instana.util.traceutils import get_tracer_tuple SUPPORTED_TYPES = [ _UnaryUnaryMultiCallable, @@ -52,10 +52,9 @@ def collect_attributes(span, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.with_call") def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -84,10 +83,9 @@ def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.future") def unary_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -116,10 +114,9 @@ def unary_unary_future_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.__call__") def unary_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -148,10 +145,9 @@ def unary_unary_call_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.__call__") def stream_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -182,10 +178,9 @@ def stream_unary_call_with_instana(wrapped, instance, argv, kwargs): "grpc._channel", "_StreamUnaryMultiCallable.with_call" ) def stream_unary_with_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -214,10 +209,9 @@ def stream_unary_with_call_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.future") def stream_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -246,10 +240,9 @@ def stream_unary_future_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._channel", "_UnaryStreamMultiCallable.__call__") def unary_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -280,10 +273,9 @@ def unary_stream_call_with_instana(wrapped, instance, argv, kwargs): "grpc._channel", "_StreamStreamMultiCallable.__call__" ) def stream_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = get_current_span() - + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if not parent_span.is_recording(): + if not tracer: return wrapped(*argv, **kwargs) parent_context = parent_span.get_span_context() if parent_span else None @@ -312,6 +304,8 @@ def stream_stream_call_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper("grpc._server", "_call_behavior") def call_behavior_with_instana(wrapped, instance, argv, kwargs): + tracer = get_tracer() + # Prep any incoming context headers metadata = argv[0].invocation_metadata metadata_dict = {} diff --git a/src/instana/instrumentation/httpx.py b/src/instana/instrumentation/httpx.py index 3c25b814..854ac376 100644 --- a/src/instana/instrumentation/httpx.py +++ b/src/instana/instrumentation/httpx.py @@ -14,7 +14,6 @@ from instana.util.traceutils import ( extract_custom_headers, get_tracer_tuple, - tracing_is_off, ) if TYPE_CHECKING: @@ -72,11 +71,11 @@ def handle_request_with_instana( args: Tuple[int, str, Tuple[Any, ...]], kwargs: Dict[str, Any], ) -> httpx.Response: + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( @@ -101,11 +100,11 @@ async def handle_async_request_with_instana( args: Tuple[int, str, Tuple[Any, ...]], kwargs: Dict[str, Any], ) -> httpx.Response: + tracer, parent_span, _ = get_tracer_tuple() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return await wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( diff --git a/src/instana/instrumentation/kafka/confluent_kafka_python.py b/src/instana/instrumentation/kafka/confluent_kafka_python.py index f2f327f1..e37d0bb6 100644 --- a/src/instana/instrumentation/kafka/confluent_kafka_python.py +++ b/src/instana/instrumentation/kafka/confluent_kafka_python.py @@ -15,7 +15,7 @@ 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, tracing_is_off + from instana.util.traceutils import get_tracer_tuple consumer_token = None consumer_span = contextvars.ContextVar("confluent_kafka_consumer_span") @@ -61,10 +61,10 @@ def trace_kafka_produce( args: Tuple[int, str, Tuple[Any, ...]], kwargs: Dict[str, Any], ) -> None: - if tracing_is_off(): + tracer, parent_span, _ = get_tracer_tuple() + if not tracer: return wrapped(*args, **kwargs) - 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 diff --git a/src/instana/instrumentation/kafka/kafka_python.py b/src/instana/instrumentation/kafka/kafka_python.py index 307b7d52..25b05e13 100644 --- a/src/instana/instrumentation/kafka/kafka_python.py +++ b/src/instana/instrumentation/kafka/kafka_python.py @@ -15,7 +15,7 @@ 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, tracing_is_off + from instana.util.traceutils import get_tracer_tuple if TYPE_CHECKING: from kafka.producer.future import FutureRecordMetadata @@ -30,10 +30,11 @@ def trace_kafka_send( args: Tuple[int, str, Tuple[Any, ...]], kwargs: Dict[str, Any], ) -> "FutureRecordMetadata": - if tracing_is_off(): + tracer, parent_span, _ = get_tracer_tuple() + + if not tracer: return wrapped(*args, **kwargs) - 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 diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index fdbaaa58..126f8cb9 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -2,78 +2,81 @@ # (c) Copyright Instana Inc. 2019 -import logging -import sys -from collections.abc import Mapping -from typing import Any, Callable, Dict, Tuple - -import wrapt - -from instana.log import logger -from instana.singletons import agent -from instana.util.runtime import get_runtime_env_info -from instana.util.traceutils import get_tracer_tuple, tracing_is_off - - -@wrapt.patch_function_wrapper("logging", "Logger._log") -def log_with_instana( - wrapped: Callable[..., None], - instance: logging.Logger, - argv: Tuple[int, str, Tuple[Any, ...]], - kwargs: Dict[str, Any], -) -> Callable[..., None]: - # argv[0] = level - # argv[1] = message - # argv[2] = args for message - - # We take into consideration if `stacklevel` is already present in `kwargs`. - # This prevents the error `_log() got multiple values for keyword argument 'stacklevel'` - stacklevel_in = kwargs.pop( - "stacklevel", 1 if get_runtime_env_info()[0] not in ["ppc64le", "s390x"] else 2 - ) - stacklevel = stacklevel_in + 1 - - try: - # Only needed if we're tracing and serious log and logging spans are not disabled - if ( - tracing_is_off() - or argv[0] < logging.WARN - or agent.options.is_span_disabled(category="logging") - ): - return wrapped(*argv, **kwargs, stacklevel=stacklevel) - - tracer, parent_span, _ = get_tracer_tuple() - - msg = str(argv[1]) - args = argv[2] - if args and len(args) == 1 and isinstance(args[0], Mapping) and args[0]: - args = args[0] - - # get the formatted log message - msg = msg % args - - # get additional information if an exception is being handled - parameters = None - (t, v, tb) = sys.exc_info() - if t is not None and v is not None: - parameters = "{} {}".format(t, v) - - parent_context = parent_span.get_span_context() if parent_span else None - - # create logging span - with tracer.start_as_current_span("log", span_context=parent_context) as span: - event_attributes = {"message": msg} - if parameters is not None: - event_attributes.update({"parameters": parameters}) - span.add_event(name="log_with_instana", attributes=event_attributes) - # extra tags for an error - if argv[0] >= logging.ERROR: - span.mark_as_errored() - - except Exception: - logger.debug("log_with_instana:", exc_info=True) - - return wrapped(*argv, **kwargs, stacklevel=stacklevel) - - -logger.debug("Instrumenting logging") +try: + import logging + import sys + from collections.abc import Mapping + from typing import Any, Callable, Dict, Tuple + + import wrapt + + from instana.log import logger + from instana.singletons import agent + from instana.util.runtime import get_runtime_env_info + from instana.util.traceutils import get_tracer_tuple + + @wrapt.patch_function_wrapper("logging", "Logger._log") + def log_with_instana( + wrapped: Callable[..., None], + instance: logging.Logger, + argv: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> Callable[..., None]: + # argv[0] = level + # argv[1] = message + # argv[2] = args for message + + # We take into consideration if `stacklevel` is already present in `kwargs`. + # This prevents the error `_log() got multiple values for keyword argument 'stacklevel'` + stacklevel_in = kwargs.pop( + "stacklevel", + 1 if get_runtime_env_info()[0] not in ["ppc64le", "s390x"] else 2, + ) + stacklevel = stacklevel_in + 1 + + try: + tracer, parent_span, _ = get_tracer_tuple() + # Only needed if we're tracing and serious log and logging spans are not disabled + if ( + not tracer + or argv[0] < logging.WARN + or agent.options.is_span_disabled(category="logging") + ): + return wrapped(*argv, **kwargs, stacklevel=stacklevel) + + msg = str(argv[1]) + args = argv[2] + if args and len(args) == 1 and isinstance(args[0], Mapping) and args[0]: + args = args[0] + + # get the formatted log message + msg = msg % args + + # get additional information if an exception is being handled + parameters = None + (t, v, tb) = sys.exc_info() + if t is not None and v is not None: + parameters = "{} {}".format(t, v) + + parent_context = parent_span.get_span_context() if parent_span else None + + # create logging span + with tracer.start_as_current_span( + "log", span_context=parent_context + ) as span: + event_attributes = {"message": msg} + if parameters is not None: + event_attributes.update({"parameters": parameters}) + span.add_event(name="log_with_instana", attributes=event_attributes) + # extra tags for an error + if argv[0] >= logging.ERROR: + span.mark_as_errored() + + except Exception: + logger.debug("log_with_instana:", exc_info=True) + + return wrapped(*argv, **kwargs, stacklevel=stacklevel) + + logger.debug("Instrumenting logging") +except ImportError: + pass diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 1108433b..b8264ccf 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -1,204 +1,206 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2018 -# This is a wrapper for PEP-0249: Python Database API Specification v2.0 -import wrapt -from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Union, Callable, Optional -from typing_extensions import Self - -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind - -from instana.log import logger -from instana.util.traceutils import get_tracer_tuple, tracing_is_off -from instana.util.sql import sql_sanitizer - -if TYPE_CHECKING: - from instana.span.span import InstanaSpan - - -class CursorWrapper(wrapt.ObjectProxy): - __slots__ = ("_module_name", "_connect_params", "_cursor_params") - - def __init__( - self, - cursor: Any, - module_name: str, - connect_params: Optional[List[Union[str, Dict[str, Any]]]] = None, - cursor_params: Optional[Dict[str, Any]] = None, - ) -> None: - super(CursorWrapper, self).__init__(wrapped=cursor) - self._module_name = module_name - self._connect_params = connect_params - self._cursor_params = cursor_params - - def _collect_kvs( - self, - span: "InstanaSpan", - sql: str, - ) -> None: - try: - db_parameter_name = next( - ( - p - for p in ("db", "database", "dbname") - if p in self._connect_params[1] - ), - None, - ) - if db_parameter_name: - span.set_attribute( - SpanAttributes.DB_NAME, - self._connect_params[1][db_parameter_name], - ) - span.set_attribute(SpanAttributes.DB_STATEMENT, sql_sanitizer(sql)) - span.set_attribute(SpanAttributes.DB_USER, self._connect_params[1]["user"]) - span.set_attribute("host", self._connect_params[1]["host"]) - span.set_attribute("port", self._connect_params[1]["port"]) - except Exception as e: - logger.debug(e) - - def __enter__(self) -> Self: - return self - - def execute( - self, - sql: str, - params: Optional[Dict[str, Any]] = None, - ) -> Callable[[str, Dict[str, Any]], None]: - tracer, parent_span, operation_name = get_tracer_tuple() - - # If not tracing or we're being called from sqlalchemy, just pass through - if tracing_is_off() or (operation_name == "sqlalchemy"): - return self.__wrapped__.execute(sql, params) - - parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_as_current_span( - self._module_name, span_context=parent_context - ) as span: +try: + # This is a wrapper for PEP-0249: Python Database API Specification v2.0 + import wrapt + from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Union, Callable, Optional + from typing_extensions import Self + + from opentelemetry.semconv.trace import SpanAttributes + + from instana.log import logger + from instana.util.traceutils import get_tracer_tuple + from instana.util.sql import sql_sanitizer + + if TYPE_CHECKING: + from instana.span.span import InstanaSpan + + class CursorWrapper(wrapt.ObjectProxy): + __slots__ = ("_module_name", "_connect_params", "_cursor_params") + + def __init__( + self, + cursor: Any, + module_name: str, + connect_params: Optional[List[Union[str, Dict[str, Any]]]] = None, + cursor_params: Optional[Dict[str, Any]] = None, + ) -> None: + super(CursorWrapper, self).__init__(wrapped=cursor) + self._module_name = module_name + self._connect_params = connect_params + self._cursor_params = cursor_params + + def _collect_kvs( + self, + span: "InstanaSpan", + sql: str, + ) -> None: try: - self._collect_kvs(span, sql) - result = self.__wrapped__.execute(sql, params) - except Exception as e: - if span: - span.record_exception(e) - raise - else: - return result - - def executemany( - self, - sql: str, - seq_of_parameters: List[Dict[str, Any]], - ) -> Callable[[str, List[Dict[str, Any]]], None]: - tracer, parent_span, operation_name = get_tracer_tuple() - - # If not tracing or we're being called from sqlalchemy, just pass through - if tracing_is_off() or (operation_name == "sqlalchemy"): - return self.__wrapped__.executemany(sql, seq_of_parameters) - - parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_as_current_span( - self._module_name, span_context=parent_context - ) as span: - try: - self._collect_kvs(span, sql) - result = self.__wrapped__.executemany(sql, seq_of_parameters) + db_parameter_name = next( + ( + p + for p in ("db", "database", "dbname") + if p in self._connect_params[1] + ), + None, + ) + if db_parameter_name: + span.set_attribute( + SpanAttributes.DB_NAME, + self._connect_params[1][db_parameter_name], + ) + + span.set_attribute(SpanAttributes.DB_STATEMENT, sql_sanitizer(sql)) + span.set_attribute( + SpanAttributes.DB_USER, self._connect_params[1]["user"] + ) + span.set_attribute("host", self._connect_params[1]["host"]) + span.set_attribute("port", self._connect_params[1]["port"]) except Exception as e: - if span: - span.record_exception(e) - raise - else: - return result - - def callproc( - self, - proc_name: str, - params: Dict[str, Any], - ) -> Callable[[str, Dict[str, Any]], None]: - tracer, parent_span, operation_name = get_tracer_tuple() - - # If not tracing or we're being called from sqlalchemy, just pass through - if tracing_is_off() or (operation_name == "sqlalchemy"): - return self.__wrapped__.execute(proc_name, params) - - parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_as_current_span( - self._module_name, span_context=parent_context - ) as span: - try: - self._collect_kvs(span, proc_name) - result = self.__wrapped__.callproc(proc_name, params) - except Exception: + logger.debug(e) + + def __enter__(self) -> Self: + return self + + def execute( + self, + sql: str, + params: Optional[Dict[str, Any]] = None, + ) -> Callable[[str, Dict[str, Any]], None]: + tracer, parent_span, operation_name = get_tracer_tuple() + + # If not tracing or we're being called from sqlalchemy, just pass through + if not tracer or (operation_name == "sqlalchemy"): + return self.__wrapped__.execute(sql, params) + + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: + try: + self._collect_kvs(span, sql) + result = self.__wrapped__.execute(sql, params) + except Exception as e: + if span: + span.record_exception(e) + raise + else: + return result + + def executemany( + self, + sql: str, + seq_of_parameters: List[Dict[str, Any]], + ) -> Callable[[str, List[Dict[str, Any]]], None]: + tracer, parent_span, operation_name = get_tracer_tuple() + + # If not tracing or we're being called from sqlalchemy, just pass through + if not tracer or (operation_name == "sqlalchemy"): + return self.__wrapped__.executemany(sql, seq_of_parameters) + + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: - result = self.__wrapped__.execute(proc_name, params) - except Exception as e_execute: + self._collect_kvs(span, sql) + result = self.__wrapped__.executemany(sql, seq_of_parameters) + except Exception as e: if span: - span.record_exception(e_execute) + span.record_exception(e) raise else: return result - else: - return result - - -class ConnectionWrapper(wrapt.ObjectProxy): - __slots__ = ("_module_name", "_connect_params") - - def __init__( - self, - connection: "ConnectionWrapper", - module_name: str, - connect_params: List[Union[str, Dict[str, Any]]], - ) -> None: - super(ConnectionWrapper, self).__init__(wrapped=connection) - self._module_name = module_name - self._connect_params = connect_params - - def __enter__(self) -> Self: - return self - - def cursor( - self, - *args: Tuple[int, str, Dict[str, Any]], - **kwargs: Dict[str, Any], - ) -> CursorWrapper: - return CursorWrapper( - cursor=self.__wrapped__.cursor(*args, **kwargs), - module_name=self._module_name, - connect_params=self._connect_params, - cursor_params=(args, kwargs) if args or kwargs else None, - ) - - def close(self) -> Callable[[], None]: - return self.__wrapped__.close() - - def commit(self) -> Callable[[], None]: - return self.__wrapped__.commit() - - def rollback(self) -> Callable[[], None]: - return self.__wrapped__.rollback() - - -class ConnectionFactory(object): - def __init__( - self, - connect_func: CursorWrapper, - module_name: str, - ) -> None: - self._connect_func = connect_func - self._module_name = module_name - self._wrapper_ctor = ConnectionWrapper - - def __call__( - self, - *args: Tuple[int, str, Dict[str, Any]], - **kwargs: Dict[str, Any], - ) -> ConnectionWrapper: - connect_params = (args, kwargs) if args or kwargs else None - return self._wrapper_ctor( - connection=self._connect_func(*args, **kwargs), - module_name=self._module_name, - connect_params=connect_params, - ) + + def callproc( + self, + proc_name: str, + params: Dict[str, Any], + ) -> Callable[[str, Dict[str, Any]], None]: + tracer, parent_span, operation_name = get_tracer_tuple() + + # If not tracing or we're being called from sqlalchemy, just pass through + if not tracer or (operation_name == "sqlalchemy"): + return self.__wrapped__.execute(proc_name, params) + + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: + try: + self._collect_kvs(span, proc_name) + result = self.__wrapped__.callproc(proc_name, params) + except Exception: + try: + result = self.__wrapped__.execute(proc_name, params) + except Exception as e_execute: + if span: + span.record_exception(e_execute) + raise + else: + return result + else: + return result + + class ConnectionWrapper(wrapt.ObjectProxy): + __slots__ = ("_module_name", "_connect_params") + + def __init__( + self, + connection: "ConnectionWrapper", + module_name: str, + connect_params: List[Union[str, Dict[str, Any]]], + ) -> None: + super(ConnectionWrapper, self).__init__(wrapped=connection) + self._module_name = module_name + self._connect_params = connect_params + + def __enter__(self) -> Self: + return self + + def cursor( + self, + *args: Tuple[int, str, Dict[str, Any]], + **kwargs: Dict[str, Any], + ) -> CursorWrapper: + return CursorWrapper( + cursor=self.__wrapped__.cursor(*args, **kwargs), + module_name=self._module_name, + connect_params=self._connect_params, + cursor_params=(args, kwargs) if args or kwargs else None, + ) + + def close(self) -> Callable[[], None]: + return self.__wrapped__.close() + + def commit(self) -> Callable[[], None]: + return self.__wrapped__.commit() + + def rollback(self) -> Callable[[], None]: + return self.__wrapped__.rollback() + + class ConnectionFactory(object): + def __init__( + self, + connect_func: CursorWrapper, + module_name: str, + ) -> None: + self._connect_func = connect_func + self._module_name = module_name + self._wrapper_ctor = ConnectionWrapper + + def __call__( + self, + *args: Tuple[int, str, Dict[str, Any]], + **kwargs: Dict[str, Any], + ) -> ConnectionWrapper: + connect_params = (args, kwargs) if args or kwargs else None + return self._wrapper_ctor( + connection=self._connect_func(*args, **kwargs), + module_name=self._module_name, + connect_params=connect_params, + ) +except ImportError: + pass diff --git a/src/instana/instrumentation/pika.py b/src/instana/instrumentation/pika.py index 9be66182..3d15aca4 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,8 +21,8 @@ from instana.log import logger from instana.propagators.format import Format - from instana.singletons import tracer - from instana.util.traceutils import get_tracer_tuple, tracing_is_off + from instana.singletons import get_tracer + from instana.util.traceutils import get_tracer_tuple if TYPE_CHECKING: import pika.adapters.blocking_connection @@ -72,11 +73,12 @@ def _bind_args( ) -> Tuple[object, ...]: return (exchange, routing_key, body, properties, args, kwargs) + tracer, parent_span, _ = get_tracer_tuple() + # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return wrapped(*args, **kwargs) - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None (exchange, routing_key, body, properties, args, kwargs) = _bind_args( @@ -124,6 +126,11 @@ def basic_get_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer = get_tracer() + + if not tracer: + return wrapped(*args, **kwargs) + def _bind_args(*args: object, **kwargs: object) -> Tuple[object, ...]: args = list(args) queue = kwargs.pop("queue", None) or args.pop(0) @@ -173,6 +180,11 @@ def basic_consume_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer = get_tracer() + + if not tracer: + return wrapped(*args, **kwargs) + def _bind_args( queue: str, on_message_callback: object, @@ -222,6 +234,11 @@ def consume_with_instana( args: Tuple[object, ...], kwargs: Dict[str, Any], ) -> object: + tracer = get_tracer() + + if not tracer: + return wrapped(*args, **kwargs) + def _bind_args( queue: str, *args: object, **kwargs: object ) -> Tuple[object, ...]: diff --git a/src/instana/instrumentation/pymongo.py b/src/instana/instrumentation/pymongo.py index 2c0bc203..c83c5278 100644 --- a/src/instana/instrumentation/pymongo.py +++ b/src/instana/instrumentation/pymongo.py @@ -2,15 +2,15 @@ # (c) Copyright Instana Inc. 2020 -from instana.span.span import InstanaSpan -from instana.log import logger -from instana.util.traceutils import get_tracer_tuple, tracing_is_off - try: import pymongo from bson import json_util from opentelemetry.semconv.trace import SpanAttributes + from instana.log import logger + from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple + class MongoCommandTracer(pymongo.monitoring.CommandListener): def __init__(self) -> None: self.__active_commands = {} @@ -18,8 +18,9 @@ def __init__(self) -> None: def started(self, event: pymongo.monitoring.CommandStartedEvent) -> None: tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing - if tracing_is_off(): + if not tracer: return + parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( diff --git a/src/instana/instrumentation/pyramid.py b/src/instana/instrumentation/pyramid.py index 6faed9db..5b80f0e0 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, Tuple + + 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, 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 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""" @@ -33,6 +33,11 @@ def __init__( self.handler = handler def __call__(self, request: "Request") -> "Response": + tracer = get_tracer() + + if not tracer: + return + 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/redis.py b/src/instana/instrumentation/redis.py index 621bca26..b4962581 100644 --- a/src/instana/instrumentation/redis.py +++ b/src/instana/instrumentation/redis.py @@ -2,15 +2,15 @@ # (c) Copyright Instana Inc. 2018 -from typing import Any, Callable, Dict, Tuple -import wrapt - -from instana.log import logger -from instana.span.span import InstanaSpan -from instana.util.traceutils import get_tracer_tuple, tracing_is_off - try: + from typing import Any, Callable, Dict, Tuple + import redis + import wrapt + + from instana.log import logger + from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple EXCLUDED_PARENT_SPANS = ["redis", "celery-client", "celery-worker"] @@ -44,12 +44,13 @@ def execute_command_with_instana( kwargs: Dict[str, Any], ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): + if not tracer or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: collect_attributes(span, instance, args, kwargs) @@ -70,12 +71,13 @@ def execute_with_instana( kwargs: Dict[str, Any], ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() - parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): + if not tracer or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: collect_attributes(span, instance, args, kwargs) diff --git a/src/instana/instrumentation/sanic.py b/src/instana/instrumentation/sanic.py index c3c1cac5..f96d2ee6 100644 --- a/src/instana/instrumentation/sanic.py +++ b/src/instana/instrumentation/sanic.py @@ -8,7 +8,9 @@ try: import sanic + from instana.log import logger + from instana.singletons import get_tracer if not (hasattr(sanic, "__version__") and sanic.__version__ >= "19.9.0"): logger.debug( @@ -16,20 +18,19 @@ ) raise ImportError - import wrapt - from typing import Callable, Tuple, Dict, Any - from sanic.exceptions import SanicException + from typing import Any, Callable, Dict, Tuple + import wrapt from opentelemetry import context, trace from opentelemetry.semconv.trace import SpanAttributes + from sanic.exceptions import SanicException + from sanic.request import Request + from sanic.response import HTTPResponse - from instana.singletons import tracer, agent + from instana.propagators.format import Format + from instana.singletons import agent from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import extract_custom_headers - from instana.propagators.format import Format - - from sanic.request import Request - from sanic.response import HTTPResponse @wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__") def init_with_instana( @@ -44,7 +45,9 @@ def init_with_instana( @app.middleware("request") def request_with_instana(request: Request) -> None: try: - if "http" not in request.scheme: + tracer = get_tracer() + + if "http" not in request.scheme or not tracer: return headers = request.headers.copy() @@ -99,7 +102,9 @@ def exception_with_instana(request: Request, exception: Exception) -> None: @app.middleware("response") def response_with_instana(request: Request, response: HTTPResponse) -> None: try: - if not hasattr(request.ctx, "span"): # pragma: no cover + tracer = get_tracer() + + if not hasattr(request.ctx, "span") or not tracer: # pragma: no cover return span = request.ctx.span diff --git a/src/instana/instrumentation/spyne.py b/src/instana/instrumentation/spyne.py index bfb4c83d..c9dcd2e7 100644 --- a/src/instana/instrumentation/spyne.py +++ b/src/instana/instrumentation/spyne.py @@ -1,22 +1,32 @@ # (c) Copyright IBM Corp. 2025 -try: - import spyne - import wrapt - from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple, Iterable, Type, Optional +try: from types import SimpleNamespace + from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + Optional, + Tuple, + Type, + ) + + import wrapt from instana.log import logger - from instana.singletons import agent, tracer from instana.propagators.format import Format + from instana.singletons import agent, get_tracer from instana.util.secrets import strip_secrets_from_query if TYPE_CHECKING: - from instana.span.span import InstanaSpan from spyne.application import Application from spyne.server.wsgi import WsgiApplication + from instana.span.span import InstanaSpan + def set_span_attributes(span: "InstanaSpan", headers: Dict[str, Any]) -> None: if "PATH_INFO" in headers: span.set_attribute("rpc.call", headers["PATH_INFO"]) @@ -32,13 +42,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]], @@ -46,16 +57,20 @@ def handle_error_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> Iterable[object]: + tracer = get_tracer() + ctx = args[0] # span created inside process_request() will be handled by finalize() method - if ctx.udc and ctx.udc.span: + if ctx.udc and ctx.udc.span or not tracer: return wrapped(*args, **kwargs) 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 @@ -76,6 +91,11 @@ def finalize_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> Tuple[()]: + tracer = get_tracer() + + if not tracer: + return wrapped(*args, **kwargs) + ctx = args[0] response_string = ctx.transport.resp_code @@ -95,6 +115,11 @@ def process_request_with_instana( args: Tuple[object], kwargs: Dict[str, Any], ) -> None: + tracer = get_tracer() + + if not tracer: + return wrapped(*args, **kwargs) + ctx = args[0] headers = ctx.transport.req_env span_context = tracer.extract(Format.HTTP_HEADERS, headers) diff --git a/src/instana/instrumentation/sqlalchemy.py b/src/instana/instrumentation/sqlalchemy.py index 3f44b526..f4d08285 100644 --- a/src/instana/instrumentation/sqlalchemy.py +++ b/src/instana/instrumentation/sqlalchemy.py @@ -2,21 +2,21 @@ # (c) Copyright Instana Inc. 2018 -import re -from typing import Any, Dict - -from opentelemetry import context, trace - -from instana.log import logger -from instana.span.span import InstanaSpan, get_current_span -from instana.span_context import SpanContext -from instana.util.traceutils import get_tracer_tuple, tracing_is_off - try: + import re + from typing import Any, Dict + + from opentelemetry import context, trace from sqlalchemy import __version__ as sqlalchemy_version from sqlalchemy import event from sqlalchemy.engine import Engine + from instana.log import logger + from instana.singletons import get_tracer + from instana.span.span import InstanaSpan, get_current_span + from instana.span_context import SpanContext + from instana.util.traceutils import get_tracer_tuple + url_regexp = re.compile(r"\/\/(\S+@)") @event.listens_for(Engine, "before_cursor_execute", named=True) @@ -24,11 +24,12 @@ def receive_before_cursor_execute( **kw: Dict[str, Any], ) -> None: try: + tracer, parent_span, _ = get_tracer_tuple() + # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return - tracer, parent_span, _ = get_tracer_tuple() parent_context = parent_span.get_span_context() if parent_span else None span = tracer.start_span("sqlalchemy", span_context=parent_context) @@ -54,8 +55,9 @@ def receive_after_cursor_execute( **kw: Dict[str, Any], ) -> None: try: + tracer = get_tracer() # If we're not tracing, just return - if tracing_is_off(): + if not tracer: return current_span = get_current_span() @@ -96,10 +98,10 @@ def receive_handle_db_error( **kw: Dict[str, Any], ) -> None: try: - if tracing_is_off(): - return + tracer, parent_span, _ = get_tracer_tuple() - current_span = get_current_span() + if not tracer: + return # support older db error event if error_event == "dbapi_error": @@ -110,7 +112,7 @@ def receive_handle_db_error( exception_string = "sqlalchemy_exception" if context: - _set_error_attributes(context, exception_string, current_span) + _set_error_attributes(context, exception_string, parent_span) except Exception: logger.debug( "Instrumenting sqlalchemy @ receive_handle_db_error", diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 52d3e9c8..8f1b7aef 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -2,25 +2,24 @@ # (c) Copyright Instana Inc. 2017 -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union +try: + from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union -import wrapt -from opentelemetry.semconv.trace import SpanAttributes + import wrapt + from opentelemetry.semconv.trace import SpanAttributes -from instana.log import logger -from instana.propagators.format import Format -from instana.singletons import agent -from instana.util.secrets import strip_secrets_from_query -from instana.util.traceutils import ( - get_tracer_tuple, - tracing_is_off, - extract_custom_headers, -) + from instana.log import logger + from instana.propagators.format import Format + from instana.singletons import agent + from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import ( + extract_custom_headers, + get_tracer_tuple, + ) -if TYPE_CHECKING: - from instana.span.span import InstanaSpan + if TYPE_CHECKING: + from instana.span.span import InstanaSpan -try: import urllib3 def _collect_kvs( @@ -107,7 +106,7 @@ def urlopen_with_instana( host = getattr(instance, "host", "") or "" if ( - tracing_is_off() + not tracer or span_name == "boto3" or "com.instana" in request_url_or_path or "com.instana" in host diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5ab7a2f7..939cdcc5 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -5,75 +5,83 @@ Instana WSGI Middleware """ -from typing import Dict, Any, Callable, List, Tuple, Optional - -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry import context, trace - -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 - - -class InstanaWSGIMiddleware(object): - """Instana WSGI middleware""" - - def __init__(self, app: object) -> None: - self.app = app - - def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: - env = environ - - 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) - - tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) - - headers_str = [ - (header[0], str(header[1])) - if not isinstance(header[1], str) - else header - for header in headers - ] - res = start_response(status, headers_str, exc_info) - - 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) +try: + from typing import Any, Callable, Dict, List, Optional, Tuple + + from opentelemetry import context, trace + from opentelemetry.semconv.trace import SpanAttributes + + 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 + + class InstanaWSGIMiddleware(object): + """Instana WSGI middleware""" + + def __init__(self, app: object) -> None: + self.app = app + + def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: + env = environ + tracer = get_tracer() + + if not tracer: + return + + 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) + + tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) + + headers_str = [ + (header[0], str(header[1])) + if not isinstance(header[1], str) + else header + for header in headers + ] + res = start_response(status, headers_str, exc_info) + + 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) +except ImportError: + pass diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index d0a3af23..c5ae5db6 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -13,11 +13,11 @@ ) 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 +from instana.span.span import InstanaSpan if TYPE_CHECKING: - from instana.span.span import InstanaSpan from instana.tracer import InstanaTracer @@ -61,22 +61,6 @@ def extract_custom_headers( logger.debug("extract_custom_headers: ", exc_info=True) -def get_active_tracer() -> Optional["InstanaTracer"]: - """Get the currently active tracer if one exists.""" - try: - current_span = get_current_span() - 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 None - return None - except Exception: - # Do not try to log this with instana, as there is no active tracer and there will be an infinite loop at least - # for PY2 - return None - - def get_tracer_tuple() -> ( Tuple[ Optional["InstanaTracer"], @@ -85,15 +69,17 @@ def get_tracer_tuple() -> ( ] ): """Get a tuple of (tracer, span, span_name) for the current context.""" - active_tracer = get_active_tracer() - current_span = get_current_span() - if active_tracer: - return (active_tracer, current_span, current_span.name) - elif agent.options.allow_exit_as_root: - return (tracer, None, None) - return (None, None, None) - - -def tracing_is_off() -> bool: - """Check if tracing is currently disabled.""" - return not (bool(get_active_tracer()) or agent.options.allow_exit_as_root) + try: + active_tracer = get_tracer() + current_span = get_current_span() + # asyncio Spans are used as NonRecording Spans solely for context propagation + if current_span and isinstance(current_span, InstanaSpan): + if current_span.is_recording() or current_span.name == "asyncio": + return (active_tracer, current_span, current_span.name) + elif agent.options.allow_exit_as_root: + return (active_tracer, None, None) + return (None, None, None) + except Exception: + # Do not try to log this with instana, as there is no active tracer and there will be an infinite loop at least + # for PY2 + return (None, None, None) diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 5e7227ac..a5312005 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -8,17 +8,17 @@ import sys import time +from instana.singletons import get_tracer + try: - from django.urls import re_path, include + from django.urls import include, re_path except ImportError: from django.conf.urls import url as re_path -from django.http import HttpResponse, Http404 +from django.http import Http404, HttpResponse from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind -from instana.singletons import tracer - filepath, extension = os.path.splitext(__file__) os.environ["DJANGO_SETTINGS_MODULE"] = os.path.basename(filepath) sys.path.insert(0, os.path.dirname(os.path.abspath(filepath))) @@ -110,6 +110,8 @@ 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/clients/test_couchbase.py b/tests/clients/test_couchbase.py index 9064fb06..406f7fab 100644 --- a/tests/clients/test_couchbase.py +++ b/tests/clients/test_couchbase.py @@ -1,7 +1,6 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os import time from typing import Generator from unittest.mock import patch @@ -1329,7 +1328,8 @@ def test_query_with_instana_tracing_off(self) -> None: res = None with tracer.start_as_current_span("test"), patch( - "instana.instrumentation.couchbase_inst.tracing_is_off", return_value=True + "instana.instrumentation.couchbase_inst.get_tracer_tuple", + return_value=(None, None, None), ): res = self.bucket.n1ql_query("SELECT 1") assert res diff --git a/tests/clients/test_google-cloud-pubsub.py b/tests/clients/test_google-cloud-pubsub.py index db262e70..fd25f71e 100644 --- a/tests/clients/test_google-cloud-pubsub.py +++ b/tests/clients/test_google-cloud-pubsub.py @@ -14,6 +14,7 @@ from instana.singletons import agent, tracer from instana.span.span import get_current_span +from tests.helpers import get_first_span_by_filter from tests.test_utils import _TraceContextMixin # Use PubSub Emulator exposed at :8085 @@ -82,8 +83,10 @@ def test_publish_as_root_exit_span(self) -> None: assert isinstance(result, str) spans = self.recorder.queued_spans() - assert len(spans) == 1 - gcps_span = spans[0] + assert len(spans) == 2 + + filter = lambda span: span.n == "gcps" # noqa: E731 + gcps_span = get_first_span_by_filter(spans, filter) current_span = get_current_span() assert not current_span.is_recording() @@ -93,6 +96,14 @@ def test_publish_as_root_exit_span(self) -> None: assert gcps_span.data["gcps"]["op"] == "publish" assert self.topic_name == gcps_span.data["gcps"]["top"] + filter = lambda span: span.n == "rpc-client" # noqa: E731 + rpc_span = get_first_span_by_filter(spans, filter) + + assert rpc_span.data["rpc"]["call_type"] == "unary" + assert rpc_span.data["rpc"]["host"] == "localhost" + assert rpc_span.data["rpc"]["port"] == "8681" + assert rpc_span.data["rpc"]["call"] == "/google.pubsub.v1.Publisher/Publish" + # Error logging self.assertErrorLogging(spans) diff --git a/tests/clients/test_google-cloud-storage.py b/tests/clients/test_google-cloud-storage.py index 51b560ba..6e67b8c7 100644 --- a/tests/clients/test_google-cloud-storage.py +++ b/tests/clients/test_google-cloud-storage.py @@ -1,24 +1,23 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import io +import json import sys +from http import client as http_client from typing import Generator -import json + import pytest import requests -import io +from google.api_core import iam, page_iterator +from google.auth.credentials import AnonymousCredentials +from google.cloud import storage +from mock import Mock, patch +from opentelemetry.trace import SpanKind from instana.singletons import agent, tracer from instana.span.span import get_current_span from tests.test_utils import _TraceContextMixin -from opentelemetry.trace import SpanKind - -from mock import patch, Mock -from http import client as http_client - -from google.cloud import storage -from google.api_core import iam, page_iterator -from google.auth.credentials import AnonymousCredentials class TestGoogleCloudStorage(_TraceContextMixin): @@ -1071,13 +1070,13 @@ def test_execute_with_instana_without_tags(self, mock_requests: Mock) -> None: pass assert isinstance(buckets, page_iterator.HTTPIterator) - def test_execute_with_instana_tracing_is_off(self) -> None: + def test_execute_with_instana_is_tracing_off(self) -> None: client = self._client( credentials=AnonymousCredentials(), project="test-project" ) with tracer.start_as_current_span("test"), patch( - "instana.instrumentation.google.cloud.storage.tracing_is_off", - return_value=True, + "instana.instrumentation.google.cloud.storage.get_tracer_tuple", + return_value=(None, None, None), ): response = client.list_buckets() assert isinstance(response.client, storage.Client) @@ -1087,7 +1086,7 @@ def test_execute_with_instana_tracing_is_off(self) -> None: reason='Avoiding "Fatal Python error: Segmentation fault"', ) @patch("requests.Session.request") - def test_download_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: + def test_download_with_instana_is_tracing_off(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( content=b"CONTENT", status_code=http_client.OK ) @@ -1095,8 +1094,8 @@ def test_download_with_instana_tracing_is_off(self, mock_requests: Mock) -> None credentials=AnonymousCredentials(), project="test-project" ) with tracer.start_as_current_span("test"), patch( - "instana.instrumentation.google.cloud.storage.tracing_is_off", - return_value=True, + "instana.instrumentation.google.cloud.storage.get_tracer_tuple", + return_value=(None, None, None), ): response = ( client.bucket("test bucket") @@ -1109,7 +1108,7 @@ def test_download_with_instana_tracing_is_off(self, mock_requests: Mock) -> None assert not response @patch("requests.Session.request") - def test_upload_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: + def test_upload_with_instana_is_tracing_off(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -1119,8 +1118,8 @@ def test_upload_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: ) with tracer.start_as_current_span("test"), patch( - "instana.instrumentation.google.cloud.storage.tracing_is_off", - return_value=True, + "instana.instrumentation.google.cloud.storage.get_tracer_tuple", + return_value=(None, None, None), ): response = ( client.bucket("test bucket") @@ -1130,7 +1129,7 @@ def test_upload_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: assert not response @patch("requests.Session.request") - def test_finish_batch_operation_tracing_is_off(self, mock_requests: Mock) -> None: + def test_finish_batch_operation_is_tracing_off(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( _TWO_PART_BATCH_RESPONSE, status_code=http_client.OK, @@ -1143,8 +1142,8 @@ 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( - "instana.instrumentation.google.cloud.storage.tracing_is_off", - return_value=True, + "instana.instrumentation.google.cloud.storage.get_tracer_tuple", + return_value=(None, None, None), ): with client.batch() as batch_response: for obj in ["obj1", "obj2"]: diff --git a/tests/clients/test_pika.py b/tests/clients/test_pika.py index d01d58d9..8d4bc41d 100644 --- a/tests/clients/test_pika.py +++ b/tests/clients/test_pika.py @@ -465,8 +465,8 @@ def test_basic_publish_with_headers(self, send_method, _unused) -> None: @mock.patch("pika.channel.Channel._send_method") def test_basic_publish_tracing_off(self, send_method, _unused, mocker) -> None: mocker.patch( - "instana.instrumentation.pika.tracing_is_off", - return_value=True, + "instana.instrumentation.pika.get_tracer_tuple", + return_value=(None, None, None), ) self.obj._set_state(self.obj.OPEN) diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index 3a2b29ea..b85761f3 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -1,17 +1,16 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -from typing import Any, Dict, Generator, Optional -import aiohttp import asyncio +from typing import Any, Dict, Generator, Optional +import aiohttp import pytest -from instana.singletons import tracer, agent -from instana.util.ids import hex_id - -import tests.apps.flask_app # noqa: F401 import tests.apps.aiohttp_app # noqa: F401 +import tests.apps.flask_app # noqa: F401 +from instana.singletons import agent, tracer +from instana.util.ids import hex_id from tests.helpers import testenv @@ -409,7 +408,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) @@ -438,7 +440,7 @@ async def test(): response = None try: response = self.loop.run_until_complete(test()) - except: + except Exception: pass spans = self.recorder.queued_spans() @@ -471,8 +473,8 @@ async def test(): def test_client_get_tracing_off(self, mocker) -> None: mocker.patch( - "instana.instrumentation.aiohttp.client.tracing_is_off", - return_value=True, + "instana.instrumentation.aiohttp.client.get_tracer_tuple", + return_value=(None, None, None), ) async def test(): diff --git a/tests/frameworks/test_grpcio.py b/tests/frameworks/test_grpcio.py index 0638f64a..47eff62a 100644 --- a/tests/frameworks/test_grpcio.py +++ b/tests/frameworks/test_grpcio.py @@ -1,22 +1,20 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time import random +import time from typing import Generator -import pytest import grpc - +import pytest from opentelemetry.trace import SpanKind import tests.apps.grpc_server # noqa: F401 import tests.apps.grpc_server.stan_pb2 as stan_pb2 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, tracer from instana.span.span import get_current_span +from tests.helpers import get_first_span_by_filter, get_first_span_by_name, testenv class TestGRPCIO: @@ -668,14 +666,18 @@ def test_root_exit_span(self) -> None: ) spans = self.recorder.queued_spans() - assert len(spans) == 1 + assert len(spans) == 2 - server_span = spans[0] + filter = lambda span: span.n == "rpc-server" # noqa: E731 + server_span = get_first_span_by_filter(spans, filter) + + filter = lambda span: span.n == "rpc-client" # noqa: E731 + client_span = get_first_span_by_filter(spans, filter) assert server_span # Parent relationships - assert not server_span.p + assert server_span.p == client_span.s # Error logging assert not server_span.ec @@ -690,6 +692,12 @@ def test_root_exit_span(self) -> None: assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) assert not server_span.data["rpc"]["error"] + # rpc-client + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["host"] == "127.0.0.1" + assert client_span.data["rpc"]["port"] == "10814" + assert client_span.data["rpc"]["call_type"] == "unary" + def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = False responses = self.server_stub.OneQuestionManyResponses( diff --git a/tests/helpers.py b/tests/helpers.py index f7c2efc4..117712eb 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,6 +5,8 @@ import pytest +from instana.singletons import get_tracer + testenv = {} """ @@ -114,6 +116,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,7 +171,12 @@ def launch_traced_request(url): import requests from instana.log import logger - from instana.singletons import tracer + + tracer = get_tracer() + + # return early if we're not tracing + if not tracer: + return logger.warn( "Launching request with a root SDK span name of 'launch_traced_request'" diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py index 3cfc87c0..af9aef9d 100644 --- a/tests/util/test_traceutils.py +++ b/tests/util/test_traceutils.py @@ -3,12 +3,9 @@ import pytest from instana.singletons import agent, tracer -from instana.tracer import InstanaTracer from instana.util.traceutils import ( extract_custom_headers, - get_active_tracer, get_tracer_tuple, - tracing_is_off, ) @@ -57,19 +54,6 @@ def test_extract_custom_headers(span, custom_headers, format) -> None: assert span.attributes["http.header.X-Capture-That-Too"] == "that too" -def test_get_activate_tracer(mocker) -> None: - assert not get_active_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() - - def test_get_tracer_tuple() -> None: response = get_tracer_tuple() assert response == (None, None, None) @@ -82,16 +66,3 @@ def test_get_tracer_tuple() -> None: with tracer.start_as_current_span("test") as span: response = get_tracer_tuple() assert response == (tracer, span, span.name) - - -def test_tracing_is_off() -> None: - response = tracing_is_off() - assert response - with 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