Skip to content

Commit 7c55154

Browse files
committed
Merge branch 'develop' into pydantic
* develop: docs: Fix doc for log sampling (#135) fix(logging): Don't include `json_default` in logs (#132) chore: bump to 1.4.0 docs: add Lambda Layer SAR App url and ARN fix: upgrade dot-prop, serialize-javascript fix heading error due to merge formatting for bash script add layer to docs and how to use it from SAR moved publish step to publish workflow after pypi push fix(ssm): Make decrypt an explicit option and refactoring (#123) change to eu-west-1 default region remove tmp release flag and set trigger to release published add overwrite flag for ssm add relase tag simulation more typos fix typo in branch trigger fix indent, yaml ... line endings
2 parents 3f9865a + 8da0cce commit 7c55154

File tree

12 files changed

+200
-80
lines changed

12 files changed

+200
-80
lines changed

.github/workflows/publish.yml

+11
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ jobs:
5656
env:
5757
PYPI_USERNAME: __token__
5858
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
59+
- name: publish lambda layer in SAR by triggering the internal codepipeline
60+
run: |
61+
aws ssm put-parameter --name "powertools-python-release-version" --value $RELEASE_TAG_VERSION --overwrite
62+
aws codepipeline start-pipeline-execution --name ${{ secrets.CODEPIPELINE_NAME }}
63+
env:
64+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
65+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
66+
AWS_DEFAULT_REGION: eu-west-1
67+
AWS_DEFAULT_OUTPUT: json
68+
69+
5970

6071
sync_master:
6172
needs: upload

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [1.4.0] - 2020-08-25
10+
11+
### Added
12+
- **All**: Official Lambda Layer via [Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/eu-west-1/057560766410/aws-lambda-powertools-python-layer)
13+
- **Tracer**: `capture_method` and `capture_lambda_handler` now support **capture_response=False** parameter to prevent Tracer to capture response as metadata to allow customers running Tracer with sensitive workloads
14+
915
### Fixed
1016
- **Metrics**: Cold start metric is now completely separate from application metrics dimensions, making it easier and cheaper to visualize.
1117
- This is a breaking change if you were graphing/alerting on both application metrics with the same name to compensate this previous malfunctioning
1218
- Marked as bugfix as this is the intended behaviour since the beginning, as you shouldn't have the same application metric with different dimensions
19+
- **Utilities**: SSMProvider within Parameters utility now have decrypt and recursive parameters correctly defined to support autocompletion
1320

1421
### Added
1522
- **Tracer**: capture_lambda_handler and capture_method decorators now support `capture_response` parameter to not include function's response as part of tracing metadata

aws_lambda_powertools/logging/formatter.py

+11-16
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from typing import Any
44

55

6-
def json_formatter(unserialized_value: Any):
7-
"""JSON custom serializer to cast unserialisable values to strings.
6+
def json_formatter(unserializable_value: Any):
7+
"""JSON custom serializer to cast unserializable values to strings.
88
99
Example
1010
-------
1111
12-
**Serialize unserialisable value to string**
12+
**Serialize unserializable value to string**
1313
1414
class X: pass
1515
value = {"x": X()}
@@ -18,10 +18,10 @@ class X: pass
1818
1919
Parameters
2020
----------
21-
unserialized_value: Any
21+
unserializable_value: Any
2222
Python object unserializable by JSON
2323
"""
24-
return str(unserialized_value)
24+
return str(unserializable_value)
2525

2626

2727
class JsonFormatter(logging.Formatter):
@@ -39,11 +39,12 @@ def __init__(self, **kwargs):
3939
"""Return a JsonFormatter instance.
4040
4141
The `json_default` kwarg is used to specify a formatter for otherwise
42-
unserialisable values. It must not throw. Defaults to a function that
42+
unserializable values. It must not throw. Defaults to a function that
4343
coerces the value to a string.
4444
4545
Other kwargs are used to specify log field format strings.
4646
"""
47+
self.default_json_formatter = kwargs.pop("json_default", json_formatter)
4748
datefmt = kwargs.pop("datefmt", None)
4849

4950
super(JsonFormatter, self).__init__(datefmt=datefmt)
@@ -54,7 +55,6 @@ def __init__(self, **kwargs):
5455
"location": "%(funcName)s:%(lineno)d",
5556
}
5657
self.format_dict.update(kwargs)
57-
self.default_json_formatter = kwargs.pop("json_default", json_formatter)
5858

5959
def update_formatter(self, **kwargs):
6060
self.format_dict.update(kwargs)
@@ -64,6 +64,7 @@ def format(self, record): # noqa: A003
6464
record_dict["asctime"] = self.formatTime(record, self.datefmt)
6565

6666
log_dict = {}
67+
6768
for key, value in self.format_dict.items():
6869
if value and key in self.reserved_keys:
6970
# converts default logging expr to its record value
@@ -84,19 +85,13 @@ def format(self, record): # noqa: A003
8485
except (json.decoder.JSONDecodeError, TypeError, ValueError):
8586
pass
8687

87-
if record.exc_info:
88+
if record.exc_info and not record.exc_text:
8889
# Cache the traceback text to avoid converting it multiple times
8990
# (it's constant anyway)
9091
# from logging.Formatter:format
91-
if not record.exc_text: # pragma: no cover
92-
record.exc_text = self.formatException(record.exc_info)
92+
record.exc_text = self.formatException(record.exc_info)
9393

9494
if record.exc_text:
9595
log_dict["exception"] = record.exc_text
9696

97-
json_record = json.dumps(log_dict, default=self.default_json_formatter)
98-
99-
if hasattr(json_record, "decode"): # pragma: no cover
100-
json_record = json_record.decode("utf-8")
101-
102-
return json_record
97+
return json.dumps(log_dict, default=self.default_json_formatter)

aws_lambda_powertools/utilities/parameters/base.py

+37-34
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from abc import ABC, abstractmethod
88
from collections import namedtuple
99
from datetime import datetime, timedelta
10-
from typing import Dict, Optional, Union
10+
from typing import Dict, Optional, Tuple, Union
1111

1212
from .exceptions import GetParameterError, TransformParameterError
1313

@@ -31,6 +31,9 @@ def __init__(self):
3131

3232
self.store = {}
3333

34+
def _has_not_expired(self, key: Tuple[str, Optional[str]]) -> bool:
35+
return key in self.store and self.store[key].ttl >= datetime.now()
36+
3437
def get(
3538
self, name: str, max_age: int = DEFAULT_MAX_AGE_SECS, transform: Optional[str] = None, **sdk_options
3639
) -> Union[str, list, dict, bytes]:
@@ -70,24 +73,26 @@ def get(
7073
# an acceptable tradeoff.
7174
key = (name, transform)
7275

73-
if key not in self.store or self.store[key].ttl < datetime.now():
74-
try:
75-
value = self._get(name, **sdk_options)
76-
# Encapsulate all errors into a generic GetParameterError
77-
except Exception as exc:
78-
raise GetParameterError(str(exc))
76+
if self._has_not_expired(key):
77+
return self.store[key].value
78+
79+
try:
80+
value = self._get(name, **sdk_options)
81+
# Encapsulate all errors into a generic GetParameterError
82+
except Exception as exc:
83+
raise GetParameterError(str(exc))
7984

80-
if transform is not None:
81-
value = transform_value(value, transform)
85+
if transform is not None:
86+
value = transform_value(value, transform)
8287

83-
self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age),)
88+
self.store[key] = ExpirableValue(value, datetime.now() + timedelta(seconds=max_age),)
8489

85-
return self.store[key].value
90+
return value
8691

8792
@abstractmethod
8893
def _get(self, name: str, **sdk_options) -> str:
8994
"""
90-
Retrieve paramater value from the underlying parameter store
95+
Retrieve parameter value from the underlying parameter store
9196
"""
9297
raise NotImplementedError()
9398

@@ -129,29 +134,22 @@ def get_multiple(
129134

130135
key = (path, transform)
131136

132-
if key not in self.store or self.store[key].ttl < datetime.now():
133-
try:
134-
values = self._get_multiple(path, **sdk_options)
135-
# Encapsulate all errors into a generic GetParameterError
136-
except Exception as exc:
137-
raise GetParameterError(str(exc))
137+
if self._has_not_expired(key):
138+
return self.store[key].value
138139

139-
if transform is not None:
140-
new_values = {}
141-
for key, value in values.items():
142-
try:
143-
new_values[key] = transform_value(value, transform)
144-
except Exception as exc:
145-
if raise_on_transform_error:
146-
raise exc
147-
else:
148-
new_values[key] = None
140+
try:
141+
values: Dict[str, Union[str, bytes, dict, None]] = self._get_multiple(path, **sdk_options)
142+
# Encapsulate all errors into a generic GetParameterError
143+
except Exception as exc:
144+
raise GetParameterError(str(exc))
149145

150-
values = new_values
146+
if transform is not None:
147+
for (key, value) in values.items():
148+
values[key] = transform_value(value, transform, raise_on_transform_error)
151149

152-
self.store[key] = ExpirableValue(values, datetime.now() + timedelta(seconds=max_age),)
150+
self.store[key] = ExpirableValue(values, datetime.now() + timedelta(seconds=max_age),)
153151

154-
return self.store[key].value
152+
return values
155153

156154
@abstractmethod
157155
def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
@@ -161,16 +159,19 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
161159
raise NotImplementedError()
162160

163161

164-
def transform_value(value: str, transform: str) -> Union[dict, bytes]:
162+
def transform_value(value: str, transform: str, raise_on_transform_error: bool = True) -> Union[dict, bytes, None]:
165163
"""
166164
Apply a transform to a value
167165
168166
Parameters
169167
---------
170168
value: str
171-
Parameter alue to transform
169+
Parameter value to transform
172170
transform: str
173171
Type of transform, supported values are "json" and "binary"
172+
raise_on_transform_error: bool, optional
173+
Raises an exception if any transform fails, otherwise this will
174+
return a None value for each transform that failed
174175
175176
Raises
176177
------
@@ -187,4 +188,6 @@ def transform_value(value: str, transform: str) -> Union[dict, bytes]:
187188
raise ValueError(f"Invalid transform type '{transform}'")
188189

189190
except Exception as exc:
190-
raise TransformParameterError(str(exc))
191+
if raise_on_transform_error:
192+
raise TransformParameterError(str(exc))
193+
return None

aws_lambda_powertools/utilities/parameters/secrets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _get(self, name: str, **sdk_options) -> str:
7777
----------
7878
name: str
7979
Name of the parameter
80-
sdk_options: dict
80+
sdk_options: dict, optional
8181
Dictionary of options that will be passed to the Secrets Manager get_secret_value API call
8282
"""
8383

aws_lambda_powertools/utilities/parameters/ssm.py

+56-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import boto3
99
from botocore.config import Config
1010

11-
from .base import DEFAULT_PROVIDERS, BaseProvider
11+
from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider
1212

1313

1414
class SSMProvider(BaseProvider):
@@ -86,6 +86,46 @@ def __init__(
8686

8787
super().__init__()
8888

89+
def get(
90+
self,
91+
name: str,
92+
max_age: int = DEFAULT_MAX_AGE_SECS,
93+
transform: Optional[str] = None,
94+
decrypt: bool = False,
95+
**sdk_options
96+
) -> Union[str, list, dict, bytes]:
97+
"""
98+
Retrieve a parameter value or return the cached value
99+
100+
Parameters
101+
----------
102+
name: str
103+
Parameter name
104+
max_age: int
105+
Maximum age of the cached value
106+
transform: str
107+
Optional transformation of the parameter value. Supported values
108+
are "json" for JSON strings and "binary" for base 64 encoded
109+
values.
110+
decrypt: bool, optional
111+
If the parameter value should be decrypted
112+
sdk_options: dict, optional
113+
Arguments that will be passed directly to the underlying API call
114+
115+
Raises
116+
------
117+
GetParameterError
118+
When the parameter provider fails to retrieve a parameter value for
119+
a given name.
120+
TransformParameterError
121+
When the parameter provider fails to transform a parameter value.
122+
"""
123+
124+
# Add to `decrypt` sdk_options to we can have an explicit option for this
125+
sdk_options["decrypt"] = decrypt
126+
127+
return super().get(name, max_age, transform, **sdk_options)
128+
89129
def _get(self, name: str, decrypt: bool = False, **sdk_options) -> str:
90130
"""
91131
Retrieve a parameter value from AWS Systems Manager Parameter Store
@@ -144,7 +184,9 @@ def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = Fals
144184
return parameters
145185

146186

147-
def get_parameter(name: str, transform: Optional[str] = None, **sdk_options) -> Union[str, list, dict, bytes]:
187+
def get_parameter(
188+
name: str, transform: Optional[str] = None, decrypt: bool = False, **sdk_options
189+
) -> Union[str, list, dict, bytes]:
148190
"""
149191
Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store
150192
@@ -154,6 +196,8 @@ def get_parameter(name: str, transform: Optional[str] = None, **sdk_options) ->
154196
Name of the parameter
155197
transform: str, optional
156198
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
199+
decrypt: bool, optional
200+
If the parameter values should be decrypted
157201
sdk_options: dict, optional
158202
Dictionary of options that will be passed to the Parameter Store get_parameter API call
159203
@@ -190,7 +234,10 @@ def get_parameter(name: str, transform: Optional[str] = None, **sdk_options) ->
190234
if "ssm" not in DEFAULT_PROVIDERS:
191235
DEFAULT_PROVIDERS["ssm"] = SSMProvider()
192236

193-
return DEFAULT_PROVIDERS["ssm"].get(name, transform=transform)
237+
# Add to `decrypt` sdk_options to we can have an explicit option for this
238+
sdk_options["decrypt"] = decrypt
239+
240+
return DEFAULT_PROVIDERS["ssm"].get(name, transform=transform, **sdk_options)
194241

195242

196243
def get_parameters(
@@ -205,10 +252,10 @@ def get_parameters(
205252
Path to retrieve the parameters
206253
transform: str, optional
207254
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
208-
decrypt: bool, optional
209-
If the parameter values should be decrypted
210255
recursive: bool, optional
211256
If this should retrieve the parameter values recursively or not, defaults to True
257+
decrypt: bool, optional
258+
If the parameter values should be decrypted
212259
sdk_options: dict, optional
213260
Dictionary of options that will be passed to the Parameter Store get_parameters_by_path API call
214261
@@ -245,4 +292,7 @@ def get_parameters(
245292
if "ssm" not in DEFAULT_PROVIDERS:
246293
DEFAULT_PROVIDERS["ssm"] = SSMProvider()
247294

248-
return DEFAULT_PROVIDERS["ssm"].get_multiple(path, transform=transform, recursive=recursive, decrypt=decrypt)
295+
sdk_options["recursive"] = recursive
296+
sdk_options["decrypt"] = decrypt
297+
298+
return DEFAULT_PROVIDERS["ssm"].get_multiple(path, transform=transform, **sdk_options)

docs/content/core/logger.mdx

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Key | Type | Example | Description
5555
**level** | str | "INFO" | Logging level
5656
**location** | str | "collect.handler:1" | Source code location where statement was executed
5757
**service** | str | "payment" | Service name defined. "service_undefined" will be used if unknown
58-
**sampling_rate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 1% in this case
58+
**sampling_rate** | int | 0.1 | Debug logging sampling rate in percentage e.g. 10% in this case
5959
**message** | any | "Collecting payment" | Log statement value. Unserializable JSON values will be casted to string
6060

6161
## Capturing Lambda context info
@@ -232,7 +232,7 @@ Sampling calculation happens at the Logger class initialization. This means, whe
232232
```python:title=collect.py
233233
from aws_lambda_powertools import Logger
234234

235-
# Sample 1% of debug logs e.g. 0.1
235+
# Sample 10% of debug logs e.g. 0.1
236236
logger = Logger(sample_rate=0.1) # highlight-line
237237

238238
def handler(event, context):

0 commit comments

Comments
 (0)