diff --git a/.vscode/settings.json b/.vscode/settings.json
index 58bda0c..6690fa8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,6 +18,7 @@
         "jobstore",
         "jobstores",
         "levelname",
+        "loguru",
         "Lovinator",
         "pycodestyle",
         "pydocstyle",
diff --git a/discord_reminder_bot/main.py b/discord_reminder_bot/main.py
index 4059ba4..c46fdb2 100644
--- a/discord_reminder_bot/main.py
+++ b/discord_reminder_bot/main.py
@@ -3,7 +3,6 @@ from __future__ import annotations
 import asyncio
 import datetime
 import json
-import logging
 import tempfile
 from pathlib import Path
 from typing import TYPE_CHECKING, Any
@@ -13,6 +12,7 @@ import sentry_sdk
 from apscheduler.job import Job
 from discord.abc import PrivateChannel
 from discord_webhook import DiscordWebhook
+from loguru import logger
 
 from discord_reminder_bot.misc import calc_time, calculate
 from discord_reminder_bot.parser import parse_time
@@ -28,17 +28,13 @@ if TYPE_CHECKING:
 
     from discord_reminder_bot import settings
 
-logger: logging.Logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG)
-logging.getLogger("discord.client").setLevel(logging.INFO)
 
 sentry_sdk.init(
     dsn="https://c4c61a52838be9b5042144420fba5aaa@o4505228040339456.ingest.us.sentry.io/4508707268984832",
     traces_sample_rate=1.0,
+    send_default_pii=True,
 )
 
-GUILD_ID = discord.Object(id=341001473661992962)
-
 scheduler: settings.AsyncIOScheduler = get_scheduler()
 msg_to_cleanup: list[discord.InteractionMessage] = []
 
@@ -58,7 +54,7 @@ class RemindBotClient(discord.Client):
     async def on_error(self, event_method: str, *args: list[Any], **kwargs: dict[str, Any]) -> None:
         """Log errors that occur in the bot."""
         # Log the error
-        logger.exception("An error occurred in %s", event_method)
+        logger.exception(f"An error occurred in {event_method} with args: {args} and kwargs: {kwargs}")
 
         # Add context to Sentry
         with sentry_sdk.push_scope() as scope:
@@ -100,26 +96,26 @@ class RemindBotClient(discord.Client):
 
     async def on_ready(self) -> None:
         """Log when the bot is ready."""
-        logger.info("Logged in as %s (%s)", self.user, self.user.id if self.user else "N/A ID")
+        logger.info(f"Logged in as {self.user} ({self.user.id if self.user else 'Unknown'})")
 
     async def close(self) -> None:
         """Close the bot and cleanup views."""
         logger.info("Closing bot and cleaning up views.")
         for msg in msg_to_cleanup:
-            logger.debug("Removing view: %s", msg.id)
+            logger.debug(f"Removing view: {msg.id}")
             try:
                 # If the message is "/remind list timed out.", skip it
-                if "/remind list timed out." in msg.content:
-                    logger.debug("Message %s is a timeout message. Skipping.", msg.id)
+                if "`/remind list` timed out." in msg.content:
+                    logger.debug(f"Message {msg.id} is a timeout message. Skipping.")
                     continue
 
                 await msg.delete()
             except discord.HTTPException as e:
                 if e.status != 401:
                     # Skip if the webhook token is invalid
-                    logger.error("Failed to remove view: %s", e)  # noqa: TRY400
+                    logger.error(f"Failed to remove view: {msg.id} - {e.text} - {e.status} - {e.code}")
             except asyncio.exceptions.CancelledError:
-                logger.error("Failed to remove view: Task was cancelled.")  # noqa: TRY400
+                logger.error("Failed to remove view: Task was cancelled.")
 
         return await super().close()
 
@@ -138,13 +134,12 @@ class RemindBotClient(discord.Client):
                 time: str = "Paused"
                 if hasattr(job, "next_run_time") and job.next_run_time and isinstance(job.next_run_time, datetime.datetime):
                     time = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
