30
30
31
31
32
32
import logging
33
+ import logging .handlers
33
34
import math
34
35
import os
35
36
import re
46
47
47
48
logger = get_logger ("elasticapm.conf" )
48
49
50
+ log_levels_map = {
51
+ "trace" : 5 ,
52
+ "debug" : logging .DEBUG ,
53
+ "info" : logging .INFO ,
54
+ "warning" : logging .WARNING ,
55
+ "warn" : logging .WARNING ,
56
+ "error" : logging .ERROR ,
57
+ "critical" : logging .CRITICAL ,
58
+ "off" : 1000 ,
59
+ }
60
+ logfile_set_up = False
61
+
49
62
50
63
class ConfigurationError (ValueError ):
51
64
def __init__ (self , msg , field_name ):
@@ -71,7 +84,19 @@ class _ConfigValue(object):
71
84
fails.
72
85
callbacks
73
86
List of functions which will be called when the config value is updated.
74
- The callbacks must match this signature: callback(dict_key, old_value, new_value)
87
+ The callbacks must match this signature:
88
+ callback(dict_key, old_value, new_value, config_instance)
89
+
90
+ Note that callbacks wait until the end of any given `update()` operation
91
+ and are called at this point. This, coupled with the fact that callbacks
92
+ receive the config instance, means that callbacks can utilize multiple
93
+ configuration values (such as is the case for logging). This is
94
+ complicated if more than one of the involved config values are
95
+ dynamic, as both would need callbacks and the callback would need to
96
+ be idempotent.
97
+ callbacks_on_default
98
+ Whether the callback should be called on config initialization if the
99
+ default value is used. Default: True
75
100
default
76
101
The default for this config value if not user-configured.
77
102
required
@@ -92,6 +117,7 @@ def __init__(
92
117
type = compat .text_type ,
93
118
validators = None ,
94
119
callbacks = None ,
120
+ callbacks_on_default = True ,
95
121
default = None ,
96
122
required = False ,
97
123
):
@@ -104,6 +130,7 @@ def __init__(
104
130
if env_key is None :
105
131
env_key = "ELASTIC_APM_" + dict_key
106
132
self .env_key = env_key
133
+ self .callbacks_on_default = callbacks_on_default
107
134
108
135
def __get__ (self , instance , owner ):
109
136
if instance :
@@ -139,19 +166,20 @@ def _callback_if_changed(self, instance, new_value):
139
166
"""
140
167
old_value = instance ._values .get (self .dict_key , self .default )
141
168
if old_value != new_value :
142
- self . call_callbacks ( old_value , new_value )
169
+ instance . callbacks_queue . append (( self . dict_key , old_value , new_value ) )
143
170
144
- def call_callbacks (self , old_value , new_value ):
171
+ def call_callbacks (self , old_value , new_value , config_instance ):
145
172
if not self .callbacks :
146
173
return
147
174
for callback in self .callbacks :
148
175
try :
149
- callback (self .dict_key , old_value , new_value )
176
+ callback (self .dict_key , old_value , new_value , config_instance )
150
177
except Exception as e :
151
178
raise ConfigurationError (
152
179
"Callback {} raised an exception when setting {} to {}: {}" .format (
153
180
callback , self .dict_key , new_value , e
154
- )
181
+ ),
182
+ self .dict_key ,
155
183
)
156
184
157
185
@@ -297,20 +325,83 @@ def __call__(self, value, field_name):
297
325
return value
298
326
299
327
328
+ class EnumerationValidator (object ):
329
+ """
330
+ Validator which ensures that a given config value is chosen from a list
331
+ of valid string options.
332
+ """
333
+
334
+ def __init__ (self , valid_values , case_sensitive = False ):
335
+ """
336
+ valid_values
337
+ List of valid string values for the config value
338
+ case_sensitive
339
+ Whether to compare case when comparing a value to the valid list.
340
+ Defaults to False (case-insensitive)
341
+ """
342
+ self .case_sensitive = case_sensitive
343
+ if case_sensitive :
344
+ self .valid_values = {s : s for s in valid_values }
345
+ else :
346
+ self .valid_values = {s .lower (): s for s in valid_values }
347
+
348
+ def __call__ (self , value , field_name ):
349
+ if self .case_sensitive :
350
+ ret = self .valid_values .get (value )
351
+ else :
352
+ ret = self .valid_values .get (value .lower ())
353
+ if ret is None :
354
+ raise ConfigurationError (
355
+ "{} is not in the list of valid values: {}" .format (value , list (self .valid_values .values ())), field_name
356
+ )
357
+ return ret
358
+
359
+
360
+ def _log_level_callback (dict_key , old_value , new_value , config_instance ):
361
+ elasticapm_logger = logging .getLogger ("elasticapm" )
362
+ elasticapm_logger .setLevel (log_levels_map .get (new_value , 100 ))
363
+
364
+ global logfile_set_up
365
+ if not logfile_set_up and config_instance .log_file :
366
+ logfile_set_up = True
367
+ filehandler = logging .handlers .RotatingFileHandler (
368
+ config_instance .log_file , maxBytes = config_instance .log_file_size , backupCount = 1
369
+ )
370
+ elasticapm_logger .addHandler (filehandler )
371
+
372
+
300
373
class _ConfigBase (object ):
301
374
_NO_VALUE = object () # sentinel object
302
375
303
- def __init__ (self , config_dict = None , env_dict = None , inline_dict = None ):
376
+ def __init__ (self , config_dict = None , env_dict = None , inline_dict = None , copy = False ):
377
+ """
378
+ config_dict
379
+ Configuration dict as is common for frameworks such as flask and django.
380
+ Keys match the _ConfigValue.dict_key (usually all caps)
381
+ env_dict
382
+ Environment variables dict. Keys match the _ConfigValue.env_key
383
+ (usually "ELASTIC_APM_" + dict_key)
384
+ inline_dict
385
+ Any config passed in as kwargs to the Client object. Typically
386
+ the keys match the names of the _ConfigValue variables in the Config
387
+ object.
388
+ copy
389
+ Whether this object is being created to copy an existing Config
390
+ object. If True, don't run the initial `update` (which would call
391
+ callbacks if present)
392
+ """
304
393
self ._values = {}
305
394
self ._errors = {}
306
395
self ._dict_key_lookup = {}
396
+ self .callbacks_queue = []
307
397
for config_value in self .__class__ .__dict__ .values ():
308
398
if not isinstance (config_value , _ConfigValue ):
309
399
continue
310
400
self ._dict_key_lookup [config_value .dict_key ] = config_value
311
- self .update (config_dict , env_dict , inline_dict )
401
+ if not copy :
402
+ self .update (config_dict , env_dict , inline_dict , initial = True )
312
403
313
- def update (self , config_dict = None , env_dict = None , inline_dict = None ):
404
+ def update (self , config_dict = None , env_dict = None , inline_dict = None , initial = False ):
314
405
if config_dict is None :
315
406
config_dict = {}
316
407
if env_dict is None :
@@ -336,20 +427,30 @@ def update(self, config_dict=None, env_dict=None, inline_dict=None):
336
427
setattr (self , field , new_value )
337
428
except ConfigurationError as e :
338
429
self ._errors [e .field_name ] = str (e )
430
+ # handle initial callbacks
431
+ if (
432
+ initial
433
+ and config_value .callbacks_on_default
434
+ and getattr (self , field ) is not None
435
+ and getattr (self , field ) == config_value .default
436
+ ):
437
+ self .callbacks_queue .append ((config_value .dict_key , self ._NO_VALUE , config_value .default ))
339
438
# if a field has not been provided by any config source, we have to check separately if it is required
340
439
if config_value .required and getattr (self , field ) is None :
341
440
self ._errors [config_value .dict_key ] = "Configuration error: value for {} is required." .format (
342
441
config_value .dict_key
343
442
)
443
+ self .call_pending_callbacks ()
344
444
345
- def call_callbacks (self , callbacks ):
445
+ def call_pending_callbacks (self ):
346
446
"""
347
447
Call callbacks for config options matching list of tuples:
348
448
349
449
(dict_key, old_value, new_value)
350
450
"""
351
- for dict_key , old_value , new_value in callbacks :
352
- self ._dict_key_lookup [dict_key ].call_callbacks (old_value , new_value )
451
+ for dict_key , old_value , new_value in self .callbacks_queue :
452
+ self ._dict_key_lookup [dict_key ].call_callbacks (old_value , new_value , self )
453
+ self .callbacks_queue = []
353
454
354
455
@property
355
456
def values (self ):
@@ -364,21 +465,21 @@ def errors(self):
364
465
return self ._errors
365
466
366
467
def copy (self ):
367
- c = self .__class__ ()
468
+ c = self .__class__ (copy = True )
368
469
c ._errors = {}
369
470
c .values = self .values .copy ()
370
471
return c
371
472
372
473
373
474
class Config (_ConfigBase ):
374
475
service_name = _ConfigValue ("SERVICE_NAME" , validators = [RegexValidator ("^[a-zA-Z0-9 _-]+$" )], required = True )
375
- service_node_name = _ConfigValue ("SERVICE_NODE_NAME" , default = None )
376
- environment = _ConfigValue ("ENVIRONMENT" , default = None )
476
+ service_node_name = _ConfigValue ("SERVICE_NODE_NAME" )
477
+ environment = _ConfigValue ("ENVIRONMENT" )
377
478
secret_token = _ConfigValue ("SECRET_TOKEN" )
378
479
api_key = _ConfigValue ("API_KEY" )
379
480
debug = _BoolConfigValue ("DEBUG" , default = False )
380
481
server_url = _ConfigValue ("SERVER_URL" , default = "http://localhost:8200" , required = True )
381
- server_cert = _ConfigValue ("SERVER_CERT" , default = None , validators = [FileIsReadableValidator ()])
482
+ server_cert = _ConfigValue ("SERVER_CERT" , validators = [FileIsReadableValidator ()])
382
483
verify_server_cert = _BoolConfigValue ("VERIFY_SERVER_CERT" , default = True )
383
484
include_paths = _ListConfigValue ("INCLUDE_PATHS" )
384
485
exclude_paths = _ListConfigValue ("EXCLUDE_PATHS" , default = compat .get_default_library_patters ())
@@ -456,9 +557,9 @@ class Config(_ConfigBase):
456
557
autoinsert_django_middleware = _BoolConfigValue ("AUTOINSERT_DJANGO_MIDDLEWARE" , default = True )
457
558
transactions_ignore_patterns = _ListConfigValue ("TRANSACTIONS_IGNORE_PATTERNS" , default = [])
458
559
service_version = _ConfigValue ("SERVICE_VERSION" )
459
- framework_name = _ConfigValue ("FRAMEWORK_NAME" , default = None )
460
- framework_version = _ConfigValue ("FRAMEWORK_VERSION" , default = None )
461
- global_labels = _DictConfigValue ("GLOBAL_LABELS" , default = None )
560
+ framework_name = _ConfigValue ("FRAMEWORK_NAME" )
561
+ framework_version = _ConfigValue ("FRAMEWORK_VERSION" )
562
+ global_labels = _DictConfigValue ("GLOBAL_LABELS" )
462
563
disable_send = _BoolConfigValue ("DISABLE_SEND" , default = False )
463
564
enabled = _BoolConfigValue ("ENABLED" , default = True )
464
565
recording = _BoolConfigValue ("RECORDING" , default = True )
@@ -470,6 +571,13 @@ class Config(_ConfigBase):
470
571
use_elastic_traceparent_header = _BoolConfigValue ("USE_ELASTIC_TRACEPARENT_HEADER" , default = True )
471
572
use_elastic_excepthook = _BoolConfigValue ("USE_ELASTIC_EXCEPTHOOK" , default = False )
472
573
cloud_provider = _ConfigValue ("CLOUD_PROVIDER" , default = True )
574
+ log_level = _ConfigValue (
575
+ "LOG_LEVEL" ,
576
+ validators = [EnumerationValidator (["trace" , "debug" , "info" , "warning" , "warn" , "error" , "critical" , "off" ])],
577
+ callbacks = [_log_level_callback ],
578
+ )
579
+ log_file = _ConfigValue ("LOG_FILE" , default = "" )
580
+ log_file_size = _ConfigValue ("LOG_FILE_SIZE" , validators = [size_validator ], type = int , default = 50 * 1024 * 1024 )
473
581
474
582
@property
475
583
def is_recording (self ):
@@ -544,7 +652,8 @@ def reset(self):
544
652
self ._version = self ._first_version
545
653
self ._config = self ._first_config
546
654
547
- self ._config .call_callbacks (callbacks )
655
+ self ._config .callbacks_queue .extend (callbacks )
656
+ self ._config .call_pending_callbacks ()
548
657
549
658
@property
550
659
def changed (self ):
@@ -578,7 +687,7 @@ def update_config(self):
578
687
logger .error ("Error applying new configuration: %s" , repr (errors ))
579
688
else :
580
689
logger .info (
581
- "Applied new configuration: %s" ,
690
+ "Applied new remote configuration: %s" ,
582
691
"; " .join (
583
692
"%s=%s" % (compat .text_type (k ), compat .text_type (v )) for k , v in compat .iteritems (new_config )
584
693
),
@@ -604,12 +713,10 @@ def stop_thread(self):
604
713
self ._update_thread = None
605
714
606
715
607
- def setup_logging (handler , exclude = ( "gunicorn" , "south" , "elasticapm.errors" ) ):
716
+ def setup_logging (handler ):
608
717
"""
609
718
Configures logging to pipe to Elastic APM.
610
719
611
- - ``exclude`` is a list of loggers that shouldn't go to ElasticAPM.
612
-
613
720
For a typical Python install:
614
721
615
722
>>> from elasticapm.handlers.logging import LoggingHandler
@@ -623,6 +730,9 @@ def setup_logging(handler, exclude=("gunicorn", "south", "elasticapm.errors")):
623
730
624
731
Returns a boolean based on if logging was configured or not.
625
732
"""
733
+ # TODO We should probably revisit this. Does it make more sense as
734
+ # a method within the Client class? The Client object could easily
735
+ # pass itself into LoggingHandler and we could eliminate args altogether.
626
736
logger = logging .getLogger ()
627
737
if handler .__class__ in map (type , logger .handlers ):
628
738
return False
0 commit comments