Right, let’s talk about logging. Because when your function vanishes into the ether milliseconds after running, a print("here") statement isn’t going to cut it. You need to know what happened, and for that, we’re stuck with CloudWatch Logs. It’s not a perfect relationship, but we can make it work.

The absolute first thing you need to get through your skull is that every print() or console.log() statement is a log event. Lambda automatically captures anything written to stdout or stderr and shoves it into a CloudWatch Logs stream. This is both a blessing and a curse. It’s dead simple, but it also means that if you log a big JSON object as a string, you’re going to have a truly miserable time trying to query it later. Which brings me to my first major point.

The Default Logging Experience is a Mess

Out of the box, your logs look like this sad, unstructured blob:

START RequestId: 8f8f6f40-7c3c-487e-b8a1-31b4d1d0c0c3 Version: $LATEST
END RequestId: 8f8f6f40-7c3c-487e-b8a1-31b4d1d0c0c3
REPORT RequestId: 8f8f6f40-7c3c-487e-b8a1-31b4d1d0c0c3 Duration: 45.67 ms Billed Duration: 46 ms Memory Size: 512 MB Max Memory Used: 123 MB Init Duration: 456.78 ms
[ERROR] 2024-05-21T17:45:02.123Z 8f8f6f40-7c4c-123e-a8a1-31b4d1d0c0c3 Something caught on fire!

The REPORT line is pure gold—it tells you the cost (Duration), the efficiency of your memory setting (Max Memory Used), and if you suffered a cold start (Init Duration). Cherish it. The problem is your log, "Something caught on fire!". How many things caught on fire today? At what time? Good luck manually sifting through a thousand log streams to find out. This is why we need structure.

Structured Logging is Non-Negotiable

Structured logging means outputting your logs as JSON objects instead of random strings. This allows CloudWatch Logs Insights, the query tool, to actually parse your logs and let you ask intelligent questions like “show me all errors from the PaymentFailed function in the last hour.”

Here’s how you might do it manually in Node.js. It’s… fine. I guess.

export const handler = async (event) => {
  const logObject = {
    level: 'INFO',
    message: 'Processed payment',
    requestId: context.awsRequestId,
    timestamp: new Date().toISOString(),
    data: {
      userId: event.userId,
      amount: event.amount
    }
  };

  console.log(JSON.stringify(logObject)); // Have to stringify it!
};

// Outputs: {"level":"INFO","message":"Processed payment","requestId":"8f8f6f40-7c4c-...","timestamp":"2024-05-21T17:45:02.123Z","data":{"userId":"usr-123","amount":49.99}}

This works, but it’s verbose and you’ll inevitably forget to stringify the object somewhere, which results in a useless log line that says [object Object]. Don’t be that person.

Enter Lambda Powertools

This is where you stop rolling your own garbage and use a library built by people who have already suffered for you. The AWS Lambda Powertools library (available for Python, TypeScript, Java, and .NET) is essentially cheat codes for production-ready Lambda functions. For logging, it handles structured JSON, allows you to inject key context (like the Lambda request ID) automatically, and provides sampling for debug logs so you’re not paying a fortune to log everything.

Here’s the TypeScript version, which is my personal favorite:

First, install it: npm install @aws-lambda-powertools/logger

import { Logger } from '@aws-lambda-powertools/logger';

// Create a logger instance, setting a default service name
const logger = new Logger({
  serviceName: 'payment-api',
  logLevel: 'INFO'
});

export const handler = async (event: any, context: any) => {
  // Add persistent context to this instance
  logger.appendKeys({
    awsRequestId: context.awsRequestId,
    userId: event.userId
  });

  logger.info('Processing payment', { amount: event.amount }); // This is automatically structured

  try {
    // ... your logic
    logger.debug('Very verbose debug output'); // Will only be logged if logLevel is set to DEBUG
  } catch (error) {
    logger.error('Payment failed miserably', { error }); // Logs the full error object
    throw error;
  }
};

This outputs a perfectly queryable JSON log event. The logger.debug call is brilliant because you can leave those lines in your code forever. In your production environment, set the POWERTOOLS_LOG_LEVEL environment variable to INFO, and those debug lines are silently dropped, saving you money and noise. Set it to DEBUG when you’re troubleshooting a specific function, and suddenly you get the full, verbose output.

Taming CloudWatch Logs Insights

Once your logs are structured JSON, CloudWatch Logs Insights becomes actually useful instead of a digital punishment. You can run queries like this:

filter @message like /PaymentFailed/
| fields awsRequestId, userId, error.message
| sort @timestamp desc
| limit 20

Or, even better, since Powertools injects your logging data directly into top-level fields, you can query more efficiently:

filter serviceName = "payment-api" and level = "ERROR"
| fields awsRequestId, userId, message
| sort @timestamp desc

This is the payoff. This is why we put in the effort. When you get paged at 3 a.m., you can find the specific error and relevant context in seconds, not minutes.

The bottom line is this: Using print statements in Lambda is like trying to fix a modern server with a stone-age tool. You can do it, but everyone who comes after you will know and resent you for it. Use structured logging from day one. Use Powertools to make it easy. Your future self, sipping a coffee instead of frantically querying logs, will thank you.