Skip to content

Commit f986512

Browse files
author
Ran Isenberg
committed
Merge branch 'develop' of github.com:risenberg-cyberark/aws-lambda-powertools-python into pydantic
# Conflicts: # pyproject.toml
2 parents 637a696 + b654ca6 commit f986512

File tree

80 files changed

+6454
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+6454
-176
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# We use poetry to run formatting and linting before commit/push
2-
# Longers checks such as tests, security and complexity baseline
2+
# Longer checks such as tests, security and complexity baseline
33
# are run as part of CI to prevent slower feedback loop
44
# All checks can be run locally via `make pr`
55

CHANGELOG.md

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

77
## [Unreleased]
88

9+
### Added
10+
- **Metrics**: Support adding multiple metric values to a metric name
11+
- **Utilities**: Add new `Validator` utility to validate inbound events and responses using JSON Schema
12+
13+
## [1.5.0] - 2020-09-04
14+
15+
### Added
16+
- **Logger**: Add `xray_trace_id` to log output to improve integration with CloudWatch Service Lens
17+
- **Logger**: Allow reordering of logged output
18+
- **Utilities**: Add new `SQS batch processing` utility to handle partial failures in processing message batches
19+
- **Utilities**: Add typing utility providing static type for lambda context object
20+
- **Utilities**: Add `transform=auto` in parameters utility to deserialize parameter values based on the key name
21+
22+
### Fixed
23+
- **Logger**: The value of `json_default` formatter is no longer written to logs
24+
925
## [1.4.0] - 2020-08-25
1026

1127
### Added

CONTRIBUTING.md

+13
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ [email protected] with any additional questions or comments.
6969
## Security issue notifications
7070
If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
7171

72+
## Troubleshooting
73+
74+
### API reference documentation
75+
76+
When you are working on the codebase and you use the local API reference documentation to preview your changes, you might see the following message: `Module aws_lambda_powertools not found`.
77+
78+
This happens when:
79+
80+
* You did not install the local dev environment yet
81+
- You can install dev deps with `make dev` command
82+
* The code in the repository is raising an exception while the `pdoc` is scanning the codebase
83+
- Unfortunately, this exception is not shown to you, but if you run, `poetry run pdoc --pdf aws_lambda_powertools`, the exception is shown and you can prevent the exception from being raised
84+
- Once resolved the documentation should load correctly again
7285

7386
## Licensing
7487

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ target:
44

55
dev:
66
pip install --upgrade pip poetry pre-commit
7-
poetry install
7+
poetry install --extras "jmespath"
88
pre-commit install
99

1010
dev-docs:

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master)
44
![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools)
55

6-
A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier.
6+
A suite of utilities for AWS Lambda functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier.
77

