diff --git a/am_bot/bot.py b/am_bot/bot.py index 9bdc428..7e40601 100644 --- a/am_bot/bot.py +++ b/am_bot/bot.py @@ -45,7 +45,6 @@ async def on_ready(self): logger.info("Bot is fully ready!") async def on_message(self, message): - logger.info(f"Message from {message.author}: {message.content}") if message.author.id == self.user.id: return await self.process_commands(message) diff --git a/am_bot/cogs/greetings.py b/am_bot/cogs/greetings.py index 338ef80..577ff10 100644 --- a/am_bot/cogs/greetings.py +++ b/am_bot/cogs/greetings.py @@ -13,7 +13,7 @@ def __init__(self): @commands.Cog.listener() async def on_member_join(self, member): - logger.debug(f"Member joined guild: {member}") + logger.info(f"New member joined {member.guild.name}: {member}") channel = member.guild.system_channel if channel is not None: await channel.send(f"Welcome {member.mention}!") @@ -21,7 +21,6 @@ async def on_member_join(self, member): @commands.command() async def hello(self, ctx, *, member: discord.Member = None): """Says Hello""" - logger.debug("HELLO") member = member or ctx.author if self._last_member is None or self._last_member.id != member.id: await ctx.send(f"Hello {member.name}.") diff --git a/am_bot/cogs/invite_response.py b/am_bot/cogs/invite_response.py index c1181fd..8d694fc 100644 --- a/am_bot/cogs/invite_response.py +++ b/am_bot/cogs/invite_response.py @@ -28,22 +28,19 @@ def _parse_embed_email(self, embed: discord.Embed) -> str | None: @commands.Cog.listener() async def on_message(self, message: discord.Message): - logger.debug("Message Received") + # Ignore messages from self, wrong channel, or without proper reference if message.author.id == self.bot.user.id: - logger.debug("Message is from self, ignore.") return if message.channel.id != INVITE_HELP_TEXT_CHANNEL_ID: - logger.debug("Message is not in Invite Help channel, ignore.") return if ( message.reference is None or message.reference.channel_id != INVITE_HELP_TEXT_CHANNEL_ID ): - logger.debug("Message reference is invalid. Ignore.") return if not message.content: - logger.debug("No Message Content. Ignoring.") return + referenced: discord.Message = ( message.reference.resolved if message.reference.resolved is not None @@ -60,25 +57,23 @@ async def on_message(self, message: discord.Message): embed = referenced.embeds[0] email = self._parse_embed_email(embed) help_request = embed.description - logger.debug(f"Parsed from embed - Email: {email}") else: # Fallback to legacy plain text format - logger.warning(referenced.content) + logger.debug(f"Legacy plain text format detected: {referenced.id}") email_pattern = re.compile(r"Email: (.*)") match = email_pattern.match(referenced.content) if match: email = match.group(1) help_request = "\n".join(referenced.content.split("\n")[7:]) - if not email: - logger.debug("Email not found in referenced message.") - return - - if not help_request: - logger.debug("Help request not found in referenced message.") + if not email or not help_request: + logger.debug( + f"Could not parse email/help request from message " + f"{referenced.id}" + ) return - logger.debug(f"Email found for referenced message: {email}") + logger.info(f"Sending invite help response to {email}") body_txt = ( f"Your Message: {help_request}\n\n" f"ARK Modding Discord Staff Response:\n\n" diff --git a/am_bot/cogs/responses.py b/am_bot/cogs/responses.py index 4bd25dc..d16b793 100644 --- a/am_bot/cogs/responses.py +++ b/am_bot/cogs/responses.py @@ -21,33 +21,26 @@ def __init__(self, bot: discord.ext.commands.Bot): @commands.Cog.listener() async def on_message(self, message: discord.Message): - logger.debug("Message Received") if message.author.id == self.bot.user.id: - logger.debug("Message is from self, ignore.") return if not message.content: - logger.debug("No Message Content. Ignoring.") return - logger.debug( - f"First Char: {message.content[0]}, " - f"Remaining Chars: {message.content[1:]}" - ) + if ( message.content[0] in COMMANDS and message.content[1:] in COMMANDS[message.content[0]] ): - logger.debug(f"Valid Response Command: {message.content}") command = COMMANDS[message.content[0]][message.content[1:]] - logger.debug(f"Command: {command}") if "duplicate" in command: # Handle duplicate commands, # grab original defined by `duplicate` command = COMMANDS[message.content[0]][command["duplicate"]] + + logger.info(f"Executing response command: {message.content}") + if "embed" in command: - logger.debug("Embed Response") await message.channel.send( embed=discord.Embed.from_dict(command["embed"]) ) elif "content" in command: - logger.debug("Content Response") await message.channel.send(content=command["content"]) diff --git a/am_bot/cogs/role_assignment.py b/am_bot/cogs/role_assignment.py index f6b1850..ff0dd9b 100644 --- a/am_bot/cogs/role_assignment.py +++ b/am_bot/cogs/role_assignment.py @@ -28,28 +28,22 @@ async def on_raw_reaction_add( ): """Add role""" if payload.member.id == self.bot.user.id: - logger.debug("Reaction from self. Ignore") return - logger.debug(f"Reaction ADD Received. Payload: {payload}") if payload.emoji.name not in ASSIGNABLE_ROLES: - logger.debug("Emoji not in ASSIGNABLE_ROLES. Skipping.") return emoji = payload.emoji.name if ( "message_id" in ASSIGNABLE_ROLES[emoji] and payload.message_id != ASSIGNABLE_ROLES[emoji]["message_id"] ): - logger.debug("Wrong Channel") return - # Add Role (no need to check) - logger.info( - f'Adding {ASSIGNABLE_ROLES[emoji]["name"]} ' - f"Role to {payload.member}" - ) + + role_name = ASSIGNABLE_ROLES[emoji]["name"] + logger.info(f"Adding {role_name} role to {payload.member}") await payload.member.add_roles( payload.member.guild.get_role(ASSIGNABLE_ROLES[emoji]["role_id"]) ) - if ASSIGNABLE_ROLES[emoji]["name"] in ["Modder", "Mapper"]: + if role_name in ["Modder", "Mapper"]: server_stats_cog = self.bot.get_cog("ServerStatsCog") if server_stats_cog: await server_stats_cog.update_role_counts() @@ -57,28 +51,23 @@ async def on_raw_reaction_add( @commands.Cog.listener() async def on_raw_reaction_remove(self, payload): """Remove role""" - logger.debug(f"Reaction REMOVE Received. Payload {payload}") if payload.emoji.name not in ASSIGNABLE_ROLES: - logger.debug("Emoji not in ASSIGNABLE_ROLES. Skipping.") return emoji = payload.emoji.name if ( "message_id" in ASSIGNABLE_ROLES[emoji] and payload.message_id != ASSIGNABLE_ROLES[emoji]["message_id"] ): - logger.debug("Wrong Channel") return - # Remove Role guild = await self.bot.fetch_guild(payload.guild_id) member = await guild.fetch_member(payload.user_id) - logger.info( - f'Removing {ASSIGNABLE_ROLES[emoji]["name"]} Role from {member}.' - ) + role_name = ASSIGNABLE_ROLES[emoji]["name"] + logger.info(f"Removing {role_name} role from {member}") await member.remove_roles( guild.get_role(ASSIGNABLE_ROLES[emoji]["role_id"]) ) - if ASSIGNABLE_ROLES[emoji]["name"] in ["Modder", "Mapper"]: + if role_name in ["Modder", "Mapper"]: server_stats_cog = self.bot.get_cog("ServerStatsCog") if server_stats_cog: await server_stats_cog.update_role_counts() @@ -86,41 +75,28 @@ async def on_raw_reaction_remove(self, payload): async def reset_reactions(self): await asyncio.sleep(10) while True: - logger.debug("Resetting reactions for Reaction Roles...") cleared_messages = [] + emoji_count = 0 + for emoji, role_details in ASSIGNABLE_ROLES.items(): - logger.debug(f"Resetting Emoji: {emoji}") channel = self.bot.get_channel(role_details["channel_id"]) message = await channel.fetch_message( role_details["message_id"] ) - # Check if we have already reset reactions - # for this message, if not, clear them if role_details["message_id"] not in cleared_messages: - logger.debug( - f'Message ID {role_details["message_id"]} has not ' - f"been reset. Resetting..." - ) await message.clear_reactions() cleared_messages.append(role_details["message_id"]) - # Add first reaction with this emoji. + if "emoji_id" in role_details: - # Custom Emoji - logger.debug( - f"Adding custom Emoji Reaction {emoji} to Message ID " - f'{role_details["message_id"]}' - ) await message.add_reaction( self.bot.get_emoji(role_details["emoji_id"]) ) else: - # Unicode Emoji - logger.debug( - f"Adding Unicode Emoji Reaction {emoji} to Message ID " - f'{role_details["message_id"]}' - ) await message.add_reaction(emoji) - logger.debug( - "Finished Reaction Role Reset. Sleeping 10 minutes..." + emoji_count += 1 + + logger.info( + f"Reaction role reset complete: {emoji_count} reactions on " + f"{len(cleared_messages)} messages" ) await asyncio.sleep(600) diff --git a/am_bot/cogs/server_stats.py b/am_bot/cogs/server_stats.py index 9a0124a..51ed4f4 100644 --- a/am_bot/cogs/server_stats.py +++ b/am_bot/cogs/server_stats.py @@ -35,56 +35,42 @@ def cog_load(self) -> None: self.bot.loop.create_task(self.update_server_stats()) async def update_member_count(self): - logger.debug("Updating Member Count...") self.guild = self.guild or self.bot.get_guild(GUILD_ID) - logger.debug(f"Guild: {self.guild}") channel = await self.bot.fetch_channel(MEMBERS_COUNT_CHANNEL_ID) - logger.debug(f"Member Count Channel: {channel}") member_count = len(self.guild.members) - logger.debug(f"Member Count: {member_count}") await channel.edit(name=f"🔹┇{member_count}︲members") - logger.debug("Member Count Channel Updated!") + logger.info(f"Updated member count: {member_count}") async def update_boost_count(self): - logger.debug("Updating Boost Count...") self.guild = self.guild or self.bot.get_guild(GUILD_ID) - logger.debug(f"Guild: {self.guild}") channel = await self.bot.fetch_channel(BOOSTS_COUNT_CHANNEL_ID) - logger.debug(f"Boost Count Channel: {channel}") - logger.debug(f"Boost Count: {self.guild.premium_subscription_count}") - await channel.edit( - name=f"🔸┇{self.guild.premium_subscription_count}︲boosts" - ) - logger.debug("Boost Count Channel Updated!") + boost_count = self.guild.premium_subscription_count + await channel.edit(name=f"🔸┇{boost_count}︲boosts") + logger.debug(f"Updated boost count: {boost_count}") async def update_role_counts(self): - logger.debug("Updating Role Counts...") self.guild = self.guild or self.bot.get_guild(GUILD_ID) - logger.debug(f"Guild: {self.guild}") + modder_channel = await self.bot.fetch_channel(MODDER_STATS_CHANNEL_ID) - logger.debug(f"Modder Count Channel: {modder_channel}") modder_role = self.guild.get_role(MODDER_ROLE_ID) - logger.debug(f"Modder Role: {modder_role}") - modder_member_count = len(modder_role.members) - logger.debug(f"Modder Member Count: {modder_member_count}") - await modder_channel.edit(name=f"🔸┇{modder_member_count}︲modders") - logger.debug("Modder Count Channel Updated!") + modder_count = len(modder_role.members) + await modder_channel.edit(name=f"🔸┇{modder_count}︲modders") mapper_channel = await self.bot.fetch_channel(MAPPER_STATS_CHANNEL_ID) - logger.debug("Getting Mapper Role...") mapper_role = self.guild.get_role(MAPPER_ROLE_ID) - logger.debug(f"Mapper Role: {mapper_role}") - mapper_member_count = len(mapper_role.members) - logger.debug(f"Mapper Member Count: {mapper_member_count}") - await mapper_channel.edit(name=f"🔹┇{mapper_member_count}︲mappers") - logger.debug("Mapper Count Channel Updated!") + mapper_count = len(mapper_role.members) + await mapper_channel.edit(name=f"🔹┇{mapper_count}︲mappers") + + logger.info( + f"Updated role counts: {modder_count} modders, " + f"{mapper_count} mappers" + ) async def update_server_stats(self): await asyncio.sleep(60) - logger.debug("Updating Server Stats...") await self.update_member_count() await self.update_role_counts() + logger.info("Initial server stats update complete") while True: await self.update_boost_count() - logger.debug("Server Stats updated! Sleeping 10 minutes...") await asyncio.sleep(600) diff --git a/am_bot/cogs/starboard.py b/am_bot/cogs/starboard.py index 5b68747..88f5e1d 100644 --- a/am_bot/cogs/starboard.py +++ b/am_bot/cogs/starboard.py @@ -21,19 +21,15 @@ def __init__(self, bot: discord.ext.commands.Bot): @commands.Cog.listener() async def on_ready(self): - logger.debug("StarboardCog On Ready!") - logger.debug("Loading Starboard Messages...") channel: discord.TextChannel = self.bot.get_channel( STARBOARD_TEXT_CHANNEL_ID ) - logger.debug(f"Found Starboard Channel: {channel}") async for message in channel.history(limit=None): if self._last_message is None: self._last_message = message - logger.debug(f"Last Message: {self._last_message}") if not message.embeds or not message.embeds[0].fields: logger.warning( - f"Message in Starboard channel missing embeds: {message}" + f"Starboard message {message.id} missing embeds" ) continue found = channel_id_pattern.findall( @@ -41,55 +37,45 @@ async def on_ready(self): ) if not found: logger.warning( - f"Message in Starboard channel, unable to parse related " - f"channel id. Message: {message}" + f"Starboard message {message.id}: unable to parse source" ) continue - logger.debug(f"Found Existing Starboard Message ID: {found[0]}") self._starred_message_ids.add(int(found[0])) - logger.debug(f"Starred Message IDs: {self._starred_message_ids}") + + logger.info( + f"Starboard initialized: {len(self._starred_message_ids)} " + f"existing entries loaded" + ) @commands.Cog.listener() async def on_raw_reaction_add( self, payload: discord.RawReactionActionEvent ): - logger.debug( - f"Payload Received. Channel ID: {payload.channel_id}, " - f"Member ID: {payload.member.id}, " - f"Message ID: {payload.message_id}, Emoji: {payload.emoji.name}" - ) + # Skip reactions that don't qualify if payload.channel_id == STARBOARD_TEXT_CHANNEL_ID: - logger.debug("Reaction in Starboard channel. Ignoring.") return if payload.member.id == self.bot.user.id: - logger.debug("Reaction is from self. Ignoring.") return if payload.message_id in self._starred_message_ids: - logger.debug("Already on starboard. Ignoring") return if payload.emoji.name != "⭐": - logger.debug("Reaction not ⭐. Ignoring.") return - # Reaction is star, and not already on starboard. Check for eligibility + # Check if message has enough stars channel: discord.TextChannel = self.bot.get_channel(payload.channel_id) message: discord.Message = await channel.fetch_message( payload.message_id ) count = 0 for reaction in message.reactions: - logger.debug(f"Reaction: {reaction}, Emoji: {reaction.emoji}") if reaction.emoji == "⭐": count += 1 - logger.debug(f"Found ⭐ reaction. Count: {count}") if count >= REACTION_LIMIT: - logger.debug("Enough star reactions for starboard.") break else: - logger.debug("Not enough star reactions") return - # Enough to become a new starboard message. + # Create starboard entry starboard_channel: discord.TextChannel = self.bot.get_channel( STARBOARD_TEXT_CHANNEL_ID ) @@ -121,12 +107,12 @@ async def on_raw_reaction_add( "type": "rich", "description": message.clean_content, } - logger.debug(f"Embed: {embed}") self._starred_message_ids.add(message.id) - logger.debug("Creating Starboard Message...") self._last_message = await starboard_channel.send( embed=discord.Embed.from_dict(embed) ) - logger.debug("Adding Star reaction to new Starboard Message...") await self._last_message.add_reaction("⭐") - logger.debug("Starboard Message Election complete!") + + logger.info( + f"New starboard entry: message {message.id} by {message.author}" + ) diff --git a/am_bot/cogs/workshop.py b/am_bot/cogs/workshop.py index 46619d7..6ff9278 100644 --- a/am_bot/cogs/workshop.py +++ b/am_bot/cogs/workshop.py @@ -23,10 +23,6 @@ def cog_load(self) -> None: @commands.Cog.listener() async def on_voice_state_update(self, member, before, after): - logger.debug( - f"Voice state activity:: Member: {member} " - f"Before: {before}, After: {after}" - ) if ( before.channel is None or before.channel.id != WORKSHOP_VOICE_CHANNEL_ID @@ -34,14 +30,9 @@ async def on_voice_state_update(self, member, before, after): after.channel is not None and after.channel.id == WORKSHOP_VOICE_CHANNEL_ID ): - # Add role for one-time join - logger.debug( - "Member joined AMC Workshop Voice Channel. " - "Adding AMC Workshop role..." - ) + # Member joined workshop voice channel + logger.info(f"{member} joined AMC Workshop voice channel") await member.add_roles(member.guild.get_role(WORKSHOP_ROLE_ID)) - # Add member overwrite permissions to view text channel - logger.debug("Adding member to text chat...") channel = member.guild.get_channel( channel_id=WORKSHOP_TEXT_CHANNEL_ID ) @@ -53,11 +44,8 @@ async def on_voice_state_update(self, member, before, after): after.channel is None or after.channel.id != WORKSHOP_VOICE_CHANNEL_ID ): - # Remove text channel member overwrite - logger.debug( - "Member has left AMC Workshop Voice Channel. " - "Removing member from text chat..." - ) + # Member left workshop voice channel + logger.info(f"{member} left AMC Workshop voice channel") channel = member.guild.get_channel( channel_id=WORKSHOP_TEXT_CHANNEL_ID ) @@ -65,14 +53,8 @@ async def on_voice_state_update(self, member, before, after): async def text_cleanup_task(self): await asyncio.sleep(10) - logger.debug( - "AMC Workshop Text Cleanup Task Starting. Fetching Channel..." - ) channel = await self.bot.fetch_channel(WORKSHOP_TEXT_CHANNEL_ID) - logger.debug(f"Channel Fetched: {channel}") while True: purge_time = datetime.utcnow() - timedelta(days=1) - logger.debug(f"Purging messages older than: {purge_time}") await channel.purge(before=purge_time) - logger.debug("Sleeping 10 minutes...") await asyncio.sleep(600) diff --git a/am_bot/logging_config.py b/am_bot/logging_config.py new file mode 100644 index 0000000..a2bfb29 --- /dev/null +++ b/am_bot/logging_config.py @@ -0,0 +1,63 @@ +"""Centralized logging configuration for the ARK Modding Discord Bot. + +This module provides a structured logging setup with timestamps, log levels, +and module names to make it easy to distinguish logs between different cogs. + +Usage: + from am_bot.logging_config import setup_logging + setup_logging() # Call once at application startup + +Environment Variables: + LOG_LEVEL: Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + Default: INFO +""" + +import logging +import os +import sys + + +# Log format with timestamp, level, module, and message +LOG_FORMAT = "%(asctime)s [%(levelname)-8s] %(name)s: %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def setup_logging(level: str | None = None) -> None: + """Configure logging for the entire application. + + Args: + level: Optional log level override. If not provided, uses LOG_LEVEL + environment variable or defaults to INFO. + """ + # Determine log level from argument, env var, or default + log_level_str = level or os.getenv("LOG_LEVEL", "INFO") + log_level = getattr(logging, log_level_str.upper(), logging.INFO) + + # Create formatter + formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # Remove any existing handlers to avoid duplicates + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Create and configure console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # Configure discord.py library logging (less verbose) + discord_logger = logging.getLogger("discord") + discord_logger.setLevel(logging.WARNING) + + # Configure discord.http specifically (very noisy at DEBUG) + discord_http_logger = logging.getLogger("discord.http") + discord_http_logger.setLevel(logging.WARNING) + + # Log initial setup message + app_logger = logging.getLogger(__name__) + app_logger.info(f"Logging configured at {log_level_str.upper()} level") diff --git a/pyproject.toml b/pyproject.toml index 6bdfa58..af9fc52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ addopts = "-v --tb=short" [tool.coverage.run] relative_files = true source = ["am_bot"] -omit = ["tests/*"] +omit = ["tests/*", "am_bot/logging_config.py"] [tool.coverage.report] exclude_lines = [ diff --git a/run.py b/run.py index b49b41e..7adf73d 100644 --- a/run.py +++ b/run.py @@ -1,13 +1,10 @@ -import logging import os from am_bot import ARKBot +from am_bot.logging_config import setup_logging -logger = logging.getLogger() -logger.addHandler(logging.StreamHandler()) -logger.setLevel(logging.DEBUG) -logger.propagate = True +setup_logging() if __name__ == "__main__":