From 66b1ef15af2c0f5df08492f651f74250ccdfcc32 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 23 Feb 2026 13:32:13 +0100 Subject: [PATCH] feat: improve logging --- src/core/logger.ts | 67 ++++++++++++++++++++++++++++++++-- src/features/joel/responder.ts | 29 ++++++++++++++- src/services/ai/image-gen.ts | 15 +++++++- src/services/ai/openrouter.ts | 36 +++++++++++++++--- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/src/core/logger.ts b/src/core/logger.ts index 73a0090..57125ea 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -67,6 +67,59 @@ function formatDate(date: Date): string { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } +/** + * Extract structured details from an error object for logging. + * Handles OpenAI APIError, standard Error, and unknown objects. + */ +function formatErrorData(error: unknown): Record { + if (error === null || error === undefined) { + return {}; + } + + if (typeof error !== "object") { + return { value: error }; + } + + const err = error as Record; + const details: Record = {}; + + // Standard Error properties + if (err.message) details.message = err.message; + if (err.name && err.name !== "Error") details.name = err.name; + + // HTTP / OpenAI APIError properties + if (err.status !== undefined) details.status = err.status; + if (err.code !== undefined) details.code = err.code; + if (err.type !== undefined) details.type = err.type; + if (err.param !== undefined) details.param = err.param; + if (err.request_id !== undefined) details.requestId = err.request_id; + + // The nested `error` body from OpenAI / OpenRouter provider responses + if (err.error !== undefined && typeof err.error === "object" && err.error !== null) { + const inner = err.error as Record; + details.providerError = { + ...(inner.message ? { message: inner.message } : {}), + ...(inner.code ? { code: inner.code } : {}), + ...(inner.type ? { type: inner.type } : {}), + ...(inner.metadata ? { metadata: inner.metadata } : {}), + }; + // If the inner error is the only useful info, keep the raw version too + if (Object.keys(details.providerError as object).length === 0) { + details.providerError = JSON.stringify(inner).slice(0, 500); + } + } + + // Response body text (some HTTP wrappers attach this) + if (typeof err.body === "string") { + details.body = (err.body as string).slice(0, 500); + } + + // Stack trace (only for error level - printed separately) + if (err.stack) details.stack = err.stack; + + return details; +} + class Logger { private context: string; @@ -93,19 +146,25 @@ class Logger { ]; const output = parts.join(" "); + + // For errors, extract structured info for better debugging + let formattedData = data; + if (level === "error" && data instanceof Error) { + formattedData = formatErrorData(data); + } switch (level) { case "debug": - console.debug(output, data !== undefined ? data : ""); + console.debug(output, formattedData !== undefined ? formattedData : ""); break; case "info": - console.log(output, data !== undefined ? data : ""); + console.log(output, formattedData !== undefined ? formattedData : ""); break; case "warn": - console.warn(output, data !== undefined ? data : ""); + console.warn(output, formattedData !== undefined ? formattedData : ""); break; case "error": - console.error(output, data !== undefined ? data : ""); + console.error(output, formattedData !== undefined ? formattedData : ""); break; } } diff --git a/src/features/joel/responder.ts b/src/features/joel/responder.ts index 2475ba4..ed233c5 100644 --- a/src/features/joel/responder.ts +++ b/src/features/joel/responder.ts @@ -436,7 +436,13 @@ The image URL will appear in your response for the user to see.`; * Handle errors gracefully */ async handleError(message: Message, error: unknown): Promise { - const err = error as Error & { status?: number }; + const err = error as Error & { + status?: number; + code?: string; + type?: string; + error?: { message?: string; code?: string | number; metadata?: unknown }; + request_id?: string; + }; if (err.status === 503) { await message.reply( @@ -445,6 +451,13 @@ The image URL will appear in your response for the user to see.`; return; } + if (err.status === 429) { + await message.reply( + "Jag pratar för mycket, ge mig en sekund... (rate limited)" + ); + return; + } + if (err.name === "TimeoutError" || err.message?.includes("timeout")) { await message.reply( `Jag tog för lång tid på mig... (timeout)\n\`\`\`\n${err.message}\`\`\`` @@ -452,8 +465,20 @@ The image URL will appear in your response for the user to see.`; return; } + // Build a detailed error summary for the Discord reply + const errorParts: string[] = []; + if (err.status) errorParts.push(`HTTP ${err.status}`); + if (err.code) errorParts.push(`code: ${err.code}`); + if (err.type) errorParts.push(`type: ${err.type}`); + if (err.error?.message) errorParts.push(`provider: ${err.error.message}`); + if (err.request_id) errorParts.push(`req: ${err.request_id}`); + + const summary = errorParts.length > 0 + ? errorParts.join(" | ") + : err.message || "Unknown error"; + await message.reply({ - content: `Något gick fel...\n\`\`\`\n${err.stack || err.message || JSON.stringify(error)}\`\`\``, + content: `Något gick fel...\n\`\`\`\n${summary}\`\`\``, }); }, }; diff --git a/src/services/ai/image-gen.ts b/src/services/ai/image-gen.ts index 1e9e7bb..15fad0a 100644 --- a/src/services/ai/image-gen.ts +++ b/src/services/ai/image-gen.ts @@ -116,7 +116,11 @@ Output ONLY the enhanced prompt, nothing else.`; return prompt; } catch (error) { - logger.error("AI prompt enhancement error", error); + logger.error("AI prompt enhancement error", { + promptLength: prompt.length, + style, + }); + logger.error("Prompt enhancement error details", error); return prompt; } } @@ -235,7 +239,14 @@ export class ImageGenService { prompt: finalPrompt, }; } catch (error) { - logger.error("Image generation failed", error); + logger.error("Image generation failed", { + model: modelId, + promptLength: finalPrompt.length, + size, + nsfw, + numImages, + }); + logger.error("Image generation error details", error); throw error; } } diff --git a/src/services/ai/openrouter.ts b/src/services/ai/openrouter.ts index fdfb2bd..b51c612 100644 --- a/src/services/ai/openrouter.ts +++ b/src/services/ai/openrouter.ts @@ -45,10 +45,11 @@ export class OpenRouterProvider implements AiProvider { async ask(options: AskOptions): Promise { const { prompt, systemPrompt, maxTokens, temperature } = options; + const model = config.ai.model; try { const completion = await this.client.chat.completions.create({ - model: config.ai.model, + model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: prompt }, @@ -62,7 +63,13 @@ export class OpenRouterProvider implements AiProvider { // Discord message limit safety return { text: text.slice(0, 1900) }; } catch (error: unknown) { - logger.error("Failed to generate response", error); + logger.error("Failed to generate response (ask)", { + method: "ask", + model, + promptLength: prompt.length, + ...(error instanceof Error ? {} : { rawError: error }), + }); + logger.error("API error details", error); throw error; } } @@ -146,7 +153,15 @@ export class OpenRouterProvider implements AiProvider { return { text: text.slice(0, 1900) }; } catch (error: unknown) { - logger.error("Failed to generate response with tools", error); + logger.error("Failed to generate response with tools (askWithTools)", { + method: "askWithTools", + model: config.ai.model, + iteration: iterations, + messageCount: messages.length, + toolCount: tools.length, + ...(error instanceof Error ? {} : { rawError: error }), + }); + logger.error("API error details", error); throw error; } } @@ -208,7 +223,13 @@ The user's Discord ID is: ${context.userId}`; } } catch (error) { // Don't throw - memory extraction is non-critical - logger.error("Memory extraction failed", error); + logger.error("Memory extraction failed", { + method: "extractMemories", + model: config.ai.model, + authorName, + messageLength: message.length, + }); + logger.error("Memory extraction error details", error); } } @@ -251,7 +272,12 @@ Category:`, logger.debug("Classification returned invalid style, defaulting to snarky", { result }); return "snarky"; } catch (error) { - logger.error("Failed to classify message", error); + logger.error("Failed to classify message", { + method: "classifyMessage", + model: config.ai.classificationModel, + messageLength: message.length, + }); + logger.error("Classification error details", error); return "snarky"; // Default to snarky on error } }