Logger

CloudWatch and local development friendly logger with optional structured logging, while still small and easy to use.

Overview

Sadly, console.log is the #1 debugging tool when writing serverless code. Logger extends it with levels, timestamps, context/category names, object formatting, and optional structured logging. It’s just a few incremental improvements, and yet together takes logging a leap forward.

If you are transpiling, be sure to enable source maps (in Typescript, Babel) and the enable use via the source-map-support library or Node.js enable-source-maps option so that you get meaningful stack traces.

Install

npm install @sailplane/logger

Examples

import {Logger, LogLevel} from "@sailplane/logger";
const logger = new Logger('name-of-module');

logger.info("Hello World!");
// INFO name-of-module: Hello World!

Logger.initialize({level: LogLevel.INFO});
logger.debug("DEBUG < INFO.");
// No output

Logger.initialize({logTimestamps: true});
logger.info("Useful local log");
// 2018-11-15T18:26:20 INFO name-of-module: Useful local log

logger.warn("Exception ", {message: "oops"});
// 2018-11-15T18:29:38 INFO name-of-module: Exception {message:"oops"}

Logger.initialize({format: "PRETTY"});
logger.error("Exception ", {message: "oops"});
// 2018-11-15T18:30:49 INFO name-of-module: Exception {
//   message: "oops"
// }

