prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Use appropriate log level for exceptions from event listeners
  From @SebastiaanZ:
  
  > Finally, `discord.py` currently "hides" errors/tracebacks that happen in event listeners as we only have a custom error handler for commands. This isn't too bad locally, since `d.py` **prints** those exceptions to stderr, but it obviously means they'll never show up in Sentry, as they are **not actually logged** with the appropriate level.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `bot/bot.py`
  Content:
  ```
  1 import asyncio
  2 import logging
  3 import socket
  4 import warnings
  5 from typing import Optional
  6 
  7 import aiohttp
  8 import discord
  9 from discord.ext import commands
  10 
  11 from bot import DEBUG_MODE, api, constants
  12 from bot.async_stats import AsyncStatsClient
  13 
  14 log = logging.getLogger('bot')
  15 
  16 
  17 class Bot(commands.Bot):
  18     """A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client."""
  19 
  20     def __init__(self, *args, **kwargs):
  21         if "connector" in kwargs:
  22             warnings.warn(
  23                 "If login() is called (or the bot is started), the connector will be overwritten "
  24                 "with an internal one"
  25             )
  26 
  27         super().__init__(*args, **kwargs)
  28 
  29         self.http_session: Optional[aiohttp.ClientSession] = None
  30         self.api_client = api.APIClient(loop=self.loop)
  31 
  32         self._connector = None
  33         self._resolver = None
  34         self._guild_available = asyncio.Event()
  35 
  36         statsd_url = constants.Stats.statsd_host
  37 
  38         if DEBUG_MODE:
  39             # Since statsd is UDP, there are no errors for sending to a down port.
  40             # For this reason, setting the statsd host to 127.0.0.1 for development
  41             # will effectively disable stats.
  42             statsd_url = "127.0.0.1"
  43 
  44         self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot")
  45 
  46     def add_cog(self, cog: commands.Cog) -> None:
  47         """Adds a "cog" to the bot and logs the operation."""
  48         super().add_cog(cog)
  49         log.info(f"Cog loaded: {cog.qualified_name}")
  50 
  51     def clear(self) -> None:
  52         """
  53         Clears the internal state of the bot and recreates the connector and sessions.
  54 
  55         Will cause a DeprecationWarning if called outside a coroutine.
  56         """
  57         # Because discord.py recreates the HTTPClient session, may as well follow suit and recreate
  58         # our own stuff here too.
  59         self._recreate()
  60         super().clear()
  61 
  62     async def close(self) -> None:
  63         """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver."""
  64         await super().close()
  65 
  66         await self.api_client.close()
  67 
  68         if self.http_session:
  69             await self.http_session.close()
  70 
  71         if self._connector:
  72             await self._connector.close()
  73 
  74         if self._resolver:
  75             await self._resolver.close()
  76 
  77         if self.stats._transport:
  78             await self.stats._transport.close()
  79 
  80     async def login(self, *args, **kwargs) -> None:
  81         """Re-create the connector and set up sessions before logging into Discord."""
  82         self._recreate()
  83         await self.stats.create_socket()
  84         await super().login(*args, **kwargs)
  85 
  86     def _recreate(self) -> None:
  87         """Re-create the connector, aiohttp session, and the APIClient."""
  88         # Use asyncio for DNS resolution instead of threads so threads aren't spammed.
  89         # Doesn't seem to have any state with regards to being closed, so no need to worry?
  90         self._resolver = aiohttp.AsyncResolver()
  91 
  92         # Its __del__ does send a warning but it doesn't always show up for some reason.
  93         if self._connector and not self._connector._closed:
  94             log.warning(
  95                 "The previous connector was not closed; it will remain open and be overwritten"
  96             )
  97 
  98         # Use AF_INET as its socket family to prevent HTTPS related problems both locally
  99         # and in production.
  100         self._connector = aiohttp.TCPConnector(
  101             resolver=self._resolver,
  102             family=socket.AF_INET,
  103         )
  104 
  105         # Client.login() will call HTTPClient.static_login() which will create a session using
  106         # this connector attribute.
  107         self.http.connector = self._connector
  108 
  109         # Its __del__ does send a warning but it doesn't always show up for some reason.
  110         if self.http_session and not self.http_session.closed:
  111             log.warning(
  112                 "The previous session was not closed; it will remain open and be overwritten"
  113             )
  114 
  115         self.http_session = aiohttp.ClientSession(connector=self._connector)
  116         self.api_client.recreate(force=True, connector=self._connector)
  117 
  118     async def on_guild_available(self, guild: discord.Guild) -> None:
  119         """
  120         Set the internal guild available event when constants.Guild.id becomes available.
  121 
  122         If the cache appears to still be empty (no members, no channels, or no roles), the event
  123         will not be set.
  124         """
  125         if guild.id != constants.Guild.id:
  126             return
  127 
  128         if not guild.roles or not guild.members or not guild.channels:
  129             msg = "Guild available event was dispatched but the cache appears to still be empty!"
  130             log.warning(msg)
  131 
  132             try:
  133                 webhook = await self.fetch_webhook(constants.Webhooks.dev_log)
  134             except discord.HTTPException as e:
  135                 log.error(f"Failed to fetch webhook to send empty cache warning: status {e.status}")
  136             else:
  137                 await webhook.send(f"<@&{constants.Roles.admin}> {msg}")
  138 
  139             return
  140 
  141         self._guild_available.set()
  142 
  143     async def on_guild_unavailable(self, guild: discord.Guild) -> None:
  144         """Clear the internal guild available event when constants.Guild.id becomes unavailable."""
  145         if guild.id != constants.Guild.id:
  146             return
  147 
  148         self._guild_available.clear()
  149 
  150     async def wait_until_guild_available(self) -> None:
  151         """
  152         Wait until the constants.Guild.id guild is available (and the cache is ready).
  153 
  154         The on_ready event is inadequate because it only waits 2 seconds for a GUILD_CREATE
  155         gateway event before giving up and thus not populating the cache for unavailable guilds.
  156         """
  157         await self._guild_available.wait()
  158
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/bot/bot.py b/bot/bot.py
  --- a/bot/bot.py
  +++ b/bot/bot.py
  @@ -7,6 +7,7 @@
   import aiohttp
   import discord
   from discord.ext import commands
  +from sentry_sdk import push_scope
   
   from bot import DEBUG_MODE, api, constants
   from bot.async_stats import AsyncStatsClient
  @@ -155,3 +156,14 @@
           gateway event before giving up and thus not populating the cache for unavailable guilds.
           """
           await self._guild_available.wait()
  +
  +    async def on_error(self, event: str, *args, **kwargs) -> None:
  +        """Log errors raised in event listeners rather than printing them to stderr."""
  +        self.stats.incr(f"errors.event.{event}")
  +
  +        with push_scope() as scope:
  +            scope.set_tag("event", event)
  +            scope.set_extra("args", args)
  +            scope.set_extra("kwargs", kwargs)
  +
  +            log.exception(f"Unhandled exception in {event}.")
  
verification_info: {"golden_diff": "diff --git a/bot/bot.py b/bot/bot.py\n--- a/bot/bot.py\n+++ b/bot/bot.py\n@@ -7,6 +7,7 @@\n import aiohttp\n import discord\n from discord.ext import commands\n+from sentry_sdk import push_scope\n \n from bot import DEBUG_MODE, api, constants\n from bot.async_stats import AsyncStatsClient\n@@ -155,3 +156,14 @@\n         gateway event before giving up and thus not populating the cache for unavailable guilds.\n         \"\"\"\n         await self._guild_available.wait()\n+\n+    async def on_error(self, event: str, *args, **kwargs) -> None:\n+        \"\"\"Log errors raised in event listeners rather than printing them to stderr.\"\"\"\n+        self.stats.incr(f\"errors.event.{event}\")\n+\n+        with push_scope() as scope:\n+            scope.set_tag(\"event\", event)\n+            scope.set_extra(\"args\", args)\n+            scope.set_extra(\"kwargs\", kwargs)\n+\n+            log.exception(f\"Unhandled exception in {event}.\")\n", "issue": "Use appropriate log level for exceptions from event listeners\nFrom @SebastiaanZ:\r\n\r\n> Finally, `discord.py` currently \"hides\" errors/tracebacks that happen in event listeners as we only have a custom error handler for commands. This isn't too bad locally, since `d.py` **prints** those exceptions to stderr, but it obviously means they'll never show up in Sentry, as they are **not actually logged** with the appropriate level.\n", "before_files": [{"content": "import asyncio\nimport logging\nimport socket\nimport warnings\nfrom typing import Optional\n\nimport aiohttp\nimport discord\nfrom discord.ext import commands\n\nfrom bot import DEBUG_MODE, api, constants\nfrom bot.async_stats import AsyncStatsClient\n\nlog = logging.getLogger('bot')\n\n\nclass Bot(commands.Bot):\n    \"\"\"A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        if \"connector\" in kwargs:\n            warnings.warn(\n                \"If login() is called (or the bot is started), the connector will be overwritten \"\n                \"with an internal one\"\n            )\n\n        super().__init__(*args, **kwargs)\n\n        self.http_session: Optional[aiohttp.ClientSession] = None\n        self.api_client = api.APIClient(loop=self.loop)\n\n        self._connector = None\n        self._resolver = None\n        self._guild_available = asyncio.Event()\n\n        statsd_url = constants.Stats.statsd_host\n\n        if DEBUG_MODE:\n            # Since statsd is UDP, there are no errors for sending to a down port.\n            # For this reason, setting the statsd host to 127.0.0.1 for development\n            # will effectively disable stats.\n            statsd_url = \"127.0.0.1\"\n\n        self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix=\"bot\")\n\n    def add_cog(self, cog: commands.Cog) -> None:\n        \"\"\"Adds a \"cog\" to the bot and logs the operation.\"\"\"\n        super().add_cog(cog)\n        log.info(f\"Cog loaded: {cog.qualified_name}\")\n\n    def clear(self) -> None:\n        \"\"\"\n        Clears the internal state of the bot and recreates the connector and sessions.\n\n        Will cause a DeprecationWarning if called outside a coroutine.\n        \"\"\"\n        # Because discord.py recreates the HTTPClient session, may as well follow suit and recreate\n        # our own stuff here too.\n        self._recreate()\n        super().clear()\n\n    async def close(self) -> None:\n        \"\"\"Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.\"\"\"\n        await super().close()\n\n        await self.api_client.close()\n\n        if self.http_session:\n            await self.http_session.close()\n\n        if self._connector:\n            await self._connector.close()\n\n        if self._resolver:\n            await self._resolver.close()\n\n        if self.stats._transport:\n            await self.stats._transport.close()\n\n    async def login(self, *args, **kwargs) -> None:\n        \"\"\"Re-create the connector and set up sessions before logging into Discord.\"\"\"\n        self._recreate()\n        await self.stats.create_socket()\n        await super().login(*args, **kwargs)\n\n    def _recreate(self) -> None:\n        \"\"\"Re-create the connector, aiohttp session, and the APIClient.\"\"\"\n        # Use asyncio for DNS resolution instead of threads so threads aren't spammed.\n        # Doesn't seem to have any state with regards to being closed, so no need to worry?\n        self._resolver = aiohttp.AsyncResolver()\n\n        # Its __del__ does send a warning but it doesn't always show up for some reason.\n        if self._connector and not self._connector._closed:\n            log.warning(\n                \"The previous connector was not closed; it will remain open and be overwritten\"\n            )\n\n        # Use AF_INET as its socket family to prevent HTTPS related problems both locally\n        # and in production.\n        self._connector = aiohttp.TCPConnector(\n            resolver=self._resolver,\n            family=socket.AF_INET,\n        )\n\n        # Client.login() will call HTTPClient.static_login() which will create a session using\n        # this connector attribute.\n        self.http.connector = self._connector\n\n        # Its __del__ does send a warning but it doesn't always show up for some reason.\n        if self.http_session and not self.http_session.closed:\n            log.warning(\n                \"The previous session was not closed; it will remain open and be overwritten\"\n            )\n\n        self.http_session = aiohttp.ClientSession(connector=self._connector)\n        self.api_client.recreate(force=True, connector=self._connector)\n\n    async def on_guild_available(self, guild: discord.Guild) -> None:\n        \"\"\"\n        Set the internal guild available event when constants.Guild.id becomes available.\n\n        If the cache appears to still be empty (no members, no channels, or no roles), the event\n        will not be set.\n        \"\"\"\n        if guild.id != constants.Guild.id:\n            return\n\n        if not guild.roles or not guild.members or not guild.channels:\n            msg = \"Guild available event was dispatched but the cache appears to still be empty!\"\n            log.warning(msg)\n\n            try:\n                webhook = await self.fetch_webhook(constants.Webhooks.dev_log)\n            except discord.HTTPException as e:\n                log.error(f\"Failed to fetch webhook to send empty cache warning: status {e.status}\")\n            else:\n                await webhook.send(f\"<@&{constants.Roles.admin}> {msg}\")\n\n            return\n\n        self._guild_available.set()\n\n    async def on_guild_unavailable(self, guild: discord.Guild) -> None:\n        \"\"\"Clear the internal guild available event when constants.Guild.id becomes unavailable.\"\"\"\n        if guild.id != constants.Guild.id:\n            return\n\n        self._guild_available.clear()\n\n    async def wait_until_guild_available(self) -> None:\n        \"\"\"\n        Wait until the constants.Guild.id guild is available (and the cache is ready).\n\n        The on_ready event is inadequate because it only waits 2 seconds for a GUILD_CREATE\n        gateway event before giving up and thus not populating the cache for unavailable guilds.\n        \"\"\"\n        await self._guild_available.wait()\n", "path": "bot/bot.py"}], "after_files": [{"content": "import asyncio\nimport logging\nimport socket\nimport warnings\nfrom typing import Optional\n\nimport aiohttp\nimport discord\nfrom discord.ext import commands\nfrom sentry_sdk import push_scope\n\nfrom bot import DEBUG_MODE, api, constants\nfrom bot.async_stats import AsyncStatsClient\n\nlog = logging.getLogger('bot')\n\n\nclass Bot(commands.Bot):\n    \"\"\"A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client.\"\"\"\n\n    def __init__(self, *args, **kwargs):\n        if \"connector\" in kwargs:\n            warnings.warn(\n                \"If login() is called (or the bot is started), the connector will be overwritten \"\n                \"with an internal one\"\n            )\n\n        super().__init__(*args, **kwargs)\n\n        self.http_session: Optional[aiohttp.ClientSession] = None\n        self.api_client = api.APIClient(loop=self.loop)\n\n        self._connector = None\n        self._resolver = None\n        self._guild_available = asyncio.Event()\n\n        statsd_url = constants.Stats.statsd_host\n\n        if DEBUG_MODE:\n            # Since statsd is UDP, there are no errors for sending to a down port.\n            # For this reason, setting the statsd host to 127.0.0.1 for development\n            # will effectively disable stats.\n            statsd_url = \"127.0.0.1\"\n\n        self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix=\"bot\")\n\n    def add_cog(self, cog: commands.Cog) -> None:\n        \"\"\"Adds a \"cog\" to the bot and logs the operation.\"\"\"\n        super().add_cog(cog)\n        log.info(f\"Cog loaded: {cog.qualified_name}\")\n\n    def clear(self) -> None:\n        \"\"\"\n        Clears the internal state of the bot and recreates the connector and sessions.\n\n        Will cause a DeprecationWarning if called outside a coroutine.\n        \"\"\"\n        # Because discord.py recreates the HTTPClient session, may as well follow suit and recreate\n        # our own stuff here too.\n        self._recreate()\n        super().clear()\n\n    async def close(self) -> None:\n        \"\"\"Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.\"\"\"\n        await super().close()\n\n        await self.api_client.close()\n\n        if self.http_session:\n            await self.http_session.close()\n\n        if self._connector:\n            await self._connector.close()\n\n        if self._resolver:\n            await self._resolver.close()\n\n        if self.stats._transport:\n            await self.stats._transport.close()\n\n    async def login(self, *args, **kwargs) -> None:\n        \"\"\"Re-create the connector and set up sessions before logging into Discord.\"\"\"\n        self._recreate()\n        await self.stats.create_socket()\n        await super().login(*args, **kwargs)\n\n    def _recreate(self) -> None:\n        \"\"\"Re-create the connector, aiohttp session, and the APIClient.\"\"\"\n        # Use asyncio for DNS resolution instead of threads so threads aren't spammed.\n        # Doesn't seem to have any state with regards to being closed, so no need to worry?\n        self._resolver = aiohttp.AsyncResolver()\n\n        # Its __del__ does send a warning but it doesn't always show up for some reason.\n        if self._connector and not self._connector._closed:\n            log.warning(\n                \"The previous connector was not closed; it will remain open and be overwritten\"\n            )\n\n        # Use AF_INET as its socket family to prevent HTTPS related problems both locally\n        # and in production.\n        self._connector = aiohttp.TCPConnector(\n            resolver=self._resolver,\n            family=socket.AF_INET,\n        )\n\n        # Client.login() will call HTTPClient.static_login() which will create a session using\n        # this connector attribute.\n        self.http.connector = self._connector\n\n        # Its __del__ does send a warning but it doesn't always show up for some reason.\n        if self.http_session and not self.http_session.closed:\n            log.warning(\n                \"The previous session was not closed; it will remain open and be overwritten\"\n            )\n\n        self.http_session = aiohttp.ClientSession(connector=self._connector)\n        self.api_client.recreate(force=True, connector=self._connector)\n\n    async def on_guild_available(self, guild: discord.Guild) -> None:\n        \"\"\"\n        Set the internal guild available event when constants.Guild.id becomes available.\n\n        If the cache appears to still be empty (no members, no channels, or no roles), the event\n        will not be set.\n        \"\"\"\n        if guild.id != constants.Guild.id:\n            return\n\n        if not guild.roles or not guild.members or not guild.channels:\n            msg = \"Guild available event was dispatched but the cache appears to still be empty!\"\n            log.warning(msg)\n\n            try:\n                webhook = await self.fetch_webhook(constants.Webhooks.dev_log)\n            except discord.HTTPException as e:\n                log.error(f\"Failed to fetch webhook to send empty cache warning: status {e.status}\")\n            else:\n                await webhook.send(f\"<@&{constants.Roles.admin}> {msg}\")\n\n            return\n\n        self._guild_available.set()\n\n    async def on_guild_unavailable(self, guild: discord.Guild) -> None:\n        \"\"\"Clear the internal guild available event when constants.Guild.id becomes unavailable.\"\"\"\n        if guild.id != constants.Guild.id:\n            return\n\n        self._guild_available.clear()\n\n    async def wait_until_guild_available(self) -> None:\n        \"\"\"\n        Wait until the constants.Guild.id guild is available (and the cache is ready).\n\n        The on_ready event is inadequate because it only waits 2 seconds for a GUILD_CREATE\n        gateway event before giving up and thus not populating the cache for unavailable guilds.\n        \"\"\"\n        await self._guild_available.wait()\n\n    async def on_error(self, event: str, *args, **kwargs) -> None:\n        \"\"\"Log errors raised in event listeners rather than printing them to stderr.\"\"\"\n        self.stats.incr(f\"errors.event.{event}\")\n\n        with push_scope() as scope:\n            scope.set_tag(\"event\", event)\n            scope.set_extra(\"args\", args)\n            scope.set_extra(\"kwargs\", kwargs)\n\n            log.exception(f\"Unhandled exception in {event}.\")\n", "path": "bot/bot.py"}]}