Printing logs in Vercel and AWS Lambda functions

Published: January 23, 2024

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:

LambdaContext and its Logger LambdaContext and its Logger

Using Logger

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:

Debug logs in the CloudWatch console Debug logs in the CloudWatch console

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:

Logs in the Vercel dashboard Logs in the Vercel dashboard

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.

Configuring the LogLevel

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:

Pure human brain content, no Generative AI added
Swift, Xcode, the Swift Package Manager, iOS, macOS, watchOS and Mac are trademarks of Apple Inc., registered in the U.S. and other countries.

© Swift Toolkit, 2024