Skip to content

Commit c16a900

Browse files
author
Michael Brewer
authored
Merge branch 'develop' into docs/1067
2 parents b699a83 + eee0acc commit c16a900

File tree

8 files changed

+156
-46
lines changed

8 files changed

+156
-46
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format for changes and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 1.25.3 - 2022-03-09
8+
9+
### Bug Fixes
10+
11+
* **logger:** ensure state is cleared for custom formatters ([#1072](https://github.com/awslabs/aws-lambda-powertools-python/issues/1072))
12+
13+
### Documentation
14+
15+
* **plugin:** add mermaid to create diagram as code ([#1070](https://github.com/awslabs/aws-lambda-powertools-python/issues/1070))
16+
717
## 1.25.2 - 2022-03-07
818

919
### Bug Fixes

aws_lambda_powertools/logging/formatter.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ def append_keys(self, **additional_keys):
4545
def remove_keys(self, keys: Iterable[str]):
4646
raise NotImplementedError()
4747

48+
@abstractmethod
49+
def clear_state(self):
50+
"""Removes any previously added logging keys"""
51+
raise NotImplementedError()
52+
4853

4954
class LambdaPowertoolsFormatter(BasePowertoolsFormatter):
5055
"""AWS Lambda Powertools Logging formatter.
@@ -180,6 +185,9 @@ def remove_keys(self, keys: Iterable[str]):
180185
for key in keys:
181186
self.log_format.pop(key, None)
182187

188+
def clear_state(self):
189+
self.log_format = dict.fromkeys(self.log_record_order)
190+
183191
@staticmethod
184192
def _build_default_keys():
185193
return {

aws_lambda_powertools/logging/logger.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,14 +385,26 @@ def structure_logs(self, append: bool = False, **keys):
385385
append : bool, optional
386386
append keys provided to logger formatter, by default False
387387
"""
388+
# There are 3 operational modes for this method
389+
## 1. Register a Powertools Formatter for the first time
390+
## 2. Append new keys to the current logger formatter; deprecated in favour of append_keys
391+
## 3. Add new keys and discard existing to the registered formatter
388392

389-
if append:
390-
# Maintenance: Add deprecation warning for major version. Refer to append_keys() when docs are updated
391-
self.append_keys(**keys)
392-
else:
393-
log_keys = {**self._default_log_keys, **keys}
393+
# Mode 1
394+
log_keys = {**self._default_log_keys, **keys}
395+
is_logger_preconfigured = getattr(self._logger, "init", False)
396+
if not is_logger_preconfigured:
394397
formatter = self.logger_formatter or LambdaPowertoolsFormatter(**log_keys) # type: ignore
395-
self.registered_handler.setFormatter(formatter)
398+
return self.registered_handler.setFormatter(formatter)
399+
400+
# Mode 2 (legacy)
401+
if append:
402+
# Maintenance: Add deprecation warning for major version
403+
return self.append_keys(**keys)
404+
405+
# Mode 3
406+
self.registered_formatter.clear_state()
407+
self.registered_formatter.append_keys(**log_keys)
396408

397409
def set_correlation_id(self, value: Optional[str]):
398410
"""Sets the correlation_id in the logging json

docs/core/logger.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,10 @@ logger.info("Collecting payment")
862862

863863
By default, Logger uses [LambdaPowertoolsFormatter](#lambdapowertoolsformatter) that persists its custom structure between non-cold start invocations. There could be scenarios where the existing feature set isn't sufficient to your formatting needs.
864864

865-
For **minor changes like remapping keys** after all log record processing has completed, you can override `serialize` method from [LambdaPowertoolsFormatter](#lambdapowertoolsformatter):
865+
???+ info
866+
The most common use cases are remapping keys by bringing your existing schema, and redacting sensitive information you know upfront.
867+
868+
For these, you can override the `serialize` method from [LambdaPowertoolsFormatter](#lambdapowertoolsformatter).
866869

867870
=== "custom_formatter.py"
868871

@@ -892,28 +895,39 @@ For **minor changes like remapping keys** after all log record processing has co
892895
}
893896
```
894897

895-
For **replacing the formatter entirely**, you can subclass `BasePowertoolsFormatter`, implement `append_keys` method, and override `format` standard logging method. This ensures the current feature set of Logger like [injecting Lambda context](#capturing-lambda-context-info) and [sampling](#sampling-debug-logs) will continue to work.
898+
The `log` argument is the final log record containing [our standard keys](#standard-structured-keys), optionally [Lambda context keys](#capturing-lambda-context-info), and any custom key you might have added via [append_keys](#append_keys-method) or the [extra parameter](#extra-parameter).
899+
900+
For exceptional cases where you want to completely replace our formatter logic, you can subclass `BasePowertoolsFormatter`.
901+
902+
???+ warning
903+
You will need to implement `append_keys`, `clear_state`, override `format`, and optionally `remove_keys` to keep the same feature set Powertools Logger provides. This also means keeping state of logging keys added.
896904

897-
???+ info
898-
You might need to implement `remove_keys` method if you make use of the feature too.
899905

900906
=== "collect.py"
901907

902-
```python hl_lines="2 4 7 12 16 27"
908+
```python hl_lines="5 7 9-10 13 17 21 24 35"
909+
import logging
910+
from typing import Iterable, List, Optional
911+
903912
from aws_lambda_powertools import Logger
904913
from aws_lambda_powertools.logging.formatter import BasePowertoolsFormatter
905914

906915
class CustomFormatter(BasePowertoolsFormatter):
907-
custom_format = {} # arbitrary dict to hold our structured keys
916+
def __init__(self, log_record_order: Optional[List[str]], *args, **kwargs):
917+
self.log_record_order = log_record_order or ["level", "location", "message", "timestamp"]
918+
self.log_format = dict.fromkeys(self.log_record_order)
919+
super().__init__(*args, **kwargs)
908920

909921
def append_keys(self, **additional_keys):
910922
# also used by `inject_lambda_context` decorator
911-
self.custom_format.update(additional_keys)
923+
self.log_format.update(additional_keys)
912924

913-
# Optional unless you make use of this Logger feature
914925
def remove_keys(self, keys: Iterable[str]):
915926
for key in keys:
916-
self.custom_format.pop(key, None)
927+
self.log_format.pop(key, None)
928+
929+
def clear_state(self):
930+
self.log_format = dict.fromkeys(self.log_record_order)
917931

918932
def format(self, record: logging.LogRecord) -> str: # noqa: A003
919933
"""Format logging record as structured JSON str"""
@@ -922,7 +936,7 @@ For **replacing the formatter entirely**, you can subclass `BasePowertoolsFormat
922936
"event": super().format(record),
923937
"timestamp": self.formatTime(record),
924938
"my_default_key": "test",
925-
**self.custom_format,
939+
**self.log_format,
926940
}
927941
)
928942

docs/index.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai
2424

2525
Powertools is available in the following formats:
2626

27-
* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:**](#){: .copyMe}
27+
* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:**](#){: .copyMe}
2828
* **PyPi**: **`pip install aws-lambda-powertools`**
2929

3030
### Lambda Layer
@@ -37,23 +37,23 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
3737

3838
| Region | Layer ARN
3939
|--------------------------- | ---------------------------
40-
| `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
41-
| `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
42-
| `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
43-
| `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
44-
| `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
45-
| `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
46-
| `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
47-
| `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
48-
| `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
49-
| `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
50-
| `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
51-
| `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
52-
| `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
53-
| `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
54-
| `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
55-
| `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
56-
| `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:11 :clipboard:](#){: .copyMe}
40+
| `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
41+
| `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
42+
| `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
43+
| `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
44+
| `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
45+
| `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
46+
| `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
47+
| `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
48+
| `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
49+
| `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
50+
| `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
51+
| `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
52+
| `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
53+
| `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
54+
| `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
55+
| `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
56+
| `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:13 :clipboard:](#){: .copyMe}
5757

5858
=== "SAM"
5959

@@ -62,7 +62,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
6262
Type: AWS::Serverless::Function
6363
Properties:
6464
Layers:
65-
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:11
65+
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:13
6666
```
6767

6868
=== "Serverless framework"
@@ -72,7 +72,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
7272
hello:
7373
handler: lambda_function.lambda_handler
7474
layers:
75-
- arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:11
75+
- arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:13
7676
```
7777

7878
=== "CDK"
@@ -88,7 +88,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
8888
powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn(
8989
self,
9090
id="lambda-powertools",
91-
layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:11"
91+
layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:13"
9292
)
9393
aws_lambda.Function(self,
9494
'sample-app-lambda',
@@ -135,7 +135,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
135135
role = aws_iam_role.iam_for_lambda.arn
136136
handler = "index.test"
137137
runtime = "python3.9"
138-
layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:11"]
138+
layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:13"]
139139

140140
source_code_hash = filebase64sha256("lambda_function_payload.zip")
141141
}
@@ -152,7 +152,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
152152
? Do you want to configure advanced settings? Yes
153153
...
154154
? Do you want to enable Lambda layers for this function? Yes
155-
? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:11
155+
? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:13
156156
❯ amplify push -y
157157

158158

@@ -163,15 +163,15 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https:
163163
- Name: <NAME-OF-FUNCTION>
164164
? Which setting do you want to update? Lambda layers configuration
165165
? Do you want to enable Lambda layers for this function? Yes
166-
? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:11
166+
? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:13
167167
? Do you want to edit the local lambda function now? No
168168
```
169169

170170
=== "Get the Layer .zip contents"
171171
Change {region} to your AWS region, e.g. `eu-west-1`
172172

173173
```bash title="AWS CLI"
174-
aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:11 --region {region}
174+
aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:13 --region {region}
175175
```
176176

177177
The pre-signed URL to download this Lambda Layer will be within `Location` key.

mkdocs.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,13 @@ markdown_extensions:
7777
- attr_list
7878
- pymdownx.emoji
7979
- pymdownx.inlinehilite
80-
- pymdownx.superfences
80+
- pymdownx.superfences:
81+
custom_fences:
82+
- name: mermaid
83+
class: mermaid
84+
format: !!python/name:pymdownx.superfences.fence_code_format
8185

82-
copyright: Copyright &copy; 2021 Amazon Web Services
86+
copyright: Copyright &copy; 2022 Amazon Web Services
8387

8488
plugins:
8589
- git-revision-date
@@ -95,3 +99,4 @@ extra:
9599
version:
96100
provider: mike
97101
default: latest
102+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "aws_lambda_powertools"
3-
version = "1.25.2"
3+
version = "1.25.3"
44
description = "A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, batching, idempotency, feature flags, and more."
55
authors = ["Amazon Web Services"]
66
include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"]

tests/functional/test_logger.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
import random
66
import re
77
import string
8+
from ast import Dict
89
from collections import namedtuple
910
from datetime import datetime, timezone
10-
from typing import Iterable
11+
from typing import Any, Callable, Iterable, List, Optional, Union
1112

1213
import pytest
1314

1415
from aws_lambda_powertools import Logger, Tracer
1516
from aws_lambda_powertools.logging import correlation_paths
1617
from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError
17-
from aws_lambda_powertools.logging.formatter import BasePowertoolsFormatter
18+
from aws_lambda_powertools.logging.formatter import BasePowertoolsFormatter, LambdaPowertoolsFormatter
1819
from aws_lambda_powertools.logging.logger import set_package_logger
1920
from aws_lambda_powertools.shared import constants
2021
from aws_lambda_powertools.utilities.data_classes import S3Event, event_source
@@ -524,6 +525,9 @@ def remove_keys(self, keys: Iterable[str]):
524525
for key in keys:
525526
self.custom_format.pop(key, None)
526527

528+
def clear_state(self):
529+
self.custom_format.clear()
530+
527531
def format(self, record: logging.LogRecord) -> str: # noqa: A003
528532
return json.dumps(
529533
{
@@ -564,6 +568,63 @@ def handler(event, context):
564568
assert logger.get_correlation_id() is None
565569

566570

571+
def test_logger_custom_powertools_formatter_clear_state(stdout, service_name, lambda_context):
572+
class CustomFormatter(LambdaPowertoolsFormatter):
573+
def __init__(
574+
self,
575+
json_serializer: Optional[Callable[[Dict], str]] = None,
576+
json_deserializer: Optional[Callable[[Union[Dict, str, bool, int, float]], str]] = None,
577+
json_default: Optional[Callable[[Any], Any]] = None,
578+
datefmt: Optional[str] = None,
579+
use_datetime_directive: bool = False,
580+
log_record_order: Optional[List[str]] = None,
581+
utc: bool = False,
582+
**kwargs,
583+
):
584+
super().__init__(
585+
json_serializer,
586+
json_deserializer,
587+
json_default,
588+
datefmt,
589+
use_datetime_directive,
590+
log_record_order,
591+
utc,
592+
**kwargs,
593+
)
594+
595+
custom_formatter = CustomFormatter()
596+
597+
# GIVEN a Logger is initialized with a custom formatter
598+
logger = Logger(service=service_name, stream=stdout, logger_formatter=custom_formatter)
599+
600+
# WHEN a lambda function is decorated with logger
601+
# and state is to be cleared in the next invocation
602+
@logger.inject_lambda_context(clear_state=True)
603+
def handler(event, context):
604+
if event.get("add_key"):
605+
logger.append_keys(my_key="value")
606+
logger.info("Hello")
607+
608+
handler({"add_key": True}, lambda_context)
609+
handler({}, lambda_context)
610+
611+
lambda_context_keys = (
612+
"function_name",
613+
"function_memory_size",
614+
"function_arn",
615+
"function_request_id",
616+
)
617+
618+
first_log, second_log = capture_multiple_logging_statements_output(stdout)
619+
620+
# THEN my_key should only present once
621+
# and lambda contextual info should also be in both logs
622+
assert "my_key" in first_log
623+
assert "my_key" not in second_log
624+
assert all(k in first_log for k in lambda_context_keys)
625+
assert all(k in second_log for k in lambda_context_keys)
626+
627+
567628
def test_logger_custom_handler(lambda_context, service_name, tmp_path):
568629
# GIVEN a Logger is initialized with a FileHandler
569630
log_file = tmp_path / "log.json"

0 commit comments

Comments
 (0)