add prometheus metrics (fixes #47)
This commit is contained in:
parent
80eebe38f0
commit
be0f53c0e2
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue