Following best practices, using structured logging, and enabling distributed tracing across all your Lambda functions can be quite a hassle. When you first start developing serverless applications with Lambda one starts to tend to copy-paste existing code from one Lambda function to another to have structured logs or the like. After some time, the pain of maintaining this one will start trimming the hedge and creating a dedicated library with a reusable logger, event validation, and parsing. Some won't leave it at that and create a dedicated Lambda layer for all the functionality needed across the majority of their Lambda function. From there on it's a walk in the park but getting there is comparable to having to fight your way through the jungle. There is no clear path. Every step is a good piece of work and the thicket makes it impossible to find the direct path.
With Powertools for AWS Lambda we don't have to find our way through the jungle any longer. We can now start our journey right at the park gate. Powertools for AWS is optimized for the Lambda function environments. It aims to facilitate best practice adoption as defined in the AWS Well-Architected Serverless Lens, keeps lean to keep our functions performant, and provides answers to users' urgent needs by listening to the community when considering what to do next.
Powertools for AWS Lambda is available for Python (first Powertools library; available on PyPi and as Lambda Layer), Java, TypeScript, and in preview for .NET.
To get a bit more insight into what Powertools for AWS Lambda has to offer, we will look at the following example architecture. It's probably one of the most used architectures, and while it doesn't matter much, it makes the example more tangible to imagine that we built an application for finding a walking buddy for the park.
The frontend will be implemented with Amplify. For the backend, we use API Gateway to provide a REST API to our frontend. API requests get processed by a Lambda function. User profiles and matches for walking buddies are stored in a single DynamoDB table. We use CloudWatch to store the Lambda function logs and X-Ray for distributed tracing.
As providing examples for every variant of Powertools for AWS Lambda would go beyond the scope of the blog post, we will use Python in any code snippets. In my eyes, it's the most established Powertools variant with the most features, of which some will likely get adopted by the other variants in the future. Within the documentation for all AWS Lambda Powertools, you can find runnable examples, tutorials, and the code to those in public repositories.
Unified structured logging is simple with Powertools for AWS Lambda. To add keys to all log message in a Lambda invocation use append_keys
.
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
def handler(event: dict, context: LambdaContext) -> str:
user_id = event.get("user_id")
# this will ensure user_id key always has the latest value before logging
logger.append_keys(user_id=user_id)
To add keys to just one log message, use extra
.
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
def handler(event: dict, context: LambdaContext) -> str:
user_id = event.get("user_id")
# adds user_id only to this log message
logger.info("Log additional key", extra={ "user_id": user_id })
Logging the incoming event can help in debugging issues. It's simple with the inject_lambda_context
logger decorator. For supported events it also simplifies tracking correlation IDs around your system.
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
app = APIGatewayRestResolver()
# adds correlation ID for REST API Gateway events to log messages and logs the incoming event
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST, log_event=True)
def lambda_handler(event: dict, context: LambdaContext) -> str:
pass
After enabling tracing and setting the right IAM permissions for the Lambda function, we just need the capture_lambda_handler
decorator to instrument tracing. The decorator will automatically add an annotation for cold starts.
from aws_lambda_powertools import Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext
tracer = Tracer()
@tracer.capture_lambda_handler
def lambda_handler(event: dict, context: LambdaContext) -> str:
# searchable metadata
tracer.put_annotation(key="UserId", value=event.get("user_id"))
tracer.put_metadata(key="Path", value=event.get("path"))
To add subsegments for certain functions use the @tracer.capture_method
decorator.
Let's say we want a metric to count how many people matched with walking buddies through our service. All it takes to log metrics is the log_metrics
decorator.
from aws_lambda_powertools import Metrics
from aws_lambda_powertools.metrics import MetricUnit
from aws_lambda_powertools.utilities.typing import LambdaContext
metrics = Metrics()
@metrics.log_metrics # ensures metrics are flushed upon request completion/failure
def lambda_handler(event: dict, context: LambdaContext):
# ...
metrics.add_metric(name="SuccessfulMatch", unit=MetricUnit.Count, value=1)
Our Lambda function gets invoked by API Gateway. We know the event format but it would be so nice if we wouldn't have to validate and parse the incoming events ourselves. Luckily, Powertools for AWS Lambda provides a solution for this.
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import APIGatewayAuthorizerRequestEvent
tracer = Tracer()
logger = Logger()
app = APIGatewayRestResolver()
# API route resolver
@app.get("/buddies")
@tracer.capture_method
def get_buddies():
return {"buddies": []}
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
# transform incoming event to data class
@event_source(data_class=APIGatewayAuthorizerRequestEvent)
def lambda_handler(event: APIGatewayAuthorizerRequestEvent, context: LambdaContext) -> dict:
# access event class attributes
return app.resolve(event.raw_event, context)
If we don't want to use the event source data classes, we can opt to custom validation with the validator
decorator, validating the event against JSON schemas. Another handy construct you can see in the example above is the use of the app resolver. You can use HTTP method decorators to match a function to an API request's method and path. No need to write custom routing logic. Errors can be handled with the @app.exception_handler(ExceptionType)
decorator that will catch exceptions of the provided type and handle them in a dedicated function.
If we wouldn't use a REST API but a GraphQL API it would look similar. Instead of the HTTP method decorator the @app.resolver
decorator matches a function to GraphQL types and fields.
Using Powertools for AWS Lambda greatly improves Developer Experience when developing Lambda functions. It helps in ensuring to stick to established best practices. AWS Lambda Powertools Python offered as a Lambda layer makes it easy to try out, use, and later adopt it to all your functions. Through the community-driven approach every voice counts and influences the Powertools roadmap.