Logging in Inngest

Log handling can have some caveats when working with serverless runtimes.

One of the main problems is due to how serverless providers terminate after a function exits. There might not be enough time for a logger to finish flushing, which results in logs being lost.

Another (opposite) problem is due to how Inngest handles memoization and code execution via HTTP calls to the SDK. A log statement outside of step function could end up running multiple times, resulting in duplicated deliveries.

example-fn.ts

async ({ event, step }) => {
  logger.info("something") // this can be run three times

  await step.run("fn", () => {
    logger.info("something else") // this will always be run once
  })

  await step.run(...)
}

We provide a thin wrapper over existing logging tools, and export it to Inngest functions in order to mitigate these problems, so you, as the user, don't need to deal with them and things should work as you expect.

Usage

A logger object is available within all Inngest functions. You can use it with the logger of your choice, or if absent, logger will default to use console.

inngest.createFunction(
  { id: "my-awesome-function" },
  { event: "func/awesome" },
  async ({ event, step, logger }) => {
    logger.info("starting function", { metadataKey: "metadataValue" });

    const val = await step.run("do-something", () => {
      if (somethingBadHappens) logger.warn("something bad happened");
    });

    return { success: true, event };
  }
);

The exported logger provides the following interface methods:

export interface Logger {
  info(...args: any[]): void;
  warn(...args: any[]): void;
  error(...args: any[]): void;
  debug(...args: any[]): void;
}

These are very typical interfaces and are also on the RFC5424 guidelines, so most loggers you choose should work without issues.

Using your preferred logger

Running console.log is good for local development, but you probably want something more when running workloads in Production.

The following is an example using winston as the logger to be passed into Inngest functions.

inngest/client.ts

import { Inngest } from "inngest";
import winston from "winston";

/// Assuming we're deploying to Vercel.
/// Other providers likely have their own pre-defined environment variables you can use.
const env = process.env.VERCEL_ENV || "development";
const ddTransportOps = {
  host: "http-intake.logs.datadoghq.com",
  path: `/api/v2/logs?dd-api-key=${process.env.DD_API_KEY}&ddsource=nextjs&service=inngest&ddtags=env:${env}`,
  ssl: true,
};

const logger = winston.createLogger({
  level: "info",
  exitOnError: false,
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.Http(ddTransportOps),
  ],
});

// Pass `logger` to the Inngest client, and this winston logger will be accessible within functions
export const inngest = new Inngest({
  id: "my-awesome-app",
  logger: logger,
  // ...
});

How it works

There is a built-in logging middleware that provides a good default to work with.

child logger

If the logger library supports a child logger .child() implementation, the built-in middleware will utilize it to add function runtime metadata for you:

  • function name
  • event name
  • run ID

Loggers supported

The following is a list of loggers we're aware of that work, but is not an exhaustive list: