Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
from dotenv import load_dotenv

from utils.config import Config
from utils.logger import (
log_command_error,
register_command_logging,
setup_logging,
)

# Load environment variables
load_dotenv()

# Configure logging
logging.basicConfig(
level=getattr(logging, os.getenv("LOG_LEVEL", "INFO")),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("bot.log"), logging.StreamHandler()],
)
# Configure logging (console + rotating file under logs/)
setup_logging(os.getenv("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -65,6 +66,8 @@ def __init__(self, config: Config):

async def setup_hook(self) -> None:
"""Setup hook called before the bot starts."""
register_command_logging(self)

# Initialize CodeBuddy database
try:
from utils.codebuddy_database import init_db
Expand Down Expand Up @@ -185,6 +188,13 @@ async def on_command_error(
if isinstance(error, commands.CommandNotFound):
return

log_command_error(
ctx.author.id,
ctx.command.qualified_name if ctx.command else "unknown",
ctx.guild.id if ctx.guild else None,
error,
)

# If a command/cog-level error handler (e.g. `cog_command_error`)
# already handled the error, don't send a generic message on top.
command = ctx.command
Expand Down Expand Up @@ -230,23 +240,23 @@ async def _safe_ctx_send(message: str) -> None:
except Exception:
return

if isinstance(error, commands.CommandOnCooldown):
await _safe_ctx_send(
f"This command is on cooldown. Try again in {error.retry_after:.2f} seconds."
)
elif isinstance(error, commands.MissingRequiredArgument):
command = getattr(ctx, "command", None)
cog = getattr(command, "cog", None)
if command and command.qualified_name == "resources" and cog is not None:
send_usage = getattr(cog, "_send_resource_usage", None)
if callable(send_usage):
await send_usage(ctx)
return
await _safe_ctx_send("A required argument is missing.")
elif isinstance(error, commands.MissingPermissions):
await _safe_ctx_send("You don't have permission to use this command.")
elif isinstance(error, commands.BadArgument):
await _safe_ctx_send("Invalid argument provided.")
if isinstance(error, commands.CommandOnCooldown):
await _safe_ctx_send(
f"This command is on cooldown. Try again in {error.retry_after:.2f} seconds."
)
elif isinstance(error, commands.MissingRequiredArgument):
command = getattr(ctx, "command", None)
cog = getattr(command, "cog", None)
if command and command.qualified_name == "resources" and cog is not None:
send_usage = getattr(cog, "_send_resource_usage", None)
if callable(send_usage):
await send_usage(ctx)
return
await _safe_ctx_send("A required argument is missing.")
elif isinstance(error, commands.MissingPermissions):
await _safe_ctx_send("You don't have permission to use this command.")
elif isinstance(error, commands.BadArgument):
await _safe_ctx_send("Invalid argument provided.")
else:
logger.error(f"Command error: {error}")
await _safe_ctx_send("An error occurred while processing your command.")
Expand All @@ -258,6 +268,14 @@ async def on_app_command_error(
# Silence unknown slash/app commands.
# `app_commands` doesn't expose a stable CommandNotFound across versions, so be conservative.

command = interaction.command
log_command_error(
interaction.user.id,
command.qualified_name if command else "unknown",
interaction.guild_id,
error,
)

# If it's a cooldown, inform user
if isinstance(error, app_commands.CommandOnCooldown):
await interaction.response.send_message(
Expand Down
69 changes: 69 additions & 0 deletions utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Logging setup and command usage/error logging helpers.

Configures a shared root logger (console + rotating file under `logs/`) and
provides small helpers/listeners to log every command invocation and error
with a consistent format: timestamp, user id, command name.
"""

from __future__ import annotations

import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

import discord
from discord import app_commands
from discord.ext import commands

command_logger = logging.getLogger("eigen.commands")


def setup_logging(log_level: str = "INFO") -> None:
"""Configure the root logger with console + rotating file output."""
logs_dir = Path("logs")
logs_dir.mkdir(exist_ok=True)

file_handler = RotatingFileHandler(
logs_dir / "bot.log",
maxBytes=5 * 1024 * 1024,
backupCount=3,
encoding="utf-8",
)
stream_handler = logging.StreamHandler()

logging.basicConfig(
level=getattr(logging, log_level.upper(), logging.INFO),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[file_handler, stream_handler],
)


def log_command_usage(user_id: int, command_name: str, guild_id: int | None) -> None:
command_logger.info(
f"command='{command_name}' user_id={user_id} guild_id={guild_id}"
)


def log_command_error(
user_id: int, command_name: str, guild_id: int | None, error: BaseException
) -> None:
command_logger.error(
f"command='{command_name}' user_id={user_id} guild_id={guild_id} error={error}"
)


def register_command_logging(bot: commands.Bot) -> None:
"""Attach listeners that log every prefix/hybrid and slash command invocation."""

@bot.listen("on_command_completion")
async def _log_prefix_command(ctx: commands.Context) -> None:
guild_id = ctx.guild.id if ctx.guild else None
log_command_usage(ctx.author.id, ctx.command.qualified_name, guild_id)

@bot.listen("on_app_command_completion")
async def _log_app_command(
interaction: discord.Interaction, command: app_commands.Command
) -> None:
log_command_usage(
interaction.user.id, command.qualified_name, interaction.guild_id
)
Loading