From 23fba3d95dc6fe3e9b4e0c63c0f59faf29325573 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Tue, 13 Aug 2024 08:13:03 -0700 Subject: [PATCH] feat: add prometheus metrics --- package.json | 1 + pnpm-lock.yaml | 30 +++++++++++++++++++++++++ src/events/client/onReady.ts | 2 ++ src/events/guild/onGuildCreate.ts | 1 + src/events/guild/onGuildDelete.ts | 1 + src/events/interaction/onInteraction.ts | 1 + src/interfaces/ExtendedClient.ts | 3 +++ src/modules/prometheus.ts | 29 ++++++++++++++++++++++++ src/server/serve.ts | 10 +++++++++ 9 files changed, 78 insertions(+) create mode 100644 src/modules/prometheus.ts diff --git a/package.json b/package.json index 52725f0..fa77d69 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "express": "4.19.2", "node-html-to-image": "4.0.0", "node-schedule": "2.1.1", + "prom-client": "15.1.3", "winston": "3.13.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7614c81..c8548e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: node-schedule: specifier: 2.1.1 version: 2.1.1 + prom-client: + specifier: 15.1.3 + version: 15.1.3 winston: specifier: 3.13.0 version: 3.13.0 @@ -252,6 +255,10 @@ packages: '@octokit/types@13.5.0': resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -547,6 +554,9 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1716,6 +1726,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1991,6 +2005,9 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} @@ -2476,6 +2493,8 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 + '@opentelemetry/api@1.9.0': {} + '@pkgr/core@0.1.1': {} '@prisma/client@5.13.0(prisma@5.13.0)': @@ -2816,6 +2835,8 @@ snapshots: before-after-hook@2.2.3: {} + bintrees@1.0.2: {} + body-parser@1.20.2: dependencies: bytes: 3.1.2 @@ -4168,6 +4189,11 @@ snapshots: progress@2.0.3: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.0 + tdigest: 0.1.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -4512,6 +4538,10 @@ snapshots: fast-fifo: 1.3.2 streamx: 2.16.1 + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + text-hex@1.0.0: {} text-table@0.2.0: {} diff --git a/src/events/client/onReady.ts b/src/events/client/onReady.ts index 7c24852..2b8f00d 100644 --- a/src/events/client/onReady.ts +++ b/src/events/client/onReady.ts @@ -3,6 +3,7 @@ import { scheduleJob } from "node-schedule"; import { ExtendedClient } from "../../interfaces/ExtendedClient"; import { maintainSecurity } from "../../modules/maintainSecurity"; import { postBirthdays } from "../../modules/postBirthdays"; +import { Prometheus } from "../../modules/prometheus"; import { registerCommands } from "../../utils/registerCommands"; import { sendDebugMessage } from "../../utils/sendDebugMessage"; @@ -14,6 +15,7 @@ import { sendDebugMessage } from "../../utils/sendDebugMessage"; export const onReady = async (bot: ExtendedClient) => { await sendDebugMessage(bot, `Logged in as ${bot.user?.tag}`); await registerCommands(bot); + bot.analytics = new Prometheus(bot); // Daily at 9am PST scheduleJob("birthdays", "0 9 * * *", async () => await postBirthdays(bot)); diff --git a/src/events/guild/onGuildCreate.ts b/src/events/guild/onGuildCreate.ts index 6db0724..a72f197 100644 --- a/src/events/guild/onGuildCreate.ts +++ b/src/events/guild/onGuildCreate.ts @@ -15,4 +15,5 @@ export const onGuildCreate = async function ( await bot.env.debugHook.send({ content: `JOINED GUILD: ${guild.name} (${guild.id}) - owned by ${owner?.displayName} (${owner.id})` }); + bot.analytics.updateGuilds(bot); }; diff --git a/src/events/guild/onGuildDelete.ts b/src/events/guild/onGuildDelete.ts index 1aff679..a8dcbec 100644 --- a/src/events/guild/onGuildDelete.ts +++ b/src/events/guild/onGuildDelete.ts @@ -36,6 +36,7 @@ export const onGuildDelete = async function ( await bot.db.security .deleteMany({ where: { serverId: guild.id } }) .catch(() => null); + bot.analytics.updateGuilds(bot); } catch (err) { await errorHandler(bot, "on guild delete", err); } diff --git a/src/events/interaction/onInteraction.ts b/src/events/interaction/onInteraction.ts index d5c1b1d..9b88e78 100644 --- a/src/events/interaction/onInteraction.ts +++ b/src/events/interaction/onInteraction.ts @@ -35,6 +35,7 @@ export const onInteraction = async ( ); return; } + bot.analytics.commandUsed(); if (interaction.isChatInputCommand()) { handleChatInputCommand(bot, interaction); } diff --git a/src/interfaces/ExtendedClient.ts b/src/interfaces/ExtendedClient.ts index d232b12..600b79b 100644 --- a/src/interfaces/ExtendedClient.ts +++ b/src/interfaces/ExtendedClient.ts @@ -4,6 +4,8 @@ import { Client, WebhookClient } from "discord.js"; import { Command } from "./Command"; import { Context } from "./Context"; +import type { Prometheus } from "../modules/prometheus.js"; + export interface ExtendedClient extends Client { env: { token: string; @@ -12,6 +14,7 @@ export interface ExtendedClient extends Client { devMode: boolean; }; db: PrismaClient; + analytics: Prometheus; commands: Command[]; contexts: Context[]; configs: { [serverId: string]: Omit }; diff --git a/src/modules/prometheus.ts b/src/modules/prometheus.ts new file mode 100644 index 0000000..7845640 --- /dev/null +++ b/src/modules/prometheus.ts @@ -0,0 +1,29 @@ +import client, { Counter, Gauge } from "prom-client"; +import type { ExtendedClient } from "../interfaces/ExtendedClient.js"; + +export class Prometheus { + private client = client; + private guilds: Gauge; + private commands: Counter; + + constructor(bot: ExtendedClient) { + this.guilds = new Gauge({ + name: "guilds", + help: "The number of guilds the bot is in." + }); + this.guilds.set(bot.guilds.cache.size); + this.commands = new Counter({ + name: "commands", + help: "The number of commands that have been used since last boot." + }); + this.client.collectDefaultMetrics(); + } + + public commandUsed() { + this.commands.inc(); + } + + public updateGuilds(bot: ExtendedClient) { + this.guilds.set(bot.guilds.cache.size); + } +} diff --git a/src/server/serve.ts b/src/server/serve.ts index daf3382..dd9e3ea 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -6,6 +6,7 @@ import https from "https"; import { Octokit } from "@octokit/rest"; import { GuildTextBasedChannel } from "discord.js"; import express from "express"; +import { register } from "prom-client"; import { IgnoredActors, ThankYou } from "../config/Github"; import { ExtendedClient } from "../interfaces/ExtendedClient"; @@ -47,6 +48,15 @@ export const serve = async (bot: ExtendedClient) => { res.send("bot online!"); }); + app.get("/metrics", async (_req, res) => { + try { + res.set("Content-Type", register.contentType); + res.end(await register.metrics()); + } catch (err) { + res.status(500).end(err); + } + }); + app.post("/kofi", async (req, res) => { const payload = JSON.parse(req.body.data); const {