Logger.initialize({
  format: "STRUCT",
  attributes: { my_trace_id: request.id }
});
logger.error("Processing Failed", new Error("Unreachable");
// {
//   "aws_region": "us-east-1",
//   "function_name": "myDataProcessor",
//   "function_version": "42",
//   "invocation_num": 1,
//   "my_trace_id": "ebfb6f2f-8f2f-4e2e-a0a9-4495e90a4316",
//   "stage": "prod",
//   "level": "ERROR",
//   "module": "name-of-module",
//   "timestamp": "2022-03-03T17:32:19.830Z",
//   "message": "Processing Failed",
//   "value": {
//     "name": "Error",
//     "message": "Unreachable",
//     "stack": "Error: Unreachable\n  at /home/adam/my-project/src/service/processor.service.ts:83\n  at ..."
//     "source": "/home/adam/my-project/src/service/processor.service.ts:83"
//   }
// }

Configuration / Behavior

The output of Logger varies based on some global settings and whether the Lambda is executing in AWS or local (serverless-offline, SAM offline).

Default behavior should work for Lambdas. If you are using Logger in another container (EC2, Fargate, …) you likely will want to adjust these settings.

CloudWatch detection

The default behaviors of some configuration change depending on whether log output is going to CloudWatch vs local console. This is because within the AWS Lambda service, logging to stdout is automatically prefixed with the log level and timestamp. Local console does not So Logger adds these for you when a login shell (offline mode) is detected. You can force

Override from environment variable export LOG_TO_CLOUDWATCH=true or export LOG_TO_CLOUDWATCH=false

Note: The above is ignored when using structured logging.

Recommendations

The best logging format often depends on the environment/stage. It may be selected via the LOG_FORMAT environment variable.

For local development, the default format is PRETTY and this is usually the most readable in a terminal window.

For deployment to AWS in lower stage environments (ex: dev), the default is FLAT and is recommended. When viewed in CloudWatch AWS Console, it will handle pretty printing JSON output when a line is expanded.

For higher environments (ex: production), the default is still FLAT but STRUCT is suggested to allow for analysis of massive log content, when using Amazon CloudWatch Logs Insights or an ELK stack.

Structured Logging Attributes

When structured logging is used, the following properties are included:

  • aws_region - the AWS region name
  • aws_request_id - the AWS generated unique identifier of the request
  • xray_trace_id - AWS X-Ray trace ID (if available)
  • function_name - name of the logging AWS Lambda
  • function_memory_size - the capacity configuration of the Lambda, in memory megabytes
  • invocation_num - count of invocations of the Lambda handler since cold start (1 = first request since cold start)
  • level - logging level
  • module - source module of the Logger instance
  • timestamp - ISO8601 date-time in UTC when the line was logged
  • message - text message of the line (first parameter to log function)
  • value - if only two parameters are given to the log function, this is the second one
  • params - when more than two parameters are given, all after the message are in this array

Sailplane’s LambdaUtils adds additional properties.

Type Declarations

export declare enum LogLevel {
    NONE = 1,
    ERROR = 2,
    WARN = 3,
    INFO = 4,
    DEBUG = 5
}
export declare enum LogFormat {
    FLAT = 1,
    PRETTY = 2,
    STRUCT = 3
}
export declare type LoggerAttributes = Record<string, string | number>;
/**
 * Signature of a Formatter function.
 * @param loggerConfig configuration of Logger instance
 * @param globalConfig global configuration
 * @param level logging level
 * @param message text to log
 * @param params A list of JavaScript objects to output.
 * @return array to pass to a console function
 */
export declare type FormatterFn = (loggerConfig: LoggerConfig, globalConfig: LoggerConfig, level: LogLevel, message: string, params: any[]) => any[];
/**
 * Configuration of a Logger.
 * See individual properties for details.
 * The default behavior of some vary based on runtime environment.
 * Some properties may be initialized via environment variables.
 * Configuration for all loggers may be set via the Logger.initialize(config) function.
 * Overrides for individual Loggers may be given in the constructor.
 */
export interface LoggerConfig {
    /**
     * Source module of the logger - prepended to each line.
     * Source file name or class name are good choices, but can be any label.
     */
    module: string;
    /**
     * Enabled logging level.
     * May be initialized via LOG_LEVEL environment variable.
     */
    level: LogLevel;
    /** Any additional context attributes to include with _structured_ format (only). */
    attributes?: LoggerAttributes;
    /**
     * Include the level in log output?
     * Defaults to true if not streaming to CloudWatch;
     * always included with _structured_ format.
     */
    outputLevels: boolean;
    /**
     * Include timestamps in log output?
     * Defaults to false if streaming to CloudWatch (CloudWatch provides timestamps.),
     * true otherwise; always included with _structured_ format.
     * May override by setting the LOG_TIMESTAMPS environment variable to 'true' or 'false'.
     */
    logTimestamps: boolean;
    /**
     * Output format to use.
     * Defaults to FLAT if streaming to CloudWatch, PRETTY otherwise.
     * (Best to let CloudWatch provide its own pretty formatting.)
     * May initialize by setting the LOG_FORMAT environment variable to
     * "FLAT", "PRETTY", or "STRUCT".
     */
    format: LogFormat;
    /**
     * Function use to format output. Set based on format property, but may
     * be programmatically set instead.
     */
    formatter: FormatterFn;
}
import { LoggerAttributes, LoggerConfig, LogLevel } from "./common";
import { Context } from "aws-lambda";
/**
 * Custom logger class.
 *
 * Works much like console's logging, but includes levels, date/time,
 * and module (file) on each line, or structured formatting if configured to do so.
 *
 * Usage:
 *   import {Logger} from "@sailplane/logger";
 *   const logger = new Logger('name-of-module');
 *   logger.info("Hello World!");
 */
export declare class Logger {
    /**
     * Configure global defaults. Individual Logger instances may override.
     * @param globalConfig configuration properties to changed - undefined properties
     *        will retain existing value
     */
    static initialize(globalConfig: Partial<LoggerConfig>): void;
    /**
     * Set some context attributes to the existing collection of global attributes
     * Use initialize({attributes: {}} to override/reset all attributes.
     */
    static addAttributes(attributes: LoggerAttributes): void;
    /**
     * Set structured logging global attributes based on Lambda Context:
     *
     * - aws_request_id: identifier of the invocation request
     * - invocation_num: number of invocations of this process (1 = cold start)
     *
     * Call this every time the Lambda handler begins.
     */
    static setLambdaContext(context: Context): void;
    private readonly config;
    /**
     * Construct.
     * @param ops LoggerConfig, or just module name as string
     */
    constructor(ops: string | Partial<LoggerConfig>);
    /**
     * The Log Level of this Logger
     */
    get level(): LogLevel;
    /**
     * Change the log level of this Logger
     */
    set level(level: LogLevel);
    /**
     * Log an item at given level.
     * Usually better to use the specific function per log level instead.
     *
     * @param level log level
     * @param message text to log
     * @param params A list of JavaScript objects to output
     */
    log(level: LogLevel, message: string, params: any[]): void;
    /**
     * Log a line at DEBUG level.
     *
     * @param message text to log
     * @param optionalParams A list of JavaScript objects to output.
     */
    debug(message: string, ...optionalParams: any[]): void;
    /**
     * Log a line at INFO level.
     *
     * @param message text to log
     * @param optionalParams A list of JavaScript objects to output.
     */
    info(message: string, ...optionalParams: any[]): void;
    /**
     * Log a line at WARN level.
     *
     * @param message text to log
     * @param optionalParams A list of JavaScript objects to output.
     */
    warn(message: string, ...optionalParams: any[]): void;
    /**
     * Log a line at ERROR level.
     *
     * @param message text or Error instance
     * @param optionalParams A list of JavaScript objects to output.
     */
    error(message: string | Error, ...optionalParams: any[]): void;
    /**
     * Log a line at DEBUG level with a stringified object.
     *
     * @param message text to log
     * @param object a Javascript object to output
     * @deprecated #debug has the same result now
     */
    debugObject(message: string, object: any): void;
    /**
     * Log a line at INFO level with a stringified object.
     *
     * @param message text to log
     * @param object a Javascript object to output
     * @deprecated #info has the same result now
     */
    infoObject(message: string, object: any): void;
    /**
     * Log a line at WARN level with a stringified object.
     *
     * @param message text to log
     * @param object a Javascript object to output
     * @deprecated #warn has the same result now
     */
    warnObject(message: string, object: any): void;
    /**
     * Log a line at ERROR level with a stringified object.
     *
     * @param message text to log
     * @param object a Javascript object to output
     * @deprecated #error has the same result now
     */
    errorObject(message: string, object: any): void;
}