# 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.

```ts {{ title: "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`.

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

```ts
inngest.createFunction(
  { id: "my-awesome-function", triggers: { event: "func/awesome" } },
  async ({ event, step, logger }) => {
    logger.info({ eventId: event.data.id }, "Starting function");

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

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

> **Tip:** We recommend using a structured logger like Pino 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.

## Using your preferred logger

While the default `ConsoleLogger` may be good enough for local development, structured logging libraries provide more features that are suitable for production use. Pass a logger to the `logger` option on the Inngest client to make it available as `ctx.logger` in all functions.

```ts {{ title: "Pino" }}
import pino from "pino";
import { Inngest } from "inngest";

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

export const inngest = new Inngest({
  id: "my-awesome-app",
  logger: logger,
});
inngest.createFunction(
  { id: 'my-fn', },
  ({ event, step, logger }) => {
    logger.info({ hello: "world" }, "this uses my pino logger");
  }
);
```

```ts {{ title: "Winston" }}
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-awesome-app",
  logger: wrapStringFirstLogger(logger),
});
inngest.createFunction(
  { id: 'my-fn', },
  ({ event, step, logger }) => {
    logger.info("this uses my winston logger", { hello: "world" });
  }
);
```

### Object-first vs string-first loggers

The SDK expects **object-first** loggers (like [Pino][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][winston] use **string-first** conventions, where the message comes first. For these loggers, use `wrapStringFirstLogger` to adapt them:

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

const logger = wrapStringFirstLogger(winstonLogger);
```

See the [Logging reference](/docs-markdown/reference/typescript/v4/logging) for more details on logger configuration, the `ConsoleLogger`, and the `internalLogger` option.

## 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

```ts {{ title: "Example usage with Pino logger" }}
await step.run("summarize-content", async ({ step, logger }) => {
  logger.info({ max_tokens: 1000 }, "Calling Claude");
});
```

```json {{ title: "Example log output" }}
{"eventName":"inngest/function.invoked","functionName":"Summarize content via GPT-4",
"level":"info","max_tokens":1000,"message":"Calling Claude",
"runID":"01KB7YQXYNPEX3XB257A3RQDRX"}
```

## Loggers supported

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

- [Pino][pino] child logger support
- [Winston][winston] child logger support (requires `wrapStringFirstLogger`)
- [Bunyan](https://github.com/trentm/node-bunyan) child logger support
- [Roarr](https://github.com/gajus/roarr) child logger support
- [LogLevel](https://github.com/pimterry/loglevel)
- [Log4js](https://github.com/log4js-node/log4js-node)
- [npmlog](https://github.com/npm/npmlog) (doesn't have `.debug()` but has a way to add custom levels)
- [Tracer](https://github.com/baryon/tracer)
- [Signale](https://github.com/klaudiosinani/signale)

## Customizing the logger

The built-in logger is implemented using [middleware](/docs-markdown/features/middleware). You can create your own middleware to customize the logger to your needs. See the [logging middleware example](/docs-markdown/reference/middleware/examples#logging) for more details.

## Further reading

- [Logging reference](/docs-markdown/reference/typescript/v4/logging) - Full details on logger configuration, `ConsoleLogger`, `internalLogger`, and `wrapStringFirstLogger`.
- [Traces](/docs-markdown/platform/monitor/traces?ref=guides-logging) - View detailed execution traces for your functions in the Inngest dashboard, including step-by-step breakdowns and timing information.

[pino]: https://github.com/pinojs/pino

[winston]: https://github.com/winstonjs/winston