Skip to content

Commit c9af237

Browse files
Parse new configuration options (#803)
* add new sampling configuration keys and converters * add types to all config methods * use 0-100 ints instead of floats * add new config tests for sampling * pin urllib3 version to fix httpretty compatibility * update default sample rate --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent bed747d commit c9af237

3 files changed

Lines changed: 176 additions & 29 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
install_requires=[
6161
"asgiref",
6262
"psutil>=5",
63-
"urllib3",
63+
"urllib3~=2.2.0",
6464
"certifi",
6565
"wrapt>=1.10,<2.0",
6666
],

src/scout_apm/core/config.py

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import re
66
import warnings
7+
from typing import Any, Dict, List, Optional, Union
78

89
from scout_apm.core import platform_detection
910

@@ -30,21 +31,21 @@ def __init__(self):
3031
Null(),
3132
]
3233

33-
def value(self, key):
34+
def value(self, key: str) -> Any:
3435
value = self.locate_layer_for_key(key).value(key)
3536
if key in CONVERSIONS:
3637
return CONVERSIONS[key](value)
3738
return value
3839

39-
def locate_layer_for_key(self, key):
40+
def locate_layer_for_key(self, key: str) -> Any:
4041
for layer in self.layers:
4142
if layer.has_config(key):
4243
return layer
4344

4445
# Should be unreachable because Null returns None for all keys.
4546
raise ValueError("key {!r} not found in any layer".format(key))
4647

47-
def log(self):
48+
def log(self) -> None:
4849
logger.debug("Configuration Loaded:")
4950
for key in self.known_keys:
5051
if key in self.secret_keys:
@@ -76,21 +77,26 @@ def log(self):
7677
"framework",
7778
"framework_version",
7879
"hostname",
79-
"ignore",
80+
"ignore", # Deprecated in favor of ignore_endpoints
81+
"ignore_endpoints",
82+
"ignore_jobs",
8083
"key",
8184
"log_level",
8285
"log_payload_content",
8386
"monitor",
8487
"name",
8588
"revision_sha",
89+
"sample_rate",
90+
"sample_endpoints",
91+
"sample_jobs",
8692
"scm_subdirectory",
8793
"shutdown_message_enabled",
8894
"shutdown_timeout_seconds",
8995
]
9096

9197
secret_keys = {"key"}
9298

93-
def core_agent_permissions(self):
99+
def core_agent_permissions(self) -> int:
94100
try:
95101
return int(str(self.value("core_agent_permissions")), 8)
96102
except ValueError:
@@ -100,7 +106,7 @@ def core_agent_permissions(self):
100106
return 0o700
101107

102108
@classmethod
103-
def set(cls, **kwargs):
109+
def set(cls, **kwargs: Any) -> None:
104110
"""
105111
Sets a configuration value for the Scout agent. Values set here will
106112
not override values set in ENV.
@@ -109,15 +115,15 @@ def set(cls, **kwargs):
109115
SCOUT_PYTHON_VALUES[key] = value
110116

111117
@classmethod
112-
def unset(cls, *keys):
118+
def unset(cls, *keys: str) -> None:
113119
"""
114120
Removes a configuration value for the Scout agent.
115121
"""
116122
for key in keys:
117123
SCOUT_PYTHON_VALUES.pop(key, None)
118124

119125
@classmethod
120-
def reset_all(cls):
126+
def reset_all(cls) -> None:
121127
"""
122128
Remove all configuration settings set via `ScoutConfig.set(...)`.
123129
@@ -135,10 +141,10 @@ class Python(object):
135141
A configuration overlay that lets other parts of python set values.
136142
"""
137143

138-
def has_config(self, key):
144+
def has_config(self, key: str) -> bool:
139145
return key in SCOUT_PYTHON_VALUES
140146

141-
def value(self, key):
147+
def value(self, key: str) -> Any:
142148
return SCOUT_PYTHON_VALUES[key]
143149

144150

@@ -151,15 +157,15 @@ class Env(object):
151157
environment variable
152158
"""
153159

154-
def has_config(self, key):
160+
def has_config(self, key: str) -> bool:
155161
env_key = self.modify_key(key)
156162
return env_key in os.environ
157163

158-
def value(self, key):
164+
def value(self, key: str) -> Any:
159165
env_key = self.modify_key(key)
160166
return os.environ[env_key]
161167

162-
def modify_key(self, key):
168+
def modify_key(self, key: str) -> str:
163169
env_key = ("SCOUT_" + key).upper()
164170
return env_key
165171

