1
1
import logging
2
- from typing import Any , Dict , List , Optional , cast
2
+ from typing import Any , Dict , List , Optional , Union , cast
3
3
4
+ from ... import Logger
4
5
from . import schema
5
6
from .base import StoreProvider
6
7
from .exceptions import ConfigurationStoreError
7
8
8
- logger = logging .getLogger (__name__ )
9
-
10
9
11
10
class FeatureFlags :
12
- def __init__ (self , store : StoreProvider ):
11
+ def __init__ (self , store : StoreProvider , logger : Optional [ Union [ logging . Logger , Logger ]] = None ):
13
12
"""Evaluates whether feature flags should be enabled based on a given context.
14
13
15
14
It uses the provided store to fetch feature flag rules before evaluating them.
@@ -35,11 +34,13 @@ def __init__(self, store: StoreProvider):
35
34
----------
36
35
store: StoreProvider
37
36
Store to use to fetch feature flag schema configuration.
37
+ logger: A logging object
38
+ Used to log messages. If None is supplied, one will be created.
38
39
"""
39
40
self .store = store
41
+ self .logger = logger or logging .getLogger (__name__ )
40
42
41
- @staticmethod
42
- def _match_by_action (action : str , condition_value : Any , context_value : Any ) -> bool :
43
+ def _match_by_action (self , action : str , condition_value : Any , context_value : Any ) -> bool :
43
44
if not context_value :
44
45
return False
45
46
mapping_by_action = {
@@ -58,7 +59,7 @@ def _match_by_action(action: str, condition_value: Any, context_value: Any) -> b
58
59
func = mapping_by_action .get (action , lambda a , b : False )
59
60
return func (context_value , condition_value )
60
61
except Exception as exc :
61
- logger .debug (f"caught exception while matching action: action={ action } , exception={ str (exc )} " )
62
+ self . logger .debug (f"caught exception while matching action: action={ action } , exception={ str (exc )} " )
62
63
return False
63
64
64
65
def _evaluate_conditions (
@@ -69,7 +70,7 @@ def _evaluate_conditions(
69
70
conditions = cast (List [Dict ], rule .get (schema .CONDITIONS_KEY ))
70
71
71
72
if not conditions :
72
- logger .debug (
73
+ self . logger .debug (
73
74
f"rule did not match, no conditions to match, rule_name={ rule_name } , rule_value={ rule_match_value } , "
74
75
f"name={ feature_name } "
75
76
)
@@ -81,13 +82,13 @@ def _evaluate_conditions(
81
82
cond_value = condition .get (schema .CONDITION_VALUE )
82
83
83
84
if not self ._match_by_action (action = cond_action , condition_value = cond_value , context_value = context_value ):
84
- logger .debug (
85
+ self . logger .debug (
85
86
f"rule did not match action, rule_name={ rule_name } , rule_value={ rule_match_value } , "
86
87
f"name={ feature_name } , context_value={ str (context_value )} "
87
88
)
88
89
return False # context doesn't match condition
89
90
90
- logger .debug (f"rule matched, rule_name={ rule_name } , rule_value={ rule_match_value } , name={ feature_name } " )
91
+ self . logger .debug (f"rule matched, rule_name={ rule_name } , rule_value={ rule_match_value } , name={ feature_name } " )
91
92
return True
92
93
93
94
def _evaluate_rules (
@@ -98,12 +99,16 @@ def _evaluate_rules(
98
99
rule_match_value = rule .get (schema .RULE_MATCH_VALUE )
99
100
100
101
# Context might contain PII data; do not log its value
101
- logger .debug (f"Evaluating rule matching, rule={ rule_name } , feature={ feature_name } , default={ feat_default } " )
102
+ self .logger .debug (
103
+ f"Evaluating rule matching, rule={ rule_name } , feature={ feature_name } , default={ feat_default } "
104
+ )
102
105
if self ._evaluate_conditions (rule_name = rule_name , feature_name = feature_name , rule = rule , context = context ):
103
106
return bool (rule_match_value )
104
107
105
108
# no rule matched, return default value of feature
106
- logger .debug (f"no rule matched, returning feature default, default={ feat_default } , name={ feature_name } " )
109
+ self .logger .debug (
110
+ f"no rule matched, returning feature default, default={ feat_default } , name={ feature_name } "
111
+ )
107
112
return feat_default
108
113
return False
109
114
@@ -150,7 +155,7 @@ def get_configuration(self) -> Dict:
150
155
```
151
156
"""
152
157
# parse result conf as JSON, keep in cache for max age defined in store
153
- logger .debug (f"Fetching schema from registered store, store={ self .store } " )
158
+ self . logger .debug (f"Fetching schema from registered store, store={ self .store } " )
154
159
config : Dict = self .store .get_configuration ()
155
160
validator = schema .SchemaValidator (schema = config )
156
161
validator .validate ()
@@ -194,21 +199,21 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
194
199
try :
195
200
features = self .get_configuration ()
196
201
except ConfigurationStoreError as err :
197
- logger .debug (f"Failed to fetch feature flags from store, returning default provided, reason={ err } " )
202
+ self . logger .debug (f"Failed to fetch feature flags from store, returning default provided, reason={ err } " )
198
203
return default
199
204
200
205
feature = features .get (name )
201
206
if feature is None :
202
- logger .debug (f"Feature not found; returning default provided, name={ name } , default={ default } " )
207
+ self . logger .debug (f"Feature not found; returning default provided, name={ name } , default={ default } " )
203
208
return default
204
209
205
210
rules = feature .get (schema .RULES_KEY )
206
211
feat_default = feature .get (schema .FEATURE_DEFAULT_VAL_KEY )
207
212
if not rules :
208
- logger .debug (f"no rules found, returning feature default, name={ name } , default={ feat_default } " )
213
+ self . logger .debug (f"no rules found, returning feature default, name={ name } , default={ feat_default } " )
209
214
return bool (feat_default )
210
215
211
- logger .debug (f"looking for rule match, name={ name } , default={ feat_default } " )
216
+ self . logger .debug (f"looking for rule match, name={ name } , default={ feat_default } " )
212
217
return self ._evaluate_rules (feature_name = name , context = context , feat_default = bool (feat_default ), rules = rules )
213
218
214
219
def get_enabled_features (self , * , context : Optional [Dict [str , Any ]] = None ) -> List [str ]:
@@ -245,20 +250,20 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
245
250
try :
246
251
features : Dict [str , Any ] = self .get_configuration ()
247
252
except ConfigurationStoreError as err :
248
- logger .debug (f"Failed to fetch feature flags from store, returning empty list, reason={ err } " )
253
+ self . logger .debug (f"Failed to fetch feature flags from store, returning empty list, reason={ err } " )
249
254
return features_enabled
250
255
251
- logger .debug ("Evaluating all features" )
256
+ self . logger .debug ("Evaluating all features" )
252
257
for name , feature in features .items ():
253
258
rules = feature .get (schema .RULES_KEY , {})
254
259
feature_default_value = feature .get (schema .FEATURE_DEFAULT_VAL_KEY )
255
260
if feature_default_value and not rules :
256
- logger .debug (f"feature is enabled by default and has no defined rules, name={ name } " )
261
+ self . logger .debug (f"feature is enabled by default and has no defined rules, name={ name } " )
257
262
features_enabled .append (name )
258
263
elif self ._evaluate_rules (
259
264
feature_name = name , context = context , feat_default = feature_default_value , rules = rules
260
265
):
261
- logger .debug (f"feature's calculated value is True, name={ name } " )
266
+ self . logger .debug (f"feature's calculated value is True, name={ name } " )
262
267
features_enabled .append (name )
263
268
264
269
return features_enabled
0 commit comments