Printing logs in Vercel and AWS Lambda functions
When developing a Lambda or Vercel function using Swift, you might in a first place try to use the print()
function to write logs. After publishing a few versions, checking what could be wrong, you’ll probably get a bit frustrated - just as I did initially - and see that no logs are displayed.
By default, printing to stdout
(or to stderr
) will display logs on the Lambda or CloudWatch consoles (or the Vercel logs tab). However, when using print()
, logs aren’t automatically sent to stdout
, but they get buffered instead. By calling fflush(stdout)
, the buffer is flushed and the logs will show up. But this solution is not the ideal, and there’s a better one.
SwiftLog and Logging
Both the swift-aws-lambda-runtime package, and the Swift runtime for Vercel functions (which uses the lambda runtime), rely on SwiftLog. This is a community-driven package, developed and maintained by Apple and the Swift Server Workgroup.
It provides the Logging
framework, which contains commonly used logging APIs via the Logger
type, such as different log levels, and also a simple stdout
log handler. You can use this simple handler, the community log handlers, or build your own log handlers.
Fortunately, for every request there is a context (of type LambdaContext
) is available, and it contains a Logger:
Logger
Using AWS Lambda
To access the logger, get the context first.
In an AWS Lambda function triggered by an API Gateway event:
import AWSLambdaRuntime
import AWSLambdaEvents
import Foundation
@main
struct APIGatewayV2Lambda: LambdaHandler {
init(context: LambdaInitializationContext) async throws {}
func handle(
_ request: APIGatewayV2Request,
context: LambdaContext
) async throws -> APIGatewayV2Response {
context.logger.debug("HTTP Method: (request.context.http.method.rawValue)")
context.logger.debug("Path: (request.rawPath)")
// convert the base64 body to a regular string
if let base64Body = request.body?.data(using: .utf8),
let decodedBodyData = Data(base64Encoded: base64Body),
let decodedBody = String(data: decodedBodyData, encoding: .utf8) {
context.logger.debug("Body: (decodedBody)")
}
return APIGatewayV2Response(statusCode: .ok, body: "{"Hello": "World"}")
}
}
To view the logs, open the AWS CloudWatch, select Log groups in the left pane, choose your Lambda function, and look for the latest Log stream. You’ll find a few internal logs from Lambda, and the ones you added as well:
Vercel
In a regular Vercel function, the approach is similar, as a LambdaContext
is provided as well, but it’s part of the Vercel.Request
struct:
import Vercel
@main
struct VercelFunction: RequestHandler {
func onRequest(_ req: Request) async throws -> Response {
// not a real warning, sample to show the different types of logs
req.context.logger.warning("Houston we have a (req.method) request")
return .status(.ok).send("Hello, World!")
}
}
To find the log after a request is called, open the app in the Vercel dashboard, navigate to the Logs tab, and search for the log in the list. When clicking it, a right pane will open with the details, and the contents of the warning above will be displayed there:
And finally, when using a Vapor application deployed to Vercel, logger is available directly in the Vapor.Request
type:
import VercelVapor
@main
struct App: VaporHandler {
static func configure(app: Vapor.Application) async throws {
app.get("hello") { req in
req.logger.debug("Request headers: (req.headers)")
return "world!"
}
}
}
Log Levels
Sometimes, when deploying some code to production, you’ll want to ignore some logs that are useful only when debugging or developing, without deleting the logs from the call sites. This is what the concept of log level is for: logs that are less severe than the log level will be ignored.
Logger
has a logLevel
property, being one of the following values described in the Logger.Level
enum:
- trace
- debug
- info
- notice
- warning
- error
As the documentation on that enum says:
Log levels are ordered by their severity, with
.trace
being the least severe and.critical
being the most severe.
LogLevel
Configuring the The log level of the lambda logger cannot be set by changing the property directly, as context.logger
is an immutable property.
To allow setting the log level, there’s an environment variable that can be set and the lambda context will initialize the logger according to the variable value.
If you’re using AWS Lambda or Vercel, you can add a new environment variable in the settings. Define the LOG_LEVEL
variable to one of the enum values, and the next time your function is called, the new log level will be used. When no value is set, Logger
defaults to info
, meaning that trace or debug logs will be ignored.
Starter Projects
Writing this post led me to prepare 3 starter projects, and you can find them in the links below: