From 39ecf4bb6c18e72eee11f4fff654f79439426503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Sun, 9 Feb 2025 00:33:39 +0100 Subject: [PATCH] Add job event listener for missed jobs and errors; integrate Sentry for error tracking --- discord_reminder_bot/main.py | 24 ++++++++++++++++++++++++ discord_reminder_bot/misc.py | 15 ++++++++++++--- discord_reminder_bot/ui.py | 34 +++++++++++++++++++--------------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py index ca353aa..d03ddf4 100644 --- a/discord_reminder_bot/main.py +++ b/discord_reminder_bot/main.py @@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Any import discord import sentry_sdk +from apscheduler import events +from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED, JobExecutionEvent from apscheduler.job import Job from discord.abc import PrivateChannel from discord_webhook import DiscordWebhook @@ -41,6 +43,27 @@ scheduler: settings.AsyncIOScheduler = get_scheduler() msg_to_cleanup: list[discord.InteractionMessage] = [] +def my_listener(event: JobExecutionEvent) -> None: + """Listener for job events. + + Args: + event: The event that occurred. + """ + if event.code == events.EVENT_JOB_MISSED: + scheduled_time: str = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S") + msg: str = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}" + send_webhook(message=msg) + + if event.exception: + with sentry_sdk.push_scope() as scope: + scope.set_extra("job_id", event.job_id) + scope.set_extra("scheduled_run_time", event.scheduled_run_time.isoformat() if event.scheduled_run_time else "None") + scope.set_extra("event_code", event.code) + sentry_sdk.capture_exception(event.exception) + + send_webhook(f"discord-reminder-bot failed to send message to Discord\n{event}") + + class RemindBotClient(discord.Client): """Custom client class for the bot.""" @@ -124,6 +147,7 @@ class RemindBotClient(discord.Client): async def setup_hook(self) -> None: """Setup the bot.""" scheduler.start() + scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR) jobs: list[Job] = scheduler.get_jobs() if not jobs: logger.info("No jobs available.") diff --git a/discord_reminder_bot/misc.py b/discord_reminder_bot/misc.py index 1fa584b..f58313e 100644 --- a/discord_reminder_bot/misc.py +++ b/discord_reminder_bot/misc.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +import sentry_sdk from apscheduler.triggers.date import DateTrigger from loguru import logger @@ -24,9 +25,17 @@ def calculate(job: Job) -> str | None: # Check if the job is paused if trigger_time is None: - logger.error(f"Couldn't calculate time for job: {job.id}") - logger.error(f"State: {job.__getstate__() if hasattr(job, '__getstate__') else 'No state'}") - return None + with sentry_sdk.push_scope() as scope: + scope.set_tag("job_id", job.id) + scope.set_extra("job_state", job.__getstate__() if hasattr(job, "__getstate__") else "No state") + sentry_sdk.capture_exception(Exception("Couldn't calculate time for job")) + + msg: str = f"Couldn't calculate time for job: {job.id}" + if hasattr(job, "__getstate__"): + msg += f"State: {job.__getstate__()}" + + logger.error(msg) + return None return f"" diff --git a/discord_reminder_bot/ui.py b/discord_reminder_bot/ui.py index a5215f7..4323bd1 100644 --- a/discord_reminder_bot/ui.py +++ b/discord_reminder_bot/ui.py @@ -11,7 +11,7 @@ from apscheduler.triggers.interval import IntervalTrigger from discord.ui import Button, Select from loguru import logger -from discord_reminder_bot.misc import DateTrigger, calc_time, calculate +from discord_reminder_bot.misc import calc_time, calculate from discord_reminder_bot.parser import parse_time if TYPE_CHECKING: @@ -309,33 +309,33 @@ class JobManagementView(discord.ui.View): job_msg: str | int = job_kwargs.get("message", "No message found") msg: str = f"**Job '{job_msg}' has been deleted.**\n" - msg += f"**Job ID**: {self.job.id}\n" + msg += f"**Job ID**: {self.job.id}" # The time the job was supposed to run if hasattr(self.job, "next_run_time"): if self.job.next_run_time: - msg += f"**Next run time**: {self.job.next_run_time} ({calculate(self.job)})\n" + msg += f"\n**Next run time**: {self.job.next_run_time} ({calculate(self.job)})" else: - msg += "**Next run time**: Paused\n" - msg += f"**Trigger**: {self.job.trigger}\n" + msg += "\n**Next run time**: Paused" + msg += f"\n**Trigger**: {self.job.trigger}" else: - msg += "**Next run time**: Pending\n" + msg += "\n**Next run time**: Pending\n" # The Discord user who created the job if job_kwargs.get("author_id"): - msg += f"**Created by**: <@{job_kwargs.get('author_id')}>\n" + msg += f"\n**Created by**: <@{job_kwargs.get('author_id')}>" # The Discord channel to send the message to if job_kwargs.get("channel_id"): - msg += f"**Channel**: <#{job_kwargs.get('channel_id')}>\n" + msg += f"\n**Channel**: <#{job_kwargs.get('channel_id')}>" # The Discord user to send the message to if job_kwargs.get("user_id"): - msg += f"**User**: <@{job_kwargs.get('user_id')}>\n" + msg += f"\n**User**: <@{job_kwargs.get('user_id')}>" # The Discord guild to send the message to if job_kwargs.get("guild_id"): - msg += f"**Guild**: {job_kwargs.get('guild_id')}\n" + msg += f"\n**Guild**: {job_kwargs.get('guild_id')}" logger.debug(f"Deletion message: {msg}") @@ -390,11 +390,15 @@ class JobManagementView(discord.ui.View): job_author: int = job_kwargs.get("author_id", 0) msg: str = f"Job '{job_msg}' has been {status} by <@{interaction.user.id}>. Job was created by <@{job_author}>." + # The time the job was supposed to run if hasattr(self.job, "next_run_time"): - trigger_time: datetime.datetime | None = ( - self.job.trigger.run_date if isinstance(self.job.trigger, DateTrigger) else self.job.next_run_time - ) - msg += f"\nNext run time: {trigger_time} {calculate(self.job)}" + if self.job.next_run_time: + msg += f"\n**Next run time**: {self.job.next_run_time} ({calculate(self.job)})" + else: + msg += "\n**Next run time**: Paused" + msg += f"\n**Trigger**: {self.job.trigger}" + else: + msg += "\n**Next run time**: Pending" await interaction.followup.send(msg) @@ -416,5 +420,5 @@ class JobManagementView(discord.ui.View): bool: Whether the interaction is valid. """ logger.info(f"Checking interaction for job: {self.job.id}") - self.update_buttons() + # self.update_buttons() return True