Skip to content

Commit cc66ec4

Browse files
committed
fix: Add support for custom span filtering
Signed-off-by: Cagri Yonca <cagri@ibm.com>
1 parent 905a32d commit cc66ec4

4 files changed

Lines changed: 205 additions & 2 deletions

File tree

src/instana/options.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,16 @@ def _add_instana_agent_span_filter(self) -> None:
145145
}
146146
],
147147
},
148+
{
149+
"name": "filter-internal-sdk-spans-by-url",
150+
"attributes": [
151+
{
152+
"key": "sdk.custom.tags.http.url",
153+
"values": ["com.instana"],
154+
"match_type": "contains",
155+
}
156+
],
157+
},
148158
]
149159
)
150160

src/instana/util/span_utils.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# (c) Copyright IBM Corp. 2025
22

33

4-
from typing import Any, List
4+
from typing import Any, Dict, List, Optional
55

66
from instana.util.config import SPAN_TYPE_TO_CATEGORY
77

@@ -37,8 +37,15 @@ def matches_rule(rule_attributes: List[Any], span_attributes: List[Any]) -> bool
3737
rule_matched = True
3838

3939
else:
40+
span_value = None
4041
if key in span_attributes:
4142
span_value = span_attributes[key]
43+
elif "." in key:
44+
# Support dot-notation paths for nested attributes
45+
# e.g. "sdk.custom.tags.http.host" -> span["sdk.custom"]["tags"]["http.host"]
46+
span_value = resolve_nested_key(span_attributes, key.split("."))
47+
48+
if span_value is not None:
4249
for rule_value in target_values:
4350
if match_key_filter(span_value, rule_value, match_type):
4451
rule_matched = True
@@ -50,6 +57,36 @@ def matches_rule(rule_attributes: List[Any], span_attributes: List[Any]) -> bool
5057
return True
5158

5259

60+
def resolve_nested_key(data: Dict[str, Any], key_parts: List[str]) -> Optional[Any]:
61+
"""Resolve a dotted key path against a potentially nested dict.
62+
63+
Tries all possible prefix lengths so that keys which themselves contain
64+
dots (e.g. ``sdk.custom`` or ``http.host``) are handled correctly.
65+
66+
Example::
67+
68+
# span_attributes = {"sdk.custom": {"tags": {"http.host": "example.com"}}}
69+
resolve_nested_key(span_attributes, ["sdk", "custom", "tags", "http", "host"])
70+
# -> "example.com"
71+
"""
72+
if not key_parts or not isinstance(data, dict):
73+
return None
74+
75+
# Try the longest prefix first so that keys with embedded dots are matched
76+
# before shorter splits (e.g. prefer "sdk.custom" over "sdk").
77+
for i in range(len(key_parts), 0, -1):
78+
candidate = ".".join(key_parts[:i])
79+
if candidate in data:
80+
remaining = key_parts[i:]
81+
if not remaining:
82+
return data[candidate]
83+
result = resolve_nested_key(data[candidate], remaining)
84+
if result is not None:
85+
return result
86+
87+
return None
88+
89+
5390
def match_key_filter(span_value: str, rule_value: str, match_type: str) -> bool:
5491
"""Check if the first value matches the second value based on the match type."""
5592
# Guard against None values

tests/test_options.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@
3939
}
4040
],
4141
},
42+
{
43+
"name": "filter-internal-sdk-spans-by-url",
44+
"attributes": [
45+
{
46+
"key": "sdk.custom.tags.http.url",
47+
"values": ["com.instana"],
48+
"match_type": "contains",
49+
}
50+
],
51+
},
4252
]
4353

4454

@@ -184,6 +194,16 @@ def test_base_options_with_env_vars(self) -> None:
184194
}
185195
],
186196
},
197+
{
198+
"name": "filter-internal-sdk-spans-by-url",
199+
"attributes": [
200+
{
201+
"key": "sdk.custom.tags.http.url",
202+
"values": ["com.instana"],
203+
"match_type": "contains",
204+
}
205+
],
206+
},
187207
],
188208
}
189209

@@ -296,6 +316,16 @@ def test_base_options_with_endpoint_file(self) -> None:
296316
}
297317
],
298318
},
319+
{
320+
"name": "filter-internal-sdk-spans-by-url",
321+
"attributes": [
322+
{
323+
"key": "sdk.custom.tags.http.url",
324+
"values": ["com.instana"],
325+
"match_type": "contains",
326+
}
327+
],
328+
},
299329
],
300330
}
301331
del self.base_options
@@ -377,6 +407,16 @@ def test_set_trace_configurations_by_env_variable(self) -> None:
377407
}
378408
],
379409
},
410+
{
411+
"name": "filter-internal-sdk-spans-by-url",
412+
"attributes": [
413+
{
414+
"key": "sdk.custom.tags.http.url",
415+
"values": ["com.instana"],
416+
"match_type": "contains",
417+
}
418+
],
419+
},
380420
],
381421
}
382422
assert not self.base_options.kafka_trace_correlation
@@ -512,6 +552,16 @@ def test_set_trace_configurations_by_in_code_configuration(self) -> None:
512552
}
513553
],
514554
},
555+
{
556+
"name": "filter-internal-sdk-spans-by-url",
557+
"attributes": [
558+
{
559+
"key": "sdk.custom.tags.http.url",
560+
"values": ["com.instana"],
561+
"match_type": "contains",
562+
}
563+
],
564+
},
515565
],
516566
}
517567

