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 }) => {
  console.log("something") // this can be run three times

  await step.run("fn", () => {
    console.log("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 as a handler argument. 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 is available as an argument
    logger.info("starting function", { metadataKey: "metadataValue" });

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

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

We recommend using a logger library that supports a child logger .child() implementation which automatically adds function runtime metadata to your logs. Read more about enriched logs with function metadata for more details.

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

While console.log may be good enough for local development, other logging libraries provide more features that are suitable for production use.

The following examples use winston for the logging library, including a basic example and an example with a Datadog transport.

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

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

// 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,
  // ...
});

Enriched logs with function metadata

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

  • Function name
  • Event name
  • Run ID

Example usage with Winston logger

await step.run("summarize-content", async ({ step, logger }) => {
  logger.info("calling Claude", { max_tokens: 1000 });
});

Example log output

{"eventName":"inngest/function.invoked","functionName":"Summarize content via GPT-4",
"level":"info","max_tokens":1000,"message":"Querying vector database",
"runID":"01KB7YQXYNPEX3XB257A3RQDRX"}

Loggers supported

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

Customizing the logger

The built-in logger is implemented using middleware. You can create your own middleware to customize the logger to your needs. See the logging middleware example for more details.