Skip to content

Commit 1845053

Browse files
gwlesterDanyC97heitorlessaGerald W. Lester
authored
feat(feature-flags): Bring your own logger for debug (aws-powertools#709)
Co-authored-by: Dani Comnea <[email protected]> Co-authored-by: heitorlessa <[email protected]> Co-authored-by: Gerald W. Lester <[email protected]>
1 parent c837e0a commit 1845053

File tree

4 files changed

+53
-37
lines changed

4 files changed

+53
-37
lines changed

aws_lambda_powertools/utilities/feature_flags/appconfig.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import logging
22
import traceback
3-
from typing import Any, Dict, Optional, cast
3+
from typing import Any, Dict, Optional, Union, cast
44

55
from botocore.config import Config
66

77
from aws_lambda_powertools.utilities.parameters import AppConfigProvider, GetParameterError, TransformParameterError
88

9+
from ... import Logger
910
from ...shared import jmespath_utils
1011
from .base import StoreProvider
1112
from .exceptions import ConfigurationStoreError, StoreClientError
1213

13-
logger = logging.getLogger(__name__)
14-
1514
TRANSFORM_TYPE = "json"
1615

1716

@@ -25,6 +24,7 @@ def __init__(
2524
sdk_config: Optional[Config] = None,
2625
envelope: Optional[str] = "",
2726
jmespath_options: Optional[Dict] = None,
27+
logger: Optional[Union[logging.Logger, Logger]] = None,
2828
):
2929
"""This class fetches JSON schemas from AWS AppConfig
3030
@@ -44,8 +44,11 @@ def __init__(
4444
JMESPath expression to pluck feature flags data from config
4545
jmespath_options : Optional[Dict]
4646
Alternative JMESPath options to be included when filtering expr
47+
logger: A logging object
48+
Used to log messages. If None is supplied, one will be created.
4749
"""
4850
super().__init__()
51+
self.logger = logger or logging.getLogger(__name__)
4952
self.environment = environment
5053
self.application = application
5154
self.name = name
@@ -60,6 +63,9 @@ def get_raw_configuration(self) -> Dict[str, Any]:
6063
"""Fetch feature schema configuration from AWS AppConfig"""
6164
try:
6265
# parse result conf as JSON, keep in cache for self.max_age seconds
66+
self.logger.debug(
67+
"Fetching configuration from the store", extra={"param_name": self.name, "max_age": self.cache_seconds}
68+
)
6369
return cast(
6470
dict,
6571
self._conf_store.get(
@@ -93,6 +99,7 @@ def get_configuration(self) -> Dict[str, Any]:
9399
config = self.get_raw_configuration
94100

95101
if self.envelope:
102+
self.logger.debug("Envelope enabled; extracting data from config", extra={"envelope": self.envelope})
96103
config = jmespath_utils.extract_data_from_envelope(
97104
data=config, envelope=self.envelope, jmespath_options=self.jmespath_options
98105
)

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

+26-21
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import logging
2-
from typing import Any, Dict, List, Optional, cast
2+
from typing import Any, Dict, List, Optional, Union, cast
33

4+
from ... import Logger
45
from . import schema
56
from .base import StoreProvider
67
from .exceptions import ConfigurationStoreError
78

8-
logger = logging.getLogger(__name__)
9-
109

1110
class FeatureFlags:
12-
def __init__(self, store: StoreProvider):
11+
def __init__(self, store: StoreProvider, logger: Optional[Union[logging.Logger, Logger]] = None):
1312
"""Evaluates whether feature flags should be enabled based on a given context.
1413
1514
It uses the provided store to fetch feature flag rules before evaluating them.
@@ -35,11 +34,13 @@ def __init__(self, store: StoreProvider):
3534
----------
3635
store: StoreProvider
3736
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.
3839
"""
3940
self.store = store
41+
self.logger = logger or logging.getLogger(__name__)
4042

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:
4344
if not context_value:
4445
return False
4546
mapping_by_action = {
@@ -58,7 +59,7 @@ def _match_by_action(action: str, condition_value: Any, context_value: Any) -> b
5859
func = mapping_by_action.get(action, lambda a, b: False)
5960
return func(context_value, condition_value)
6061
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)}")
6263
return False
6364

6465
def _evaluate_conditions(
@@ -69,7 +70,7 @@ def _evaluate_conditions(
6970
conditions = cast(List[Dict], rule.get(schema.CONDITIONS_KEY))
7071

7172
if not conditions:
72-
logger.debug(
73+
self.logger.debug(
7374
f"rule did not match, no conditions to match, rule_name={rule_name}, rule_value={rule_match_value}, "
7475
f"name={feature_name} "
7576
)
@@ -81,13 +82,13 @@ def _evaluate_conditions(
8182
cond_value = condition.get(schema.CONDITION_VALUE)
8283

8384
if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value):
84-
logger.debug(
85+
self.logger.debug(
8586
f"rule did not match action, rule_name={rule_name}, rule_value={rule_match_value}, "
8687
f"name={feature_name}, context_value={str(context_value)} "
8788
)
8889
return False # context doesn't match condition
8990

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}")
9192
return True
9293

9394
def _evaluate_rules(
@@ -98,12 +99,16 @@ def _evaluate_rules(
9899
rule_match_value = rule.get(schema.RULE_MATCH_VALUE)
99100

100101
# 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+
)
102105
if self._evaluate_conditions(rule_name=rule_name, feature_name=feature_name, rule=rule, context=context):
103106
return bool(rule_match_value)
104107

105108
# 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+
)
107112
return feat_default
108113
return False
109114

@@ -150,7 +155,7 @@ def get_configuration(self) -> Dict:
150155
```
151156
"""
152157
# 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}")
154159
config: Dict = self.store.get_configuration()
155160
validator = schema.SchemaValidator(schema=config)
156161
validator.validate()
@@ -194,21 +199,21 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
194199
try:
195200
features = self.get_configuration()
196201
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}")
198203
return default
199204

200205
feature = features.get(name)
201206
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}")
203208
return default
204209

205210
rules = feature.get(schema.RULES_KEY)
206211
feat_default = feature.get(schema.FEATURE_DEFAULT_VAL_KEY)
207212
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}")
209214
return bool(feat_default)
210215

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}")
212217
return self._evaluate_rules(feature_name=name, context=context, feat_default=bool(feat_default), rules=rules)
213218

214219
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
245250
try:
246251
features: Dict[str, Any] = self.get_configuration()
247252
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}")
249254
return features_enabled
250255

251-
logger.debug("Evaluating all features")
256+
self.logger.debug("Evaluating all features")
252257
for name, feature in features.items():
253258
rules = feature.get(schema.RULES_KEY, {})
254259
feature_default_value = feature.get(schema.FEATURE_DEFAULT_VAL_KEY)
255260
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}")
257262
features_enabled.append(name)
258263
elif self._evaluate_rules(
259264
feature_name=name, context=context, feat_default=feature_default_value, rules=rules
260265
):
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}")
262267
features_enabled.append(name)
263268

264269
return features_enabled

aws_lambda_powertools/utilities/feature_flags/schema.py

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import logging
22
from enum import Enum
3-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Dict, List, Optional, Union
44

5+
from ... import Logger
56
from .base import BaseValidator
67
from .exceptions import SchemaValidationError
78

8-
logger = logging.getLogger(__name__)
9-
109
RULES_KEY = "rules"
1110
FEATURE_DEFAULT_VAL_KEY = "default"
1211
CONDITIONS_KEY = "conditions"
@@ -111,11 +110,12 @@ class SchemaValidator(BaseValidator):
111110
```
112111
"""
113112

114-
def __init__(self, schema: Dict[str, Any]):
113+
def __init__(self, schema: Dict[str, Any], logger: Optional[Union[logging.Logger, Logger]] = None):
115114
self.schema = schema
115+
self.logger = logger or logging.getLogger(__name__)
116116

117117
def validate(self) -> None:
118-
logger.debug("Validating schema")
118+
self.logger.debug("Validating schema")
119119
if not isinstance(self.schema, dict):
120120
raise SchemaValidationError(f"Features must be a dictionary, schema={str(self.schema)}")
121121

@@ -126,12 +126,13 @@ def validate(self) -> None:
126126
class FeaturesValidator(BaseValidator):
127127
"""Validates each feature and calls RulesValidator to validate its rules"""
128128

129-
def __init__(self, schema: Dict):
129+
def __init__(self, schema: Dict, logger: Optional[Union[logging.Logger, Logger]] = None):
130130
self.schema = schema
131+
self.logger = logger or logging.getLogger(__name__)
131132

132133
def validate(self):
133134
for name, feature in self.schema.items():
134-
logger.debug(f"Attempting to validate feature '{name}'")
135+
self.logger.debug(f"Attempting to validate feature '{name}'")
135136
self.validate_feature(name, feature)
136137
rules = RulesValidator(feature=feature)
137138
rules.validate()
@@ -149,21 +150,22 @@ def validate_feature(name, feature):
149150
class RulesValidator(BaseValidator):
150151
"""Validates each rule and calls ConditionsValidator to validate each rule's conditions"""
151152

152-
def __init__(self, feature: Dict[str, Any]):
153+
def __init__(self, feature: Dict[str, Any], logger: Optional[Union[logging.Logger, Logger]] = None):
153154
self.feature = feature
154155
self.feature_name = next(iter(self.feature))
155156
self.rules: Optional[Dict] = self.feature.get(RULES_KEY)
157+
self.logger = logger or logging.getLogger(__name__)
156158

157159
def validate(self):
158160
if not self.rules:
159-
logger.debug("Rules are empty, ignoring validation")
161+
self.logger.debug("Rules are empty, ignoring validation")
160162
return
161163

162164
if not isinstance(self.rules, dict):
163165
raise SchemaValidationError(f"Feature rules must be a dictionary, feature={self.feature_name}")
164166

165167
for rule_name, rule in self.rules.items():
166-
logger.debug(f"Attempting to validate rule '{rule_name}'")
168+
self.logger.debug(f"Attempting to validate rule '{rule_name}'")
167169
self.validate_rule(rule=rule, rule_name=rule_name, feature_name=self.feature_name)
168170
conditions = ConditionsValidator(rule=rule, rule_name=rule_name)
169171
conditions.validate()
@@ -189,24 +191,25 @@ def validate_rule_default_value(rule: Dict, rule_name: str):
189191

190192

191193
class ConditionsValidator(BaseValidator):
192-
def __init__(self, rule: Dict[str, Any], rule_name: str):
194+
def __init__(self, rule: Dict[str, Any], rule_name: str, logger: Optional[Union[logging.Logger, Logger]] = None):
193195
self.conditions: List[Dict[str, Any]] = rule.get(CONDITIONS_KEY, {})
194196
self.rule_name = rule_name
197+
self.logger = logger or logging.getLogger(__name__)
195198

196199
def validate(self):
197200
if not self.conditions or not isinstance(self.conditions, list):
198201
raise SchemaValidationError(f"Invalid condition, rule={self.rule_name}")
199202

200203
for condition in self.conditions:
204+
# Condition can contain PII data; do not log condition value
205+
self.logger.debug(f"Attempting to validate condition for '{self.rule_name}'")
201206
self.validate_condition(rule_name=self.rule_name, condition=condition)
202207

203208
@staticmethod
204209
def validate_condition(rule_name: str, condition: Dict[str, str]) -> None:
205210
if not condition or not isinstance(condition, dict):
206211
raise SchemaValidationError(f"Feature rule condition must be a dictionary, rule={rule_name}")
207212

208-
# Condition can contain PII data; do not log condition value
209-
logger.debug(f"Attempting to validate condition for '{rule_name}'")
210213
ConditionsValidator.validate_condition_action(condition=condition, rule_name=rule_name)
211214
ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name)
212215
ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name)

docs/utilities/feature_flags.md

+1
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ Parameter | Default | Description
580580
**max_age** | `5` | Number of seconds to cache feature flags configuration fetched from AWS AppConfig
581581
**sdk_config** | `None` | [Botocore Config object](https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html){target="_blank"}
582582
**jmespath_options** | `None` | For advanced use cases when you want to bring your own [JMESPath functions](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"}
583+
**logger** | `logging.Logger` | Logger to use for debug. You can optionally supply an instance of Powertools Logger.
583584

584585
=== "appconfig_store_example.py"
585586

0 commit comments

Comments
 (0)