88
**[📜Documentation](https://awslabs.github.io/aws-lambda-powertools-python/)** | **[API Docs](https://awslabs.github.io/aws-lambda-powertools-python/api/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Feature request](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[Kitchen sink example](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/example)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/)**
99

10+
> **Join us on the AWS Developers Slack at `#lambda-powertools`** - **[Invite, if you don't have an account](https://join.slack.com/t/awsdevelopers/shared_invite/zt-gu30gquv-EhwIYq3kHhhysaZ2aIX7ew)**
11+
1012
## Features
1113

1214
* **[Tracing](https://awslabs.github.io/aws-lambda-powertools-python/core/tracer/)** - Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions

aws_lambda_powertools/logging/formatter.py

+32-29
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
11
import json
22
import logging
3-
from typing import Any
4-
5-
6-
def json_formatter(unserializable_value: Any):
7-
"""JSON custom serializer to cast unserializable values to strings.
8-
9-
Example
10-
-------
11-
12-
**Serialize unserializable value to string**
13-
14-
class X: pass
15-
value = {"x": X()}
16-
17-
json.dumps(value, default=json_formatter)
18-
19-
Parameters
20-
----------
21-
unserializable_value: Any
22-
Python object unserializable by JSON
23-
"""
24-
return str(unserializable_value)
3+
import os
254

265

276
class JsonFormatter(logging.Formatter):
@@ -42,19 +21,37 @@ def __init__(self, **kwargs):
4221
unserializable values. It must not throw. Defaults to a function that
4322
coerces the value to a string.
4423
24+
The `log_record_order` kwarg is used to specify the order of the keys used in
25+
the structured json logs. By default the order is: "level", "location", "message", "timestamp",
26+
"service" and "sampling_rate".
27+
4528
Other kwargs are used to specify log field format strings.
4629
"""
47-
self.default_json_formatter = kwargs.pop("json_default", json_formatter)
48-
datefmt = kwargs.pop("datefmt", None)
49-
50-
super(JsonFormatter, self).__init__(datefmt=datefmt)
30+
# Set the default unserializable function, by default values will be cast as str.
31+
self.default_json_formatter = kwargs.pop("json_default", str)
32+
# Set the insertion order for the log messages
33+
self.format_dict = dict.fromkeys(kwargs.pop("log_record_order", ["level", "location", "message", "timestamp"]))
5134
self.reserved_keys = ["timestamp", "level", "location"]
52-
self.format_dict = {
53-
"timestamp": "%(asctime)s",
35+
# Set the date format used by `asctime`
36+
super(JsonFormatter, self).__init__(datefmt=kwargs.pop("datefmt", None))
37+
38+
self.format_dict.update(self._build_root_keys(**kwargs))
39+
40+
@staticmethod
41+
def _build_root_keys(**kwargs):
42+
return {
5443
"level": "%(levelname)s",
5544
"location": "%(funcName)s:%(lineno)d",
45+
"timestamp": "%(asctime)s",
46+
**kwargs,
5647
}
57-
self.format_dict.update(kwargs)
48+
49+
@staticmethod
50+
def _get_latest_trace_id():
51+
xray_trace_id = os.getenv("_X_AMZN_TRACE_ID")
52+
trace_id = xray_trace_id.split(";")[0].replace("Root=", "") if xray_trace_id else None
53+
54+
return trace_id
5855

5956
def update_formatter(self, **kwargs):
6057
self.format_dict.update(kwargs)
@@ -94,4 +91,10 @@ def format(self, record): # noqa: A003
9491
if record.exc_text:
9592
log_dict["exception"] = record.exc_text
9693

94+
# fetch latest X-Ray Trace ID, if any
95+
log_dict.update({"xray_trace_id": self._get_latest_trace_id()})
96+
97+
# Filter out top level key with values that are None
98+
log_dict = {k: v for k, v in log_dict.items() if v is not None}
99+
97100
return json.dumps(log_dict, default=self.default_json_formatter)

aws_lambda_powertools/logging/logger.py

+29-25
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,6 @@ def __getattr__(self, name):
136136
# https://github.com/awslabs/aws-lambda-powertools-python/issues/97
137137
return getattr(self._logger, name)
138138

139-
def _get_log_level(self, level: Union[str, int]) -> Union[str, int]:
140-
""" Returns preferred log level set by the customer in upper case """
141-
if isinstance(level, int):
142-
return level
143-
144-
log_level: str = level or os.getenv("LOG_LEVEL")
145-
log_level = log_level.upper() if log_level is not None else logging.INFO
146-
147-
return log_level
148-
149139
def _get_logger(self):
150140
""" Returns a Logger named {self.service}, or {self.service.filename} for child loggers"""
151141
logger_name = self.service
@@ -154,17 +144,6 @@ def _get_logger(self):
154144

155145
return logging.getLogger(logger_name)
156146

157-
def _get_caller_filename(self):
158-
""" Return caller filename by finding the caller frame """
159-
# Current frame => _get_logger()
160-
# Previous frame => logger.py
161-
# Before previous frame => Caller
162-
frame = inspect.currentframe()
163-
caller_frame = frame.f_back.f_back.f_back
164-
filename = caller_frame.f_globals["__name__"]
165-
166-
return filename
167-
168147
def _init_logger(self, **kwargs):
169148
"""Configures new logger"""
170149

@@ -207,6 +186,8 @@ def inject_lambda_context(self, lambda_handler: Callable[[Dict, Any], Any] = Non
207186
208187
Parameters
209188
----------
189+
lambda_handler : Callable
190+
Method to inject the lambda context
210191
log_event : bool, optional
211192
Instructs logger to log Lambda Event, by default False
212193
@@ -254,14 +235,14 @@ def handler(event, context):
254235

255236
@functools.wraps(lambda_handler)
256237
def decorate(event, context):
238+
lambda_context = build_lambda_context_model(context)
239+
cold_start = _is_cold_start()
240+
self.structure_logs(append=True, cold_start=cold_start, **lambda_context.__dict__)
241+
257242
if log_event:
258243
logger.debug("Event received")
259244
self.info(event)
260245

261-
lambda_context = build_lambda_context_model(context)
262-
cold_start = _is_cold_start()
263-
264-
self.structure_logs(append=True, cold_start=cold_start, **lambda_context.__dict__)
265246
return lambda_handler(event, context)
266247

267248
return decorate
@@ -291,6 +272,29 @@ def structure_logs(self, append: bool = False, **kwargs):
291272
# Set a new formatter for a logger handler
292273
handler.setFormatter(JsonFormatter(**self._default_log_keys, **kwargs))
293274

275+
@staticmethod
276+
def _get_log_level(level: Union[str, int]) -> Union[str, int]:
277+
""" Returns preferred log level set by the customer in upper case """
278+
if isinstance(level, int):
279+
return level
280+
281+
log_level: str = level or os.getenv("LOG_LEVEL")
282+
log_level = log_level.upper() if log_level is not None else logging.INFO
283+
284+
return log_level
285+
286+
@staticmethod
287+
def _get_caller_filename():
288+
""" Return caller filename by finding the caller frame """
289+
# Current frame => _get_logger()
290+
# Previous frame => logger.py
291+
# Before previous frame => Caller
292+
frame = inspect.currentframe()
293+
caller_frame = frame.f_back.f_back.f_back
294+
filename = caller_frame.f_globals["__name__"]
295+
296+
return filename
297+
294298

295299
def set_package_logger(
296300
level: Union[str, int] = logging.DEBUG, stream: sys.stdout = None, formatter: logging.Formatter = None

aws_lambda_powertools/metrics/base.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numbers
55
import os
66
import pathlib
7+
from collections import defaultdict
78
from enum import Enum
89
from typing import Any, Dict, List, Union
910

@@ -93,7 +94,7 @@ def __init__(
9394
self._metric_unit_options = list(MetricUnit.__members__)
9495
self.metadata_set = self.metadata_set if metadata_set is not None else {}
9596

96-
def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
97+
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
9798
"""Adds given metric
9899
99100
Example
@@ -110,7 +111,7 @@ def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
110111
----------
111112
name : str
112113
Metric name
113-
unit : MetricUnit
114+
unit : Union[MetricUnit, str]
114115
`aws_lambda_powertools.helper.models.MetricUnit`
115116
value : float
116117
Metric value
@@ -124,7 +125,9 @@ def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]):
124125
raise MetricValueError(f"{value} is not a valid number")
125126

126127
unit = self.__extract_metric_unit_value(unit=unit)
127-
metric = {"Unit": unit, "Value": float(value)}
128+
metric = self.metric_set.get(name, defaultdict(list))
129+
metric["Unit"] = unit
130+
metric["Value"].append(float(value))
128131
logger.debug(f"Adding metric: {name} with {metric}")
129132
self.metric_set[name] = metric
130133

@@ -146,6 +149,8 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None, me
146149
Dictionary of metrics to serialize, by default None
147150
dimensions : Dict, optional
148151
Dictionary of dimensions to serialize, by default None
152+
metadata: Dict, optional
153+
Dictionary of metadata to serialize, by default None
149154
150155
Example
151156
-------
@@ -183,7 +188,7 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None, me
183188
metric_names_and_values: Dict[str, str] = {} # { "metric_name": 1.0 }
184189

185190
for metric_name in metrics:
186-
metric: str = metrics[metric_name]
191+
metric: dict = metrics[metric_name]
187192
metric_value: int = metric.get("Value", 0)
188193
metric_unit: str = metric.get("Unit", "")
189194

@@ -257,7 +262,7 @@ def add_metadata(self, key: str, value: Any):
257262
258263
Parameters
259264
----------
260-
name : str
265+
key : str
261266
Metadata key
262267
value : any
263268
Metadata value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Batch processing utility
5+
"""
6+
7+
from .base import BasePartialProcessor, batch_processor
8+
from .sqs import PartialSQSProcessor, sqs_batch_processor
9+
10+
__all__ = ("BasePartialProcessor", "PartialSQSProcessor", "batch_processor", "sqs_batch_processor")

0 commit comments

Comments
 (0)