diff --git a/discordbot/__init__.py b/discordbot/__init__.py index 61cf539..34a4789 100644 --- a/discordbot/__init__.py +++ b/discordbot/__init__.py @@ -30,11 +30,12 @@ from discordbot.patreon import checkPatreonPosts from discordbot.youtube import checkYouTubeVideos from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_reaction_add_auto_rooms, on_message_auto_rooms, cleanup_orphaned_auto_rooms from discordbot.member_stats import record_message, on_voice_state_update_track_voice +from discordbot.rate_limit_logging import build_discord_http_trace_config from protondb import searhProtonDb class DiscordBot(discord.Client): def __init__(self, *, intents: discord.Intents): - super().__init__(intents=intents) + super().__init__(intents=intents, http_trace=build_discord_http_trace_config()) self.tree = app_commands.CommandTree(self) self.synced = False diff --git a/discordbot/rate_limit_logging.py b/discordbot/rate_limit_logging.py new file mode 100644 index 0000000..b42f8ee --- /dev/null +++ b/discordbot/rate_limit_logging.py @@ -0,0 +1,67 @@ +""" +Journalisation des en-têtes HTTP sur les réponses 429 (rate limit Discord). + +Réf. Discord : https://support-dev.discord.com/hc/en-us/articles/6223003921559-My-Bot-is-Being-Rate-Limited + +discord.py transmet ce TraceConfig aiohttp via l’option Client(http_trace=...). +Le corps JSON (retry_after, global) est toujours loggé par le logger discord.http. +""" +import logging +from typing import Any + +import aiohttp + +logger = logging.getLogger('discord.ratelimit_headers') + +# En-têtes utiles pour identifier le type de limite (global / user / shared, etc.) +_HEADER_KEYS = ( + 'X-RateLimit-Limit', + 'X-RateLimit-Remaining', + 'X-RateLimit-Reset', + 'X-RateLimit-Reset-After', + 'X-RateLimit-Scope', + 'X-Ratelimit-Bucket', + 'X-Ratelimit-Limit', + 'X-Ratelimit-Remaining', + 'X-Ratelimit-Reset', + 'Retry-After', + 'Via', +) + + +def _collect_headers(resp: aiohttp.ClientResponse) -> dict[str, str]: + h = resp.headers + out: dict[str, str] = {} + for key in _HEADER_KEYS: + val = h.get(key) + if val is not None: + out[key] = val + return out + + +def build_discord_http_trace_config() -> aiohttp.TraceConfig: + trace = aiohttp.TraceConfig() + + async def on_request_end( + session: aiohttp.ClientSession, + trace_config_ctx: Any, + params: Any, + ) -> None: + try: + resp = getattr(params, 'response', None) + if resp is None or getattr(resp, 'status', None) != 429: + return + method = getattr(params, 'method', '?') + url = getattr(params, 'url', '?') + hdr = _collect_headers(resp) + logger.warning( + 'Discord API 429 %s %s | rate_limit_headers=%s', + method, + url, + hdr, + ) + except Exception: + logger.debug('rate_limit trace callback failed', exc_info=True) + + trace.on_request_end.append(on_request_end) + return trace diff --git a/run-web.py b/run-web.py index b407ee0..291d658 100644 --- a/run-web.py +++ b/run-web.py @@ -55,6 +55,8 @@ if __name__ == '__main__': # Calmer les logs verbeux de certaines libs si besoin logging.getLogger('werkzeug').setLevel(logging.WARNING) logging.getLogger('discord').setLevel(logging.WARNING) + # 429 : en-têtes X-RateLimit-* (voir discordbot/rate_limit_logging.py + doc Discord rate limits) + logging.getLogger('discord.ratelimit_headers').setLevel(logging.WARNING) # Hook exceptions non-capturées (threads inclus) def _log_uncaught(exc_type, exc, tb):