# Configuring the logger

Set where your bot's logs go and how much of them prints, through the logger block on your bot config. Covers the level floor, a floor per channel, replacing the sinks, writing your own sink, and installing one alongside the rest.

By default a node bot prints every level in development, drops `trace` in staging, and prints `info` and above everywhere else. It writes readable columns plus a log file in development, and JSON lines in every other environment. Your bot config takes a `logger` block for when that doesn't fit, like a noisy channel you want quieter or logs you need sent to a service. Its three keys are `level`, `channels`, and `sinks`, all optional.

## The level floor

A bot that prints every `trace` line hides the one warning you're looking for. `level` sets the floor for your whole bot, the lowest level that still prints, and takes `error`, `warn`, `info`, `debug`, or `trace`. Without it, seedcord reads your environment: `trace` in development, `debug` in staging, `info` everywhere else.

```ts title="src/bot.ts"
import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        ...paths
    },
    subscribers: { path: null },
    logger: {
        level: 'debug'
    }
});
```

`level: 'debug'` applies in every environment, production included. Leave `level` out when you want the per-environment default.

### One channel at a time

Setting the whole bot to `debug` to chase one bug in your handlers also prints every other channel's `debug` lines. `channels` sets a separate floor for a single channel, so `interactions` can print at `trace` while the rest of your bot stays at `info`.

```ts title="src/bot.ts"
export const seedcord = new Seedcord({
    bot: paths,
    subscribers: { path: null },
    logger: {
        level: 'info',
        channels: {
            interactions: { level: 'trace' },
            gates: { level: 'warn' },
            '
        }
    }
});
```

`interactions` ignores the top-level `info` because it has its own `level`, and `gates` only prints warnings and errors. Every channel you leave out keeps the top-level floor.

Your editor lists every framework channel as you type the next key. The key takes your own names too, so a plugin you attached as `db` gets a `db` entry here for the channel it logs on.

## Where a record goes

One logger call builds one *record*. A sink is where that record goes after it passes the floor, and every sink you name receives all of them.

These sinks ship with seedcord.

{/* prettier-ignore-start */}

| sink                                                                                          | what it writes                                    | runtime  |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------- | -------- |
| [`ObjectConsoleSink`](https://docs.seedcord.org/packages/logger/latest/object-console-sink)   | one structured object per line, through `console` | anywhere |
| [`WinstonConsoleSink`](https://docs.seedcord.org/packages/logger/latest/winston-console-sink) | readable columns, or JSON                         | node     |
| [`WinstonFileSink`](https://docs.seedcord.org/packages/logger/latest/winston-file-sink)       | one combined file per run                         | node     |

{/* prettier-ignore-end */}

A node bot starts with the winston console sink, which writes readable columns in development and JSON everywhere else, plus the file sink in development. An edge bot starts with `ObjectConsoleSink`, since both winston sinks require node. That sink puts an object you pass after the message into the record's own fields, so a Workers drain indexes `userId` on its own. A sink you write yourself runs on either runtime.

### Replacing the sinks

You'll replace the sinks when you want a different format or file layout, like JSON in development or rotated files. Passing `sinks` replaces the defaults completely, so list every destination you want back.

```ts title="src/bot.ts"
import {
    Seedcord,
    WinstonConsoleSink,
    WinstonFileSink
} from '@seedcord/gateway';

export const seedcord = new Seedcord({
    bot: paths,
    subscribers: { path: null },
    logger: {
        sinks: [
            new WinstonConsoleSink({ format: 'json' }),
            new WinstonFileSink({
                filename: 'logs/bot-{date}.log',
                maxSize: 5_000_000,
                maxFiles: 5
            })
        ]
    }
});
```

> **Gateway and http differ**
>
> `@seedcord/gateway` and `@seedcord/http` both export `WinstonConsoleSink` and `WinstonFileSink`. `@seedcord/http/edge` doesn't, since both need node, so an edge bot configures `ObjectConsoleSink` or a sink you write.

`WinstonConsoleSink` takes one option, `format`, which defaults to `'pretty'` and prints the readable columns you see in development. `'json'` writes one object per line.

`WinstonFileSink` takes these, all optional.

{/* prettier-ignore-start */}

| option     | default                         | what it sets                            |
| ---------- | ------------------------------- | --------------------------------------- |
| `filename` | `logs/combined-{timestamp}.log` | the file to write                       |
| `format`   | `'pretty'`                      | the same two values as the console sink |
| `maxSize`  | none                            | the size winston rotates at             |
| `maxFiles` | none                            | how many rotated files winston keeps    |

{/* prettier-ignore-end */}

seedcord replaces `{date}` with `2026-01-31` and `{timestamp}` with `2026-01-31-143005-812` when you construct the sink, so two runs in the same second still get their own file. Without `maxSize` or `maxFiles`, the sink writes a single file that keeps growing.

A channel can carry its own `sinks` as well. That list replaces the top-level one for that channel.

### Writing your own sink

A hosting platform often reads only the console, and a log service takes records over http. A sink is a class with a `kind` and an `onLog`, so you can send records anywhere.

```ts title="src/logging/HttpSink.ts"
import type { ILogSink, LogRecord } from '@seedcord/gateway';

export class HttpSink implements ILogSink {
    public readonly kind = 'capture';

    public async onLog(record: LogRecord): Promise<void> {
        await fetch('https://logs.example.com/ingest', {
            method: 'POST',
            body: JSON.stringify(record)
        });
    }
}
```

A [`LogRecord`](https://docs.seedcord.org/packages/types/latest/log-record) carries the `level`, the `message`, the `label`, the `channel`, and a `timestamp` in milliseconds. It also carries `args`, the values you passed after the message. A message on its own leaves `args` absent.

Four things shape how seedcord treats your sink.

* **`onLog` can return a promise.** seedcord calls it and continues, so a slow sink does not delay the handler that logged.
* **A throw turns the sink off.** seedcord catches it, prints one console line with the error, and stops sending that sink records for the rest of the process. A rejected promise counts the same way. Catching inside `onLog` keeps that from happening, so write your retry or backoff there when a failed request shouldn't cost you every later line.
* **`kind` decides which sinks the dev terminal silences.** The dev terminal silences every `'console'` sink from `config.logger` while it draws its own lines. `'file'` and `'capture'` keep writing.
* **`dispose` is optional.** seedcord calls it when a later `logger` config drops the sink. The winston file handle closes this way.

### Adding a sink alongside the others

A sink in `sinks` replaces the defaults. A plugin or tool that only reads your logs shouldn't change which sinks your bot runs. [`installSink`](https://docs.seedcord.org/packages/logger/latest/logger-channel-registry#install-sink) puts a sink alongside whatever `config.logger` already set, and setting `config.logger` again leaves it in place.

```ts title="src/bot.ts"
import { LoggerChannelRegistry } from '@seedcord/gateway';

const handle = LoggerChannelRegistry.instance.installSink(
    new HttpSink()
);
```

A sink added this way still only gets records that clear the floor.

The handle you get back has a `dispose` method. Declaring the handle with `using` disposes it at the end of the scope. `installSink` also takes a second argument, `{ muteConsole: true }`, which silences every console sink from `config.logger` until you dispose the handle. Pass it when you want your own sink to be the only thing printing to the console. The dev terminal does that while it runs.
