44import os
55import re
66import warnings
7+ from typing import Any , Dict , List , Optional , Union
78
89from 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+
299366CONVERSIONS = {
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