3333import os
3434import re
3535import socket
36+ import threading
3637
3738from elasticapm .utils import compat , starmatch_to_regex
3839
3940__all__ = ("setup_logging" , "Config" )
4041
42+ logger = logging .getLogger ("elasticapm.conf" )
43+
4144
4245class ConfigurationError (ValueError ):
4346 def __init__ (self , msg , field_name ):
@@ -64,8 +67,6 @@ def __get__(self, instance, owner):
6467
6568 def __set__ (self , instance , value ):
6669 value = self ._validate (instance , value )
67- if value is not None :
68- value = self .type (value )
6970 instance ._values [self .dict_key ] = value
7071
7172 def _validate (self , instance , value ):
@@ -76,6 +77,11 @@ def _validate(self, instance, value):
7677 if self .validators and value is not None :
7778 for validator in self .validators :
7879 value = validator (value , self .dict_key )
80+ if self .type and value is not None :
81+ try :
82+ value = self .type (value )
83+ except ValueError as e :
84+ raise ConfigurationError ("{}: {}" .format (self .dict_key , compat .text_type (e )), self .dict_key )
7985 instance ._errors .pop (self .dict_key , None )
8086 return value
8187
@@ -183,6 +189,9 @@ class _ConfigBase(object):
183189 def __init__ (self , config_dict = None , env_dict = None , inline_dict = None ):
184190 self ._values = {}
185191 self ._errors = {}
192+ self .update (config_dict , env_dict , inline_dict )
193+
194+ def update (self , config_dict = None , env_dict = None , inline_dict = None ):
186195 if config_dict is None :
187196 config_dict = {}
188197 if env_dict is None :
@@ -209,6 +218,14 @@ def __init__(self, config_dict=None, env_dict=None, inline_dict=None):
209218 except ConfigurationError as e :
210219 self ._errors [e .field_name ] = str (e )
211220
221+ @property
222+ def values (self ):
223+ return self ._values
224+
225+ @values .setter
226+ def values (self , values ):
227+ self ._values = values
228+
212229 @property
213230 def errors (self ):
214231 return self ._errors
@@ -263,6 +280,7 @@ class Config(_ConfigBase):
263280 )
264281 breakdown_metrics = _BoolConfigValue ("BREAKDOWN_METRICS" , default = True )
265282 disable_metrics = _ListConfigValue ("DISABLE_METRICS" , type = starmatch_to_regex , default = [])
283+ central_config = _BoolConfigValue ("CENTRAL_CONFIG" , default = True )
266284 api_request_size = _ConfigValue ("API_REQUEST_SIZE" , type = int , validators = [size_validator ], default = 750 * 1024 )
267285 api_request_time = _ConfigValue ("API_REQUEST_TIME" , type = int , validators = [duration_validator ], default = 10 * 1000 )
268286 transaction_sample_rate = _ConfigValue ("TRANSACTION_SAMPLE_RATE" , type = float , default = 1.0 )
@@ -296,6 +314,95 @@ class Config(_ConfigBase):
296314 django_transaction_name_from_route = _BoolConfigValue ("DJANGO_TRANSACTION_NAME_FROM_ROUTE" , default = False )
297315
298316
317+ class VersionedConfig (object ):
318+ """
319+ A thin layer around Config that provides versioning
320+ """
321+
322+ __slots__ = ("_config" , "_version" , "_first_config" , "_first_version" , "_lock" )
323+
324+ def __init__ (self , config_object , version ):
325+ """
326+ Create a new VersionedConfig with an initial Config object
327+ :param config_object: the initial Config object
328+ :param version: a version identifier for the configuration
329+ """
330+ self ._config = self ._first_config = config_object
331+ self ._version = self ._first_version = version
332+ self ._lock = threading .Lock ()
333+
334+ def update (self , version , ** config ):
335+ """
336+ Update the configuration version
337+ :param version: version identifier for the new configuration
338+ :param config: a key/value map of new configuration
339+ :return: configuration errors, if any
340+ """
341+ new_config = Config ()
342+ new_config .values = self ._config .values .copy ()
343+
344+ # pass an empty env dict to ensure the environment doesn't get precedence
345+ new_config .update (inline_dict = config , env_dict = {})
346+ if not new_config .errors :
347+ with self ._lock :
348+ self ._version = version
349+ self ._config = new_config
350+ else :
351+ return new_config .errors
352+
353+ def reset (self ):
354+ """
355+ Reset state to the original configuration
356+ """
357+ with self ._lock :
358+ self ._version = self ._first_version
359+ self ._config = self ._first_config
360+
361+ @property
362+ def changed (self ):
363+ return self ._config != self ._first_config
364+
365+ def __getattr__ (self , item ):
366+ return getattr (self ._config , item )
367+
368+ def __setattr__ (self , name , value ):
369+ if name not in self .__slots__ :
370+ setattr (self ._config , name , value )
371+ else :
372+ super (VersionedConfig , self ).__setattr__ (name , value )
373+
374+ @property
375+ def config_version (self ):
376+ return self ._version
377+
378+
379+ def update_config (agent ):
380+ logger .debug ("Checking for new config..." )
381+ transport = agent ._transport
382+ keys = {"service" : {"name" : agent .config .service_name }}
383+ if agent .config .environment :
384+ keys ["service" ]["environment" ] = agent .config .environment
385+ new_version , new_config , next_run = transport .get_config (agent .config .config_version , keys )
386+ if new_version and new_config :
387+ errors = agent .config .update (new_version , ** new_config )
388+ if errors :
389+ logger .error ("Error applying new configuration: %s" , repr (errors ))
390+ else :
391+ logger .info (
392+ "Applied new configuration: %s" ,
393+ "; " .join (
394+ "%s=%s" % (compat .text_type (k ), compat .text_type (v )) for k , v in compat .iteritems (new_config )
395+ ),
396+ )
397+ elif new_version == agent .config .config_version :
398+ logger .debug ("Remote config unchanged" )
399+ elif not new_config and agent .config .changed :
400+ logger .debug ("Remote config disappeared, resetting to original" )
401+ agent .config .reset ()
402+
403+ return next_run
404+
405+
299406def setup_logging (handler , exclude = ("gunicorn" , "south" , "elasticapm.errors" )):
300407 """
301408 Configures logging to pipe to Elastic APM.
0 commit comments