Skip to content

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.

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.

src/bot.ts
export const seedcord = new Seedcord({
    bot: paths,
    subscribers: { path: null },
    logger: {
        level: 'info',
        channels: {
            interactions: { level: 'trace' },
            gates: { level: 'warn' },
            '
  • bot
  • cli
  • commands
  • default
  • errors
  • events
  • gates
  • health
  • hmr
  • interactions
  • lifecycle
  • plugins
  • subscribers
  • tsc
} } });

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.

sinkwhat it writesruntime
ObjectConsoleSinkone structured object per line, through consoleanywhere
WinstonConsoleSinkreadable columns, or JSONnode
WinstonFileSinkone combined file per runnode

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.

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.

optiondefaultwhat it sets
filenamelogs/combined-{timestamp}.logthe file to write
format'pretty'the same two values as the console sink
maxSizenonethe size winston rotates at
maxFilesnonehow many rotated files winston keeps

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.

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 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 puts a sink alongside whatever config.logger already set, and setting config.logger again leaves it in place.

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.