Skip to content

Logging

The Inngest SDK uses Pino-style object-first logging, where structured data is passed before the message string:

ts
logger.info({ userId: "abc123" }, "Processing user event");

A logger is available on the function context as ctx.logger. It provides .info(), .warn(), .error(), and .debug() methods.

ts
export default inngest.createFunction(
  {
    id: "process-upload",
    triggers: { event: "app/file.uploaded" },
  },
  async ({ event, step, logger }) => {
    logger.info({ fileId: event.data.fileId }, "Starting upload processing");

    const result = await step.run("process", () => {
      logger.debug({ fileId: event.data.fileId }, "Processing file");
      return processFile(event.data.fileId);
    });

    logger.info({ fileId: event.data.fileId, result }, "Upload processed");
    return result;
  }
);

Logger interface

Any object that implements these four methods can be used as a logger:

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

Setting a logger

Pass a logger to the logger option on the Inngest client. This logger will be available on ctx.logger in all functions.

ts
import pino from "pino";
import { Inngest } from "inngest";

const logger = pino({ level: "debug" });

export const inngest = new Inngest({
  id: "my-app",
  logger: logger,
});
ts
import winston from "winston";
import { Inngest, wrapStringFirstLogger } from "inngest";

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

// Winston uses string-first logging, so wrap it for compatibility
export const inngest = new Inngest({
  id: "my-app",
  logger: wrapStringFirstLogger(logger),
});

Object-first vs string-first loggers

The SDK expects object-first loggers (like Pino), where structured data comes before the message:

ts
// Object-first (Pino style) - works out of the box
logger.info({ userId: "abc" }, "User created");

Some loggers like Winston use string-first conventions, where the message comes first:

ts
// String-first (Winston style) - needs wrapping
logger.info("User created", { userId: "abc" });

For string-first loggers, use wrapStringFirstLogger to adapt them:

ts
import { wrapStringFirstLogger } from "inngest";
import winston from "winston";

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

const logger = wrapStringFirstLogger(winstonLogger);

Default logger

If no logger is set, the SDK uses a built-in ConsoleLogger that defaults to "info" level. You can customize the level:

ts
import { ConsoleLogger, Inngest } from "inngest";

export const inngest = new Inngest({
  id: "my-app",
  logger: new ConsoleLogger({ level: "debug" }),
});

ConsoleLogger is intended for local development. For production, use a structured logger like Pino.

Internal logger

The SDK produces two categories of logs:

  • Function logs - your logs via ctx.logger inside Inngest functions
  • SDK internal logs - registration, request handling, middleware errors, etc.

By default, both use the same logger. Set internalLogger to route SDK internal logs separately:

ts
import pino from "pino";
import { Inngest } from "inngest";

const logger = pino();

export const inngest = new Inngest({
  id: "my-app",
  logger: logger,
  internalLogger: logger.child({ component: "inngest-sdk" }),
});

This is useful for filtering or routing SDK internals to a different destination without affecting your function logs.

ts
import pino from "pino";
import { Inngest } from "inngest";

const appLogger = pino({ level: "info" });
const sdkLogger = pino({ level: "warn" });

export const inngest = new Inngest({
  id: "my-app",
  logger: appLogger,
  internalLogger: sdkLogger,
});
ts
import pino from "pino";
import { ConsoleLogger, Inngest } from "inngest";

const appLogger = pino({ level: "info" });

export const inngest = new Inngest({
  id: "my-app",
  logger: appLogger,
  // Silence all SDK internal logs
  internalLogger: new ConsoleLogger({ level: "silent" }),
});
ts
import winston from "winston";
import { Inngest, wrapStringFirstLogger } from "inngest";

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

const sdkLogger = winston.createLogger({
  level: "error",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

export const inngest = new Inngest({
  id: "my-app",
  logger: wrapStringFirstLogger(appLogger),
  internalLogger: wrapStringFirstLogger(sdkLogger),
});

If internalLogger is not set, it falls back to logger.