diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bec493a..4eacba4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,6 +24,7 @@ jobs: run: | python -m pip install --upgrade pip pip install click discord.py python-dotenv + pip install prometheus_client pip install requests sentry_sdk python-logging-loki aiohttp systemd-python - name: Install linters run: | diff --git a/cogs/inclusive_language.py b/cogs/inclusive_language.py index 2596702..40c9b91 100755 --- a/cogs/inclusive_language.py +++ b/cogs/inclusive_language.py @@ -6,6 +6,7 @@ from typing import cast, Sequence import discord +import prometheus_client # type: ignore from discord.ext import commands from cogs import base_cog @@ -42,6 +43,9 @@ def __init__( ) -> None: super().__init__(**kwargs) self.patterns = patterns + self.prom_counter = prometheus_client.Counter( + "inclusive_language_triggered", "How many times inclusive language was triggered." + ) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: @@ -54,6 +58,7 @@ async def on_message(self, message: discord.Message) -> None: if not any(pattern.search(message.content) for pattern in self.patterns): return self.usage_stats[message.author.display_name].append(int(time.time())) + self.prom_counter.inc() if channel.type == discord.ChannelType.public_thread: await message.reply(MESSAGE, delete_after=DURATION, suppress_embeds=True) elif channel.type == discord.ChannelType.text: diff --git a/cogs/mentor_requests.py b/cogs/mentor_requests.py index ec3d8c4..867c385 100755 --- a/cogs/mentor_requests.py +++ b/cogs/mentor_requests.py @@ -6,6 +6,7 @@ from typing import Sequence import discord +import prometheus_client # type: ignore from discord.ext import commands from discord.ext import tasks from exercism_lib import exercism @@ -21,6 +22,12 @@ "get_theads": "SELECT track_slug, message_id FROM track_threads", "add_thead": "INSERT INTO track_threads VALUES (:track_slug, :message_id)", } +PROM_TRACK_COUNT = prometheus_client.Gauge("mentor_requests_tracks", "Number of tracks") +PROM_REQUEST_COUNT = prometheus_client.Gauge("mentor_requests", "Number of requests") +PROM_UPDATE_HIST = prometheus_client.Histogram("mentor_requests_update", "Update requests") +PROM_UPDATE_TRACK_HIST = prometheus_client.Histogram( + "mentor_requests_update_track", "Update one track", ["track"], +) class RequestNotifier(base_cog.BaseCog): @@ -45,10 +52,11 @@ def __init__( self.lock = asyncio.Lock() if tracks: - self.tracks = tracks + self.tracks = list(tracks) else: self.tracks = exercism.Exercism().all_tracks() - self.tracks.sort() + self.tracks.sort() + PROM_TRACK_COUNT.set(len(self.tracks)) self.synced_tracks: set[str] = set() self.task_update_mentor_requests.start() # pylint: disable=E1101 @@ -105,6 +113,7 @@ async def update_track_requests(self, track: str) -> dict[str, str]: self.conn.execute(QUERY["add_request"], data) return requests + @PROM_UPDATE_HIST.time() async def update_mentor_requests(self) -> None: """Update threads with new/expires requests.""" logger.debug("Start update_mentor_requests()") @@ -114,7 +123,8 @@ async def update_mentor_requests(self) -> None: for track in self.tracks: try: async with asyncio.timeout(30): - requests = await self.update_track_requests(track) + with PROM_UPDATE_TRACK_HIST.labels(track).time(): + requests = await self.update_track_requests(track) synced_tracks.add(track) except asyncio.TimeoutError: logging.warning("update_track_requests timed out for track %s.", track) @@ -156,6 +166,7 @@ async def update_mentor_requests(self) -> None: except discord.errors.NotFound: logger.info("Message not found; dropping from DB. %s", message.jump_url) await asyncio.sleep(0.1) + PROM_REQUEST_COUNT.set(len(self.requests)) logger.debug("End update_mentor_requests()") @tasks.loop(minutes=10) diff --git a/cogs/mod_message.py b/cogs/mod_message.py index 5ecafea..81c74af 100644 --- a/cogs/mod_message.py +++ b/cogs/mod_message.py @@ -7,9 +7,13 @@ from discord import app_commands from discord.ext import commands +import prometheus_client # type: ignore from cogs import base_cog logger = logging.getLogger(__name__) +PROM_MESSAGE_COUNTER = prometheus_client.Counter( + "mod_message_count", "Mod Message counter", ["message"], +) class ModMessage(base_cog.BaseCog): @@ -104,6 +108,7 @@ async def mod_message( if mention: content = f"{mention.mention} {content}" self.usage_stats[message.value] += 1 + PROM_MESSAGE_COUNTER.labels("message.value").inc() await channel.send(content, suppress_embeds=True) return mod_message diff --git a/cogs/track_react.py b/cogs/track_react.py index 482b8d9..715d1ac 100755 --- a/cogs/track_react.py +++ b/cogs/track_react.py @@ -5,6 +5,7 @@ import re import discord +import prometheus_client # type: ignore from discord.ext import commands from cogs import base_cog @@ -12,6 +13,7 @@ logger = logging.getLogger(__name__) URL_RE = re.compile(r"https?://\S+") +PROM_REACT_COUNTER = prometheus_client.Counter("react_count", "Reaction counter", ["emoji"]) class TrackReact(base_cog.BaseCog): @@ -112,6 +114,7 @@ async def add_reacts(self, message: discord.Message, content: str) -> None: for reaction in reactions: self.usage_stats[reaction.name] += 1 + PROM_REACT_COUNTER.labels(reaction.name).inc() await message.add_reaction(reaction) await asyncio.sleep(0.1) diff --git a/exercism_discord_bot.py b/exercism_discord_bot.py index 5168d21..f59580d 100755 --- a/exercism_discord_bot.py +++ b/exercism_discord_bot.py @@ -14,6 +14,7 @@ import discord import dotenv import logging_loki # type: ignore +import prometheus_client # type: ignore import sentry_sdk # type: ignore import systemd.journal # type: ignore from discord.ext import commands @@ -57,6 +58,7 @@ def __init__( super().__init__(*args, **kwargs) self.cogs_to_load = cogs_to_load self.exercism_guild_id = exercism_guild_id + self.gauge_cogs_loaded = prometheus_client.Gauge("cogs_loaded", "Number of cogs running") async def setup_hook(self): """Configure the bot with various Cogs.""" @@ -82,6 +84,7 @@ async def setup_hook(self): logger.info("Loading cog %s", cog.__name__) instance = cog(**combined) await self.add_cog(instance, guild=guild) + self.gauge_cogs_loaded.inc() async def on_error(self, event_method, /, *args, **kwargs) -> None: """Capture and log errors.""" @@ -211,6 +214,9 @@ def main( if not has_setting("DISCORD_TOKEN"): raise RuntimeError("Missing DISCORD_TOKEN") + if find_setting("PROMETHEUS_PORT"): + prometheus_client.start_http_server(int(find_setting("PROMETHEUS_PORT"))) + # Start the bot. intents = discord.Intents.default() intents.members = True