+                logger.info(rf"\t{job.id}: {job.name} - {time} - {msg}")
 
-                logger.info("\t%s: %s (%s)", msg[:50] or "No message", time, job.id)
-        except Exception:
+        except (AttributeError, LookupError):
             logger.exception("Failed to loop through jobs")
 
-        self.tree.copy_global_to(guild=GUILD_ID)
-        await self.tree.sync(guild=GUILD_ID)
+        await self.tree.sync()
 
 
 class RemindGroup(discord.app_commands.Group):
@@ -178,8 +173,8 @@ class RemindGroup(discord.app_commands.Group):
         # TODO(TheLovinator): Check if we have access to the channel and user # noqa: TD003
         await interaction.response.defer()
 
-        logger.info("New reminder from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel)
-        logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None})
+        logger.info(f"New reminder from {interaction.user} ({interaction.user.id}) in {interaction.channel}")
+        logger.info(f"Arguments: {locals()}")
 
         # Check if we have access to the specified channel or the current channel
         target_channel: InteractionChannel | None = channel or interaction.channel
@@ -218,7 +213,7 @@ class RemindGroup(discord.app_commands.Group):
                     "message": message,
                 },
             )
-            logger.info("User reminder job created: %s for %s at %s", user_reminder, user.id, time)
+            logger.info(f"User reminder job created: {user_reminder} for {user.id} at {parsed_time}")
 
             dm_message = f" and a DM to {user.display_name}"
             if not dm_and_current_channel:
@@ -239,7 +234,7 @@ class RemindGroup(discord.app_commands.Group):
                 "author_id": interaction.user.id,
             },
         )
