diff --git a/.env.example b/.env.example index a3f55f8..cf99056 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ WHISPER_CLI_PATH=whisper-cli # optional: override whisper-cli executable pa # mkdir -p ./models && curl -L -o models/ggml-base.en.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin WHISPER_MODEL_PATH=models/ggml-base.en.bin # optional: whisper.cpp model path WHISPER_LANGUAGE=auto # optional: whisper language code, or 'auto' for detection +LOG_LEVEL=info # optional: minimum log level: debug | info | warn | error (default: info) # --- Discord provider (loaded only if 'discord' is in ENABLED_PROVIDERS) --- DISCORD_BOT_TOKEN=your_bot_token_here diff --git a/.gitignore b/.gitignore index 9a2a9e1..b46f021 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ dist/ .env *.db +logs/ Auto Run Docs/ models diff --git a/src/__tests__/logger.test.ts b/src/__tests__/logger.test.ts new file mode 100644 index 0000000..d395716 --- /dev/null +++ b/src/__tests__/logger.test.ts @@ -0,0 +1,126 @@ +import test, { afterEach } from 'node:test'; +import assert from 'node:assert/strict'; + +const mod: { logger?: typeof import('../core/logger').logger } = {}; + +test('logger is a singleton with the expected surface', async () => { + mod.logger = (await import('../core/logger')).logger; + assert.equal(typeof mod.logger!.debug, 'function'); + assert.equal(typeof mod.logger!.info, 'function'); + assert.equal(typeof mod.logger!.warn, 'function'); + assert.equal(typeof mod.logger!.error, 'function'); + assert.equal(typeof mod.logger!.setLevel, 'function'); + assert.equal(typeof mod.logger!.isEnabled, 'function'); + assert.equal(typeof mod.logger!.getLevel, 'function'); +}); + +afterEach(() => { + mod.logger?.setLevel('info'); +}); + +test('default level is info', async () => { + const { logger } = await import('../core/logger'); + assert.equal(logger.getLevel(), 'info'); + assert.equal(logger.isEnabled('debug'), false); + assert.equal(logger.isEnabled('info'), true); + assert.equal(logger.isEnabled('warn'), true); + assert.equal(logger.isEnabled('error'), true); +}); + +test('setLevel changes the gate', async () => { + const { logger } = await import('../core/logger'); + logger.setLevel('warn'); + assert.equal(logger.isEnabled('debug'), false); + assert.equal(logger.isEnabled('info'), false); + assert.equal(logger.isEnabled('warn'), true); + assert.equal(logger.isEnabled('error'), true); + logger.setLevel('debug'); + assert.equal(logger.isEnabled('debug'), true); + assert.equal(logger.isEnabled('info'), true); + logger.setLevel('error'); + assert.equal(logger.isEnabled('debug'), false); + assert.equal(logger.isEnabled('info'), false); + assert.equal(logger.isEnabled('warn'), false); + assert.equal(logger.isEnabled('error'), true); +}); + +test('unknown levels fall back to info', async () => { + const { logger } = await import('../core/logger'); + logger.setLevel('bogus'); + assert.equal(logger.getLevel(), 'info'); +}); + +test('level-gated methods do not emit when disabled', async () => { + const { logger } = await import('../core/logger'); + const origDebug = console.debug; + const origInfo = console.info; + const origWarn = console.warn; + const debugCalls: string[] = []; + const infoCalls: string[] = []; + const warnCalls: string[] = []; + console.debug = (line: string) => debugCalls.push(line); + console.info = (line: string) => infoCalls.push(line); + console.warn = (line: string) => warnCalls.push(line); + try { + logger.setLevel('warn'); + logger.debug('test/ctx', 'debug-detail'); + logger.info('test/ctx', 'info-detail'); + logger.warn('test/ctx', 'warn-detail'); + assert.equal(debugCalls.length, 0, 'debug should be suppressed at warn level'); + assert.equal(infoCalls.length, 0, 'info should be suppressed at warn level'); + assert.equal(warnCalls.length, 1, 'warn should pass at warn level'); + assert.match(warnCalls[0], /\[WARN\] \[test\/ctx\] warn-detail/); + } finally { + console.debug = origDebug; + console.info = origInfo; + console.warn = origWarn; + } +}); + +test('debug emits when level is debug', async () => { + const { logger } = await import('../core/logger'); + const orig = console.debug; + const calls: string[] = []; + console.debug = (line: string) => calls.push(line); + try { + logger.setLevel('debug'); + logger.debug('test/ctx', 'detail'); + assert.equal(calls.length, 1); + assert.match(calls[0], /\[DEBUG\] \[test\/ctx\] detail/); + } finally { + console.debug = orig; + } +}); + +test('error always emits at any level that includes error', async () => { + const { logger } = await import('../core/logger'); + const orig = console.error; + const calls: string[] = []; + console.error = (line: string) => calls.push(line); + try { + for (const level of ['debug', 'info', 'warn', 'error'] as const) { + logger.setLevel(level); + calls.length = 0; + await logger.error('test/ctx', 'err-detail'); + assert.equal(calls.length, 1, `error should fire at level=${level}`); + assert.match(calls[0], /\[ERROR\] \[test\/ctx\] err-detail/); + } + } finally { + console.error = orig; + } +}); + +test('log lines are sanitized to keep them single-line', async () => { + const { logger } = await import('../core/logger'); + const orig = console.error; + const calls: string[] = []; + console.error = (line: string) => calls.push(line); + try { + await logger.error('test/ctx', 'line one\nline two\rline three'); + assert.equal(calls.length, 1); + assert.ok(!calls[0].includes('\n'), 'log line should not contain raw newlines'); + assert.ok(!calls[0].includes('\r'), 'log line should not contain raw carriage returns'); + } finally { + console.error = orig; + } +}); diff --git a/src/__tests__/mockProvider.test.ts b/src/__tests__/mockProvider.test.ts index 3a2341d..72bc551 100644 --- a/src/__tests__/mockProvider.test.ts +++ b/src/__tests__/mockProvider.test.ts @@ -75,10 +75,13 @@ test('a minimal MockProvider satisfies BridgeProvider and works with the kernel }, getProvider: (name) => (name === 'mock' ? mockProvider : undefined), splitMessage: (text) => [text], - logger: { error: () => {} }, + logger: { error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }, }); - await mockProvider.start({ enqueue: queue.enqueue, logger: { error: () => {} } }); + await mockProvider.start({ + enqueue: queue.enqueue, + logger: { error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }, + }); const message: IncomingMessage = { provider: 'mock', diff --git a/src/__tests__/queue.test.ts b/src/__tests__/queue.test.ts index cb3eb4a..e84e820 100644 --- a/src/__tests__/queue.test.ts +++ b/src/__tests__/queue.test.ts @@ -87,7 +87,12 @@ function createMocks(overrides: Partial = {}): MockSetup { splitMessage: (text: string) => [text], downloadAttachments: mockDownload as any, formatAttachmentRefs: mockFormat as any, - logger: { error: mockLoggerError as any }, + logger: { + error: mockLoggerError as any, + warn: () => undefined, + info: () => undefined, + debug: () => undefined, + }, _mocks: { getAgentCwd: mockGetAgentCwd, send: mockSend, diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 15dd33a..df34ecb 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -54,7 +54,12 @@ function makeDeps(overrides: Partial = {}): ApiDeps { return { providers: new Map([['discord', makeProvider('discord')]]), splitMessage: (s: string) => [s], - logger: { error: async () => undefined }, + logger: { + error: async () => undefined, + warn: () => undefined, + info: () => undefined, + debug: () => undefined, + }, ...overrides, }; } diff --git a/src/core/api.ts b/src/core/api.ts index 933ceb5..e2223d2 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -16,7 +16,7 @@ export type ApiDeps = { /** Map provider-name → BridgeProvider instance. */ providers: Map; splitMessage?: (text: string) => string[]; - logger?: { error(...args: unknown[]): unknown }; + logger?: import('./types').KernelLogger; }; const MAX_BODY_SIZE = 1_048_576; // 1 MB @@ -207,16 +207,17 @@ export function startServer(providers: Map): http.Server const server = http.createServer(handler); server.on('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE') { - console.error(`API server failed to start: port ${config.apiPort} is already in use`); - } else { - console.error('API server error:', err.message); - } + void logger.error( + 'api/startup', + err.code === 'EADDRINUSE' + ? `API server failed to start: port ${config.apiPort} is already in use` + : `API server error: ${err.message}`, + ); process.exit(1); }); server.listen(config.apiPort, '127.0.0.1', () => { - console.log(`API server listening on http://127.0.0.1:${config.apiPort}`); + logger.info('api/startup', `API server listening on http://127.0.0.1:${config.apiPort}`); }); return server; diff --git a/src/core/attachments.ts b/src/core/attachments.ts index e2b0a33..2899c26 100644 --- a/src/core/attachments.ts +++ b/src/core/attachments.ts @@ -2,6 +2,7 @@ import { mkdir, rm, writeFile } from 'fs/promises'; import { randomUUID } from 'crypto'; import path from 'path'; import type { IncomingAttachment } from './types'; +import { logger } from './logger'; export interface DownloadedFile { originalName: string; @@ -30,7 +31,7 @@ export async function downloadAttachments( try { await mkdir(targetDir, { recursive: true }); } catch (err) { - console.warn(`[attachments] Failed to create directory "${targetDir}":`, err); + logger.warn('attachments/mkdir', `Failed to create directory "${targetDir}": ${String(err)}`); return { downloaded: [], failed: attachments.map((a) => a.name) }; } @@ -39,8 +40,9 @@ export async function downloadAttachments( for (const attachment of attachments) { if (attachment.size > MAX_FILE_SIZE) { - console.warn( - `[attachments] Skipping "${attachment.name}" (${attachment.size} bytes) — exceeds ${MAX_FILE_SIZE} byte limit`, + logger.warn( + 'attachments/skip', + `Skipping "${attachment.name}" (${attachment.size} bytes) — exceeds ${MAX_FILE_SIZE} byte limit`, ); failed.push(attachment.name); continue; @@ -53,8 +55,9 @@ export async function downloadAttachments( try { const response = await fetch(attachment.url); if (!response.ok) { - console.warn( - `[attachments] Failed to download "${attachment.name}": HTTP ${response.status}`, + logger.warn( + 'attachments/download', + `Failed to download "${attachment.name}": HTTP ${response.status}`, ); failed.push(attachment.name); continue; @@ -64,7 +67,7 @@ export async function downloadAttachments( await writeFile(savedPath, buffer); downloaded.push({ originalName: attachment.name, savedPath }); } catch (err) { - console.warn(`[attachments] Error downloading "${attachment.name}":`, err); + logger.warn('attachments/download', `Error downloading "${attachment.name}": ${String(err)}`); failed.push(attachment.name); } } diff --git a/src/core/config.ts b/src/core/config.ts index fe955cb..1705d98 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -42,4 +42,13 @@ export const config = { get whisperLanguage() { return process.env.WHISPER_LANGUAGE || 'auto'; }, + /** + * Minimum log level for console output. One of `debug`, `info`, `warn`, + * `error`. Defaults to `info`. Console output for every level (including + * `error`) is gated by this level, but `error` always appends to the log + * file regardless of level. + */ + get logLevel(): string { + return (process.env.LOG_LEVEL || 'info').toLowerCase(); + }, }; diff --git a/src/core/logger.ts b/src/core/logger.ts index 0f38740..d0bb26d 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -1,11 +1,22 @@ import { appendFile, mkdir } from 'fs/promises'; import { join } from 'path'; +import { config } from './config'; const LOG_DIR = process.env.LOG_DIR || join(process.cwd(), 'logs'); const LOG_FILE = join(LOG_DIR, 'errors.log'); let dirReady = false; +const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 } as const; +export type LogLevel = keyof typeof LEVELS; + +function parseLevel(raw: string): number { + const key = raw.toLowerCase() as LogLevel; + return LEVELS[key] ?? LEVELS.info; +} + +let currentLevel = parseLevel(config.logLevel); + async function ensureDir(): Promise { if (dirReady) return; await mkdir(LOG_DIR, { recursive: true }); @@ -21,15 +32,48 @@ function formatEntry(level: string, context: string, detail: string): string { return `[${ts}] ${level} [${sanitize(context)}] ${sanitize(detail)}\n`; } +function formatLine(level: string, context: string, detail: string): string { + return `[${level}] [${sanitize(context)}] ${sanitize(detail)}`; +} + +function shouldEmit(level: LogLevel): boolean { + return LEVELS[level] >= currentLevel; +} + +function emit(level: LogLevel, context: string, detail: string, sink: (line: string) => void) { + if (!shouldEmit(level)) return; + sink(formatLine(level.toUpperCase(), context, detail)); +} + export const logger = { + /** Update the minimum log level at runtime (e.g. for tests or operator hot-toggle). */ + setLevel(level: LogLevel | string): void { + currentLevel = parseLevel(level); + }, + /** Current minimum level. */ + getLevel(): LogLevel { + return (Object.keys(LEVELS) as LogLevel[]).find((k) => LEVELS[k] === currentLevel) ?? 'info'; + }, + /** Returns true if messages at the given level would be emitted. */ + isEnabled(level: LogLevel): boolean { + return shouldEmit(level); + }, + debug(context: string, detail: string): void { + emit('debug', context, detail, (line) => console.debug(line)); + }, + info(context: string, detail: string): void { + emit('info', context, detail, (line) => console.info(line)); + }, + warn(context: string, detail: string): void { + emit('warn', context, detail, (line) => console.warn(line)); + }, async error(context: string, detail: string): Promise { - const line = formatEntry('ERROR', context, detail); - console.error(line.trimEnd()); + if (shouldEmit('error')) console.error(formatLine('ERROR', context, detail)); try { await ensureDir(); - await appendFile(LOG_FILE, line); + await appendFile(LOG_FILE, formatEntry('ERROR', context, detail)); } catch { - // If file logging fails, console.error above still ran + // If file logging fails, console.error above still ran (if enabled) } }, }; diff --git a/src/core/maestro.ts b/src/core/maestro.ts index d748475..78abfc3 100644 --- a/src/core/maestro.ts +++ b/src/core/maestro.ts @@ -1,5 +1,6 @@ import { execFile, spawn } from 'child_process'; import { promisify } from 'util'; +import { logger } from './logger'; const execFileAsync = promisify(execFile); @@ -212,7 +213,7 @@ async function run(args: string[], opts: RunOptions = {}): Promise { if (e.stdout?.trim()) parts.push(`stdout: ${e.stdout.trim()}`); if (parts.length === 0) parts.push(e.message || String(err)); const detail = parts.join(' | '); - console.error(`[maestro-cli ${args[0]}] ${detail}`); + void logger.error(`maestro-cli/${args[0]}`, detail); throw new Error(`maestro-cli ${args[0]} failed: ${detail}`, { cause: err }); } } @@ -232,7 +233,7 @@ function runSpawn(args: string[]): Promise { child.stderr.on('data', (data: Buffer) => stderrChunks.push(data)); child.on('error', (err) => { - console.error(`[maestro-cli ${args[0]}] spawn error: ${err.message}`); + void logger.error(`maestro-cli/${args[0]}`, `spawn error: ${err.message}`); reject(new Error(`maestro-cli ${args[0]} failed: spawn error: ${err.message}`)); }); @@ -248,7 +249,7 @@ function runSpawn(args: string[]): Promise { if (stderr) parts.push(`stderr: ${stderr}`); if (stdout) parts.push(`stdout: ${stdout}`); const detail = parts.join(' | '); - console.error(`[maestro-cli ${args[0]}] ${detail}`); + void logger.error(`maestro-cli/${args[0]}`, detail); reject(new Error(`maestro-cli ${args[0]} failed: ${detail}`)); } }); diff --git a/src/core/providers.ts b/src/core/providers.ts index a724533..f55d9b1 100644 --- a/src/core/providers.ts +++ b/src/core/providers.ts @@ -1,4 +1,5 @@ import type { BridgeProvider } from './types'; +import { logger } from './logger'; /** * Build the set of provider instances enabled in this deployment. @@ -27,7 +28,7 @@ async function loadProvider(name: string): Promise { return new SlackProvider(); } default: - console.warn(`[providers] Unknown provider "${name}" — ignoring.`); + logger.warn('providers/load', `Unknown provider "${name}" — ignoring.`); return null; } } diff --git a/src/core/transcription.ts b/src/core/transcription.ts index 104446f..2b9c79d 100644 --- a/src/core/transcription.ts +++ b/src/core/transcription.ts @@ -80,13 +80,14 @@ export async function checkTranscriptionDependencies(): Promise { } if (missing.length > 0) { - console.warn( - `⚠️ Transcription disabled: missing dependencies: ${missing.join(', ')}. ` + + logger.warn( + 'transcription/deps', + `Transcription disabled: missing dependencies: ${missing.join(', ')}. ` + 'Voice message transcription will be unavailable. See README for setup instructions.', ); transcriberAvailable = false; } else { - console.info('✅ Voice transcription enabled.'); + logger.info('transcription/deps', 'Voice transcription enabled.'); transcriberAvailable = true; } } diff --git a/src/core/types.ts b/src/core/types.ts index ca67730..a9a3d8e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -115,6 +115,9 @@ export type EnqueueOptions = { export interface KernelLogger { error(context: string, detail: string): void | Promise; + warn(context: string, detail: string): void; + info(context: string, detail: string): void; + debug(context: string, detail: string): void; } export interface KernelContext { diff --git a/src/index.ts b/src/index.ts index 123aa63..e216bf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,8 @@ import type { KernelContext } from './core/types'; async function main() { const providers = await buildProviders(config.enabledProviders); if (providers.size === 0) { - console.error( + await logger.error( + 'bridge/startup', `No providers enabled. Set ENABLED_PROVIDERS in .env (default 'discord'). Exiting.`, ); process.exit(1); @@ -30,9 +31,9 @@ async function main() { for (const [name, provider] of providers) { try { await provider.start(ctx); - console.log(`[bridge] provider "${name}" started`); + logger.info('bridge/startup', `provider "${name}" started`); } catch (err) { - console.error(`[bridge] provider "${name}" failed to start:`, err); + await logger.error('bridge/startup', `provider "${name}" failed to start: ${String(err)}`); process.exit(1); } } @@ -40,20 +41,20 @@ async function main() { const server = startServer(providers); const shutdown = async (signal: string) => { - console.log(`\n[bridge] received ${signal}, shutting down...`); + logger.info('bridge/shutdown', `received ${signal}, shutting down...`); server.close(); for (const [name, provider] of providers) { try { await provider.stop(); } catch (err) { - console.error(`[bridge] error stopping provider "${name}":`, err); + await logger.error('bridge/shutdown', `error stopping provider "${name}": ${String(err)}`); } } try { db.exec('PRAGMA wal_checkpoint(RESTART);'); db.close(); } catch (err) { - console.error('[bridge] db shutdown error:', err); + await logger.error('bridge/shutdown', `db shutdown error: ${String(err)}`); } process.exit(0); }; diff --git a/src/providers/discord/adapter.ts b/src/providers/discord/adapter.ts index e928abc..f18754f 100644 --- a/src/providers/discord/adapter.ts +++ b/src/providers/discord/adapter.ts @@ -70,7 +70,7 @@ export class DiscordProvider implements BridgeProvider { ); client.once('ready', async (c) => { - console.log(`[discord] logged in as ${c.user.tag}`); + logger.info('discord/ready', `logged in as ${c.user.tag}`); await checkTranscriptionDependencies(); }); @@ -89,7 +89,7 @@ export class DiscordProvider implements BridgeProvider { try { await cmd.autocomplete(interaction); } catch (err) { - console.error('Autocomplete error:', err); + await logger.error('discord/autocomplete', String(err)); } } return; @@ -109,7 +109,7 @@ export class DiscordProvider implements BridgeProvider { try { await cmd.execute(interaction); } catch (err) { - console.error('Command error:', err); + await logger.error('discord/command', `${interaction.commandName}: ${String(err)}`); const msg = { content: '❌ An error occurred.', ephemeral: true }; if (interaction.replied || interaction.deferred) { await interaction.followUp(msg); @@ -129,7 +129,7 @@ export class DiscordProvider implements BridgeProvider { transcribeVoiceAttachment, isTranscriberAvailable, splitMessage, - logger: console, + logger: ctx.logger, }); client.on('messageCreate', handleMessageCreate); diff --git a/src/providers/discord/commands/agents.ts b/src/providers/discord/commands/agents.ts index 2814d0c..494f861 100644 --- a/src/providers/discord/commands/agents.ts +++ b/src/providers/discord/commands/agents.ts @@ -11,6 +11,7 @@ import { threadDb } from '../threadsDb'; import { cleanupAgentFiles } from '../../../core/attachments'; import { clampFieldValue, clampTitle } from '../embed'; import { discordConfig } from '../config'; +import { logger } from '../../../core/logger'; function missingBotScopeMessage(): string { return ( @@ -334,14 +335,18 @@ async function handleDisconnect(interaction: ChatInputCommandInteraction): Promi const agentCwd = await maestro.getAgentCwd(agentId); if (agentCwd) { await cleanupAgentFiles(agentCwd); - console.log(`[disconnect] Cleaned up files for agent ${agentId}`); + logger.info('discord/disconnect', `Cleaned up files for agent ${agentId}`); } } catch (err) { - console.warn(`[disconnect] Failed to clean up files for agent ${agentId}:`, err); + void logger.error( + 'discord/disconnect', + `Failed to clean up files for agent ${agentId}: ${String(err)}`, + ); } } else { - console.log( - `[disconnect] Skipping file cleanup for agent ${agentId} — ${otherChannels.length} other channel(s) and ${otherThreads.length} other thread(s) still active`, + logger.info( + 'discord/disconnect', + `Skipping file cleanup for agent ${agentId} - ${otherChannels.length} other channel(s) and ${otherThreads.length} other thread(s) still active`, ); } diff --git a/src/providers/discord/messageCreate.ts b/src/providers/discord/messageCreate.ts index 015aeab..a029d37 100644 --- a/src/providers/discord/messageCreate.ts +++ b/src/providers/discord/messageCreate.ts @@ -4,11 +4,13 @@ import type { EnqueueOptions, IncomingAttachment, IncomingMessage, + KernelLogger, } from '../../core/types'; import { isTranscriberAvailable, transcribeVoiceAttachment, } from '../../core/transcription'; +import { logger as defaultLogger } from '../../core/logger'; import { splitMessage } from '../../core/splitMessage'; import { channelDb } from './channelsDb'; import { threadDb } from './threadsDb'; @@ -26,7 +28,7 @@ export type MessageCreateDeps = { transcribeVoiceAttachment: typeof transcribeVoiceAttachment; isTranscriberAvailable: typeof isTranscriberAvailable; splitMessage: typeof splitMessage; - logger?: Pick; + logger?: KernelLogger; }; function toIncoming(message: Message, attachmentSource?: IncomingAttachment[]): IncomingMessage { @@ -47,6 +49,8 @@ function toIncoming(message: Message, attachmentSource?: IncomingAttachment[]): } export function createMessageCreateHandler(deps: MessageCreateDeps) { + const log: KernelLogger = deps.logger ?? defaultLogger; + return async function handleMessageCreate(message: Message): Promise { if (message.author.bot) return; if (!message.guild) return; @@ -55,15 +59,14 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { const botUserId = deps.getBotUserId(message); if (!botUserId) { - const warn = deps.logger?.warn ?? console.warn; - warn('messageCreate: bot user ID missing, skipping message handling'); + log.warn('messageCreate', 'bot user ID missing, skipping message handling'); return; } if (!message.channel.isThread()) { const channelInfo = deps.channelDb.get(message.channel.id); if (!channelInfo) { - console.debug(`[mention] channel ${message.channel.id} not registered, ignoring`); + log.debug('messageCreate/mention', `channel ${message.channel.id} not registered, ignoring`); return; } @@ -74,8 +77,9 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { message.content.includes(`<@${botUserId}>`) || message.content.includes(`<@!${botUserId}>`) || (!!botRoleId && message.content.includes(`<@&${botRoleId}>`)); - console.debug( - `[mention] botUserId=${botUserId} botRoleId=${botRoleId} user=${mentionedByUser} role=${mentionedByRole} content=${mentionedByContent} raw=${JSON.stringify(message.content)}`, + log.debug( + 'messageCreate/mention', + `botUserId=${botUserId} botRoleId=${botRoleId} user=${mentionedByUser} role=${mentionedByRole} content=${mentionedByContent} raw=${JSON.stringify(message.content)}`, ); if (!mentionedByUser && !mentionedByRole && !mentionedByContent) return; @@ -116,8 +120,7 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { deps.enqueue(toIncoming(threadMessage)); } } catch (err) { - const log = deps.logger?.error ?? console.error; - log('messageCreate: failed to create thread for mention:', err); + await log.error('messageCreate/thread', `failed to create thread for mention: ${String(err)}`); try { await message.reply('❌ Failed to create a thread for this mention.'); } catch { @@ -188,9 +191,9 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { ); const failedReplies = replyResults.filter((result) => result.status === 'rejected'); if (failedReplies.length > 0) { - const logWarn = deps.logger?.warn ?? console.warn; - logWarn( - `messageCreate: failed to send ${failedReplies.length} transcription reply part(s)`, + log.warn( + 'messageCreate/transcription', + `failed to send ${failedReplies.length} transcription reply part(s)`, ); } @@ -217,8 +220,7 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { /* ignore cleanup */ } - const log = deps.logger?.error ?? console.error; - log('messageCreate: failed to transcribe voice message:', err); + await log.error('messageCreate/transcription', `failed to transcribe voice message: ${String(err)}`); try { await message.reply({ content: @@ -226,8 +228,7 @@ export function createMessageCreateHandler(deps: MessageCreateDeps) { allowedMentions: { parse: [] }, }); } catch (replyErr) { - const logErr = deps.logger?.error ?? console.error; - logErr('messageCreate: failed to send transcription error reply:', replyErr); + await log.error('messageCreate/transcription', `failed to send transcription error reply: ${String(replyErr)}`); } deps.enqueue(toIncoming(message)); }