Skip to content
Merged
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
18 changes: 1 addition & 17 deletions src/instana/instrumentation/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,23 +94,7 @@ def urlopen_with_instana(
tracer, parent_span, span_name = get_tracer_tuple()

# If we're not tracing, just return; boto3 has it's own visibility
# Also, skip creating spans for internal Instana calls when
# 'com.instana' appears in either the full URL, the path argument,
# or the connection host.
request_url_or_path = (
kwargs.get("request_url")
or kwargs.get("url")
or (args[1] if len(args) >= 2 else "")
or ""
)
host = getattr(instance, "host", "") or ""

if (
not tracer
or span_name == "boto3"
or "com.instana" in request_url_or_path
or "com.instana" in host
):
if not tracer or span_name == "boto3":
return wrapped(*args, **kwargs)

parent_context = parent_span.get_span_context() if parent_span else None
Expand Down
75 changes: 54 additions & 21 deletions src/instana/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
get_disable_trace_configurations_from_yaml,
get_stack_trace_config_from_yaml,
is_truthy,
parse_filtered_endpoints,
parse_filtered_endpoints_from_yaml,
parse_filter_rules,
parse_filter_rules_yaml,
parse_span_disabling,
parse_span_filter_env_vars,
parse_filter_rules_env_vars,
parse_technology_stack_trace_config,
validate_stack_trace_length,
validate_stack_trace_level,
Expand Down Expand Up @@ -106,23 +106,6 @@ def set_trace_configurations(self) -> None:
):
self.allow_exit_as_root = True

# The priority is as follows:
# environment variables > in-code configuration >
# > agent config (configuration.yaml) > default value
if any(k.startswith("INSTANA_TRACING_FILTER_") for k in os.environ):
# Check for new span filtering env vars
parsed_filter = parse_span_filter_env_vars()
if parsed_filter["exclude"] or parsed_filter["include"]:
self.span_filters = parsed_filter
elif "INSTANA_CONFIG_PATH" in os.environ:
self.span_filters = parse_filtered_endpoints_from_yaml(
os.environ["INSTANA_CONFIG_PATH"]
)
elif isinstance(config.get("tracing"), dict) and "filter" in config["tracing"]:
self.span_filters = parse_filtered_endpoints(
config["tracing"]["filter"],
)

