diff --git a/src/instana/options.py b/src/instana/options.py index cb709918..d2d3e258 100644 --- a/src/instana/options.py +++ b/src/instana/options.py @@ -145,6 +145,16 @@ def _add_instana_agent_span_filter(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ] ) diff --git a/src/instana/util/span_utils.py b/src/instana/util/span_utils.py index e736be0d..9a912623 100644 --- a/src/instana/util/span_utils.py +++ b/src/instana/util/span_utils.py @@ -1,7 +1,7 @@ # (c) Copyright IBM Corp. 2025 -from typing import Any, List +from typing import Any, Dict, List, Optional from instana.util.config import SPAN_TYPE_TO_CATEGORY @@ -37,8 +37,15 @@ def matches_rule(rule_attributes: List[Any], span_attributes: List[Any]) -> bool rule_matched = True else: + span_value = None if key in span_attributes: span_value = span_attributes[key] + elif "." in key: + # Support dot-notation paths for nested attributes + # e.g. "sdk.custom.tags.http.host" -> span["sdk.custom"]["tags"]["http.host"] + span_value = resolve_nested_key(span_attributes, key.split(".")) + + if span_value is not None: for rule_value in target_values: if match_key_filter(span_value, rule_value, match_type): rule_matched = True @@ -50,6 +57,36 @@ def matches_rule(rule_attributes: List[Any], span_attributes: List[Any]) -> bool return True +def resolve_nested_key(data: Dict[str, Any], key_parts: List[str]) -> Optional[Any]: + """Resolve a dotted key path against a potentially nested dict. + + Tries all possible prefix lengths so that keys which themselves contain + dots (e.g. ``sdk.custom`` or ``http.host``) are handled correctly. + + Example:: + + # span_attributes = {"sdk.custom": {"tags": {"http.host": "example.com"}}} + resolve_nested_key(span_attributes, ["sdk", "custom", "tags", "http", "host"]) + # -> "example.com" + """ + if not key_parts or not isinstance(data, dict): + return None + + # Try the longest prefix first so that keys with embedded dots are matched + # before shorter splits (e.g. prefer "sdk.custom" over "sdk"). + for i in range(len(key_parts), 0, -1): + candidate = ".".join(key_parts[:i]) + if candidate in data: + remaining = key_parts[i:] + if not remaining: + return data[candidate] + result = resolve_nested_key(data[candidate], remaining) + if result is not None: + return result + + return None + + def match_key_filter(span_value: str, rule_value: str, match_type: str) -> bool: """Check if the first value matches the second value based on the match type.""" # Guard against None values diff --git a/tests/test_options.py b/tests/test_options.py index e0a4d35f..7433c0a1 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -39,6 +39,16 @@ } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ] @@ -184,6 +194,16 @@ def test_base_options_with_env_vars(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ], } @@ -296,6 +316,16 @@ def test_base_options_with_endpoint_file(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ], } del self.base_options @@ -377,6 +407,16 @@ def test_set_trace_configurations_by_env_variable(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ], } assert not self.base_options.kafka_trace_correlation @@ -512,6 +552,16 @@ def test_set_trace_configurations_by_in_code_configuration(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ], } @@ -779,6 +829,16 @@ def test_tracing_filter_environment_variables(self) -> None: } ], }, + { + "name": "filter-internal-sdk-spans-by-url", + "attributes": [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ], + }, ], } diff --git a/tests/util/test_span_utils.py b/tests/util/test_span_utils.py index 0f4d248c..5fbd5c68 100644 --- a/tests/util/test_span_utils.py +++ b/tests/util/test_span_utils.py @@ -1,6 +1,13 @@ # (c) Copyright IBM Corp. 2025 -from instana.util.span_utils import matches_rule, match_key_filter, get_span_kind +from collections import defaultdict + +from instana.util.span_utils import ( + get_span_kind, + match_key_filter, + matches_rule, + resolve_nested_key, +) class TestSpanUtils: @@ -144,3 +151,111 @@ def test_matches_rule_with_none_attribute_value(self) -> None: {"key": "http.method", "values": ["GET"], "match_type": "strict"} ] assert matches_rule(rule_method, span_attrs) + + def test_resolve_nested_key_embedded_dot_keys(self) -> None: + """Resolves sdk.custom.tags.http.host through a defaultdict structure — + the exact layout produced by real SDK spans.""" + sdk_custom = defaultdict(dict) + sdk_custom["tags"] = defaultdict(str) + sdk_custom["tags"]["http.host"] = "agent.com.instana.io" + + assert ( + resolve_nested_key( + {"sdk.custom": sdk_custom}, ["sdk", "custom", "tags", "http", "host"] + ) + == "agent.com.instana.io" + ) + + def test_resolve_nested_key_returns_none_when_missing(self) -> None: + """Returns None when the dotted path does not exist in the data.""" + assert ( + resolve_nested_key( + {"sdk.custom": {"tags": {}}}, ["sdk", "custom", "tags", "http", "host"] + ) + is None + ) + + def test_resolve_nested_key_with_empty_key_parts(self) -> None: + """Returns None when key_parts is an empty list.""" + data = {"sdk.custom": {"tags": {"http.host": "example.com"}}} + assert resolve_nested_key(data, []) is None + + def test_resolve_nested_key_with_non_dict_data(self) -> None: + """Returns None when data is not a dictionary.""" + # Test with string + assert resolve_nested_key("not a dict", ["key"]) is None + + # Test with list + assert resolve_nested_key(["not", "a", "dict"], ["key"]) is None + + # Test with None + assert resolve_nested_key(None, ["key"]) is None + + # Test with integer + assert resolve_nested_key(42, ["key"]) is None + + def test_matches_rule_sdk_span_host_match(self) -> None: + """SDK span whose sdk.custom.tags.http.host contains 'com.instana' should be filtered.""" + sdk_custom = defaultdict(dict) + sdk_custom["tags"] = {"http.host": "agent.com.instana.io"} + span_attrs = { + "type": "sdk", + "kind": 3, + "sdk.name": "my-span", + "sdk.custom": sdk_custom, + } + + rule = [ + { + "key": "sdk.custom.tags.http.host", + "values": ["com.instana"], + "match_type": "contains", + } + ] + assert matches_rule(rule, span_attrs) + + def test_matches_rule_sdk_span_host_no_match(self) -> None: + """SDK span with an unrelated host should NOT be filtered.""" + sdk_custom = defaultdict(dict) + sdk_custom["tags"] = {"http.host": "myapp.example.com"} + span_attrs = { + "type": "sdk", + "kind": 3, + "sdk.name": "my-span", + "sdk.custom": sdk_custom, + } + + rule = [ + { + "key": "sdk.custom.tags.http.host", + "values": ["com.instana"], + "match_type": "contains", + } + ] + assert not matches_rule(rule, span_attrs) + + def test_matches_rule_sdk_span_url_match(self) -> None: + """SDK span whose sdk.custom.tags.http.url contains 'com.instana' should be filtered. + + Covers the span shape: + data.sdk.custom.tags.http.url = 'http://localhost:42699/com.instana.plugin.python.89262' + """ + sdk_custom = defaultdict(dict) + sdk_custom["tags"] = { + "http.url": "http://localhost:42699/com.instana.plugin.python.89262" + } + span_attrs = { + "type": "sdk", + "kind": 3, + "sdk.name": "HEAD", + "sdk.custom": sdk_custom, + } + + rule = [ + { + "key": "sdk.custom.tags.http.url", + "values": ["com.instana"], + "match_type": "contains", + } + ] + assert matches_rule(rule, span_attrs)