From 7018ff80c8bebedb699a7c2281e8cd31a18d2eab Mon Sep 17 00:00:00 2001
From: Noelle Wang <73260931+No767@users.noreply.github.com>
Date: Mon, 13 May 2024 23:42:54 -0700
Subject: [PATCH] Implement Prometheus observability (#119)
---
bot/cogs/config.py | 2 +
bot/cogs/ext/prometheus.py | 122 +++++++++++++++++++++++++++++++++++
bot/cogs/tickets.py | 5 ++
bot/rodhaj.py | 13 ++++
docs/dev-guide/intro.rst | 23 ++++++-
docs/user-guide/features.rst | 15 ++++-
poetry.lock | 119 +++++++++++++++++++++++++++++++++-
pyproject.toml | 2 +
requirements.txt | 2 +
9 files changed, 300 insertions(+), 3 deletions(-)
create mode 100644 bot/cogs/ext/prometheus.py
diff --git a/bot/cogs/config.py b/bot/cogs/config.py
index 6a1bf6a..6a59431 100644
--- a/bot/cogs/config.py
+++ b/bot/cogs/config.py
@@ -623,6 +623,7 @@ async def blocklist_add(
await tr.rollback()
await ctx.send("Unable to block user")
else:
+ self.bot.metrics.features.blocked_users.inc()
await tr.commit()
self.bot.blocklist.replace(blocklist)
@@ -680,6 +681,7 @@ async def blocklist_remove(self, ctx: GuildContext, entity: discord.Member) -> N
await tr.rollback()
await ctx.send("Unable to block user")
else:
+ self.bot.metrics.features.blocked_users.dec()
await tr.commit()
self.bot.blocklist.replace(blocklist)
await block_ticket.cog.soft_unlock_ticket(
diff --git a/bot/cogs/ext/prometheus.py b/bot/cogs/ext/prometheus.py
new file mode 100644
index 0000000..3879cd2
--- /dev/null
+++ b/bot/cogs/ext/prometheus.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+import platform
+from typing import TYPE_CHECKING
+
+import discord
+from discord.ext import commands, tasks
+
+try:
+
+ from prometheus_async.aio.web import start_http_server
+ from prometheus_client import Counter, Enum, Gauge, Info, Summary
+except ImportError:
+ raise RuntimeError(
+ "Prometheus libraries are required to be installed. "
+ "Either install those libraries or disable Prometheus extension"
+ )
+
+if TYPE_CHECKING:
+ from bot.rodhaj import Rodhaj
+
+METRIC_PREFIX = "discord_"
+
+
+class FeatureCollector:
+ __slots__ = (
+ "bot",
+ "active_tickets",
+ "closed_tickets",
+ "locked_tickets",
+ "blocked_users",
+ )
+
+ def __init__(self, bot: Rodhaj):
+ self.bot = bot
+ self.active_tickets = Gauge(
+ f"{METRIC_PREFIX}active_tickets", "Amount of active tickets"
+ )
+ self.closed_tickets = Counter(
+ f"{METRIC_PREFIX}closed_tickets", "Number of closed tickets in this session"
+ )
+ self.locked_tickets = Gauge(
+ f"{METRIC_PREFIX}locked_tickets",
+ "Number of soft locked tickets in this session",
+ )
+ self.blocked_users = Gauge(
+ f"{METRIC_PREFIX}blocked_users", "Number of currently blocked users"
+ )
+
+
+# Maybe load all of these from an json file next time
+class Metrics:
+ __slots__ = ("bot", "connected", "latency", "commands", "version", "features")
+
+ def __init__(self, bot: Rodhaj):
+ self.bot = bot
+ self.connected = Enum(
+ f"{METRIC_PREFIX}connected",
+ "Connected to Discord",
+ ["shard"],
+ states=["connected", "disconnected"],
+ )
+ self.latency = Gauge(f"{METRIC_PREFIX}latency", "Latency to Discord", ["shard"])
+ self.commands = Summary(f"{METRIC_PREFIX}commands", "Total commands executed")
+ self.version = Info(f"{METRIC_PREFIX}version", "Versions of the bot")
+ self.features = FeatureCollector(self.bot)
+
+ def get_commands(self) -> int:
+ total_commands = 0
+ for _ in self.bot.walk_commands():
+ # As some of the commands are parents,
+ # Grouped commands are also counted here
+ total_commands += 1
+
+ return total_commands
+
+ def fill(self) -> None:
+ self.version.info(
+ {
+ "build_version": self.bot.version,
+ "dpy_version": discord.__version__,
+ "python_version": platform.python_version(),
+ }
+ )
+ self.commands.observe(self.get_commands())
+
+ async def start(self, host: str, port: int) -> None:
+ await start_http_server(addr=host, port=port)
+
+
+class Prometheus(commands.Cog):
+ """Prometheus exporter extension for Rodhaj"""
+
+ def __init__(self, bot: Rodhaj):
+ self.bot = bot
+ self._connected_label = self.bot.metrics.connected.labels(None)
+
+ async def cog_load(self) -> None:
+ self.latency_loop.start()
+
+ async def cog_unload(self) -> None:
+ self.latency_loop.stop()
+
+ @tasks.loop(seconds=5)
+ async def latency_loop(self) -> None:
+ self.bot.metrics.latency.labels(None).set(self.bot.latency)
+
+ @commands.Cog.listener()
+ async def on_connect(self) -> None:
+ self._connected_label.state("connected")
+
+ @commands.Cog.listener()
+ async def on_resumed(self) -> None:
+ self._connected_label.state("connected")
+
+ @commands.Cog.listener()
+ async def on_disconnect(self) -> None:
+ self._connected_label.state("disconnected")
+
+
+async def setup(bot: Rodhaj) -> None:
+ await bot.add_cog(Prometheus(bot))
diff --git a/bot/cogs/tickets.py b/bot/cogs/tickets.py
index 850eae3..543e07e 100644
--- a/bot/cogs/tickets.py
+++ b/bot/cogs/tickets.py
@@ -151,6 +151,7 @@ async def lock_ticket(
async def soft_lock_ticket(
self, thread: discord.Thread, reason: Optional[str] = None
) -> discord.Thread:
+ self.bot.metrics.features.locked_tickets.inc()
tags = thread.applied_tags
locked_tag = self.get_locked_tag(thread.parent)
@@ -162,6 +163,7 @@ async def soft_lock_ticket(
async def soft_unlock_ticket(
self, thread: discord.Thread, reason: Optional[str] = None
) -> discord.Thread:
+ self.bot.metrics.features.locked_tickets.dec()
tags = thread.applied_tags
locked_tag = self.get_locked_tag(thread.parent)
@@ -176,6 +178,8 @@ async def close_ticket(
connection: Union[asyncpg.Pool, asyncpg.Connection],
author: Optional[Union[discord.User, discord.Member]] = None,
) -> Optional[discord.Thread]:
+ self.bot.metrics.features.closed_tickets.inc()
+ self.bot.metrics.features.active_tickets.dec()
if isinstance(user, int):
user = self.bot.get_user(user) or (await self.bot.fetch_user(user))
@@ -280,6 +284,7 @@ async def create_ticket(self, ticket: TicketThread) -> Optional[TicketOutput]:
status=False, ticket=created_ticket, msg="Could not create ticket"
)
else:
+ self.bot.metrics.features.active_tickets.inc()
await tr.commit()
return TicketOutput(
status=True,
diff --git a/bot/rodhaj.py b/bot/rodhaj.py
index 57e4ab1..903b39d 100644
--- a/bot/rodhaj.py
+++ b/bot/rodhaj.py
@@ -9,6 +9,7 @@
from aiohttp import ClientSession
from cogs import EXTENSIONS, VERSION
from cogs.config import Blocklist, GuildWebhookDispatcher
+from cogs.ext.prometheus import Metrics
from discord.ext import commands
from libs.tickets.structs import PartialConfig, ReservedTags, StatusChecklist
from libs.tickets.utils import get_cached_thread, get_partial_ticket
@@ -56,6 +57,7 @@ def __init__(
self.blocklist = Blocklist(self)
self.default_prefix = "r>"
self.logger = logging.getLogger("rodhaj")
+ self.metrics = Metrics(self)
self.session = session
self.partial_config: Optional[PartialConfig] = None
self.pool = pool
@@ -65,6 +67,7 @@ def __init__(
)
self._dev_mode = config.rodhaj.get("dev_mode", False)
self._reloader = Reloader(self, Path(__file__).parent)
+ self._prometheus = config.rodhaj.get("prometheus", {})
### Ticket related utils
async def fetch_partial_config(self) -> Optional[PartialConfig]:
@@ -214,6 +217,16 @@ async def setup_hook(self) -> None:
await self.blocklist.load()
self.partial_config = await self.fetch_partial_config()
+ if self._prometheus.get("enabled", False):
+ await self.load_extension("cogs.ext.prometheus")
+ prom_host = self._prometheus.get("host", "127.0.0.1")
+ prom_port = self._prometheus.get("port", 8555)
+
+ await self.metrics.start(host=prom_host, port=prom_port)
+ self.logger.info("Prometheus Server started on %s:%s", prom_host, prom_port)
+
+ self.metrics.fill()
+
if self._dev_mode:
self.logger.info("Dev mode is enabled. Loading Reloader")
self._reloader.start()
diff --git a/docs/dev-guide/intro.rst b/docs/dev-guide/intro.rst
index 8cebede..f3424ad 100644
--- a/docs/dev-guide/intro.rst
+++ b/docs/dev-guide/intro.rst
@@ -103,4 +103,25 @@ pre-built Docker Compose file is provided. Setup instructions are as follows:
.. code-block:: bash
- docker compose -f docker-compose-dev.yml up -d
\ No newline at end of file
+ docker compose -f docker-compose-dev.yml up -d
+
+Extensions
+==========
+
+Rodhaj includes the following extensions as noted:
+
+Prometheus Exporter
+^^^^^^^^^^^^^^^^^^^
+
+Rodhaj currently includes an `Prometheus `_ exporter.
+This exporter is intended to be used in production environments, where
+metrics surrounding ticket usage, bot health, and others would provide
+valuable insight. This exporter can be enabled by setting the
+``rodhaj.prometheus.enabled`` key within ``config.yml``.
+
+.. note::
+
+ Prometheus client libraries are listed within the
+ ``requirements.txt`` file. By default, these libraries
+ should be installed, but disabling the exporter will not
+ affect the usage of these libraries.
\ No newline at end of file
diff --git a/docs/user-guide/features.rst b/docs/user-guide/features.rst
index 6e52134..d1479fd 100644
--- a/docs/user-guide/features.rst
+++ b/docs/user-guide/features.rst
@@ -52,4 +52,17 @@ Blocklist
This feature acts very similar to an block/unblock feature. All blocked users
as of writing will not get a message from the bot. Planned features with this feature
include an timer to automatically remove those who are on the blocklist and
-an history feature to track past incidents.
\ No newline at end of file
+an history feature to track past incidents.
+
+Prometheus Extension
+--------------------
+
+In order to aid in observability, Rodhaj includes an `Prometheus `_ exporter.
+This is included as an extension to Rodhaj, which when used, provides valuable information
+in regards to usage, and other metrics. This extension is designed primarily to be used in
+production environments.
+
+.. note::
+
+ Disabling this extension will have no effect
+ on the bot itself.
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 1646740..f645ceb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1421,6 +1421,44 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
+[[package]]
+name = "prometheus-async"
+version = "22.2.0"
+description = "Async helpers for prometheus_client."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "prometheus-async-22.2.0.tar.gz", hash = "sha256:b0426370eb3b3bacd99afcf1fcc669c118cb67603cc951a6fe12434e9d4307f2"},
+ {file = "prometheus_async-22.2.0-py3-none-any.whl", hash = "sha256:5cbfa535561342b834c087c4f3f3be0a3cb8785a0b8748111c916f3d68bbc370"},
+]
+
+[package.dependencies]
+prometheus_client = ">=0.8.0"
+typing_extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
+wrapt = "*"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3)"]
+consul = ["aiohttp (>=3)"]
+dev = ["aiohttp", "cogapp", "coverage[toml]", "furo", "mypy", "myst-parser", "pre-commit", "pytest", "pytest-asyncio", "pytest-twisted", "sphinx", "sphinx-notfound-page", "sphinxcontrib-asyncio", "tomli", "twisted"]
+docs = ["aiohttp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-asyncio", "twisted"]
+tests = ["coverage[toml]", "pytest", "pytest-asyncio"]
+twisted = ["twisted"]
+
+[[package]]
+name = "prometheus-client"
+version = "0.20.0"
+description = "Python client for the Prometheus monitoring system."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"},
+ {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"},
+]
+
+[package.extras]
+twisted = ["twisted"]
+
[[package]]
name = "psutil"
version = "5.9.8"
@@ -2349,6 +2387,85 @@ files = [
[package.dependencies]
cython = "*"
+[[package]]
+name = "wrapt"
+version = "1.16.0"
+description = "Module for decorators, wrappers and monkey patching."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"},
+ {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"},
+ {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"},
+ {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"},
+ {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"},
+ {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"},
+ {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"},
+ {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"},
+ {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"},
+ {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"},
+ {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"},
+ {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"},
+ {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"},
+ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
+]
+
[[package]]
name = "yarl"
version = "1.9.4"
@@ -2470,4 +2587,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<4"
-content-hash = "c50327840495c94943b219db9fe4dd6184ce3d22b6724668e294bffe05a97c6e"
+content-hash = "97cfcfafe42cf554264e5095bf98474e570224e0cb8fe08201a558f1fdff8d67"
diff --git a/pyproject.toml b/pyproject.toml
index 637a32c..4464d8c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,6 +23,8 @@ jishaku = "^2.5.2"
pyyaml = "^6.0.1"
watchfiles = "^0.21.0"
typing-extensions = "^4.11.0"
+prometheus-client = "^0.20.0"
+prometheus-async = "^22.2.0"
[tool.poetry.group.dev.dependencies]
# These are pinned by major version
diff --git a/requirements.txt b/requirements.txt
index 0d445f3..c3d2bfa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,8 @@ pygit2>=1.13.3,<2
python-dateutil>=2.8.2,<3
click>=8.1.7,<9
typing-extensions>=4.9.0,<5
+prometheus-client>=0.20.0,<1
+prometheus-async>=22.2.0,<23
async-lru>=2.0.4,<3
msgspec>=0.18.6,<1
jishaku>=2.5.2,<3