-        logger.info("Channel reminder job created: %s for %s at %s", channel_job, channel_id, time)
+        logger.info(f"Channel reminder job created: {channel_job} for {channel_id}")
 
         msg: str = (
             f"Hello {interaction.user.display_name},\n"
@@ -251,7 +246,7 @@ class RemindGroup(discord.app_commands.Group):
 
     # /remind event
     @discord.app_commands.command(name="event", description="Add a new Discord event.")
-    async def add_event(  # noqa: PLR0913, PLR0917, PLR6301
+    async def add_event(  # noqa: C901, PLR0913, PLR0917, PLR6301
         self,
         interaction: discord.Interaction,
         message: str,
@@ -272,8 +267,8 @@ class RemindGroup(discord.app_commands.Group):
         """
         await interaction.response.defer()
 
-        logger.info("New event from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel)
-        logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None})
+        logger.info(f"New event from {interaction.user} ({interaction.user.id}) in {interaction.channel}")
+        logger.info(f"Arguments: {locals()}")
 
         guild: discord.Guild | None = interaction.guild
         if not guild:
@@ -296,15 +291,19 @@ class RemindGroup(discord.app_commands.Group):
 
         reason_msg: str = f"Event created by {interaction.user} ({interaction.user.id})."
 
-        event: discord.ScheduledEvent = await guild.create_scheduled_event(
-            name=message,
-            start_time=event_start_time,
-            entity_type=discord.EntityType.external,
-            privacy_level=discord.PrivacyLevel.guild_only,
-            end_time=event_end_time,
-            reason=reason or reason_msg,
-            location=location,
-        )
+        try:
+            event: discord.ScheduledEvent = await guild.create_scheduled_event(
+                name=message,
+                start_time=event_start_time,
+                entity_type=discord.EntityType.external,
+                privacy_level=discord.PrivacyLevel.guild_only,
+                end_time=event_end_time,
+                reason=reason or reason_msg,
+                location=location,
+            )
+        except discord.Forbidden as e:
+            await interaction.followup.send(content=f"I don't have permission to create events in this guild: {e}", ephemeral=True)
+            return
 
         if start_immediately:
             await event.start()
@@ -354,12 +353,12 @@ class RemindGroup(discord.app_commands.Group):
         for job in jobs:
             # If the job has guild_id and it's not the current guild, skip it
             if job.kwargs.get("guild_id") and job.kwargs.get("guild_id") != guild.id:
-                logger.debug("Skipping job: %s because it's not in the current guild.", job.id)
+                logger.debug(f"Skipping job: {job.id} because it's not in the current guild.")
                 continue
 
             # If the job has channel_id and it's not in the current guild, skip it
             if job.kwargs.get("channel_id") and job.kwargs.get("channel_id") not in list_of_channels_in_current_guild:
-                logger.debug("Skipping job: %s because it's not in the current guild's channels.", job.id)
+                logger.debug(f"Skipping job: {job.id} because it's not in the current guild.")
                 continue
 
             jobs_in_guild.append(job)
@@ -421,15 +420,15 @@ class RemindGroup(discord.app_commands.Group):
         try:
             await interaction.response.defer()
         except discord.HTTPException as e:
-            logger.exception("Failed to defer interaction: text=%s, status=%s, code=%s", e.text, e.status, e.code)
+            logger.exception(f"Failed to defer interaction: {e.text=}, {e.status=}, {e.code=}")
             return
         except discord.InteractionResponded as e:
-            logger.exception("Interaction already responded to - interaction: %s", e.interaction)
+            logger.exception(f"Interaction already responded to - interaction: {interaction}, {e}")
             return
 
         # Log kwargs
-        logger.info("New cron job from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel)
-        logger.info("Cron job arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None})
+        logger.info(f"New cron job from {interaction.user} ({interaction.user.id}) in {interaction.channel}")
+        logger.info(f"Cron job arguments: {locals()}")
 
         # Get the channel ID
         channel_id: int | None = channel.id if channel else (interaction.channel.id if interaction.channel else None)
@@ -548,8 +547,8 @@ class RemindGroup(discord.app_commands.Group):
         """  # noqa: E501
         await interaction.response.defer()
 
-        logger.info("New interval job from %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel)
-        logger.info("Arguments: %s", {k: v for k, v in locals().items() if k != "self" and v is not None})
+        logger.info(f"New interval job from {interaction.user} ({interaction.user.id}) in {interaction.channel}")
+        logger.info(f"Arguments: {locals()}")
 
         # Only allow intervals of 30 seconds or more so we don't spam the channel
         if weeks == days == hours == minutes == 0 and seconds < 30:
@@ -673,18 +672,18 @@ class RemindGroup(discord.app_commands.Group):
             # Can't be 0 because that's the default value for jobs without a guild
             guild_id: int = interaction.guild.id if interaction.guild else -1
             channels_in_this_guild: list[int] = [c.id for c in interaction.guild.channels] if interaction.guild else []
-            logger.debug("Guild ID: %s, Channels in this guild: %s", guild_id, channels_in_this_guild)
+            logger.debug(f"Guild ID: {guild_id}")
 
             for job in jobs_data.get("jobs", []):
                 # Check if the job is in the current guild
                 job_guild_id = job.get("kwargs", {}).get("guild_id", 0)
                 if job_guild_id and job_guild_id != guild_id:
-                    logger.debug("Removing job: %s because it's not in the current guild. %s vs %s", job.get("id"), job_guild_id, guild_id)
+                    logger.debug(f"Removing job: {job.get('id')} because it's not in the current guild.")
                     jobs_data["jobs"].remove(job)
 
                 # Check if the channel is in the current guild
                 if job.get("kwargs", {}).get("channel_id") not in channels_in_this_guild:
-                    logger.debug("Removing job: %s because it's not in the current guild's channels.", job.get("id"))
+                    logger.debug(f"Removing job: {job.get('id')} because it's not in the current guild.")
                     jobs_data["jobs"].remove(job)
 
         msg: str = "All reminders in this server have been backed up." if not all_servers else "All reminders have been backed up."
@@ -712,7 +711,7 @@ class RemindGroup(discord.app_commands.Group):
         """
         await interaction.response.defer()
 
-        logger.info("Restoring reminders from file for %s (%s) in %s", interaction.user, interaction.user.id, interaction.channel)
+        logger.info(f"Restoring reminders from file for {interaction.user} ({interaction.user.id}) in {interaction.channel}")
 
         # Get the old jobs
         old_jobs: list[Job] = scheduler.get_jobs()
@@ -755,7 +754,7 @@ class RemindGroup(discord.app_commands.Group):
 
         # Save the file to a temporary file and import the jobs
         with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding="utf-8", suffix=".json") as temp_file:
-            logger.info("Saving attachment to %s", temp_file.name)
+            logger.info(f"Saving attachment to {temp_file.name}")
             await attachment.save(Path(temp_file.name))
 
             # Load the jobs data from the file
@@ -763,7 +762,7 @@ class RemindGroup(discord.app_commands.Group):
             jobs_data: dict = json.load(temp_file)
 
             logger.info("Importing jobs from file")
-            logger.debug("Jobs data: %s", jobs_data)
+            logger.debug(f"Jobs data: {jobs_data}")
 
             with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding="utf-8", suffix=".json") as temp_import_file:
                 # We can't import jobs with the same ID so remove them from the JSON
@@ -771,10 +770,10 @@ class RemindGroup(discord.app_commands.Group):
                 jobs_already_exist = [job.get("id") for job in jobs_data.get("jobs", []) if scheduler.get_job(job.get("id"))]
                 jobs_data["jobs"] = jobs
                 for job_id in jobs_already_exist:
-                    logger.debug("Removed job: %s because it already exists.", job_id)
+                    logger.debug(f"Removed job: {job_id} because it already exists.")
 
-                logger.debug("Jobs data after removing existing jobs: %s", jobs_data)
-                logger.info("Jobs already exist: %s", jobs_already_exist)
+                logger.debug(f"Jobs data after removing existing jobs: {jobs_data}")
+                logger.info(f"Jobs already exist: {jobs_already_exist}")
 
                 # Write the new data to a temporary file
                 json.dump(jobs_data, temp_import_file)
@@ -823,7 +822,7 @@ def send_webhook(url: str = "", message: str = "") -> None:
 
     if not url:
         url = get_webhook_url()
-        logger.error("No webhook URL provided. Using the one from settings.")
+        logger.error(f"No webhook URL provided. Using the one from settings: {url}")
         webhook: DiscordWebhook = DiscordWebhook(
             url=url,
             username="discord-reminder-bot",
@@ -851,7 +850,7 @@ async def send_to_discord(channel_id: int, message: str, author_id: int) -> None
 
     # Channels we can't send messages to
     if isinstance(channel, discord.ForumChannel | discord.CategoryChannel | PrivateChannel):
-        logger.warning("We haven't implemented sending messages to this channel type (%s)", type(channel))
+        logger.warning(f"We haven't implemented sending messages to this channel type {type(channel)}")
         return
 
     await channel.send(f"<@{author_id}>\n{message}")
@@ -865,17 +864,17 @@ async def send_to_user(user_id: int, guild_id: int, message: str) -> None:
         guild_id: The guild ID to get the user from.
         message: The message to send.
     """
-    logger.info("Sending message to user %s in guild %s:\n%s", user_id, guild_id, message)
+    logger.info(f"Sending message to user {user_id} in guild {guild_id}")
     try:
         guild: discord.Guild | None = bot.get_guild(guild_id)
         if guild is None:
             guild = await bot.fetch_guild(guild_id)
     except discord.NotFound:
         current_guilds: Sequence[discord.Guild] = bot.guilds
-        logger.exception("Guild not found. Current guilds: %s", current_guilds)
+        logger.exception(f"Guild not found. Current guilds: {current_guilds}")
         return
     except discord.HTTPException:
-        logger.exception("Failed to fetch guild")
+        logger.exception(f"Failed to fetch guild {guild_id}")
         return
 
     try:
@@ -883,21 +882,21 @@ async def send_to_user(user_id: int, guild_id: int, message: str) -> None:
         if member is None:
             member = await guild.fetch_member(user_id)
     except discord.Forbidden:
-        logger.exception("We do not have access to the guild. Guild: %s, User: %s", guild_id, user_id)
+        logger.exception(f"We do not have access to the guild. Guild: {guild_id}, User: {user_id}")
         return
     except discord.NotFound:
-        logger.exception("Member not found. Guild: %s, User: %s", guild_id, user_id)
+        logger.exception(f"Member not found. Guild: {guild_id}, User: {user_id}")
         return
     except discord.HTTPException:
-        logger.exception("Fetching the member failed. Guild: %s, User: %s", guild_id, user_id)
+        logger.exception(f"Fetching the member failed. Guild: {guild_id}, User: {user_id}")
         return
 
     try:
         await member.send(message)
     except discord.HTTPException:
-        logger.exception("Failed to send message (%s) to user (%s) in guild (%s)", message, user_id, guild_id)
+        logger.exception(f"Failed to send message '{message}' to user '{user_id}' in guild '{guild_id}'")
 
 
 if __name__ == "__main__":
     bot_token: str = get_bot_token()
-    bot.run(bot_token, root_logger=True)
+    bot.run(bot_token)
diff --git a/discord_reminder_bot/misc.py b/discord_reminder_bot/misc.py
index fc53bce..1fa584b 100644
--- a/discord_reminder_bot/misc.py
+++ b/discord_reminder_bot/misc.py
@@ -1,17 +1,15 @@
 from __future__ import annotations
 
-import logging
 from typing import TYPE_CHECKING
 
 from apscheduler.triggers.date import DateTrigger
+from loguru import logger
 
 if TYPE_CHECKING:
     import datetime
 
     from apscheduler.job import Job
 
-logger: logging.Logger = logging.getLogger(__name__)
-
 
 def calculate(job: Job) -> str | None:
     """Calculate the time left for a job.
@@ -26,8 +24,8 @@ def calculate(job: Job) -> str | None:
 
     # Check if the job is paused
     if trigger_time is None:
-        logger.error("Couldn't calculate time for job: %s: %s", job.id, job.name)
-        logger.error("State: %s", job.__getstate__() if hasattr(job, "__getstate__") else "No state")
+        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
 
     return f"<t:{int(trigger_time.timestamp())}:R>"
diff --git a/discord_reminder_bot/parser.py b/discord_reminder_bot/parser.py
index 9850d22..3d23220 100644
--- a/discord_reminder_bot/parser.py
+++ b/discord_reminder_bot/parser.py
@@ -1,15 +1,13 @@
 from __future__ import annotations
 
 import datetime
-import logging
 from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
 
 import dateparser
+from loguru import logger
 
 from discord_reminder_bot.settings import get_timezone
 
-logger: logging.Logger = logging.getLogger(__name__)
-
 
 def parse_time(date_to_parse: str | None, timezone: str | None = None, use_dotenv: bool = True) -> datetime.datetime | None:  # noqa: FBT001, FBT002
     """Parse a date string into a datetime object.
@@ -22,7 +20,7 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten
     Returns:
         datetime.datetime: The parsed datetime object.
     """
-    logger.info("Parsing date: '%s' with timezone: '%s'", date_to_parse, timezone)
+    logger.info(f"Parsing date: '{date_to_parse}' with timezone: '{timezone}'")
 
     if not date_to_parse:
         logger.error("No date provided to parse.")
@@ -35,7 +33,7 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten
     try:
         tz = ZoneInfo(timezone)
     except (ZoneInfoNotFoundError, ModuleNotFoundError):
-        logger.error("Invalid timezone provided: '%s'. Using default timezone: '%s'", timezone, get_timezone(use_dotenv))  # noqa: TRY400
+        logger.error(f"Invalid timezone provided: '{timezone}'. Using {get_timezone(use_dotenv)} instead.")
         tz = ZoneInfo("UTC")
 
     try:
@@ -51,4 +49,6 @@ def parse_time(date_to_parse: str | None, timezone: str | None = None, use_doten
     except (ValueError, TypeError):
         return None
 
+    logger.debug(f"Parsed date: {parsed_date} from '{date_to_parse}'")
+
     return parsed_date
diff --git a/discord_reminder_bot/ui.py b/discord_reminder_bot/ui.py
index 7b734bd..8faac34 100644
--- a/discord_reminder_bot/ui.py
+++ b/discord_reminder_bot/ui.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import logging
 from typing import TYPE_CHECKING
 
 import discord
@@ -10,6 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
 from apscheduler.triggers.cron import CronTrigger
 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.parser import parse_time
@@ -21,9 +21,6 @@ if TYPE_CHECKING:
     from apscheduler.schedulers.asyncio import AsyncIOScheduler
 
 
-logger: logging.Logger = logging.getLogger(__name__)
-
-
 class ModifyJobModal(discord.ui.Modal, title="Modify Job"):
     """Modal for modifying a job."""
 
@@ -53,12 +50,12 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"):
         self.job_name.placeholder = self.job.kwargs.get("message", "No message found")
         self.job_date.placeholder = self.job.next_run_time.strftime("%Y-%m-%d %H:%M:%S %Z")
 
-        logger.info("Job '%s' Modal created", self.job.name)
-        logger.info("\tCurrent date: '%s'", self.job.next_run_time)
-        logger.info("\tCurrent message: '%s'", self.job.kwargs.get("message", "N/A"))
+        logger.info(f"Job '{job_name_label}' modified: Initializing modal")
+        logger.info(f"\tCurrent date: '{self.job.next_run_time}'")
+        logger.info(f"\tCurrent message: '{self.job.kwargs.get('message', 'No message found')}")
 
-        logger.info("\tName label: '%s'", self.job_name.label)
-        logger.info("\tDate label: '%s'", self.job_date.label)
+        logger.info(f"\tName label: '{self.job_name.label}'")
+        logger.info(f"\tDate label: '{self.job_date.label}'")
 
     async def on_submit(self, interaction: discord.Interaction) -> None:
         """Submit the job modifications.
@@ -66,27 +63,29 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"):
         Args:
             interaction: The interaction object for the command.
         """
-        logger.info("Job '%s' modified: Submitting changes", self.job.name)
+        job_msg: str = self.job.kwargs.get("message", "No message found")
+        logger.info(f"Job '{job_msg}' modified: Submitting changes")
         new_name: str = self.job_name.value
         new_date_str: str = self.job_date.value
         old_date: datetime.datetime = self.job.next_run_time
 
         # if both are empty, do nothing
         if not new_name and not new_date_str:
-            logger.info("Job '%s' modified: No changes submitted", self.job.name)
+            logger.info(f"Job '{job_msg}' modified: No changes submitted.")
 
             await interaction.response.send_message(
-                content=f"Job **{self.job.name}** was not modified by {interaction.user.mention}.\nNo changes submitted.",
+                content=f"Job **{job_msg}**.\nNo changes submitted.",
+                ephemeral=True,
             )
             return
 
         if new_date_str and new_date_str != old_date.strftime("%Y-%m-%d %H:%M:%S %Z"):
             new_date: datetime.datetime | None = parse_time(new_date_str)
             if not new_date:
-                logger.error("Job '%s' modified: Failed to parse date: '%s'", self.job.name, new_date_str)
+                logger.error(f"Job '{job_msg}' modified: Failed to parse date: '{new_date_str}'")
                 await interaction.response.send_message(
                     content=(
-                        f"Failed modifying job **{self.job.name}**\n"
+                        f"Failed modifying job **{job_msg}**\n"
                         f"Job ID: **{self.job.id}**\n"
                         f"Failed to parse date: **{new_date_str}**\n"
                         f"Defaulting to old date: **{old_date.strftime('%Y-%m-%d %H:%M:%S')}** {calc_time(old_date)}"
@@ -94,8 +93,8 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"):
                 )
                 return
 
-            logger.info("Job '%s' modified: New date: '%s'", self.job.name, new_date)
-            logger.info("Job '%s' modified: Old date: '%s'", self.job.name, old_date)
+            logger.info(f"Job '{job_msg}' modified: New date: '{new_date}'")
+            logger.info(f"Job '{job_msg}' modified: Old date: '{old_date}'")
             self.job.modify(next_run_time=new_date)
 
             old_date_str: str = old_date.strftime("%Y-%m-%d %H:%M:%S")
@@ -103,16 +102,16 @@ class ModifyJobModal(discord.ui.Modal, title="Modify Job"):
 
             await interaction.response.send_message(
                 content=(
-                    f"Job **{self.job.name}** was modified by {interaction.user.mention}:\n"
+                    f"Job **{job_msg}** was modified by {interaction.user.mention}:\n"
                     f"Job ID: **{self.job.id}**\n"
                     f"Old date: **{old_date_str}** {calculate(self.job)} {calc_time(old_date)}\n"
                     f"New date: **{new_date_str}** {calculate(self.job)} {calc_time(new_date)}"
                 ),
             )
 
-        if self.job_name.value and self.job.name != new_name:
-            logger.info("Job '%s' modified: New name: '%s'", self.job.name, new_name)
-            logger.info("Job '%s' modified: Old name: '%s'", self.job.name, self.job.name)
+        if self.job_name.value and job_msg != new_name:
+            logger.info(f"Job '{job_msg}' modified: New name: '{new_name}'")
+            logger.info(f"Job '{job_msg}' modified: Old name: '{job_msg}'")
             self.job.modify(name=new_name)
 
             await interaction.response.send_message(
@@ -187,12 +186,12 @@ class JobSelector(Select):
         for job in jobs:
             # If the job has guild_id and it's not the current guild, skip it
             if job.kwargs.get("guild_id") and job.kwargs.get("guild_id") != guild.id:
-                logger.debug("Skipping job: %s because it's not in the current guild.", job.id)
+                logger.debug(f"Skipping job: {job.id} because it's not in the current guild.")
                 continue
 
             # If the job has channel_id and it's not in the current guild, skip it
             if job.kwargs.get("channel_id") and job.kwargs.get("channel_id") not in list_of_channels_in_current_guild:
-                logger.debug("Skipping job: %s because it's not in the current guild's channels.", job.id)
+                logger.debug(f"Skipping job: {job.id} because it's not from a channel in the current guild.")
                 continue
 
             jobs_in_guild.append(job)
@@ -252,14 +251,14 @@ class JobManagementView(discord.ui.View):
         self.add_item(JobSelector(scheduler, self.guild))
         self.update_buttons()
 
-        logger.debug("JobManagementView created for job: %s", job.id)
+        logger.debug(f"JobManagementView created for job: {self.job.id}")
 
     async def on_timeout(self) -> None:
         """Handle the view timeout."""
         if self.message:
             await self.message.edit(content="`/remind list` timed out.", embed=None, view=None)
         else:
-            logger.debug("No message to edit for job: %s", self.job.id)
+            logger.debug(f"No message to edit for job: {self.job.id}")
         self.stop()
 
     async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
@@ -312,9 +311,9 @@ class JobManagementView(discord.ui.View):
         """
         job_kwargs: dict = self.job.kwargs or {}
 
-        logger.info("Deleting job: %s because %s clicked the button.", self.job.id, interaction.user.name)
+        logger.info(f"Deleting job: {self.job.id}. Clicked by {interaction.user.name}")
         if hasattr(self.job, "__getstate__"):
-            logger.debug("State: %s", self.job.__getstate__() if hasattr(self.job, "__getstate__") else "No state")
+            logger.debug(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}")
 
         job_msg: str | int = job_kwargs.get("message", "No message found")
         msg: str = f"**Job '{job_msg}' has been deleted.**\n"
@@ -346,7 +345,7 @@ class JobManagementView(discord.ui.View):
         if job_kwargs.get("guild_id"):
             msg += f"**Guild**: {job_kwargs.get('guild_id')}\n"
 
-        logger.debug("Deletion message: %s", msg)
+        logger.debug(f"Deletion message: {msg}")
 
         self.job.remove()
         await interaction.response.send_message(msg)
@@ -360,9 +359,9 @@ class JobManagementView(discord.ui.View):
             interaction: The interaction object for the command.
             button: The button that was clicked.
         """
-        logger.info("Modifying job: %s. Clicked by %s", self.job.id, interaction.user.name)
+        logger.info(f"Modifying job: {self.job.id}. Clicked by {interaction.user.name}")
         if hasattr(self.job, "__getstate__"):
-            logger.debug("State: %s", self.job.__getstate__() if hasattr(self.job, "__getstate__") else "No state")
+            logger.debug(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}")
 
         modal = ModifyJobModal(self.job, self.scheduler)
         await interaction.response.send_modal(modal)
@@ -377,17 +376,17 @@ class JobManagementView(discord.ui.View):
         """
         if hasattr(self.job, "next_run_time"):
             if self.job.next_run_time is None:
-                logger.info("State: %s", self.job.__getstate__())
+                logger.info(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}")
                 self.job.resume()
                 status = "resumed"
                 button.label = "Pause"
             else:
-                logger.info("State: %s", self.job.__getstate__())
+                logger.info(f"State: {self.job.__getstate__() if hasattr(self.job, '__getstate__') else 'No state'}")
                 self.job.pause()
                 status = "paused"
                 button.label = "Resume"
         else:
-            logger.error("Got a job without a next_run_time: %s", self.job.id)
+            logger.error(f"Got a job without a next_run_time: {self.job.id}")
             status: str = f"What is this? {self.job.__getstate__()}"
             button.label = "What?"
 
@@ -409,11 +408,11 @@ class JobManagementView(discord.ui.View):
 
     def update_buttons(self) -> None:
         """Update the visibility of buttons based on job status."""
-        logger.debug("Updating buttons for job: %s", self.job.id)
+        logger.debug(f"Updating buttons for job: {self.job.id}")
         self.pause_button.label = "Resume" if self.job.next_run_time is None else "Pause"
 
-        logger.debug("Pause button disabled: %s", self.pause_button.disabled)
-        logger.debug("Pause button label: %s", self.pause_button.label)
+        logger.debug(f"Pause button disabled: {self.pause_button.disabled}")
+        logger.debug(f"Pause button label: {self.pause_button.label}")
 
     async def interaction_check(self, interaction: discord.Interaction) -> bool:  # noqa: ARG002
         """Check the interaction and update buttons before responding.
@@ -424,11 +423,11 @@ class JobManagementView(discord.ui.View):
         Returns:
             bool: Whether the interaction is valid.
         """
-        logger.info("Interaction check for job: %s", self.job.id)
-        logger.debug("Timeout was %s before interaction check", self.timeout)
+        logger.info(f"Interaction check for job: {self.job.id}")
+        logger.debug(f"Timeout was {self.timeout} before interaction check.")
 
         self.timeout = 30
-        logger.debug("Checking interaction for job: %s", self.job.id)
+        logger.debug(f"Checking interaction for job: {self.job.id}")
 
         self.update_buttons()
         return True
diff --git a/pyproject.toml b/pyproject.toml
index a50b2c2..3212db4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,9 @@ dependencies = [
 
     # For error tracking
     "sentry-sdk>=2.20.0,<3.0.0", # https://github.com/getsentry/sentry-python
+
+    # For logging
+    "loguru>=0.7.3,<1.0.0", # https://github.com/Delgan/loguru
 ]
 
 [dependency-groups]
@@ -75,9 +78,14 @@ discord-webhook = {version = ">=1.3.1,<2.0.0"}
 # For loading environment variables from a .env file
 python-dotenv = {version = ">=1.0.1,<2.0.0"}
 
+# https://github.com/getsentry/sentry-python
 # For error tracking
 sentry-sdk = {version = ">=2.20.0,<3.0.0"}
 
+# https://github.com/Delgan/loguru
+# For logging
+loguru = {version = ">=0.7.3,<1.0.0"}
+
 [tool.poetry.dev-dependencies]
 pytest = "*"
 pre-commit = "*"