@@ -779,6 +829,16 @@ def test_tracing_filter_environment_variables(self) -> None:
779829
}
780830
],
781831
},
832+
{
833+
"name": "filter-internal-sdk-spans-by-url",
834+
"attributes": [
835+
{
836+
"key": "sdk.custom.tags.http.url",
837+
"values": ["com.instana"],
838+
"match_type": "contains",
839+
}
840+
],
841+
},
782842
],
783843
}
784844

tests/util/test_span_utils.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# (c) Copyright IBM Corp. 2025
22

3-
from instana.util.span_utils import matches_rule, match_key_filter, get_span_kind
3+
from collections import defaultdict
4+
5+
from instana.util.span_utils import (
6+
get_span_kind,
7+
match_key_filter,
8+
matches_rule,
9+
resolve_nested_key,
10+
)
411

512

613
class TestSpanUtils:
@@ -144,3 +151,92 @@ def test_matches_rule_with_none_attribute_value(self) -> None:
144151
{"key": "http.method", "values": ["GET"], "match_type": "strict"}
145152
]
146153
assert matches_rule(rule_method, span_attrs)
154+
155+
def test_resolve_nested_key_embedded_dot_keys(self) -> None:
156+
"""Resolves sdk.custom.tags.http.host through a defaultdict structure —
157+
the exact layout produced by real SDK spans."""
158+
sdk_custom = defaultdict(dict)
159+
sdk_custom["tags"] = defaultdict(str)
160+
sdk_custom["tags"]["http.host"] = "agent.com.instana.io"
161+
162+
assert (
163+
resolve_nested_key(
164+
{"sdk.custom": sdk_custom}, ["sdk", "custom", "tags", "http", "host"]
165+
)
166+
== "agent.com.instana.io"
167+
)
168+
169+
def test_resolve_nested_key_returns_none_when_missing(self) -> None:
170+
"""Returns None when the dotted path does not exist in the data."""
171+
assert (
172+
resolve_nested_key(
173+
{"sdk.custom": {"tags": {}}}, ["sdk", "custom", "tags", "http", "host"]
174+
)
175+
is None
176+
)
177+
178+
def test_matches_rule_sdk_span_host_match(self) -> None:
179+
"""SDK span whose sdk.custom.tags.http.host contains 'com.instana' should be filtered."""
180+
sdk_custom = defaultdict(dict)
181+
sdk_custom["tags"] = {"http.host": "agent.com.instana.io"}
182+
span_attrs = {
183+
"type": "sdk",
184+
"kind": 3,
185+
"sdk.name": "my-span",
186+
"sdk.custom": sdk_custom,
187+
}
188+
189+
rule = [
190+
{
191+
"key": "sdk.custom.tags.http.host",
192+
"values": ["com.instana"],
193+
"match_type": "contains",
194+
}
195+
]
196+
assert matches_rule(rule, span_attrs)
197+
198+
def test_matches_rule_sdk_span_host_no_match(self) -> None:
199+
"""SDK span with an unrelated host should NOT be filtered."""
200+
sdk_custom = defaultdict(dict)
201+
sdk_custom["tags"] = {"http.host": "myapp.example.com"}
202+
span_attrs = {
203+
"type": "sdk",
204+
"kind": 3,
205+
"sdk.name": "my-span",
206+
"sdk.custom": sdk_custom,
207+
}
208+
209+
rule = [
210+
{
211+
"key": "sdk.custom.tags.http.host",
212+
"values": ["com.instana"],
213+
"match_type": "contains",
214+
}
215+
]
216+
assert not matches_rule(rule, span_attrs)
217+
218+
def test_matches_rule_sdk_span_url_match(self) -> None:
219+
"""SDK span whose sdk.custom.tags.http.url contains 'com.instana' should be filtered.
220+
221+
Covers the span shape:
222+
data.sdk.custom.tags.http.url = 'http://localhost:42699/com.instana.plugin.python.89262'
223+
"""
224+
sdk_custom = defaultdict(dict)
225+
sdk_custom["tags"] = {
226+
"http.url": "http://localhost:42699/com.instana.plugin.python.89262"
227+
}
228+
span_attrs = {
229+
"type": "sdk",
230+
"kind": 3,
231+
"sdk.name": "HEAD",
232+
"sdk.custom": sdk_custom,
233+
}
234+
235+
rule = [
236+
{
237+
"key": "sdk.custom.tags.http.url",
238+
"values": ["com.instana"],
239+
"match_type": "contains",
240+
}
241+
]
242+
assert matches_rule(rule, span_attrs)

0 commit comments

Comments
 (0)