diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 7a9ec0b1..7add8c29 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -13,12 +13,14 @@ import importlib import os import sys +from importlib import util as importlib_util from typing import Tuple from instana.collector.helpers.runtime import ( is_autowrapt_instrumented, is_webhook_instrumented, ) +from instana.util.config import is_truthy from instana.version import VERSION __author__ = "Instana Inc." @@ -70,7 +72,7 @@ def load(_: object) -> None: def apply_gevent_monkey_patch() -> None: from gevent import monkey - if os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS"): + if provided_options := os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS"): def short_key(k: str) -> str: return k[3:] if k.startswith("no-") else k @@ -81,12 +83,8 @@ def key_to_bool(k: str) -> bool: import inspect all_accepted_patch_all_args = inspect.getfullargspec(monkey.patch_all)[0] - provided_options = ( - os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS") - .replace(" ", "") - .replace("--", "") - .split(",") - ) + provided_options.replace(" ", "").replace("--", "").split(",") + provided_options = [ k for k in provided_options if short_key(k) in all_accepted_patch_all_args ] @@ -115,9 +113,7 @@ def get_aws_lambda_handler() -> Tuple[str, str]: handler_function = "lambda_handler" try: - handler = os.environ.get("LAMBDA_HANDLER", False) - - if handler: + if handler := os.environ.get("LAMBDA_HANDLER", None): parts = handler.split(".") handler_function = parts.pop().strip() handler_module = ".".join(parts).strip() @@ -159,13 +155,10 @@ def boot_agent() -> None: import instana.singletons # noqa: F401 - # Instrumentation + # Import & initialize instrumentation if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ: - # TODO: remove the following entries as the migration of the - # instrumentation codes are finalised. - - # Import & initialize instrumentation from instana.instrumentation import ( + aio_pika, # noqa: F401 aioamqp, # noqa: F401 asyncio, # noqa: F401 cassandra, # noqa: F401 @@ -173,7 +166,6 @@ def boot_agent() -> None: couchbase, # noqa: F401 fastapi, # noqa: F401 flask, # noqa: F401 - # gevent_inst, # noqa: F401 grpcio, # noqa: F401 httpx, # noqa: F401 logging, # noqa: F401 @@ -186,11 +178,10 @@ def boot_agent() -> None: pyramid, # noqa: F401 redis, # noqa: F401 sanic, # noqa: F401 + spyne, # noqa: F401 sqlalchemy, # noqa: F401 starlette, # noqa: F401 urllib3, # noqa: F401 - spyne, # noqa: F401 - aio_pika, # noqa: F401 ) from instana.instrumentation.aiohttp import ( client as aiohttp_client, # noqa: F401 @@ -218,6 +209,8 @@ def boot_agent() -> None: server as tornado_server, # noqa: F401 ) + # from instana.instrumentation import gevent_inst # noqa: F401 + # Hooks from instana.hooks import ( hook_gunicorn, # noqa: F401 @@ -225,7 +218,23 @@ def boot_agent() -> None: ) -if "INSTANA_DISABLE" not in os.environ: +def _start_profiler() -> None: + """Start the Instana Auto Profile.""" + from instana.singletons import get_profiler + + if profiler := get_profiler(): + profiler.start() + + +if "INSTANA_DISABLE" in os.environ: # pragma: no cover + import warnings + + message = "Instana: The INSTANA_DISABLE environment variable is deprecated. Please use INSTANA_TRACING_DISABLE=True instead." + warnings.simplefilter("always") + warnings.warn(message, DeprecationWarning) + + +if not is_truthy(os.environ.get("INSTANA_TRACING_DISABLE", None)): # There are cases when sys.argv may not be defined at load time. Seems to happen in embedded Python, # and some Pipenv installs. If this is the case, it's best effort. if ( @@ -243,15 +252,12 @@ def boot_agent() -> None: if ( (is_autowrapt_instrumented() or is_webhook_instrumented()) and "INSTANA_DISABLE_AUTO_INSTR" not in os.environ - and importlib.util.find_spec("gevent") + and importlib_util.find_spec("gevent") ): apply_gevent_monkey_patch() + # AutoProfile if "INSTANA_AUTOPROFILE" in os.environ: - from instana.singletons import get_profiler - - profiler = get_profiler() - if profiler: - profiler.start() + _start_profiler() boot_agent() diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index 9bb58885..8bc9acd6 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -10,6 +10,7 @@ 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 @@ -27,12 +28,18 @@ def log_with_instana( # 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_in = kwargs.pop( + "stacklevel", 1 if get_runtime_env_info()[0] not in ["ppc64le", "s390x"] else 2 + ) stacklevel = stacklevel_in + 1 + (sys.version_info >= (3, 14)) try: - # Only needed if we're tracing and serious log - if tracing_is_off() or argv[0] < logging.WARN: + # 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() diff --git a/src/instana/options.py b/src/instana/options.py index 356ea961..affaa266 100644 --- a/src/instana/options.py +++ b/src/instana/options.py @@ -16,12 +16,20 @@ import logging import os -from typing import Any, Dict +from typing import Any, Dict, Sequence from instana.configurator import config from instana.log import logger -from instana.util.config import (is_truthy, parse_ignored_endpoints, - parse_ignored_endpoints_from_yaml) +from instana.util.config import ( + SPAN_TYPE_TO_CATEGORY, + get_disable_trace_configurations_from_env, + get_disable_trace_configurations_from_local, + get_disable_trace_configurations_from_yaml, + is_truthy, + parse_ignored_endpoints, + parse_ignored_endpoints_from_yaml, + parse_span_disabling, +) from instana.util.runtime import determine_service_name @@ -37,6 +45,11 @@ def __init__(self, **kwds: Dict[str, Any]) -> None: self.ignore_endpoints = [] self.kafka_trace_correlation = True + # disabled_spans lists all categories and types that should be disabled + self.disabled_spans = [] + # enabled_spans lists all categories and types that should be enabled, preceding disabled_spans + self.enabled_spans = [] + self.set_trace_configurations() # Defaults @@ -75,8 +88,9 @@ def set_trace_configurations(self) -> None: ) # Check if either of the environment variables is truthy - if is_truthy(os.environ.get("INSTANA_ALLOW_EXIT_AS_ROOT", None)) or \ - is_truthy(os.environ.get("INSTANA_ALLOW_ROOT_EXIT_SPAN", None)): + if is_truthy(os.environ.get("INSTANA_ALLOW_EXIT_AS_ROOT", None)) or is_truthy( + os.environ.get("INSTANA_ALLOW_ROOT_EXIT_SPAN", None) + ): self.allow_exit_as_root = True # The priority is as follows: @@ -99,12 +113,69 @@ def set_trace_configurations(self) -> None: ) if "INSTANA_KAFKA_TRACE_CORRELATION" in os.environ: - self.kafka_trace_correlation = is_truthy(os.environ["INSTANA_KAFKA_TRACE_CORRELATION"]) + self.kafka_trace_correlation = is_truthy( + os.environ["INSTANA_KAFKA_TRACE_CORRELATION"] + ) elif isinstance(config.get("tracing"), dict) and "kafka" in config["tracing"]: self.kafka_trace_correlation = config["tracing"]["kafka"].get( "trace_correlation", True ) + self.set_disable_trace_configurations() + + def set_disable_trace_configurations(self) -> None: + disabled_spans = [] + enabled_spans = [] + + # The precedence is as follows: + # environment variables > in-code (local) config > agent config (configuration.yaml) + # For the env vars: INSTANA_TRACING_DISABLE > INSTANA_CONFIG_PATH + if "INSTANA_TRACING_DISABLE" in os.environ: + disabled_spans, enabled_spans = get_disable_trace_configurations_from_env() + elif "INSTANA_CONFIG_PATH" in os.environ: + disabled_spans, enabled_spans = get_disable_trace_configurations_from_yaml() + else: + # In-code (local) config + # The agent config (configuration.yaml) is handled in StandardOptions.set_disable_tracing() + disabled_spans, enabled_spans = ( + get_disable_trace_configurations_from_local() + ) + + self.disabled_spans.extend(disabled_spans) + self.enabled_spans.extend(enabled_spans) + + def is_span_disabled(self, category=None, span_type=None) -> bool: + """ + Check if a span is disabled based on its category and type. + + Args: + category (str): The span category (e.g., "logging", "databases") + span_type (str): The span type (e.g., "redis", "kafka") + + Returns: + bool: True if the span is disabled, False otherwise + """ + # If span_type is provided, check if it's disabled + if span_type and span_type in self.disabled_spans: + return True + + # If category is provided directly, check if it's disabled + if category and category in self.disabled_spans: + return True + + # If span_type is provided but not explicitly configured, + # check if its parent category is disabled. Also check for the precedence rules + if span_type and span_type in SPAN_TYPE_TO_CATEGORY: + parent_category = SPAN_TYPE_TO_CATEGORY[span_type] + if ( + parent_category in self.disabled_spans + and span_type not in self.enabled_spans + ): + return True + + # Default: not disabled + return False + class StandardOptions(BaseOptions): """The options class used when running directly on a host/node with an Instana agent""" @@ -177,6 +248,26 @@ def set_tracing(self, tracing: Dict[str, Any]) -> None: if "extra-http-headers" in tracing: self.extra_http_headers = tracing["extra-http-headers"] + # Handle span disabling configuration + if "disable" in tracing: + self.set_disable_tracing(tracing["disable"]) + + def set_disable_tracing(self, tracing_config: Sequence[Dict[str, Any]]) -> None: + # The precedence is as follows: + # environment variables > in-code (local) config > agent config (configuration.yaml) + if ( + "INSTANA_TRACING_DISABLE" not in os.environ + and "INSTANA_CONFIG_PATH" not in os.environ + and not ( + isinstance(config.get("tracing"), dict) + and "disable" in config["tracing"] + ) + ): + # agent config (configuration.yaml) + disabled_spans, enabled_spans = parse_span_disabling(tracing_config) + self.disabled_spans.extend(disabled_spans) + self.enabled_spans.extend(enabled_spans) + def set_from(self, res_data: Dict[str, Any]) -> None: """ Set the source identifiers given to use by the Instana Host agent. diff --git a/src/instana/util/config.py b/src/instana/util/config.py index 887c2292..6cc1e109 100644 --- a/src/instana/util/config.py +++ b/src/instana/util/config.py @@ -2,11 +2,43 @@ import itertools import os -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Sequence, Tuple, Union +from instana.configurator import config from instana.log import logger from instana.util.config_reader import ConfigReader +# List of supported span categories (technology or protocol) +SPAN_CATEGORIES = [ + "logging", + "databases", + "messaging", + "protocols", # http, grpc, etc. +] + +# Mapping of span type calls (framework, library name, instrumentation name) to categories +SPAN_TYPE_TO_CATEGORY = { + # Database types + "redis": "databases", + "mysql": "databases", + "postgresql": "databases", + "mongodb": "databases", + "cassandra": "databases", + "couchbase": "databases", + "dynamodb": "databases", + "sqlalchemy": "databases", + # Messaging types + "kafka": "messaging", + "rabbitmq": "messaging", + "pika": "messaging", + "aio_pika": "messaging", + "aioamqp": "messaging", + # Protocol types + "http": "protocols", + "grpc": "protocols", + "graphql": "protocols", +} + def parse_service_pair(pair: str) -> List[str]: """ @@ -151,10 +183,10 @@ def parse_ignored_endpoints_from_yaml(file_path: str) -> List[str]: def is_truthy(value: Any) -> bool: """ Check if a value is truthy, accepting various formats. - + @param value: The value to check @return: True if the value is considered truthy, False otherwise - + Accepts the following as True: - True (Python boolean) - "True", "true" (case-insensitive string) @@ -163,17 +195,128 @@ def is_truthy(value: Any) -> bool: """ if value is None: return False - + if isinstance(value, bool): return value - + if isinstance(value, int): return value == 1 - + if isinstance(value, str): value_lower = value.lower() return value_lower == "true" or value == "1" - + return False + +def parse_span_disabling( + disable_list: Sequence[Union[str, Dict[str, Any]]], +) -> Tuple[List[str], List[str]]: + """ + Process a list of span disabling configurations and return lists of disabled and enabled spans. + + @param disable_list: List of span disabling configurations + @return: Tuple of (disabled_spans, enabled_spans) + """ + if not isinstance(disable_list, list): + logger.debug( + f"parse_span_disabling: Invalid disable_list type: {type(disable_list)}" + ) + return [], [] + + disabled_spans = [] + enabled_spans = [] + + for item in disable_list: + if isinstance(item, str): + disabled = parse_span_disabling_str(item) + disabled_spans.extend(disabled) + elif isinstance(item, dict): + disabled, enabled = parse_span_disabling_dict(item) + disabled_spans.extend(disabled) + enabled_spans.extend(enabled) + else: + logger.debug( + f"parse_span_disabling: Invalid disable_list item type: {type(item)}" + ) + + return disabled_spans, enabled_spans + + +def parse_span_disabling_str(item: str) -> List[str]: + """ + Process a string span disabling configuration and return a list of disabled spans. + + @param item: String span disabling configuration + @return: List of disabled spans + """ + if item.lower() in SPAN_CATEGORIES or item.lower() in SPAN_TYPE_TO_CATEGORY.keys(): + return [item.lower()] + else: + logger.debug(f"set_span_disabling_str: Invalid span category/type: {item}") + return [] + + +def parse_span_disabling_dict(items: Dict[str, bool]) -> Tuple[List[str], List[str]]: + """ + Process a dictionary span disabling configuration and return lists of disabled and enabled spans. + + @param items: Dictionary span disabling configuration + @return: Tuple of (disabled_spans, enabled_spans) + """ + disabled_spans = [] + enabled_spans = [] + + for key, value in items.items(): + if key in SPAN_CATEGORIES or key in SPAN_TYPE_TO_CATEGORY.keys(): + if is_truthy(value): + disabled_spans.append(key) + else: + enabled_spans.append(key) + else: + logger.debug(f"set_span_disabling_dict: Invalid span category/type: {key}") + + return disabled_spans, enabled_spans + + +def get_disable_trace_configurations_from_env() -> Tuple[List[str], List[str]]: + # Read INSTANA_TRACING_DISABLE environment variable + if tracing_disable := os.environ.get("INSTANA_TRACING_DISABLE", None): + if is_truthy(tracing_disable): + # INSTANA_TRACING_DISABLE is True/true/1, then we disable all tracing + disabled_spans = [] + for category in SPAN_CATEGORIES: + disabled_spans.append(category) + return disabled_spans, [] + else: + # INSTANA_TRACING_DISABLE is a comma-separated list of span categories/types + tracing_disable_list = [x.strip() for x in tracing_disable.split(",")] + return parse_span_disabling(tracing_disable_list) + return [], [] + + +def get_disable_trace_configurations_from_yaml() -> Tuple[List[str], List[str]]: + config_reader = ConfigReader(os.environ.get("INSTANA_CONFIG_PATH", "")) + + if "tracing" in config_reader.data: + root_key = "tracing" + elif "com.instana.tracing" in config_reader.data: + logger.warning( + 'Please use "tracing" instead of "com.instana.tracing" for local configuration file.' + ) + root_key = "com.instana.tracing" + else: + return [], [] + + tracing_disable_config = config_reader.data[root_key].get("disable", "") + return parse_span_disabling(tracing_disable_config) + + +def get_disable_trace_configurations_from_local() -> Tuple[List[str], List[str]]: + if "tracing" in config: + if tracing_disable_config := config["tracing"].get("disable", None): + return parse_span_disabling(tracing_disable_config) + return [], [] + + # Made with Bob diff --git a/src/instana/util/config_reader.py b/src/instana/util/config_reader.py index ddec31ec..87b5f8c1 100644 --- a/src/instana/util/config_reader.py +++ b/src/instana/util/config_reader.py @@ -1,15 +1,18 @@ # (c) Copyright IBM Corp. 2025 -from typing import Union -from instana.log import logger import yaml +from instana.log import logger + class ConfigReader: - def __init__(self, file_path: Union[str]) -> None: + def __init__(self, file_path: str) -> None: self.file_path = file_path - self.data = None - self.load_file() + self.data = {} + if file_path: + self.load_file() + else: + logger.warning("ConfigReader: No configuration file specified") def load_file(self) -> None: """Loads and parses the YAML file""" @@ -17,6 +20,8 @@ def load_file(self) -> None: with open(self.file_path, "r") as file: self.data = yaml.safe_load(file) except FileNotFoundError: - logger.error(f"Configuration file has not found: {self.file_path}") + logger.error( + f"ConfigReader: Configuration file has not found: {self.file_path}" + ) except yaml.YAMLError as e: - logger.error(f"Error parsing YAML file: {e}") + logger.error(f"ConfigReader: Error parsing YAML file: {e}") diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index e924ac1c..0fa5d2dc 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -70,7 +70,7 @@ def test_parameters(self) -> None: try: a = 42 b = 0 - c = a / b + c = a / b # noqa: F841 except Exception as e: self.logger.exception("Exception: %s", str(e)) @@ -168,3 +168,78 @@ def main(): assert spans[0].k is SpanKind.CLIENT assert spans[0].data["log"].get("message") == "foo bar" + + +class TestLoggingDisabling: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + # Setup + self.recorder = tracer.span_processor + self.recorder.clear_spans() + self.logger = logging.getLogger("unit test") + + # Save original options + self.original_options = agent.options + + yield + + # Teardown + agent.options = self.original_options + agent.options.allow_exit_as_root = False + + def test_logging_enabled(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("test message") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"].get("message") == "test message" + + def test_logging_disabled(self) -> None: + # Disable logging spans + agent.options.disabled_spans = ["logging"] + + with tracer.start_as_current_span("test"): + self.logger.warning("test message") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 # Only the parent span, no logging span + + def test_logging_disabled_via_env_var(self, monkeypatch): + # Disable logging spans via environment variable + monkeypatch.setenv("INSTANA_TRACING_DISABLE", "logging") + + # Create new options to read from environment + original_options = agent.options + agent.options = type(original_options)() + + with tracer.start_as_current_span("test"): + self.logger.warning("test message") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 # Only the parent span, no logging span + + # Restore original options + agent.options = original_options + + def test_logging_disabled_via_yaml(self) -> None: + # Disable logging spans via YAML configuration + original_options = agent.options + agent.options = type(original_options)() + + # Simulate YAML configuration + tracing_config = {"disable": [{"logging": True}]} + agent.options.set_tracing(tracing_config) + + with tracer.start_as_current_span("test"): + self.logger.warning("test message") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 # Only the parent span, no logging span + + # Restore original options + agent.options = original_options + + +# Made with Bob diff --git a/tests/test_options.py b/tests/test_options.py index a2130c38..4c3d3869 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -4,8 +4,8 @@ import os from typing import Generator -from mock import patch import pytest +from mock import patch from instana.configurator import config from instana.options import ( @@ -41,6 +41,8 @@ def test_base_options(self) -> None: assert self.base_options.secrets_matcher == "contains-ignore-case" assert self.base_options.secrets_list == ["key", "pass", "secret"] assert not self.base_options.secrets + assert self.base_options.disabled_spans == [] + assert self.base_options.enabled_spans == [] def test_base_options_with_config(self) -> None: config["tracing"] = { @@ -62,6 +64,7 @@ def test_base_options_with_config(self) -> None: "INSTANA_EXTRA_HTTP_HEADERS": "SOMETHING;HERE", "INSTANA_IGNORE_ENDPOINTS": "service1;service2:method1,method2", "INSTANA_SECRETS": "secret1:username,password", + "INSTANA_TRACING_DISABLE": "logging, redis,kafka", }, ) def test_base_options_with_env_vars(self) -> None: @@ -80,6 +83,11 @@ def test_base_options_with_env_vars(self) -> None: assert self.base_options.secrets_matcher == "secret1" assert self.base_options.secrets_list == ["username", "password"] + assert "logging" in self.base_options.disabled_spans + assert "redis" in self.base_options.disabled_spans + assert "kafka" in self.base_options.disabled_spans + assert len(self.base_options.enabled_spans) == 0 + @patch.dict( os.environ, {"INSTANA_IGNORE_ENDPOINTS_PATH": "tests/util/test_configuration-1.yaml"}, @@ -108,17 +116,29 @@ def test_base_options_with_endpoint_file(self) -> None: "INSTANA_IGNORE_ENDPOINTS": "env_service1;env_service2:method1,method2", "INSTANA_KAFKA_TRACE_CORRELATION": "false", "INSTANA_IGNORE_ENDPOINTS_PATH": "tests/util/test_configuration-1.yaml", + "INSTANA_TRACING_DISABLE": "logging,redis, kafka", }, ) def test_set_trace_configurations_by_env_variable(self) -> None: # The priority is as follows: # environment variables > in-code configuration > # > agent config (configuration.yaml) > default value + + # in-code configuration + config["tracing"] = {} config["tracing"]["ignore_endpoints"] = ( "config_service1;config_service2:method1,method2" ) config["tracing"]["kafka"] = {"trace_correlation": True} - test_tracing = {"ignore-endpoints": "service1;service2:method1,method2"} + config["tracing"]["disable"] = [{"databases": True}] + + # agent config (configuration.yaml) + test_tracing = { + "ignore-endpoints": "service1;service2:method1,method2", + "disable": [ + {"messaging": True}, + ], + } # Setting by env variable self.base_options = StandardOptions() @@ -131,6 +151,14 @@ def test_set_trace_configurations_by_env_variable(self) -> None: ] assert not self.base_options.kafka_trace_correlation + # Check disabled_spans list + assert "logging" in self.base_options.disabled_spans + assert "redis" in self.base_options.disabled_spans + assert "kafka" in self.base_options.disabled_spans + assert "databases" not in self.base_options.disabled_spans + assert "messaging" not in self.base_options.disabled_spans + assert len(self.base_options.enabled_spans) == 0 + @patch.dict( os.environ, { @@ -138,12 +166,25 @@ def test_set_trace_configurations_by_env_variable(self) -> None: "INSTANA_IGNORE_ENDPOINTS_PATH": "tests/util/test_configuration-1.yaml", }, ) - def test_set_trace_configurations_by_local_configuration_file(self) -> None: + def test_set_trace_configurations_by_in_code_configuration(self) -> None: + # The priority is as follows: + # in-code configuration > agent config (configuration.yaml) > default value + + # in-code configuration + config["tracing"] = {} config["tracing"]["ignore_endpoints"] = ( "config_service1;config_service2:method1,method2" ) config["tracing"]["kafka"] = {"trace_correlation": True} - test_tracing = {"ignore-endpoints": "service1;service2:method1,method2"} + config["tracing"]["disable"] = [{"databases": True}] + + # agent config (configuration.yaml) + test_tracing = { + "ignore-endpoints": "service1;service2:method1,method2", + "disable": [ + {"messaging": True}, + ], + } self.base_options = StandardOptions() self.base_options.set_tracing(test_tracing) @@ -163,7 +204,16 @@ def test_set_trace_configurations_by_local_configuration_file(self) -> None: "kafka.*.topic4", ] + # Check disabled_spans list + assert "databases" in self.base_options.disabled_spans + assert "logging" not in self.base_options.disabled_spans + assert "redis" not in self.base_options.disabled_spans + assert "kafka" not in self.base_options.disabled_spans + assert "messaging" not in self.base_options.disabled_spans + assert len(self.base_options.enabled_spans) == 0 + def test_set_trace_configurations_by_in_code_variable(self) -> None: + config["tracing"] = {} config["tracing"]["ignore_endpoints"] = ( "config_service1;config_service2:method1,method2" ) @@ -184,6 +234,13 @@ def test_set_trace_configurations_by_agent_configuration(self) -> None: test_tracing = { "ignore-endpoints": "service1;service2:method1,method2", "trace-correlation": True, + "disable": [ + { + "messaging": True, + "logging": True, + "kafka": False, + }, + ], } self.base_options = StandardOptions() @@ -196,12 +253,78 @@ def test_set_trace_configurations_by_agent_configuration(self) -> None: ] assert self.base_options.kafka_trace_correlation + # Check disabled_spans list + assert "databases" not in self.base_options.disabled_spans + assert "logging" in self.base_options.disabled_spans + assert "messaging" in self.base_options.disabled_spans + assert "kafka" in self.base_options.enabled_spans + def test_set_trace_configurations_by_default(self) -> None: self.base_options = StandardOptions() self.base_options.set_tracing({}) assert not self.base_options.ignore_endpoints assert self.base_options.kafka_trace_correlation + assert len(self.base_options.disabled_spans) == 0 + assert len(self.base_options.enabled_spans) == 0 + + @patch.dict( + os.environ, + {"INSTANA_TRACING_DISABLE": "true"}, + ) + def test_set_trace_configurations_disable_all_tracing(self) -> None: + self.base_options = BaseOptions() + + # All categories should be disabled + assert "logging" in self.base_options.disabled_spans + assert "databases" in self.base_options.disabled_spans + assert "messaging" in self.base_options.disabled_spans + assert "protocols" in self.base_options.disabled_spans + + # Check is_span_disabled method + assert self.base_options.is_span_disabled(category="logging") + assert self.base_options.is_span_disabled(category="databases") + assert self.base_options.is_span_disabled(span_type="redis") + + @patch.dict( + os.environ, + { + "INSTANA_CONFIG_PATH": "tests/util/test_configuration-1.yaml", + }, + ) + def test_set_trace_configurations_disable_local_yaml(self) -> None: + self.base_options = BaseOptions() + + # All categories should be disabled + assert "logging" in self.base_options.disabled_spans + assert "databases" in self.base_options.disabled_spans + assert "redis" not in self.base_options.disabled_spans + assert "redis" in self.base_options.enabled_spans + + # Check is_span_disabled method + assert self.base_options.is_span_disabled(category="logging") + assert self.base_options.is_span_disabled(category="databases") + assert not self.base_options.is_span_disabled(span_type="redis") + + def test_is_span_disabled_method(self) -> None: + self.base_options = BaseOptions() + + # Default behavior - nothing disabled + assert not self.base_options.is_span_disabled(category="logging") + assert not self.base_options.is_span_disabled(span_type="redis") + + # Disable a category + self.base_options.disabled_spans = ["databases"] + assert not self.base_options.is_span_disabled(category="logging") + assert self.base_options.is_span_disabled(category="databases") + assert self.base_options.is_span_disabled(span_type="redis") + assert self.base_options.is_span_disabled(span_type="mysql") + + # Test precedence rules + self.base_options.enabled_spans = ["redis"] + assert self.base_options.is_span_disabled(category="databases") + assert self.base_options.is_span_disabled(span_type="mysql") + assert not self.base_options.is_span_disabled(span_type="redis") class TestStandardOptions: @@ -258,6 +381,25 @@ def test_set_tracing( ) assert not self.standart_options.extra_http_headers + def test_set_tracing_with_span_disabling(self) -> None: + self.standart_options = StandardOptions() + + test_tracing = { + "disable": [{"logging": True}, {"redis": False}, {"databases": True}] + } + self.standart_options.set_tracing(test_tracing) + + # Check disabled_spans and enabled_spans lists + assert "logging" in self.standart_options.disabled_spans + assert "databases" in self.standart_options.disabled_spans + assert "redis" in self.standart_options.enabled_spans + + # Check is_span_disabled method + assert self.standart_options.is_span_disabled(category="logging") + assert self.standart_options.is_span_disabled(category="databases") + assert self.standart_options.is_span_disabled(span_type="mysql") + assert not self.standart_options.is_span_disabled(span_type="redis") + def test_set_from(self) -> None: self.standart_options = StandardOptions() test_res_data = { @@ -493,3 +635,6 @@ def test_gcr_options_with_env_vars(self) -> None: assert self.gcr_options.endpoint_proxy == {"https": "proxy1"} assert self.gcr_options.timeout == 3 assert self.gcr_options.log_level == logging.INFO + + +# Made with Bob diff --git a/tests/test_span_disabling.py b/tests/test_span_disabling.py new file mode 100644 index 00000000..e1e1cbf5 --- /dev/null +++ b/tests/test_span_disabling.py @@ -0,0 +1,79 @@ +# (c) Copyright IBM Corp. 2025 + +import pytest + +from instana.options import BaseOptions, StandardOptions +from instana.singletons import agent + + +class TestSpanDisabling: + @pytest.fixture(autouse=True) + def setup(self): + # Save original options + self.original_options = agent.options + yield + # Restore original options + agent.options = self.original_options + + def test_is_span_disabled_default(self): + options = BaseOptions() + assert not options.is_span_disabled(category="logging") + assert not options.is_span_disabled(category="databases") + assert not options.is_span_disabled(span_type="redis") + + def test_disable_category(self): + options = BaseOptions() + options.disabled_spans = ["logging"] + assert options.is_span_disabled(category="logging") + assert not options.is_span_disabled(category="databases") + + def test_disable_type(self): + options = BaseOptions() + options.disabled_spans = ["redis"] + assert options.is_span_disabled(span_type="redis") + assert not options.is_span_disabled(span_type="mysql") + + def test_type_category_relationship(self): + options = BaseOptions() + options.disabled_spans = ["databases"] + assert options.is_span_disabled(span_type="redis") + assert options.is_span_disabled(span_type="mysql") + + def test_precedence_rules(self): + options = BaseOptions() + options.disabled_spans = ["databases"] + options.enabled_spans = ["redis"] + assert options.is_span_disabled(category="databases") + assert options.is_span_disabled(span_type="mysql") + assert not options.is_span_disabled(span_type="redis") + + @pytest.mark.parametrize("value", ["True", "true", "1"]) + def test_env_var_disable_all(self, value, monkeypatch): + monkeypatch.setenv("INSTANA_TRACING_DISABLE", value) + options = BaseOptions() + assert options.is_span_disabled(category="logging") is True + assert options.is_span_disabled(category="databases") is True + assert options.is_span_disabled(category="messaging") is True + assert options.is_span_disabled(category="protocols") is True + + def test_env_var_disable_specific(self, monkeypatch): + monkeypatch.setenv("INSTANA_TRACING_DISABLE", "logging, redis") + options = BaseOptions() + assert options.is_span_disabled(category="logging") is True + assert options.is_span_disabled(category="databases") is False + assert options.is_span_disabled(span_type="redis") is True + assert options.is_span_disabled(span_type="mysql") is False + + def test_yaml_config(self): + options = StandardOptions() + tracing_config = { + "disable": [{"logging": True}, {"redis": False}, {"databases": True}] + } + options.set_tracing(tracing_config) + assert options.is_span_disabled(category="logging") + assert options.is_span_disabled(category="databases") + assert options.is_span_disabled(span_type="mysql") + assert not options.is_span_disabled(span_type="redis") + + +# Made with Bob diff --git a/tests/util/test_config_reader.py b/tests/util/test_config_reader.py index b9bb063d..c5753f8e 100644 --- a/tests/util/test_config_reader.py +++ b/tests/util/test_config_reader.py @@ -1,16 +1,78 @@ # (c) Copyright IBM Corp. 2025 import logging +import os +from typing import TYPE_CHECKING, Generator import pytest +from yaml import YAMLError -from instana.util.config import parse_ignored_endpoints_from_yaml +from instana.util.config import ( + get_disable_trace_configurations_from_yaml, + parse_ignored_endpoints_from_yaml, +) +from instana.util.config_reader import ConfigReader + +if TYPE_CHECKING: + from pytest import LogCaptureFixture + from pytest_mock import MockerFixture class TestConfigReader: - def test_load_configuration_with_tracing( - self, caplog: pytest.LogCaptureFixture + @pytest.fixture(autouse=True) + def _resource( + self, + caplog: "LogCaptureFixture", + ) -> Generator[None, None, None]: + yield + caplog.clear() + if "INSTANA_CONFIG_PATH" in os.environ: + os.environ.pop("INSTANA_CONFIG_PATH") + + def test_config_reader_null(self, caplog: "LogCaptureFixture") -> None: + config_reader = ConfigReader(os.environ.get("INSTANA_CONFIG_PATH", "")) + assert config_reader.file_path == "" + assert config_reader.data == {} + assert "ConfigReader: No configuration file specified" in caplog.messages + + def test_config_reader_default(self) -> None: + filename = "tests/util/test_configuration-1.yaml" + os.environ["INSTANA_CONFIG_PATH"] = filename + config_reader = ConfigReader(os.environ.get("INSTANA_CONFIG_PATH", "")) + assert config_reader.file_path == filename + assert "tracing" in config_reader.data + assert len(config_reader.data["tracing"]) == 2 + + def test_config_reader_file_not_found_error( + self, caplog: "LogCaptureFixture" + ) -> None: + filename = "tests/util/test_configuration-3.yaml" + os.environ["INSTANA_CONFIG_PATH"] = filename + config_reader = ConfigReader(os.environ.get("INSTANA_CONFIG_PATH", "")) + assert config_reader.file_path == filename + assert config_reader.data == {} + assert ( + f"ConfigReader: Configuration file has not found: {filename}" + in caplog.messages + ) + + def test_config_reader_yaml_error( + self, caplog: "LogCaptureFixture", mocker: "MockerFixture" ) -> None: + filename = "tests/util/test_configuration-1.yaml" + exception_message = "BLAH" + mocker.patch( + "instana.util.config_reader.yaml.safe_load", + side_effect=YAMLError(exception_message), + ) + + config_reader = ConfigReader(filename) # noqa: F841 + assert ( + f"ConfigReader: Error parsing YAML file: {exception_message}" + in caplog.messages + ) + + def test_load_configuration_with_tracing(self, caplog: "LogCaptureFixture") -> None: caplog.set_level(logging.DEBUG, logger="instana") ignore_endpoints = parse_ignored_endpoints_from_yaml( @@ -32,12 +94,20 @@ def test_load_configuration_with_tracing( "kafka.*.topic4", ] + os.environ["INSTANA_CONFIG_PATH"] = "tests/util/test_configuration-1.yaml" + disabled_spans, enabled_spans = get_disable_trace_configurations_from_yaml() + # Check disabled_spans list + assert "logging" in disabled_spans + assert "databases" in disabled_spans + assert "redis" not in disabled_spans + assert "redis" in enabled_spans + assert ( 'Please use "tracing" instead of "com.instana.tracing" for local configuration file.' not in caplog.messages ) - def test_load_configuration_legacy(self, caplog: pytest.LogCaptureFixture) -> None: + def test_load_configuration_legacy(self, caplog: "LogCaptureFixture") -> None: caplog.set_level(logging.DEBUG, logger="instana") ignore_endpoints = parse_ignored_endpoints_from_yaml( @@ -58,6 +128,15 @@ def test_load_configuration_legacy(self, caplog: pytest.LogCaptureFixture) -> No "kafka.*.span-topic", "kafka.*.topic4", ] + + os.environ["INSTANA_CONFIG_PATH"] = "tests/util/test_configuration-2.yaml" + disabled_spans, enabled_spans = get_disable_trace_configurations_from_yaml() + # Check disabled_spans list + assert "logging" in disabled_spans + assert "databases" in disabled_spans + assert "redis" not in disabled_spans + assert "redis" in enabled_spans + assert ( 'Please use "tracing" instead of "com.instana.tracing" for local configuration file.' in caplog.messages diff --git a/tests/util/test_configuration-1.yaml b/tests/util/test_configuration-1.yaml index af890a35..ac61d362 100644 --- a/tests/util/test_configuration-1.yaml +++ b/tests/util/test_configuration-1.yaml @@ -17,3 +17,8 @@ tracing: endpoints: ["span-topic", "topic4"] # - methods: ["consume", "send"] # endpoints: ["*"] # Applied to all topics + disable: + - "logging": true + - "databases": true + - "redis": false + \ No newline at end of file diff --git a/tests/util/test_configuration-2.yaml b/tests/util/test_configuration-2.yaml index b418cd55..5ed83ec1 100644 --- a/tests/util/test_configuration-2.yaml +++ b/tests/util/test_configuration-2.yaml @@ -18,3 +18,7 @@ com.instana.tracing: endpoints: ["span-topic", "topic4"] # - methods: ["consume", "send"] # endpoints: ["*"] # Applied to all topics + disable: + - "logging": true + - "databases": true + - "redis": false