Skip to content

Logging

Log from your handlers, subscribers, and plugins through the logger each of them carries. Covers the five levels, the label on every line, extra arguments, format specifiers, channels, and logging several lines at once.

A console.log in a handler prints a bare string. You can't tell which handler wrote it, you can't hide it in production without deleting it, and the dev terminal can't filter it. Every handler, subscriber, and plugin carries a logger that fixes all three, and you call it with one of five methods.

src/handlers/Ping.ts
import { SlashHandler, SlashRoute } from '@seedcord/gateway';

@SlashRoute('ping')
export class Ping extends SlashHandler<'ping'> {
    public async execute(): Promise<void> {
        this.logger.info('checking latency');
        await this.reply('Pong');
    }
}

Ping logs checking latency at the info level before it replies.

The five levels

A level marks how much a line matters, so you can keep detailed logging in your code and print only part of it in production.

methodwhat it marks
errorsomething broke and you want to know about it
warnsomething looks wrong, though your bot kept running
infothe ordinary record of what your bot did
debugdetail you want while you debug something
tracethe noisiest detail, usually one step inside a loop

The level you configure is a floor. seedcord prints that level and every more serious one. Your environment sets the default: trace in development, so every line prints, debug in staging, and info everywhere else, which hides debug and trace. Configuring the logger shows how to change it.

Every line carries your class name

When two handlers log the same message, you tell them apart by the label. A logger takes its label from the class holding it, so a line from Ping shows Ping as its label.

More than a string

A message alone often omits the one value you needed, like which user ran the command. Every method takes extra arguments after the message.

this.logger.debug(
    '%s ran %s',
    this.event.user.username,
    this.event.commandName
);

this.logger.info('replying', {
    userId: this.event.user.id
});

try {
    await this.reply('Pong');
} catch (error) {
    this.logger.error('reply failed', error);
}

Those three calls print this in the dev terminal.

materwelon ran ping
replying
{
  "userId": "1176558419439161344"
}
reply failed
TypeError: Cannot read properties of undefined
    at Ping.execute (src/handlers/Ping.ts:14:19)

An object prints as a block under the message, and the caught error prints its stack there. A string or a number prints on the same line, after the message. A JSON sink writes one record per call, a single object. Every one of those arguments arrives in an extras array on it, and the first Error fills its stack as well.

Format specifiers

The message itself takes format specifiers, like the two %s in the debug call. Each one consumes the next argument.

specifierthe argumentwhat prints
%s'materwelon'materwelon
%d and %i3.73
%f3.73.7
%j{ id: 4 }{"id":4}
%o and %O{ id: 4 }{ id: 4 }
%%none%

%o and %O print an object the way console.log does. ObjectConsoleSink, which an edge bot runs, prints {"id":4} for %o and %O too. Any argument the specifiers don't consume is treated like the extra arguments above.

Channels

A bot writes lines about commands, events, plugins, and startup all at once, and you usually care about one of those at a time. Every line also carries a channel, which is a label for the part of your bot the line came from. seedcord picks the channel for its own classes. For example, an interaction handler logs on interactions, an event handler on events, and a subscriber on subscribers.

These names belong to the framework, and FRAMEWORK_CHANNELS holds every one of them.

channelwhat arrives there
defaulta logger built without a channel
botthe client login on gateway, the interactions server on http, plus emoji and command injection
lifecycleeach startup and shutdown task, and the signal that stopped your bot
healththe health check server binding and its errors
interactionswhich handler ran, unmatched routes, and how long dispatch and the reply took
eventswhich listeners attached, and which handler ran for an event. Gateway only
commandsthe commands seedcord loaded, and the deploy summary
subscribersbus activity, unreachable webhooks, and errors thrown inside a subscriber
errorsuncaught process errors and fault reports. An http bot reports faults on interactions
gatesa warning when your gate checks use too much of Discord's 3s window
pluginshow long each plugin took to start
hmrwhat reloaded while seedcord dev runs
clistatus and errors from the CLI, codegen and the tunnel included
tscoutput from tsc --watch, once you turn hmr.typecheck on

Your own bot adds channels to that set. A plugin logs on a channel named after the attach key you chose, so attaching one as db sends its lines to the db channel. If your attach key matches one of the names above, attach throws CorePluginReservedChannel.

The dev terminal draws every channel as a filter chip, so the more you split your bot into channels, the more precisely you can filter it.

Sending one line somewhere else

A handler that charges a user belongs on interactions, but you'd want its payment lines next to the rest of your payment logs. inChannel returns a second logger with the same label, on the channel you pass.

this.logger.inChannel('payments').info('charge accepted');

That line shows Ping as its label and arrives on payments. Name your own channels after the parts of your bot you'll want to filter. setChannel moves the logger you already have to a new channel for every later line.

Several lines at once

Logging a list one call at a time gives the dev terminal several records, and output from elsewhere in your bot can appear between them. block on logger.utils writes the whole group as a single record.

this.logger.utils.block(
    'Cache warmed',
    ['guilds 41', 'members 1204'],
    'info'
);

block takes a heading (Cache warmed here), its lines, and a level. The level defaults to trace, which only prints in development, so the sample passes 'info'. The other helpers on logger.utils default to info, and list writes one record per line, so it gets the interleaving block avoids. LoggerUtilities lists all of them.