Skip to content

Commit 1992162

Browse files
pvitalGSVarsha
authored andcommitted
feat: Add Span Disabling.
The span disabling allows the exclusion of specific traces or calls from tracing based on the category (technology) or type (frameworks, libraries, instrumentations) supported by the traces. This commit adds support to handle the configuration from the INSTANA_TRACING_DISABLE, INSTANA_CONFIG_PATH, and Agent-provided configuration. Signed-off-by: Paulo Vital <[email protected]>
1 parent 1b0a7e8 commit 1992162

File tree

8 files changed

+510
-19
lines changed

8 files changed

+510
-19
lines changed

src/instana/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
is_autowrapt_instrumented,
2121
is_webhook_instrumented,
2222
)
23+
from instana.util.config import is_truthy
2324
from instana.version import VERSION
2425

2526
__author__ = "Instana Inc."
@@ -225,7 +226,9 @@ def _start_profiler() -> None:
225226
profiler.start()
226227

227228

228-
if "INSTANA_DISABLE" not in os.environ:
229+
if "INSTANA_DISABLE" not in os.environ and not is_truthy(
230+
os.environ.get("INSTANA_TRACING_DISABLE", None)
231+
):
229232
# There are cases when sys.argv may not be defined at load time. Seems to happen in embedded Python,
230233
# and some Pipenv installs. If this is the case, it's best effort.
231234
if (

src/instana/options.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@
1616

1717
import logging
1818
import os
19-
from typing import Any, Dict
19+
from typing import Any, Dict, Sequence
2020

2121
from instana.configurator import config
2222
from instana.log import logger
23-
from instana.util.config import (is_truthy, parse_ignored_endpoints,
24-
parse_ignored_endpoints_from_yaml)
23+
from instana.util.config import (
24+
SPAN_TYPE_TO_CATEGORY,
25+
get_disable_trace_configurations_from_env,
26+
get_disable_trace_configurations_from_local,
27+
get_disable_trace_configurations_from_yaml,
28+
is_truthy,
29+
parse_ignored_endpoints,
30+
parse_ignored_endpoints_from_yaml,
31+
parse_span_disabling,
32+
)
2533
from instana.util.runtime import determine_service_name
2634

2735

@@ -37,6 +45,11 @@ def __init__(self, **kwds: Dict[str, Any]) -> None:
3745
self.ignore_endpoints = []
3846
self.kafka_trace_correlation = True
3947

48+
# disabled_spans lists all categories and types that should be disabled
49+
self.disabled_spans = []
50+
# enabled_spans lists all categories and types that should be enabled, preceding disabled_spans
51+
self.enabled_spans = []
52+
4053
self.set_trace_configurations()
4154

4255
# Defaults
@@ -75,8 +88,9 @@ def set_trace_configurations(self) -> None:
7588
)
7689

7790
# Check if either of the environment variables is truthy
78-
if is_truthy(os.environ.get("INSTANA_ALLOW_EXIT_AS_ROOT", None)) or \
79-
is_truthy(os.environ.get("INSTANA_ALLOW_ROOT_EXIT_SPAN", None)):
91+
if is_truthy(os.environ.get("INSTANA_ALLOW_EXIT_AS_ROOT", None)) or is_truthy(
92+
os.environ.get("INSTANA_ALLOW_ROOT_EXIT_SPAN", None)
93+
):
8094
self.allow_exit_as_root = True
8195

8296
# The priority is as follows:
@@ -99,12 +113,69 @@ def set_trace_configurations(self) -> None:
99113
)
100114

101115
if "INSTANA_KAFKA_TRACE_CORRELATION" in os.environ:
102-
self.kafka_trace_correlation = is_truthy(os.environ["INSTANA_KAFKA_TRACE_CORRELATION"])
116+
self.kafka_trace_correlation = is_truthy(
117+
os.environ["INSTANA_KAFKA_TRACE_CORRELATION"]
118+
)
103119
elif isinstance(config.get("tracing"), dict) and "kafka" in config["tracing"]:
104120
self.kafka_trace_correlation = config["tracing"]["kafka"].get(
105121
"trace_correlation", True
106122
)
107123

