feat: improve logging
This commit is contained in:
@@ -67,6 +67,59 @@ function formatDate(date: Date): string {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
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<string, unknown> {
|
||||||
|
if (error === null || error === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error !== "object") {
|
||||||
|
return { value: error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = error as Record<string, unknown>;
|
||||||
|
const details: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
// 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<string, unknown>;
|
||||||
|
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 {
|
class Logger {
|
||||||
private context: string;
|
private context: string;
|
||||||
|
|
||||||
@@ -93,19 +146,25 @@ class Logger {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const output = parts.join(" ");
|
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) {
|
switch (level) {
|
||||||
case "debug":
|
case "debug":
|
||||||
console.debug(output, data !== undefined ? data : "");
|
console.debug(output, formattedData !== undefined ? formattedData : "");
|
||||||
break;
|
break;
|
||||||
case "info":
|
case "info":
|
||||||
console.log(output, data !== undefined ? data : "");
|
console.log(output, formattedData !== undefined ? formattedData : "");
|
||||||
break;
|
break;
|
||||||
case "warn":
|
case "warn":
|
||||||
console.warn(output, data !== undefined ? data : "");
|
console.warn(output, formattedData !== undefined ? formattedData : "");
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
console.error(output, data !== undefined ? data : "");
|
console.error(output, formattedData !== undefined ? formattedData : "");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,7 +436,13 @@ The image URL will appear in your response for the user to see.`;
|
|||||||
* Handle errors gracefully
|
* Handle errors gracefully
|
||||||
*/
|
*/
|
||||||
async handleError(message: Message<true>, error: unknown): Promise<void> {
|
async handleError(message: Message<true>, error: unknown): Promise<void> {
|
||||||
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) {
|
if (err.status === 503) {
|
||||||
await message.reply(
|
await message.reply(
|
||||||
@@ -445,6 +451,13 @@ The image URL will appear in your response for the user to see.`;
|
|||||||
return;
|
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")) {
|
if (err.name === "TimeoutError" || err.message?.includes("timeout")) {
|
||||||
await message.reply(
|
await message.reply(
|
||||||
`Jag tog för lång tid på mig... (timeout)\n\`\`\`\n${err.message}\`\`\``
|
`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;
|
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({
|
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}\`\`\``,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ Output ONLY the enhanced prompt, nothing else.`;
|
|||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
} catch (error) {
|
} 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;
|
return prompt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +239,14 @@ export class ImageGenService {
|
|||||||
prompt: finalPrompt,
|
prompt: finalPrompt,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ export class OpenRouterProvider implements AiProvider {
|
|||||||
|
|
||||||
async ask(options: AskOptions): Promise<AiResponse> {
|
async ask(options: AskOptions): Promise<AiResponse> {
|
||||||
const { prompt, systemPrompt, maxTokens, temperature } = options;
|
const { prompt, systemPrompt, maxTokens, temperature } = options;
|
||||||
|
const model = config.ai.model;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const completion = await this.client.chat.completions.create({
|
const completion = await this.client.chat.completions.create({
|
||||||
model: config.ai.model,
|
model,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: systemPrompt },
|
{ role: "system", content: systemPrompt },
|
||||||
{ role: "user", content: prompt },
|
{ role: "user", content: prompt },
|
||||||
@@ -62,7 +63,13 @@ export class OpenRouterProvider implements AiProvider {
|
|||||||
// Discord message limit safety
|
// Discord message limit safety
|
||||||
return { text: text.slice(0, 1900) };
|
return { text: text.slice(0, 1900) };
|
||||||
} catch (error: unknown) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +153,15 @@ export class OpenRouterProvider implements AiProvider {
|
|||||||
|
|
||||||
return { text: text.slice(0, 1900) };
|
return { text: text.slice(0, 1900) };
|
||||||
} catch (error: unknown) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,7 +223,13 @@ The user's Discord ID is: ${context.userId}`;
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Don't throw - memory extraction is non-critical
|
// 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 });
|
logger.debug("Classification returned invalid style, defaulting to snarky", { result });
|
||||||
return "snarky";
|
return "snarky";
|
||||||
} catch (error) {
|
} 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
|
return "snarky"; // Default to snarky on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user