prometheus + load-testing

This commit is contained in:
PxlLoewe
2025-06-28 16:05:44 -07:00
parent 246ce0ce22
commit 453cf9a414
20 changed files with 8171 additions and 404 deletions

View File

@@ -14,6 +14,7 @@ import cookieParser from "cookie-parser";
import cors from "cors";
import { authMiddleware } from "modules/expressMiddleware";
import "modules/chron";
import { socketConnections } from "modules/prometheus";
const app = express();
const server = createServer(app);
@@ -25,7 +26,10 @@ export const io = new Server(server, {
io.use(jwtMiddleware);
io.on("connection", (socket) => {
console.log("New socket connection", socket.id);
socketConnections.inc();
socket.on("disconnect", () => {
socketConnections.dec();
});
socket.on("connect-dispatch", handleConnectDispatch(socket, io));
socket.on("connect-pilot", handleConnectPilot(socket, io));
socket.on("connect-desktop", handleConnectDesktop(socket, io));

View File

@@ -0,0 +1,50 @@
import { prisma } from "@repo/db";
import promClient from "prom-client";
export const promRegister = new promClient.Registry();
promClient.collectDefaultMetrics({ register: promRegister });
export const socketConnections = new promClient.Gauge({
name: "socket_connections",
help: "Number of active socket connections",
registers: [promRegister],
});
export const aircraftPatches = new promClient.Counter({
name: "aircraft_patches",
help: "Counts patch requests for aircrafts",
registers: [promRegister],
});
export const connectedPilots = new promClient.Gauge({
name: "connected_pilots",
help: "Counts connected pilots",
registers: [promRegister],
collect: async () => {
const count = await prisma.connectedAircraft.count({
where: {
logoutTime: null,
},
});
connectedPilots.set(count);
},
});
export const connectedDispatcher = new promClient.Gauge({
name: "connected_dispatcher",
help: "Counts connected dispatchers",
registers: [promRegister],
collect: async () => {
const count = await prisma.connectedDispatcher.count({
where: {
logoutTime: null,
},
});
connectedDispatcher.set(count);
},
});
promRegister.registerMetric(socketConnections);
promRegister.registerMetric(aircraftPatches);
promRegister.registerMetric(connectedPilots);
promRegister.registerMetric(connectedDispatcher);

View File

@@ -5,7 +5,7 @@ import jwt from "jsonwebtoken";
export const jwtMiddleware = async (socket: Socket, next: (err?: ExtendedError) => void) => {
try {
const { uid } = socket.handshake.auth;
const uid = socket.handshake.auth.uid || socket.handshake.query.uid;
if (!uid) return new Error("Authentication error");
/* const token = socket.handshake.auth?.token;
if (!token) return new Error("Authentication error");

View File

@@ -11,8 +11,8 @@
"packageManager": "pnpm@10.11.0",
"devDependencies": {
"@repo/db": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/shared-components": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.18",
"@types/express": "^5.0.2",
@@ -37,6 +37,7 @@
"node-cron": "^4.1.0",
"nodemailer": "^7.0.3",
"nodemon": "^3.1.10",
"prom-client": "^15.1.3",
"react": "^19.1.0",
"redis": "^5.1.1",
"socket.io": "^4.8.1",

View File

@@ -1,13 +1,7 @@
import {
AdminMessage,
ConnectedAircraft,
getPublicUser,
MissionLog,
Prisma,
prisma,
} from "@repo/db";
import { AdminMessage, getPublicUser, MissionLog, Prisma, prisma } from "@repo/db";
import { Router } from "express";
import { io } from "../index";
import { aircraftPatches } from "modules/prometheus";
const router: Router = Router();
@@ -97,7 +91,7 @@ router.patch("/:id", async (req, res) => {
},
});
}
aircraftPatches.inc();
res.json(updatedConnectedAircraft);
// When change is only the estimated logout time, we don't need to emit an event
if (Object.keys(aircraftUpdate).length === 1 && aircraftUpdate.esimatedLogoutTime) return;

View File

@@ -0,0 +1,9 @@
import { Router } from "express";
import { promRegister } from "modules/prometheus";
export const metricsRouter: Router = Router();
metricsRouter.get("/", async (req, res) => {
res.setHeader("Content-Type", promRegister.contentType);
res.end(await promRegister.metrics());
});

View File

@@ -4,6 +4,7 @@ import missionRouter from "./mission";
import statusRouter from "./status";
import aircraftsRouter from "./aircraft";
import reportRouter from "./report";
import { metricsRouter } from "routes/metrics";
const router: Router = Router();
@@ -12,5 +13,6 @@ router.use("/mission", missionRouter);
router.use("/status", statusRouter);
router.use("/aircrafts", aircraftsRouter);
router.use("/report", reportRouter);
router.use("/metrics", metricsRouter);
export default router;

View File

@@ -16,6 +16,7 @@ export const handleConnectPilot =
debug: boolean;
}) => {
try {
console.log("Connecting pilot:", socket.id, "Station ID:", stationId, "Debug mode:", debug);
if (!stationId) return Error("Station ID is required");
const user: User = socket.data.user; // User ID aus dem JWT-Token
const userId = socket.data.user.id; // User ID aus dem JWT-Token
@@ -43,7 +44,7 @@ export const handleConnectPilot =
},
});
if (existingConnection) {
if (existingConnection && !debug) {
await io.to(`user:${user.id}`).emit("force-disconnect", "double-connection");
await prisma.connectedAircraft.updateMany({
where: {

View File

@@ -13,7 +13,7 @@ import { redirect } from "next/navigation";
import { ListInput } from "_components/ui/List";
export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
const form = useForm<z.infer<typeof KeywordOptionalDefaultsSchema>>({
const form = useForm({
resolver: zodResolver(KeywordOptionalDefaultsSchema),
defaultValues: keyword,
});
@@ -36,9 +36,7 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
<FileText className="w-5 h-5" /> Allgemeines
</h2>
<label className="form-control w-full ">
<span className="label-text text-lg flex items-center gap-2">
Kategorie
</span>
<span className="label-text text-lg flex items-center gap-2">Kategorie</span>
<select
className="input-sm select select-bordered select-sm w-full"
{...form.register("category")}
@@ -50,12 +48,7 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
))}
</select>
</label>
<Input
form={form}
label="Abkürzung"
name="abreviation"
className="input-sm"
/>
<Input form={form} label="Abkürzung" name="abreviation" className="input-sm" />
<Input
form={form}
label="Name"
@@ -82,11 +75,7 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">
<Button
isLoading={loading}
type="submit"
className="btn btn-primary flex-1"
>
<Button isLoading={loading} type="submit" className="btn btn-primary flex-1">
Speichern
</Button>
{keyword && (

View File

@@ -12,7 +12,7 @@ import { Button } from "../../../../_components/ui/Button";
import { redirect } from "next/navigation";
export const StationForm = ({ station }: { station?: Station }) => {
const form = useForm<z.infer<typeof StationOptionalDefaultsSchema>>({
const form = useForm({
resolver: zodResolver(StationOptionalDefaultsSchema),
defaultValues: station,
});

View File

@@ -10,50 +10,50 @@
"lint": "next lint"
},
"dependencies": {
"@eslint/eslintrc": "^3",
"@hookform/resolvers": "^5.0.1",
"@eslint/eslintrc": "^3.3.1",
"@hookform/resolvers": "^5.1.1",
"@next-auth/prisma-adapter": "^1.0.7",
"@radix-ui/react-icons": "^1.3.2",
"@repo/db": "workspace:*",
"@repo/eslint-config": "workspace:*",
"@repo/shared-components": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.8",
"@tanstack/react-query": "^5.79.2",
"@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-table": "^8.21.3",
"@types/bcryptjs": "^3.0.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.15.34",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@uiw/react-md-editor": "^4.0.7",
"axios": "^1.9.0",
"axios": "^1.10.0",
"bcryptjs": "^3.0.2",
"clsx": "^2.1.1",
"daisyui": "^5.0.43",
"date-fns": "^4.1.0",
"eslint": "^9.15.0",
"eslint-config-next": "^15.3.3",
"eslint": "^9.30.0",
"eslint-config-next": "^15.3.4",
"i": "^0.3.7",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"lucide-react": "^0.511.0",
"next": "^15.3.3",
"next": "^15.3.4",
"next-auth": "^4.24.11",
"next-remove-imports": "^1.0.12",
"npm": "^11.4.1",
"postcss": "^8.5.4",
"npm": "^11.4.2",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-hook-form": "^7.59.0",
"react-hot-toast": "^2.5.2",
"react-select": "^5.10.1",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.8",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"zod": "^3.25.46"
"zod": "^3.25.67",
"zustand": "^5.0.6"
}
}