Skip to content

Commit 2ee3887

Browse files
committed
fix: Add support for custom span filtering
Signed-off-by: Cagri Yonca <cagri@ibm.com>
1 parent 0a0dc38 commit 2ee3887

4 files changed

Lines changed: 224 additions & 2 deletions

File tree

src/instana/options.py

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

149159
def _apply_env_stack_trace_config(self) -> None:

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

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

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

5158

59+
def resolve_nested_key(data: Dict[str, Any], key_parts: List[str]) -> Optional[Any]:
60+
"""Resolve a dotted key path against a potentially nested dict.
61+
62+
Tries all possible prefix lengths so that keys which themselves contain
63+
dots (e.g. ``sdk.custom`` or ``http.host``) are handled correctly.
64+
65+
Example::
66+
67+
# span_attributes = {"sdk.custom": {"tags": {"http.host": "example.com"}}}
68+
resolve_nested_key(span_attributes, ["sdk", "custom", "tags", "http", "host"])
69+
# -> "example.com"
70+
"""
71+
if not key_parts or not isinstance(data, dict):
72+
return None
73+
74+
# Try the longest prefix first so that keys with embedded dots are matched
75+
# before shorter splits (e.g. prefer "sdk.custom" over "sdk").
76+
for i in range(len(key_parts), 0, -1):
77+
candidate = ".".join(key_parts[:i])
78+
if candidate in data:
79+
remaining = key_parts[i:]
80+
if not remaining:
81+
return data[candidate]
82+
result = resolve_nested_key(data[candidate], remaining)
83+
if result is not None:
84+
return result
85+
86+
return None
87+
88+
5289
def match_key_filter(span_value: str, rule_value: str, match_type: str) -> bool:
5390
"""Check if the first value matches the second value based on the match type."""
5491
# 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: 116 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,111 @@ 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_resolve_nested_key_with_empty_key_parts(self) -> None:
179+
"""Returns None when key_parts is an empty list."""
180+
data = {"sdk.custom": {"tags": {"http.host": "example.com"}}}
181+
assert resolve_nested_key(data, []) is None
182+
183+
def test_resolve_nested_key_with_non_dict_data(self) -> None:
184+
"""Returns None when data is not a dictionary."""
185+
# Test with string
186+
assert resolve_nested_key("not a dict", ["key"]) is None
187+
188+
# Test with list
189+
assert resolve_nested_key(["not", "a", "dict"], ["key"]) is None
190+
191+
# Test with None
192+
assert resolve_nested_key(None, ["key"]) is None
193+
194+
# Test with integer
195+
assert resolve_nested_key(42, ["key"]) is None
196+
197+
def test_matches_rule_sdk_span_host_match(self) -> None:
198+
"""SDK span whose sdk.custom.tags.http.host contains 'com.instana' should be filtered."""
199+
sdk_custom = defaultdict(dict)
200+
sdk_custom["tags"] = {"http.host": "agent.com.instana.io"}
201+
span_attrs = {
202+
"type": "sdk",
203+
"kind": 3,
204+
"sdk.name": "my-span",
205+
"sdk.custom": sdk_custom,
206+
}
207+
208+
rule = [
209+
{
210+
"key": "sdk.custom.tags.http.host",
211+
"values": ["com.instana"],
212+
"match_type": "contains",
213+
}
214+
]
215+
assert matches_rule(rule, span_attrs)
216+
217+
def test_matches_rule_sdk_span_host_no_match(self) -> None:
218+
"""SDK span with an unrelated host should NOT be filtered."""
219+
sdk_custom = defaultdict(dict)
220+
sdk_custom["tags"] = {"http.host": "myapp.example.com"}
221+
span_attrs = {
222+
"type": "sdk",
223+
"kind": 3,
224+
"sdk.name": "my-span",
225+
"sdk.custom": sdk_custom,
226+
}
227+
228+
rule = [
229+
{
230+
"key": "sdk.custom.tags.http.host",
231+
"values": ["com.instana"],
232+
"match_type": "contains",
233+
}
234+
]
235+
assert not matches_rule(rule, span_attrs)
236+
237+
def test_matches_rule_sdk_span_url_match(self) -> None:
238+
"""SDK span whose sdk.custom.tags.http.url contains 'com.instana' should be filtered.
239+
240+
Covers the span shape:
241+
data.sdk.custom.tags.http.url = 'http://localhost:42699/com.instana.plugin.python.89262'
242+
"""
243+
sdk_custom = defaultdict(dict)
244+
sdk_custom["tags"] = {
245+
"http.url": "http://localhost:42699/com.instana.plugin.python.89262"
246+
}
247+
span_attrs = {
248+
"type": "sdk",
249+
"kind": 3,
250+
"sdk.name": "HEAD",
251+
"sdk.custom": sdk_custom,
252+
}
253+
254+
rule = [
255+
{
256+
"key": "sdk.custom.tags.http.url",
257+
"values": ["com.instana"],
258+
"match_type": "contains",
259+
}
260+
]
261+
assert matches_rule(rule, span_attrs)

0 commit comments

Comments
 (0)