Add job event listener for missed jobs and errors; integrate Sentry for error tracking

This commit is contained in:
2025-02-09 00:33:39 +01:00
parent b843364e1e
commit 39ecf4bb6c
3 changed files with 55 additions and 18 deletions

View File

@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Any
import discord import discord
import sentry_sdk import sentry_sdk
from apscheduler import events
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED, JobExecutionEvent
from apscheduler.job import Job from apscheduler.job import Job
from discord.abc import PrivateChannel from discord.abc import PrivateChannel
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
@ -41,6 +43,27 @@ scheduler: settings.AsyncIOScheduler = get_scheduler()
msg_to_cleanup: list[discord.InteractionMessage] = [] 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): class RemindBotClient(discord.Client):
"""Custom client class for the bot.""" """Custom client class for the bot."""
@ -124,6 +147,7 @@ class RemindBotClient(discord.Client):
async def setup_hook(self) -> None: async def setup_hook(self) -> None:
"""Setup the bot.""" """Setup the bot."""
scheduler.start() scheduler.start()
scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
jobs: list[Job] = scheduler.get_jobs() jobs: list[Job] = scheduler.get_jobs()
if not jobs: if not jobs:
logger.info("No jobs available.") logger.info("No jobs available.")

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import sentry_sdk
from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.date import DateTrigger
from loguru import logger from loguru import logger
@ -24,8 +25,16 @@ def calculate(job: Job) -> str | None:
# Check if the job is paused # Check if the job is paused
if trigger_time is None: if trigger_time is None:
logger.error(f"Couldn't calculate time for job: {job.id}") with sentry_sdk.push_scope() as scope:
logger.error(f"State: {job.__getstate__() if hasattr(job, '__getstate__') else 'No state'}") 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 None
return f"<t:{int(trigger_time.timestamp())}:R>" return f"<t:{int(trigger_time.timestamp())}:R>"

View File

@ -11,7 +11,7 @@ from apscheduler.triggers.interval import IntervalTrigger
from discord.ui import Button, Select from discord.ui import Button, Select
from loguru import logger 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 from discord_reminder_bot.parser import parse_time
if TYPE_CHECKING: if TYPE_CHECKING:
@ -309,33 +309,33 @@ class JobManagementView(discord.ui.View):
job_msg: str | int = job_kwargs.get("message", "No message found") job_msg: str | int = job_kwargs.get("message", "No message found")
msg: str = f"**Job '{job_msg}' has been deleted.**\n" 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 # The time the job was supposed to run
if hasattr(self.job, "next_run_time"): if hasattr(self.job, "next_run_time"):
if 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: else:
msg += "**Next run time**: Paused\n" msg += "\n**Next run time**: Paused"
msg += f"**Trigger**: {self.job.trigger}\n" msg += f"\n**Trigger**: {self.job.trigger}"
else: else:
msg += "**Next run time**: Pending\n" msg += "\n**Next run time**: Pending\n"
# The Discord user who created the job # The Discord user who created the job
if job_kwargs.get("author_id"): 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 # The Discord channel to send the message to
if job_kwargs.get("channel_id"): 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 # The Discord user to send the message to
if job_kwargs.get("user_id"): 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 # The Discord guild to send the message to
if job_kwargs.get("guild_id"): 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}") logger.debug(f"Deletion message: {msg}")
@ -390,11 +390,15 @@ class JobManagementView(discord.ui.View):
job_author: int = job_kwargs.get("author_id", 0) 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}>." 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"): if hasattr(self.job, "next_run_time"):
trigger_time: datetime.datetime | None = ( if self.job.next_run_time:
self.job.trigger.run_date if isinstance(self.job.trigger, DateTrigger) else self.job.next_run_time msg += f"\n**Next run time**: {self.job.next_run_time} ({calculate(self.job)})"
) else:
msg += f"\nNext run time: {trigger_time} {calculate(self.job)}" 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) await interaction.followup.send(msg)
@ -416,5 +420,5 @@ class JobManagementView(discord.ui.View):
bool: Whether the interaction is valid. bool: Whether the interaction is valid.
""" """
logger.info(f"Checking interaction for job: {self.job.id}") logger.info(f"Checking interaction for job: {self.job.id}")
self.update_buttons() # self.update_buttons()
return True return True