if "INSTANA_KAFKA_TRACE_CORRELATION" in os.environ:
self.kafka_trace_correlation = is_truthy(
os.environ["INSTANA_KAFKA_TRACE_CORRELATION"]
Expand All @@ -134,6 +117,36 @@ def set_trace_configurations(self) -> None:

self.set_disable_trace_configurations()
self.set_stack_trace_configurations()
self.set_span_filter_configurations()

def _add_instana_agent_span_filter(self) -> None:
"""Add Instana agent span filter to exclude internal spans."""
if "exclude" not in self.span_filters:
self.span_filters["exclude"] = []
self.span_filters["exclude"].extend(
[
{
"name": "filter-internal-spans-by-url",
"attributes": [
{
"key": "http.url",
"values": ["com.instana"],
"match_type": "contains",
}
],
},
{
"name": "filter-internal-spans-by-host",
"attributes": [
{
"key": "http.host",
"values": ["com.instana"],
"match_type": "contains",
}
],
},
]
)

def _apply_env_stack_trace_config(self) -> None:
"""Apply stack trace configuration from environment variables."""
Expand Down Expand Up @@ -235,6 +248,26 @@ def set_disable_trace_configurations(self) -> None:
self.disabled_spans.extend(disabled_spans)
self.enabled_spans.extend(enabled_spans)

def set_span_filter_configurations(self) -> None:
# The precedence is as follows:
# environment variables > in-code configuration >
# > agent config (configuration.yaml) > default value
if any(k.startswith("INSTANA_TRACING_FILTER_") for k in os.environ):
# Check for new span filtering env vars
parsed_filter = parse_filter_rules_env_vars()
if parsed_filter["exclude"] or parsed_filter["include"]:
self.span_filters = parsed_filter
elif "INSTANA_CONFIG_PATH" in os.environ:
self.span_filters = parse_filter_rules_yaml(
os.environ["INSTANA_CONFIG_PATH"]
)
elif isinstance(config.get("tracing"), dict) and "filter" in config["tracing"]:
self.span_filters = parse_filter_rules(
config["tracing"]["filter"],
)

self._add_instana_agent_span_filter()

def is_span_disabled(self, category=None, span_type=None) -> bool:
"""
Check if a span is disabled based on its category and type.
Expand Down Expand Up @@ -342,7 +375,7 @@ def set_tracing(self, tracing: Dict[str, Any]) -> None:
@return: None
"""
if "filter" in tracing and not self.span_filters:
self.span_filters = parse_filtered_endpoints(tracing["filter"])
self.span_filters = parse_filter_rules(tracing["filter"])

if "kafka" in tracing:
if (
Expand Down
148 changes: 70 additions & 78 deletions src/instana/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,46 +42,54 @@
}


def parse_service_pair(pair: str) -> List[str]:
def parse_filter_rules_string(
params: str,
intermediate: Dict[str, Any],
policy: str,
name: str,
) -> Dict[str, List[str]]:
"""
Parses a string to prepare filtered endpoint rules.

@param params: String format with rules separated by '|':
- "key;values;match_type|key;values;match_type"
- Example: "http.target;/health;strict|kafka.service;topic1,topic2;strict"
- match_type is optional and defaults to "strict"
@param intermediate: Dictionary to store parsed rules
@param policy: Policy type ("exclude" or "include")
@param name: Name of the filter rule
@return: Updated intermediate dictionary with parsed attribute rules
"""
Parses a pair string to prepare a list of ignored endpoints.

@param pair: String format:
- "service1:method1,method2" or "service1:method1" or "service1"
@return: List of strings in format ["service1.method1", "service1.method2", "service2.*"]
"""
pair_list = []
if ":" in pair:
service, methods = pair.split(":", 1)
service = service.strip()
method_list = [ep.strip() for ep in methods.split(",") if ep.strip()]

for method in method_list:
pair_list.append(f"{service}.{method}")
else:
pair_list.append(f"{pair}.*")
return pair_list
try:
# Rule format: key;values;match_type|key;values;match_type
rules = params.split("|")
for rule in rules:
rule_parts = rule.split(";")
if len(rule_parts) < 2:
continue

key = rule_parts[0].strip()
values_str = rule_parts[1]
match_type = (
rule_parts[2].strip().lower() if len(rule_parts) > 2 else "strict"
)

def parse_filtered_endpoints_string(params: Union[str, os.PathLike]) -> List[str]:
"""
Parses a string to prepare a list of ignored endpoints.
# Split values by comma (simple split, assuming no commas in values or user handles escaping if needed?)
# Spec says "values": Mandatory - List of Strings.
# Env var examples: "http.target;/health" -> values=["/health"]
# "kafka.service;topic1,topic2;strict" -> values=["topic1", "topic2"]
values = [v.strip() for v in values_str.split(",") if v.strip()]

@param params: String format:
- "service1:method1,method2;service2:method3" or "service1;service2"
@return: List of strings in format ["service1.method1", "service1.method2", "service2.*"]
"""
span_filters = []
if params:
service_pairs = params.lower().split(";")
attr_data = {"key": key, "values": values, "match_type": match_type}
intermediate[policy][name]["attributes"].append(attr_data)

for pair in service_pairs:
if pair.strip():
span_filters += parse_service_pair(pair)
return span_filters
return intermediate
except Exception as e:
logger.error(f"Failed to parse filter params: {e}")
return {}


def parse_filtered_endpoints_dict(filter_dict: dict[str, Any]) -> dict[str, list[Any]]:
def parse_filter_rules_dict(filter_dict: Dict[str, Any]) -> Dict[str, List[Any]]:
"""
Parses 'exclude' and 'include' blocks from the filter dict.

Expand Down Expand Up @@ -132,37 +140,36 @@ def parse_filtered_endpoints_dict(filter_dict: dict[str, Any]) -> dict[str, list
return {"exclude": [], "include": []}


def parse_filtered_endpoints(
params: Union[Dict[str, Any], str],
) -> Union[List[str], dict[str, list[Any]]]:
def parse_filter_rules(
params: Dict[str, Any],
) -> Dict[str, List[Any]]:
"""
Parses input to prepare a list for ignored endpoints.
Parses input to prepare filtered endpoints.

@param params: Can be either:
- String: "service1:method1,method2;service2:method3" or "service1;service2"
- Dict: {"exclude": [{"name": "foo", "attributes": ...}], "include": []}
@return: List of strings in format ["service1.method1", "service1.method2", "service2.*"]
@param params: Dict with structure:
{"exclude": [{"name": "foo", "attributes": ...}], "include": [{"name": "foo", "attributes": ...}]}
@return: Dict with structure {"exclude": [...], "include": [...]}
"""
try:
if isinstance(params, str):
return parse_filtered_endpoints_string(params)
elif isinstance(params, dict):
return parse_filtered_endpoints_dict(params)
else:
return []
return parse_filter_rules_dict(params)
except Exception as e:
logger.debug("Error parsing ignored endpoints: %s", str(e))
return []
logger.debug("Error parsing filtered endpoints: %s", str(e))
return {}


def parse_filtered_endpoints_from_yaml(
def parse_filter_rules_yaml(
file_path: str,
) -> Union[List[str], dict[str, list[Any]]]:
) -> Dict[str, List[Any]]:
"""
Parses configuration yaml file and prepares a list of ignored endpoints.
Parses configuration YAML file and prepares filtered endpoint rules.

@param file_path: Path of the file as a string
@return: List of strings in format ["service1.method1", "service1.method2", "service2.*", "kafka.method.topic", "kafka.*.topic", "kafka.method.*"]
@param file_path: Path to the YAML configuration file
@return: Dictionary containing parsed filter rules with structure:
{
"exclude": [{"name": str, "suppression": bool, "attributes": [{"key": str, "values": list, "match_type": str}]}],
"include": [{"name": str, "suppression": None, "attributes": [{"key": str, "values": list, "match_type": str}]}]
}
Returns empty dict {} if no filter configuration is found or on error.
"""
config_reader = ConfigReader(file_path)
span_filters_dict = None
Expand All @@ -172,13 +179,13 @@ def parse_filtered_endpoints_from_yaml(
logger.warning(DEPRECATED_CONFIG_KEY_WARNING)
span_filters_dict = config_reader.data["com.instana.tracing"].get("filter")
if span_filters_dict:
span_filters = parse_filtered_endpoints(span_filters_dict)
span_filters = parse_filter_rules(span_filters_dict)
return span_filters
else:
return []
return {}


def parse_span_filter_env_vars() -> Dict[str, List[Any]]:
def parse_filter_rules_env_vars() -> Dict[str, List[Any]]:
"""
Parses INSTANA_TRACING_FILTER_<POLICY>_<NAME>_ATTRIBUTES environment variables.

Expand Down Expand Up @@ -216,27 +223,12 @@ def parse_span_filter_env_vars() -> Dict[str, List[Any]]:
}

if suffix == "ATTRIBUTES":
# Rule format: key;values;match_type|key;values;match_type
rules = env_value.split("|")
for rule in rules:
rule_parts = rule.split(";")
if len(rule_parts) < 2:
continue

key = rule_parts[0].strip()
values_str = rule_parts[1]
match_type = (
rule_parts[2].strip().lower() if len(rule_parts) > 2 else "strict"
)

# Split values by comma (simple split, assuming no commas in values or user handles escaping if needed?)
# Spec says "values": Mandatory - List of Strings.
# Env var examples: "http.target;/health" -> values=["/health"]
# "kafka.service;topic1,topic2;strict" -> values=["topic1", "topic2"]
values = [v.strip() for v in values_str.split(",") if v.strip()]

attr_data = {"key": key, "values": values, "match_type": match_type}
intermediate[policy][name]["attributes"].append(attr_data)
intermediate = parse_filter_rules_string(
env_value,
intermediate,
policy,
name,
)

elif suffix == "SUPPRESSION" and policy == "exclude":
intermediate[policy][name]["suppression"] = is_truthy(env_value)
Expand Down
Loading
Loading