Logging with AWS Lambda Powertools for TypeScript

Logging and tracing offer invaluable insights to the state of your application. Supercharge your Serverless logging with AWS Lambda Powertools for TypeScript by creating a single re-usable log instance using custom log formats driven by the runtime environment

Logging with AWS Lambda Powertools for TypeScript
Photo by @etiennegirardet / Unsplash
This read will enable you to:
  • Seamlessly integrate AWS Powertools logger using a one-liner middleware approach for all of your Lambda handlers
  • Get highly detailed logs in AWS
  • Get simple one-liner log entries for localhost development
  • Capture custom fields specific to your logging and reporting needs

Source code on GitHub

AWS Lambda Powertools for TypeScript is a long awaited package enabling TypeScript developers to use similar functionality as provided by the equivalent Python package 🤘

While the official guide offers a number of implementations, I'd like to share my approach using middleware and custom tailored log formats for AWS and localhost development respectively.

Reduce the clutter while capturing a high level of detail


On localhost using serverless-offline plugin, I want a simple, flat and readable format with a minimum of noise:

{"logLevel":"INFO","message":"Invoking Lambda handler function","data":{"type":"user"}}

Nah. Too busy. Let's dumb it down further:

INFO Invoking Lambda handler function { "type":"user" }

That's it. A simple one-liner is what I need. That's simple enough to squeeze a cluster of these into multiple events and get a clear picture of what's going on:

INFO Invoking Lambda handler function { type: "user" } 
DEBUG Updating user profile { lastLogin: '2022-11-23T05:01:30.051Z' }
ERROR Lambda execution failed: Can not read undefined of foo

While on AWS I'd expect a far greater level of detail:

We need all of these details to help troubleshoot, build dashboards and correlate events across multiple data sources.

I can't stress enough how important it is to capture correlation-id since this will help you correlate logs from multiple data sources and follow a customer journey though multiple systems.

Let's build it!

Following the basic setup from the GitHub code repo, we're focusing on building:

  • A handler function wrapped in Middy middleware
  • A middleware function attaching the Lambda context to the logger
  • A logger factory method returning a decorated Logger instance

Middified handler

Nothing fancy here. A plain handler wrapped in Middy using a single middleware function.

import { APIGatewayProxyResult } from 'aws-lambda';
import logger from '@/services/logger';
import middy from '@middy/core';
import { setLoggerContext } from '@/middleware/set-logger-context';

export const baseHandler = async (): Promise<APIGatewayProxyResult> => {
  logger.info('Invoking Lambda handler function', { data: { type: 'user' } });

  return {
    statusCode: 200,
    body: JSON.stringify({ OK: 200 }),
  };
};

export const handler = middy(baseHandler
  .use(setLoggerContext(logger));
/handlers/my-handler.ts

The middleware function

This middleware is attaching the Lambda application context to the logger instance (I'm aware that the library comes with a similar function).

import type { Logger } from '@aws-lambda-powertools/logger';
import middy from '@middy/core';
import {
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
  Context,
} from 'aws-lambda';

export const setLoggerContext = (
  logger: Logger,
): middy.MiddlewareObj<APIGatewayProxyEvent, APIGatewayProxyResult> => {
  const before = (request: middy.Request): void => {
    const { context }: { context: Context } = request;
    logger.addContext(context);
  };

  return {
    before,
  };
};
/middleware/set-logger-context.ts 

Log service

Factory style function returning a singleton instance of the Logger class. It's key that the instance is created outside of the function scope. This means the Logger class is instantiated before the middleware and function handler are invoked.

Also, notice how the log format is driven by the runtime environment using the isAWS() ternary expression:

import { LogFormatter, Logger } from '@aws-lambda-powertools/logger';
import { isAWS } from '../env';
import { DetailedLogFormatter } from './formatters/aws';
import { LocalhostLogFormatter } from './formatters/local';

const logFormatter: LogFormatter = isAWS()
  ? new DetailedLogFormatter()
  : new LocalhostLogFormatter();

const logger: Logger = new Logger({
  logFormatter,
  logLevel: process.env.LOG_LEVEL || 'DEBUG',
  serviceName: process.env.AWS_LAMBDA_FUNCTION_NAME,
});

// Singleton getter
const getLogger = (): Logger => logger;
export default getLogger();
/services/logger/index.ts

Putting it all together

Now you can simply import the wrapped Powertools logger and use it anywhere in your source code:

import logger from '@/services/logger';

export const login = async(
  email: string,
  password: string,
): Promise<void> => {
  logger.debug('Authenticating user', { data: { email } });
  // Implementation
};

Happy coding.
Source code on GitHub