@@ -169,27 +175,27 @@ class Derived(object):
169175
A configuration overlay that calculates from other values.
170176
"""
171177

172-
def __init__(self, config):
178+
def __init__(self, config: ScoutConfig):
173179
"""
174180
config argument is the overall ScoutConfig var, so we can lookup the
175181
components of the derived info.
176182
"""
177183
self.config = config
178184

179-
def has_config(self, key):
185+
def has_config(self, key: str) -> bool:
180186
return self.lookup_func(key) is not None
181187

182-
def value(self, key):
188+
def value(self, key: str) -> Any:
183189
return self.lookup_func(key)()
184190

185-
def lookup_func(self, key):
191+
def lookup_func(self, key: str) -> Optional[Any]:
186192
"""
187193
Returns the derive_#{key} function, or None if it isn't defined
188194
"""
189195
func_name = "derive_" + key
190196
return getattr(self, func_name, None)
191197

192-
def derive_core_agent_full_name(self):
198+
def derive_core_agent_full_name(self) -> str:
193199
triple = self.config.value("core_agent_triple")
194200
if not platform_detection.is_valid_triple(triple):
195201
warnings.warn(
@@ -201,7 +207,7 @@ def derive_core_agent_full_name(self):
201207
triple=triple,
202208
)
203209

204-
def derive_core_agent_triple(self):
210+
def derive_core_agent_triple(self) -> str:
205211
return platform_detection.get_triple()
206212

207213

@@ -223,34 +229,43 @@ def __init__(self):
223229
"core_agent_socket_path": "tcp://127.0.0.1:6590",
224230
"core_agent_version": "v1.5.0", # can be an exact tag name, or 'latest'
225231
"disabled_instruments": [],
226-
"download_url": "https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release", # noqa: B950
232+
"download_url": (
233+
"https://s3-us-west-1.amazonaws.com/scout-public-downloads/"
234+
"apm_core_agent/release"
235+
), # noqa: B950
227236
"errors_batch_size": 5,
228237
"errors_enabled": True,
229238
"errors_ignored_exceptions": (),
230239
"errors_host": "https://errors.scoutapm.com",
231240
"framework": "",
232241
"framework_version": "",
233242
"hostname": None,
243+
"ignore": [],
244+
"ignore_endpoints": [],
245+
"ignore_jobs": [],
234246
"key": "",
235247
"log_payload_content": False,
236248
"monitor": False,
237249
"name": "Python App",
238250
"revision_sha": self._git_revision_sha(),
251+
"sample_rate": 100,
252+
"sample_endpoints": [],
253+
"sample_jobs": [],
239254
"scm_subdirectory": "",
240255
"shutdown_message_enabled": True,
241256
"shutdown_timeout_seconds": 2.0,
242257
"uri_reporting": "filtered_params",
243258
}
244259

245-
def _git_revision_sha(self):
260+
def _git_revision_sha(self) -> str:
246261
# N.B. The environment variable SCOUT_REVISION_SHA may also be used,
247262
# but that will be picked up by Env
248263
return os.environ.get("HEROKU_SLUG_COMMIT", "")
249264

250-
def has_config(self, key):
265+
def has_config(self, key: str) -> bool:
251266
return key in self.defaults
252267

253-
def value(self, key):
268+
def value(self, key: str) -> Any:
254269
return self.defaults[key]
255270

256271

@@ -261,14 +276,14 @@ class Null(object):
261276
Used as the last step of the layered configuration.
262277
"""
263278

264-
def has_config(self, key):
279+
def has_config(self, key: str) -> bool:
265280
return True
266281

267-
def value(self, key):
282+
def value(self, key: str) -> None:
268283
return None
269284

270285

271-
def convert_to_bool(value):
286+
def convert_to_bool(value: Any) -> bool:
272287
if isinstance(value, bool):
273288
return value
274289
if isinstance(value, str):
@@ -277,14 +292,35 @@ def convert_to_bool(value):
277292
return False
278293

279294

280-
def convert_to_float(value):
295+
def convert_to_float(value: Any) -> float:
281296
try:
282297
return float(value)
283298
except ValueError:
284299
return 0.0
285300

286301

287-
def convert_to_list(value):
302+
def convert_sample_rate(value: Any) -> int:
303+
"""
304+
Converts sample rate to integer, ensuring it's between 0 and 100
305+
"""
306+
try:
307+
rate = int(value)
308+
if not (0 <= rate <= 100):
309+
logger.warning(
310+
f"Invalid sample rate {rate}. Must be between 0 and 100. "
311+
"Defaulting to 100."
312+
)
313+
return 100
314+
return rate
315+
except (TypeError, ValueError):
316+
logger.warning(
317+
f"Invalid sample rate {value}. Must be a number between 0 and 100. "
318+
"Defaulting to 100."
319+
)
320+
return 100
321+
322+
323+
def convert_to_list(value: Any) -> List[Any]:
288324
if isinstance(value, list):
289325
return value
290326
if isinstance(value, tuple):
@@ -296,13 +332,49 @@ def convert_to_list(value):
296332
return []
297333

298334

335+
def convert_endpoint_sampling(value: Union[str, Dict[str, Any]]) -> Dict[str, int]:
336+
"""
337+
Converts endpoint sampling configuration from string or dict format
338+
to a normalized dict.
339+
Example: '/endpoint:40,/test:0' -> {'/endpoint': 40, '/test': 0}
340+
"""
341+
if isinstance(value, dict):
342+
return {k: int(v) for k, v in value.items()}
343+
if isinstance(value, str):
344+
if not value.strip():
345+
return {}
346+
result = {}
347+
pairs = [pair.strip() for pair in value.split(",")]
348+
for pair in pairs:
349+
try:
350+
endpoint, rate = pair.split(":")
351+
rate_int = int(rate)
352+
if not (0 <= rate_int <= 100):
353+
logger.warning(
354+
f"Invalid sampling rate {rate} for endpoint {endpoint}. "
355+
"Must be between 0 and 100."
356+
)
357+
continue
358+
result[endpoint.strip()] = rate_int
359+
except ValueError:
360+
logger.warning(f"Invalid sampling configuration: {pair}")
361+
continue
362+
return result
363+
return {}
364+
365+
299366
CONVERSIONS = {
300367
"collect_remote_ip": convert_to_bool,
301368
"core_agent_download": convert_to_bool,
302369
"core_agent_launch": convert_to_bool,
303370
"disabled_instruments": convert_to_list,
304371
"ignore": convert_to_list,
372+
"ignore_endpoints": convert_to_list,
373+
"ignore_jobs": convert_to_list,
305374
"monitor": convert_to_bool,
375+
"sample_rate": convert_sample_rate,
376+
"sample_endpoints": convert_endpoint_sampling,
377+
"sample_jobs": convert_endpoint_sampling,
306378
"shutdown_message_enabled": convert_to_bool,
307379
"shutdown_timeout_seconds": convert_to_float,
308380
}

0 commit comments

Comments
 (0)