Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/instana/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"attributes": [
{
"key": "http.url",
"values": ["com.instana"],

Check failure on line 133 in src/instana/options.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "com.instana" 3 times.

See more on https://sonarcloud.io/project/issues?id=instana_python-sensor&issues=AZ1JhmZOxDMSx2rFjl3e&open=AZ1JhmZOxDMSx2rFjl3e&pullRequest=859
"match_type": "contains",
}
],
Expand All @@ -145,6 +145,16 @@
}
],
},
{
"name": "filter-internal-sdk-spans-by-url",
"attributes": [
{
"key": "sdk.custom.tags.http.url",
"values": ["com.instana"],
"match_type": "contains",
}
],
},
]
)

Expand Down
39 changes: 38 additions & 1 deletion src/instana/util/span_utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
60 changes: 60 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
}
],
},
{
"name": "filter-internal-sdk-spans-by-url",
"attributes": [
{
"key": "sdk.custom.tags.http.url",
"values": ["com.instana"],
"match_type": "contains",
}
],
},
]


Expand Down Expand Up @@ -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",
}
],
},
],
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
}
],
},
],
}

Expand Down Expand Up @@ -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",
}
],
},
],
}

Expand Down
117 changes: 116 additions & 1 deletion tests/util/test_span_utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
Loading