124+
self.set_disable_trace_configurations()
125+
126+
def set_disable_trace_configurations(self) -> None:
127+
disabled_spans = []
128+
enabled_spans = []
129+
130+
# The precedence is as follows:
131+
# environment variables > in-code (local) config > agent config (configuration.yaml)
132+
# For the env vars: INSTANA_TRACING_DISABLE > INSTANA_CONFIG_PATH
133+
if "INSTANA_TRACING_DISABLE" in os.environ:
134+
disabled_spans, enabled_spans = get_disable_trace_configurations_from_env()
135+
elif "INSTANA_CONFIG_PATH" in os.environ:
136+
disabled_spans, enabled_spans = get_disable_trace_configurations_from_yaml()
137+
else:
138+
# In-code (local) config
139+
# The agent config (configuration.yaml) is handled in StandardOptions.set_disable_tracing()
140+
disabled_spans, enabled_spans = (
141+
get_disable_trace_configurations_from_local()
142+
)
143+
144+
self.disabled_spans.extend(disabled_spans)
145+
self.enabled_spans.extend(enabled_spans)
146+
147+
def is_span_disabled(self, category=None, span_type=None) -> bool:
148+
"""
149+
Check if a span is disabled based on its category and type.
150+
151+
Args:
152+
category (str): The span category (e.g., "logging", "databases")
153+
span_type (str): The span type (e.g., "redis", "kafka")
154+
155+
Returns:
156+
bool: True if the span is disabled, False otherwise
157+
"""
158+
# If span_type is provided, check if it's disabled
159+
if span_type and span_type in self.disabled_spans:
160+
return True
161+
162+
# If category is provided directly, check if it's disabled
163+
if category and category in self.disabled_spans:
164+
return True
165+
166+
# If span_type is provided but not explicitly configured,
167+
# check if its parent category is disabled. Also check for the precedence rules
168+
if span_type and span_type in SPAN_TYPE_TO_CATEGORY:
169+
parent_category = SPAN_TYPE_TO_CATEGORY[span_type]
170+
if (
171+
parent_category in self.disabled_spans
172+
and span_type not in self.enabled_spans
173+
):
174+
return True
175+
176+
# Default: not disabled
177+
return False
178+
108179

109180
class StandardOptions(BaseOptions):
110181
"""The options class used when running directly on a host/node with an Instana agent"""
@@ -177,6 +248,26 @@ def set_tracing(self, tracing: Dict[str, Any]) -> None:
177248
if "extra-http-headers" in tracing:
178249
self.extra_http_headers = tracing["extra-http-headers"]
179250

251+
# Handle span disabling configuration
252+
if "disable" in tracing:
253+
self.set_disable_tracing(tracing["disable"])
254+
255+
def set_disable_tracing(self, tracing_config: Sequence[Dict[str, Any]]) -> None:
256+
# The precedence is as follows:
257+
# environment variables > in-code (local) config > agent config (configuration.yaml)
258+
if (
259+
"INSTANA_TRACING_DISABLE" not in os.environ
260+
and "INSTANA_CONFIG_PATH" not in os.environ
261+
and not (
262+
isinstance(config.get("tracing"), dict)
263+
and "disable" in config["tracing"]
264+
)
265+
):
266+
# agent config (configuration.yaml)
267+
disabled_spans, enabled_spans = parse_span_disabling(tracing_config)
268+
self.disabled_spans.extend(disabled_spans)
269+
self.enabled_spans.extend(enabled_spans)
270+
180271
def set_from(self, res_data: Dict[str, Any]) -> None:
181272
"""
182273
Set the source identifiers given to use by the Instana Host agent.

src/instana/util/config.py

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,43 @@
22

33
import itertools
44
import os
5-
from typing import Any, Dict, List, Union
5+
from typing import Any, Dict, List, Sequence, Tuple, Union
66

7+
from instana.configurator import config
78
from instana.log import logger
89
from instana.util.config_reader import ConfigReader
910

11+
# List of supported span categories (technology or protocol)
12+
SPAN_CATEGORIES = [
13+
"logging",
14+
"databases",
15+
"messaging",
16+
"protocols", # http, grpc, etc.
17+
]
18+
19+
# Mapping of span type calls (framework, library name, instrumentation name) to categories
20+
SPAN_TYPE_TO_CATEGORY = {
21+
# Database types
22+
"redis": "databases",
23+
"mysql": "databases",
24+
"postgresql": "databases",
25+
"mongodb": "databases",
26+
"cassandra": "databases",
27+
"couchbase": "databases",
28+
"dynamodb": "databases",
29+
"sqlalchemy": "databases",
30+
# Messaging types
31+
"kafka": "messaging",
32+
"rabbitmq": "messaging",
33+
"pika": "messaging",
34+
"aio_pika": "messaging",
35+
"aioamqp": "messaging",
36+
# Protocol types
37+
"http": "protocols",
38+
"grpc": "protocols",
39+
"graphql": "protocols",
40+
}
41+
1042

1143
def parse_service_pair(pair: str) -> List[str]:
1244
"""
@@ -151,10 +183,10 @@ def parse_ignored_endpoints_from_yaml(file_path: str) -> List[str]:
151183
def is_truthy(value: Any) -> bool:
152184
"""
153185
Check if a value is truthy, accepting various formats.
154-
186+
155187
@param value: The value to check
156188
@return: True if the value is considered truthy, False otherwise
157-
189+
158190
Accepts the following as True:
159191
- True (Python boolean)
160192
- "True", "true" (case-insensitive string)
@@ -163,17 +195,128 @@ def is_truthy(value: Any) -> bool:
163195
"""
164196
if value is None:
165197
return False
166-
198+
167199
if isinstance(value, bool):
168200
return value
169-
201+
170202
if isinstance(value, int):
171203
return value == 1
172-
204+
173205
if isinstance(value, str):
174206
value_lower = value.lower()
175207
return value_lower == "true" or value == "1"
176-
208+
177209
return False
178210

