Skip to content

Commit 9c7637b

Browse files
committed
feat: Add span filtering feature
Signed-off-by: Cagri Yonca <[email protected]>
1 parent c8e5db7 commit 9c7637b

File tree

13 files changed

+4121
-222
lines changed

13 files changed

+4121
-222
lines changed

src/instana/agent/host.py

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
from instana.options import StandardOptions
2323
from instana.util import to_json
2424
from instana.util.runtime import get_py_source, log_runtime_env_info
25-
from instana.util.span_utils import get_operation_specifiers
25+
from instana.util.span_utils import (
26+
extract_span_attributes,
27+
get_operation_specifiers,
28+
matches_filter_rule,
29+
)
2630
from instana.version import VERSION
2731

2832
if TYPE_CHECKING:
@@ -357,27 +361,42 @@ def report_spans(self, payload: Dict[str, Any]) -> Optional[Response]:
357361

358362
def filter_spans(self, spans: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
359363
"""
360-
Filters given span list using ignore-endpoint variable and returns the list of filtered spans.
364+
Filters given span list using span_filters and ignore-endpoint configurations.
365+
366+
Returns the list of spans that should be kept (not filtered out).
361367
"""
362368
filtered_spans = []
363369
endpoint = ""
370+
364371
for span in spans:
365-
if (hasattr(span, "n") or hasattr(span, "name")) and hasattr(span, "data"):
366-
service = span.n
367-
operation_specifier_key, service_specifier_key = (
368-
get_operation_specifiers(service)
369-
)
370-
if service == "kafka":
371-
endpoint = span.data[service][service_specifier_key]
372-
method = span.data[service][operation_specifier_key]
373-
if isinstance(method, str) and self.__is_endpoint_ignored(
374-
service, method, endpoint
375-
):
372+
try:
373+
# Check new span_filters first (if configured)
374+
if self.options.span_filters and self.__is_span_filtered(span):
376375
continue
377-
else:
378-
filtered_spans.append(span)
379-
else:
376+
377+
# Backward compatibility: check ignore_endpoints
378+
if (hasattr(span, "n") or hasattr(span, "name")) and hasattr(
379+
span, "data"
380+
):
381+
service = span.n
382+
operation_specifier_key, service_specifier_key = (
383+
get_operation_specifiers(service)
384+
)
385+
if service == "kafka":
386+
endpoint = span.data[service][service_specifier_key]
387+
method = span.data[service][operation_specifier_key]
388+
if isinstance(method, str) and self.__is_endpoint_ignored(
389+
service, method, endpoint
390+
):
391+
continue
392+
393+
# Keep the span
380394
filtered_spans.append(span)
395+
except Exception:
396+
# Defensive: if filtering fails, keep the span
397+
logger.debug("Error filtering span", exc_info=True)
398+
filtered_spans.append(span)
399+
381400
return filtered_spans
382401

383402
def __is_endpoint_ignored(
@@ -403,6 +422,47 @@ def __is_endpoint_ignored(
403422
]
404423
return any(rule in self.options.ignore_endpoints for rule in filter_rules)
405424

425+
def __is_span_filtered(self, span: Dict[str, Any]) -> bool:
426+
"""
427+
Check if span should be filtered based on span_filters configuration.
428+
429+
Implements the span filtering logic following the specification:
430+
- include rules have higher precedence than exclude rules
431+
- Within a rule, attributes use AND logic (all must match)
432+
- Between rules, use OR logic (any rule can match)
433+
434+
Args:
435+
span: Span dictionary to check
436+
437+
Returns:
438+
True if span should be EXCLUDED (filtered out)
439+
False if span should be INCLUDED (kept)
440+
"""
441+
if not self.options.span_filters:
442+
return False
443+
444+
try:
445+
# Extract span attributes for filtering
446+
span_attrs = extract_span_attributes(span)
447+
448+
# Check include rules first (higher precedence)
449+
if "include" in self.options.span_filters:
450+
for rule in self.options.span_filters["include"]:
451+
if matches_filter_rule(span_attrs, rule):
452+
return False # Keep span (include match)
453+
454+
# Check exclude rules
455+
if "exclude" in self.options.span_filters:
456+
for rule in self.options.span_filters["exclude"]:
457+
if matches_filter_rule(span_attrs, rule):
458+
return True # Filter out span (exclude match)
459+
460+
# Default: keep span
461+
return False
462+
except Exception:
463+
# Defensive: if filtering fails, keep the span
464+
return False
465+
406466
def handle_agent_tasks(self, task: Dict[str, Any]) -> None:
407467
"""
408468
When request(s) are received by the host agent, it is sent here

src/instana/options.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
get_disable_trace_configurations_from_env,
2626
get_disable_trace_configurations_from_local,
2727
get_disable_trace_configurations_from_yaml,
28+
get_span_filter_config_from_yaml,
2829
get_stack_trace_config_from_yaml,
2930
is_truthy,
3031
parse_ignored_endpoints,
3132
parse_ignored_endpoints_from_yaml,
3233
parse_span_disabling,
34+
parse_span_filter_config,
35+
parse_span_filter_from_env,
3336
parse_technology_stack_trace_config,
3437
validate_stack_trace_length,
3538
validate_stack_trace_level,
@@ -47,6 +50,7 @@ def __init__(self, **kwds: Dict[str, Any]) -> None:
4750
self.extra_http_headers = None
4851
self.allow_exit_as_root = False
4952
self.ignore_endpoints = []
53+
self.span_filters = {}
5054
self.kafka_trace_correlation = True
5155

5256
# disabled_spans lists all categories and types that should be disabled
@@ -83,6 +87,10 @@ def __init__(self, **kwds: Dict[str, Any]) -> None:
8387

8488
self.__dict__.update(kwds)
8589

90+
def _has_env_filter_vars(self) -> bool:
91+
"""Check if any INSTANA_TRACING_FILTER_* environment variables exist."""
92+
return any(key.startswith("INSTANA_TRACING_FILTER_") for key in os.environ)
93+
8694
def set_trace_configurations(self) -> None:
8795
"""
8896
Set tracing configurations from the environment variables and config file.
@@ -105,10 +113,16 @@ def set_trace_configurations(self) -> None:
105113
):
106114
self.allow_exit_as_root = True
107115

116+
# Span filter configuration priority:
117+
# INSTANA_TRACING_FILTER_* env vars > INSTANA_CONFIG_PATH > in-code > agent config
118+
if self._has_env_filter_vars():
119+
self.span_filters = parse_span_filter_from_env()
120+
elif "INSTANA_CONFIG_PATH" in os.environ:
121+
self.span_filters = get_span_filter_config_from_yaml()
108122
# The priority is as follows:
109123
# environment variables > in-code configuration >
110124
# > agent config (configuration.yaml) > default value
111-
if "INSTANA_IGNORE_ENDPOINTS" in os.environ:
125+
elif "INSTANA_IGNORE_ENDPOINTS" in os.environ:
112126
self.ignore_endpoints = parse_ignored_endpoints(
113127
os.environ["INSTANA_IGNORE_ENDPOINTS"]
114128
)
@@ -146,7 +160,8 @@ def _apply_env_stack_trace_config(self) -> None:
146160

147161
if "INSTANA_STACK_TRACE_LENGTH" in os.environ:
148162
if validated_length := validate_stack_trace_length(
149-
os.environ["INSTANA_STACK_TRACE_LENGTH"], "from INSTANA_STACK_TRACE_LENGTH"
163+
os.environ["INSTANA_STACK_TRACE_LENGTH"],
164+
"from INSTANA_STACK_TRACE_LENGTH",
150165
):
151166
self.stack_trace_length = validated_length
152167

@@ -161,31 +176,41 @@ def _apply_yaml_stack_trace_config(self) -> None:
161176

162177
def _apply_in_code_stack_trace_config(self) -> None:
163178
"""Apply stack trace configuration from in-code config."""
164-
if not isinstance(config.get("tracing"), dict) or "global" not in config["tracing"]:
179+
if (
180+
not isinstance(config.get("tracing"), dict)
181+
or "global" not in config["tracing"]
182+
):
165183
return
166-
184+
167185
global_config = config["tracing"]["global"]
168-
186+
169187
if "INSTANA_STACK_TRACE" not in os.environ and "stack_trace" in global_config:
170-
if validated_level := validate_stack_trace_level(global_config["stack_trace"], "from in-code config"):
188+
if validated_level := validate_stack_trace_level(
189+
global_config["stack_trace"], "from in-code config"
190+
):
171191
self.stack_trace_level = validated_level
172-
173-
if "INSTANA_STACK_TRACE_LENGTH" not in os.environ and "stack_trace_length" in global_config:
174-
if validated_length := validate_stack_trace_length(global_config["stack_trace_length"], "from in-code config"):
192+
193+
if (
194+
"INSTANA_STACK_TRACE_LENGTH" not in os.environ
195+
and "stack_trace_length" in global_config
196+
):
197+
if validated_length := validate_stack_trace_length(
198+
global_config["stack_trace_length"], "from in-code config"
199+
):
175200
self.stack_trace_length = validated_length
176-
201+
177202
# Technology-specific overrides from in-code config
178203
for tech_name, tech_data in config["tracing"].items():
179204
if tech_name == "global" or not isinstance(tech_data, dict):
180205
continue
181-
206+
182207
tech_stack_config = parse_technology_stack_trace_config(
183208
tech_data,
184209
level_key="stack_trace",
185210
length_key="stack_trace_length",
186-
tech_name=tech_name
211+
tech_name=tech_name,
187212
)
188-
213+
189214
if tech_stack_config:
190215
self.stack_trace_technology_config[tech_name] = tech_stack_config
191216

@@ -196,7 +221,7 @@ def set_stack_trace_configurations(self) -> None:
196221
"""
197222
# 1. Environment variables (highest priority)
198223
self._apply_env_stack_trace_config()
199-
224+
200225
# 2. INSTANA_CONFIG_PATH (YAML file) - includes tech-specific overrides
201226
if "INSTANA_CONFIG_PATH" in os.environ:
202227
self._apply_yaml_stack_trace_config()
@@ -261,10 +286,10 @@ def get_stack_trace_config(self, span_name: str) -> Tuple[str, int]:
261286
"""
262287
Get stack trace configuration for a specific span type.
263288
Technology-specific configuration overrides global configuration.
264-
289+
265290
Args:
266291
span_name: The name of the span (e.g., "kafka-producer", "redis", "mysql")
267-
292+
268293
Returns:
269294
Tuple of (level, length) where:
270295
- level: "all", "error", or "none"
@@ -273,17 +298,17 @@ def get_stack_trace_config(self, span_name: str) -> Tuple[str, int]:
273298
# Start with global defaults
274299
level = self.stack_trace_level
275300
length = self.stack_trace_length
276-
301+
277302
# Check for technology-specific overrides
278303
# Extract base technology name from span name
279304
# Examples: "kafka-producer" -> "kafka", "mysql" -> "mysql"
280305
tech_name = span_name.split("-")[0] if "-" in span_name else span_name
281-
306+
282307
if tech_name in self.stack_trace_technology_config:
283308
tech_config = self.stack_trace_technology_config[tech_name]
284309
level = tech_config.get("level", level)
285310
length = tech_config.get("length", length)
286-
311+
287312
return level, length
288313

289314

@@ -334,6 +359,9 @@ def set_tracing(self, tracing: Dict[str, Any]) -> None:
334359
if "ignore-endpoints" in tracing and not self.ignore_endpoints:
335360
self.ignore_endpoints = parse_ignored_endpoints(tracing["ignore-endpoints"])
336361

362+
if "filter" in tracing and not self.span_filters:
363+
self.span_filters = parse_span_filter_config(tracing["filter"])
364+
337365
if "kafka" in tracing:
338366
if (
339367
"INSTANA_KAFKA_TRACE_CORRELATION" not in os.environ
@@ -361,7 +389,7 @@ def set_tracing(self, tracing: Dict[str, Any]) -> None:
361389
# Handle span disabling configuration
362390
if "disable" in tracing:
363391
self.set_disable_tracing(tracing["disable"])
364-
392+
365393
# Handle stack trace configuration from agent config
366394
self.set_stack_trace_from_agent(tracing)
367395

@@ -375,48 +403,56 @@ def _should_apply_agent_global_config(self) -> bool:
375403
has_in_code_config = (
376404
isinstance(config.get("tracing"), dict)
377405
and "global" in config["tracing"]
378-
and ("stack_trace" in config["tracing"]["global"]
379-
or "stack_trace_length" in config["tracing"]["global"])
406+
and (
407+
"stack_trace" in config["tracing"]["global"]
408+
or "stack_trace_length" in config["tracing"]["global"]
409+
)
380410
)
381411
return not (has_env_vars or has_yaml_config or has_in_code_config)
382412

383-
def _apply_agent_global_stack_trace_config(self, global_config: Dict[str, Any]) -> None:
413+
def _apply_agent_global_stack_trace_config(
414+
self, global_config: Dict[str, Any]
415+
) -> None:
384416
"""Apply global stack trace configuration from agent config."""
385417
if "stack-trace" in global_config:
386-
if validated_level := validate_stack_trace_level(global_config["stack-trace"], "in agent config"):
418+
if validated_level := validate_stack_trace_level(
419+
global_config["stack-trace"], "in agent config"
420+
):
387421
self.stack_trace_level = validated_level
388-
422+
389423
if "stack-trace-length" in global_config:
390-
if validated_length := validate_stack_trace_length(global_config["stack-trace-length"], "in agent config"):
424+
if validated_length := validate_stack_trace_length(
425+
global_config["stack-trace-length"], "in agent config"
426+
):
391427
self.stack_trace_length = validated_length
392428

393429
def _apply_agent_tech_stack_trace_config(self, tracing: Dict[str, Any]) -> None:
394430
"""Apply technology-specific stack trace configuration from agent config."""
395431
for tech_name, tech_config in tracing.items():
396432
if tech_name == "global" or not isinstance(tech_config, dict):
397433
continue
398-
434+
399435
tech_stack_config = parse_technology_stack_trace_config(
400436
tech_config,
401437
level_key="stack-trace",
402438
length_key="stack-trace-length",
403-
tech_name=tech_name
439+
tech_name=tech_name,
404440
)
405-
441+
406442
if tech_stack_config:
407443
self.stack_trace_technology_config[tech_name] = tech_stack_config
408444

409445
def set_stack_trace_from_agent(self, tracing: Dict[str, Any]) -> None:
410446
"""
411447
Set stack trace configuration from agent config (configuration.yaml).
412448
Only applies if not already set by higher priority sources.
413-
449+
414450
@param tracing: tracing configuration dictionary from agent
415451
"""
416452
# Apply global config if no higher priority source exists
417453
if self._should_apply_agent_global_config() and "global" in tracing:
418454
self._apply_agent_global_stack_trace_config(tracing["global"])
419-
455+
420456
# Apply technology-specific config if not already set by YAML or in-code config
421457
if not self.stack_trace_technology_config:
422458
self._apply_agent_tech_stack_trace_config(tracing)

0 commit comments

Comments
 (0)