add prometheus metrics (fixes #47)

This commit is contained in:
Grant 2024-06-18 16:19:23 -06:00
parent 80eebe38f0
commit be0f53c0e2
7 changed files with 177 additions and 2 deletions

26
package-lock.json generated
View File

@ -7453,6 +7453,11 @@
"node": ">=8"
}
},
"node_modules/bintrees": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -12428,6 +12433,18 @@
"node": ">=0.4.0"
}
},
"node_modules/prom-client": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.2.tgz",
"integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==",
"dependencies": {
"@opentelemetry/api": "^1.4.0",
"tdigest": "^0.1.1"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -14215,6 +14232,14 @@
"node": ">=6"
}
},
"node_modules/tdigest": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
"dependencies": {
"bintrees": "1.0.2"
}
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@ -16298,6 +16323,7 @@
"express-session": "^1.17.3",
"openid-client": "^5.6.5",
"prisma-dbml-generator": "^0.12.0",
"prom-client": "^15.1.2",
"rate-limit-redis": "^4.2.0",
"redis": "^4.6.12",
"socket.io": "^4.7.2",

View File

@ -39,6 +39,7 @@
"express-session": "^1.17.3",
"openid-client": "^5.6.5",
"prisma-dbml-generator": "^0.12.0",
"prom-client": "^15.1.2",
"rate-limit-redis": "^4.2.0",
"redis": "^4.6.12",
"socket.io": "^4.7.2",

View File

@ -28,6 +28,18 @@ if (!process.env.SESSION_SECRET) {
process.exit(1);
}
if (!process.env.NODE_APP_INSTANCE) {
Logger.warn(
"NODE_APP_INSTANCE is not defined, metrics will not include process label"
);
}
if (!process.env.PROMETHEUS_TOKEN) {
Logger.warn(
"PROMETHEUS_TOKEN is not defined, /metrics will not be accessable"
);
}
if (!process.env.REDIS_HOST) {
Logger.error("REDIS_HOST is not defined");
process.exit(1);

View File

@ -141,6 +141,23 @@ class Canvas {
return await this.canvasToRedis();
}
/**
* Get if a pixel is maybe empty
* @param x
* @param y
* @returns
*/
async isPixelEmpty(x: number, y: number) {
const redis = await Redis.getClient();
const pixelColor = await redis.get(Redis.key("pixelColor", x, y));
if (pixelColor === null) {
return true;
}
return pixelColor === "transparent";
}
async getPixel(x: number, y: number) {
return (
await prisma.pixel.findMany({

View File

@ -9,6 +9,7 @@ import APIRoutes_client from "../api/client";
import APIRoutes_admin from "../api/admin";
import { Logger } from "./Logger";
import bodyParser from "body-parser";
import { handleMetricsEndpoint } from "./Prometheus";
export const session = expressSession({
secret: process.env.SESSION_SECRET,
@ -93,6 +94,7 @@ export class ExpressServer {
this.app.use(bodyParser.json());
this.app.use("/api", APIRoutes_client);
this.app.use("/api/admin", APIRoutes_admin);
this.app.use("/metrics", handleMetricsEndpoint);
this.httpServer.listen(parseInt(process.env.PORT), () => {
Logger.info("Listening on :" + process.env.PORT);

View File

@ -0,0 +1,113 @@
import client, { register } from "prom-client";
import { prisma } from "./prisma";
import e from "express";
import { SocketServer } from "./SocketServer";
import Canvas from "./Canvas";
import { Redis } from "./redis";
client.collectDefaultMetrics({
labels: process.env.NODE_APP_INSTANCE
? {
NODE_APP_INSTANCE: process.env.NODE_APP_INSTANCE,
}
: {},
});
export const PixelCount = new client.Gauge({
name: "pixel_count",
help: "total pixel count",
async collect() {
this.set(await prisma.pixel.count());
},
});
export const UserCount = new client.Gauge({
name: "user_count",
help: "total user count",
async collect() {
this.set(await prisma.user.count());
},
});
export const OnlineUsers = new client.Gauge({
name: "connected_count",
help: "total connected sockets",
async collect() {
this.set((await SocketServer.instance.io.fetchSockets()).length);
},
});
/**
* Rough estimate of empty pixels
*/
export const EmptyPixels = new client.Gauge({
name: "empty_pixels",
help: "total number of empty pixels",
async collect() {
let queries: Promise<boolean>[] = [];
for (let x = 0; x < Canvas.getCanvasConfig().size[0]; x++) {
for (let y = 0; y < Canvas.getCanvasConfig().size[1]; y++) {
queries.push(Canvas.isPixelEmpty(x, y));
}
}
let count = 0;
const allSettled = await Promise.allSettled(queries);
for (const settle of allSettled) {
if (settle.status === "fulfilled") {
count += Number(settle.value);
} else {
count++;
}
}
this.set(count);
},
});
export const TotalPixels = new client.Gauge({
name: "total_pixels",
help: "total number of pixels the canvas allows",
async collect() {
const [width, height] = Canvas.getCanvasConfig().size;
this.set(width * height);
},
});
export const UniqueInstances = new client.Gauge({
name: "instance_count",
help: "total number of unique instances",
async collect() {
this.set(await prisma.instance.count());
},
});
export const handleMetricsEndpoint = async (
req: e.Request,
res: e.Response
) => {
if (!process.env.PROMETHEUS_TOKEN) {
res.status(500);
res.send("PROMETHEUS_TOKEN is not set.");
return;
}
if (req.headers.authorization !== "Bearer " + process.env.PROMETHEUS_TOKEN) {
res.status(401);
res.send("Invalid bearer token");
return;
}
res.setHeader("Content-Type", register.contentType);
res.send(await register.metrics());
res.end();
};

View File

@ -19,12 +19,16 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
NODE_APP_INSTANCE?: string;
PORT: string;
LOG_LEVEL?: string;
SESSION_SECRET: string;
PROMETHEUS_TOKEN?: string;
REDIS_HOST: string;
REDIS_SESSION_PREFIX: string;
REDIS_RATELIMIT_PREFIX: string;
REDIS_SESSION_PREFIX?: string;
REDIS_RATELIMIT_PREFIX?: string;
/**
* hostname that is used in the callback