diff --git a/.env.example b/.env.example index 38edbc1e..86baffc7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # SETUP DISCORD_TOKEN= +LOGLEVEL= # DB REDIS_URL= diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 4dec9bf7..8243441b 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -21,6 +21,10 @@ settings: import/resolver: rules: + no-restricted-syntax: + - error + - selector: CallExpression[callee.object.name='console'][callee.property.name!=/^(log|warn|error|info|trace)$/] + message: Use logger instead of console import/exports-last: error import/first: error import/no-duplicates: error diff --git a/src/core/createModule.ts b/src/core/createModule.ts index 048bab2d..4882887a 100644 --- a/src/core/createModule.ts +++ b/src/core/createModule.ts @@ -1,5 +1,6 @@ import { constantCase } from 'change-case'; import type { ClientEvents, ClientOptions } from 'discord.js'; +import type { Logger } from 'pino'; import type { ZodTypeAny } from 'zod'; import { z } from 'zod'; @@ -11,6 +12,7 @@ type InferredZodShape> = { interface Context> { env: InferredZodShape; + logger: Logger; } type ModuleFunction, ReturnType> = ( @@ -31,6 +33,7 @@ type BotModule> = { interface CreatedModuleInput { env: unknown; + logger: Logger; } type ModuleFactory = (input: CreatedModuleInput) => Promise; diff --git a/src/core/loadModules.ts b/src/core/loadModules.ts index 309680b9..39a6fd87 100644 --- a/src/core/loadModules.ts +++ b/src/core/loadModules.ts @@ -4,6 +4,7 @@ import { checkUniqueSlashCommandNames } from './checkUniqueSlashCommandNames'; import type { CreatedModule } from './createModule'; import { env } from './env'; import { pushCommands, routeCommands } from './loaderCommands'; +import { coreLogger } from './logger'; import { routeHandlers } from './routeHandlers'; export const loadModules = async ( @@ -15,6 +16,7 @@ export const loadModules = async ( const botCommands = modules.flatMap((module) => module.slashCommands ?? []); checkUniqueSlashCommandNames(botCommands); + coreLogger.info('Routing slashcommands to interactionCreate event.'); routeCommands(client, botCommands); const clientId = client.application?.id; diff --git a/src/core/loaderCommands.ts b/src/core/loaderCommands.ts index 56edd4d3..9eec622f 100644 --- a/src/core/loaderCommands.ts +++ b/src/core/loaderCommands.ts @@ -8,6 +8,7 @@ import { import type { BotCommand } from '../types/bot'; import { deleteExistingCommands } from './deleteExistingCommands'; +import { coreLogger } from './logger'; interface PushCommandsOptions { commands: RESTPostAPIChatInputApplicationCommandsJSONBody[]; @@ -62,6 +63,7 @@ export const pushCommands = async ({ await rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands, }); + coreLogger.info('All commands are pushed.'); }; export const routeCommands = (client: Client, botCommands: BotCommand[]) => diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 00000000..bf343f0d --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,18 @@ +import createLogger, { type LoggerOptions } from 'pino'; + +const developmentOptionsOverride: LoggerOptions = { + transport: { + target: './pinoTransportModule', + }, + level: process.env['LOGLEVEL'] ?? 'debug', +}; + +const defaultLogger = createLogger({ + level: process.env['LOGLEVEL'] ?? 'info', + ...(process.env['NODE_ENV'] === 'production' ? {} : developmentOptionsOverride), +}); + +export const createLoggerForModule = (moduleName: string) => + defaultLogger.child({ module: moduleName }); + +export const coreLogger = createLoggerForModule('core'); diff --git a/src/core/pinoTransportModule.ts b/src/core/pinoTransportModule.ts new file mode 100644 index 00000000..6e86809e --- /dev/null +++ b/src/core/pinoTransportModule.ts @@ -0,0 +1,11 @@ +import { red, white } from 'colorette'; +import pretty from 'pino-pretty'; + +export default (opts: Parameters) => + pretty({ + ...opts, + colorize: true, + messageFormat: white(`[${red('{module}')}] {msg}`), + hideObject: true, + ignore: 'pid,hostname', + }); diff --git a/src/core/routeHandlers.ts b/src/core/routeHandlers.ts index 511b24c2..00c8b99d 100644 --- a/src/core/routeHandlers.ts +++ b/src/core/routeHandlers.ts @@ -2,6 +2,7 @@ import type { Client, ClientEvents } from 'discord.js'; import type { EventHandler } from '../types/bot'; import type { CreatedModule } from './createModule'; +import { coreLogger } from './logger'; const handleEvent = async ( eventHandlers: EventHandler[], diff --git a/src/main.ts b/src/main.ts index 38851607..837a1c88 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,4 +41,4 @@ if (!client.isReady()) { await loadModules(client, createdModules); -console.log('Bot started.'); +coreLogger.info('Bot fully started.'); diff --git a/src/modules/recurringMessage/recurringMessage.helpers.ts b/src/modules/recurringMessage/recurringMessage.helpers.ts index e5ced91e..978b92da 100644 --- a/src/modules/recurringMessage/recurringMessage.helpers.ts +++ b/src/modules/recurringMessage/recurringMessage.helpers.ts @@ -51,8 +51,7 @@ export const createRecurringMessage = ( () => { const channel = client.channels.cache.get(channelId); if (!channel || !channel.isTextBased()) { - console.error(`Channel ${channelId} not found`); - return; + throw new Error(`Channel ${channelId} not found`); } void channel.send(message); }, diff --git a/src/modules/voiceOnDemand/voiceOnDemand.module.ts b/src/modules/voiceOnDemand/voiceOnDemand.module.ts index e661f3c8..3e588f11 100644 --- a/src/modules/voiceOnDemand/voiceOnDemand.module.ts +++ b/src/modules/voiceOnDemand/voiceOnDemand.module.ts @@ -52,6 +52,7 @@ export const voiceOnDemand = createModule({ //NOTES: this is a potential race condition. await cache.set('lobbyIds', [...lobbyIds, id]); + logger.info(`Created voice on demand voice channel ${id}`); await interaction.reply({ content: 'Created voice on demand voice channel.', ephemeral: true, @@ -60,7 +61,7 @@ export const voiceOnDemand = createModule({ }, }, ], - eventHandlers: () => ({ + eventHandlers: ({ logger }) => ({ voiceStateUpdate: async (oldState, newState) => { const lobbyIds = await cache.get('lobbyIds', []); const onDemandChannels = await cache.get('onDemandChannels', []); @@ -73,10 +74,18 @@ export const voiceOnDemand = createModule({ } if (isOnDemandChannel && isLeaveState(oldState)) { + logger.debug( + { guild: newState.guild.id }, + `User ${oldState.member.displayName} left the on-demand channel`, + ); await handleLeaveOnDemand(oldState); } if (isLobbyChannel && isJoinState(newState)) { + logger.debug( + { guild: newState.guild.id }, + `User ${newState.member.displayName} joined the lobby`, + ); await handleJoinLobby(newState); } }, @@ -92,6 +101,11 @@ export const voiceOnDemand = createModule({ return; } + logger.info( + { guild: channel.guild.id }, + `Voice on demand voice channel ${channel.id} was deleted`, + ); + await cache.set( 'lobbyIds', lobbyIds.filter((lobbyId) => lobbyId !== channel.id), diff --git a/tsup.config.ts b/tsup.config.ts index d6ad64d4..770cf9d3 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,12 @@ +//eslint-disable-next-line import/no-extraneous-dependencies import { defineConfig } from 'tsup'; export default defineConfig({ clean: true, + entry: { + main: 'src/main.ts', + pinoTransportModule: 'src/core/pinoTransportModule.ts', + }, format: ['esm'], keepNames: true, minify: true,