211+
212+
def parse_span_disabling(
213+
disable_list: Sequence[Union[str, Dict[str, Any]]],
214+
) -> Tuple[List[str], List[str]]:
215+
"""
216+
Process a list of span disabling configurations and return lists of disabled and enabled spans.
217+
218+
@param disable_list: List of span disabling configurations
219+
@return: Tuple of (disabled_spans, enabled_spans)
220+
"""
221+
if not isinstance(disable_list, list):
222+
logger.debug(
223+
f"parse_span_disabling: Invalid disable_list type: {type(disable_list)}"
224+
)
225+
return [], []
226+
227+
disabled_spans = []
228+
enabled_spans = []
229+
230+
for item in disable_list:
231+
if isinstance(item, str):
232+
disabled = parse_span_disabling_str(item)
233+
disabled_spans.extend(disabled)
234+
elif isinstance(item, dict):
235+
disabled, enabled = parse_span_disabling_dict(item)
236+
disabled_spans.extend(disabled)
237+
enabled_spans.extend(enabled)
238+
else:
239+
logger.debug(
240+
f"parse_span_disabling: Invalid disable_list item type: {type(item)}"
241+
)
242+
243+
return disabled_spans, enabled_spans
244+
245+
246+
def parse_span_disabling_str(item: str) -> List[str]:
247+
"""
248+
Process a string span disabling configuration and return a list of disabled spans.
249+
250+
@param item: String span disabling configuration
251+
@return: List of disabled spans
252+
"""
253+
if item.lower() in SPAN_CATEGORIES or item.lower() in SPAN_TYPE_TO_CATEGORY.keys():
254+
return [item.lower()]
255+
else:
256+
logger.debug(f"set_span_disabling_str: Invalid span category/type: {item}")
257+
return []
258+
259+
260+
def parse_span_disabling_dict(items: Dict[str, bool]) -> Tuple[List[str], List[str]]:
261+
"""
262+
Process a dictionary span disabling configuration and return lists of disabled and enabled spans.
263+
264+
@param items: Dictionary span disabling configuration
265+
@return: Tuple of (disabled_spans, enabled_spans)
266+
"""
267+
disabled_spans = []
268+
enabled_spans = []
269+
270+
for key, value in items.items():
271+
if key in SPAN_CATEGORIES or key in SPAN_TYPE_TO_CATEGORY.keys():
272+
if is_truthy(value):
273+
disabled_spans.append(key)
274+
else:
275+
enabled_spans.append(key)
276+
else:
277+
logger.debug(f"set_span_disabling_dict: Invalid span category/type: {key}")
278+
279+
return disabled_spans, enabled_spans
280+
281+
282+
def get_disable_trace_configurations_from_env() -> Tuple[List[str], List[str]]:
283+
# Read INSTANA_TRACING_DISABLE environment variable
284+
if tracing_disable := os.environ.get("INSTANA_TRACING_DISABLE", None):
285+
if is_truthy(tracing_disable):
286+
# INSTANA_TRACING_DISABLE is True/true/1, then we disable all tracing
287+
disabled_spans = []
288+
for category in SPAN_CATEGORIES:
289+
disabled_spans.append(category)
290+
return disabled_spans, []
291+
else:
292+
# INSTANA_TRACING_DISABLE is a comma-separated list of span categories/types
293+
tracing_disable_list = [x.strip() for x in tracing_disable.split(",")]
294+
return parse_span_disabling(tracing_disable_list)
295+
return [], []
296+
297+
298+
def get_disable_trace_configurations_from_yaml() -> Tuple[List[str], List[str]]:
299+
config_reader = ConfigReader(os.environ.get("INSTANA_CONFIG_PATH", ""))
300+
301+
if "tracing" in config_reader.data:
302+
root_key = "tracing"
303+
elif "com.instana.tracing" in config_reader.data:
304+
logger.warning(
305+
'Please use "tracing" instead of "com.instana.tracing" for local configuration file.'
306+
)
307+
root_key = "com.instana.tracing"
308+
else:
309+
return [], []
310+
311+
tracing_disable_config = config_reader.data[root_key].get("disable", "")
312+
return parse_span_disabling(tracing_disable_config)
313+
314+
315+
def get_disable_trace_configurations_from_local() -> Tuple[List[str], List[str]]:
316+
if "tracing" in config:
317+
if tracing_disable_config := config["tracing"].get("disable", None):
318+
return parse_span_disabling(tracing_disable_config)
319+
return [], []
320+
321+
179322
# Made with Bob

0 commit comments

Comments
 (0)