Files
overlord/Overlord-Server/src/metrics.test.ts
T

267 lines
9.0 KiB
TypeScript

import { describe, expect, test, beforeEach } from "bun:test";
import { metrics } from "./metrics";
beforeEach(() => {
metrics.reset();
});
describe("MetricsCollector", () => {
describe("connection tracking", () => {
test("records connections and disconnections", () => {
metrics.recordConnection();
metrics.recordConnection();
metrics.recordDisconnection();
const snap = metrics.getSnapshot();
expect(snap.connections.totalConnections).toBe(2);
expect(snap.connections.totalDisconnections).toBe(1);
expect(snap.connections.activeConnections).toBe(1);
});
test("active connections can go to zero", () => {
const before = metrics.getSnapshot();
const baseConns = before.connections.totalConnections;
const baseDisconns = before.connections.totalDisconnections;
metrics.recordConnection();
metrics.recordDisconnection();
const snap = metrics.getSnapshot();
expect(snap.connections.totalConnections).toBe(baseConns + 1);
expect(snap.connections.totalDisconnections).toBe(baseDisconns + 1);
expect(snap.connections.activeConnections).toBe(before.connections.activeConnections);
});
});
describe("command tracking", () => {
test("records command count and type breakdown", () => {
metrics.recordCommand("console");
metrics.recordCommand("console");
metrics.recordCommand("desktop_start");
const snap = metrics.getSnapshot();
expect(snap.commands.total).toBe(3);
expect(snap.commands.byType["console"]).toBe(2);
expect(snap.commands.byType["desktop_start"]).toBe(1);
});
test("commands within last minute are counted", () => {
metrics.recordCommand("test");
const snap = metrics.getSnapshot();
expect(snap.commands.lastMinute).toBeGreaterThanOrEqual(1);
expect(snap.commands.lastHour).toBeGreaterThanOrEqual(1);
});
});
describe("bandwidth tracking", () => {
test("records bytes sent and received", () => {
metrics.recordBytesSent(1024);
metrics.recordBytesSent(2048);
metrics.recordBytesReceived(512);
const snap = metrics.getSnapshot();
expect(snap.bandwidth.sent).toBe(3072);
expect(snap.bandwidth.received).toBe(512);
});
});
describe("ping tracking", () => {
test("returns null stats when no pings recorded", () => {
const snap = metrics.getSnapshot();
expect(snap.ping.min).toBeNull();
expect(snap.ping.max).toBeNull();
expect(snap.ping.avg).toBeNull();
expect(snap.ping.count).toBe(0);
});
test("computes min/max/avg correctly", () => {
metrics.recordPing(10);
metrics.recordPing(20);
metrics.recordPing(30);
const snap = metrics.getSnapshot();
expect(snap.ping.min).toBe(10);
expect(snap.ping.max).toBe(30);
expect(snap.ping.avg).toBe(20);
expect(snap.ping.count).toBe(3);
});
test("single ping has same min/max/avg", () => {
metrics.recordPing(42);
const snap = metrics.getSnapshot();
expect(snap.ping.min).toBe(42);
expect(snap.ping.max).toBe(42);
expect(snap.ping.avg).toBe(42);
expect(snap.ping.count).toBe(1);
});
});
describe("HTTP request tracking", () => {
test("records total HTTP requests", () => {
metrics.recordHttpRequest(10, 200);
metrics.recordHttpRequest(20, 200);
metrics.recordHttpRequest(5, 404);
const snap = metrics.getSnapshot();
expect(snap.http.total).toBe(3);
});
test("tracks error responses in last minute", () => {
metrics.recordHttpRequest(10, 200);
metrics.recordHttpRequest(20, 500);
metrics.recordHttpRequest(5, 404);
const snap = metrics.getSnapshot();
expect(snap.http.lastMinuteErrors).toBeGreaterThanOrEqual(2);
});
test("computes latency avg and p95", () => {
for (let i = 1; i <= 100; i++) {
metrics.recordHttpRequest(i, 200);
}
const snap = metrics.getSnapshot();
expect(snap.http.latencyAvg).toBeCloseTo(50.5, 0);
expect(snap.http.latencyP95).toBeGreaterThanOrEqual(90);
expect(snap.http.latencyP99).toBeGreaterThanOrEqual(99);
});
test("tracks slow HTTP routes", () => {
metrics.recordHttpRequest(15, 200, "GET /api/metrics");
metrics.recordHttpRequest(120, 200, "GET /api/clients/:id/rd/ws");
metrics.recordHttpRequest(200, 500, "GET /api/clients/:id/rd/ws");
const snap = metrics.getSnapshot();
expect(snap.http.routes[0].route).toBe("GET /api/clients/:id/rd/ws");
expect(snap.http.routes[0].countLastMinute).toBe(2);
expect(snap.http.routes[0].errorsLastMinute).toBe(1);
expect(snap.http.routes[0].latencyP95).toBeGreaterThanOrEqual(120);
});
test("does not count the metrics dashboard poll as HTTP pressure", () => {
metrics.recordHttpRequest(100, 200, "GET /api/metrics");
metrics.recordHttpRequest(20, 200, "GET /api/clients");
const snap = metrics.getSnapshot();
expect(snap.http.total).toBe(1);
expect(snap.http.lastMinute).toBe(1);
expect(snap.http.routes.map((route) => route.route)).toEqual([
"GET /api/clients",
]);
});
});
describe("snapshot structure", () => {
test("returns expected top-level keys", () => {
const snap = metrics.getSnapshot();
expect(snap).toHaveProperty("timestamp");
expect(snap).toHaveProperty("clients");
expect(snap).toHaveProperty("connections");
expect(snap).toHaveProperty("commands");
expect(snap).toHaveProperty("sessions");
expect(snap).toHaveProperty("bandwidth");
expect(snap).toHaveProperty("server");
expect(snap).toHaveProperty("ping");
expect(snap).toHaveProperty("http");
expect(snap).toHaveProperty("eventLoop");
});
test("server uptime is positive", () => {
const snap = metrics.getSnapshot();
expect(snap.server.uptime).toBeGreaterThanOrEqual(0);
expect(snap.server.startTime).toBeGreaterThan(0);
});
test("system memory values are reasonable", () => {
const snap = metrics.getSnapshot();
expect(snap.server.systemMemory.total).toBeGreaterThan(0);
expect(snap.server.systemMemory.free).toBeGreaterThanOrEqual(0);
expect(snap.server.systemMemory.usedPercent).toBeGreaterThanOrEqual(0);
expect(snap.server.systemMemory.usedPercent).toBeLessThanOrEqual(100);
});
test("cpu cores is positive", () => {
const snap = metrics.getSnapshot();
expect(snap.server.cpu.cores).toBeGreaterThan(0);
});
});
describe("history", () => {
test("starts empty", () => {
expect(metrics.getHistory()).toEqual([]);
});
test("recordHistoryEntry adds to history", () => {
const snap = metrics.getSnapshot();
metrics.recordHistoryEntry(snap);
const history = metrics.getHistory();
expect(history).toHaveLength(1);
expect(history[0].timestamp).toBe(snap.timestamp);
});
test("history captures pressure metrics for charting", () => {
metrics.recordHttpRequest(25, 200);
metrics.recordHttpRequest(80, 500);
metrics.recordBytesSent(1024);
metrics.recordBytesReceived(2048);
const snap = metrics.getSnapshot();
metrics.recordHistoryEntry(snap);
const [entry] = metrics.getHistory();
expect(entry.httpRequestsPerMinute).toBeGreaterThanOrEqual(2);
expect(entry.httpErrorsPerMinute).toBeGreaterThanOrEqual(1);
expect(entry.httpLatencyAvg).toBeGreaterThan(0);
expect(entry.httpLatencyP95).toBeGreaterThan(0);
expect(entry.heapUsed).toBeGreaterThan(0);
expect(entry.rss).toBeGreaterThan(0);
expect(entry.systemMemoryUsedPercent).toBeGreaterThanOrEqual(0);
expect(entry.activeSessions).toBe(0);
});
test("history is capped at maxHistoryPoints", () => {
const weekOfFiveSecondSamples = 7 * 24 * 60 * 12;
for (let i = 0; i < weekOfFiveSecondSamples + 180; i++) {
const snap = metrics.getSnapshot();
metrics.recordHistoryEntry(snap);
}
const history = metrics.getHistory();
expect(history.length).toBeLessThanOrEqual(weekOfFiveSecondSamples);
});
test("getHistory returns a copy", () => {
const snap = metrics.getSnapshot();
metrics.recordHistoryEntry(snap);
const h1 = metrics.getHistory();
const h2 = metrics.getHistory();
expect(h1).not.toBe(h2);
expect(h1).toEqual(h2);
});
});
describe("reset", () => {
test("clears resettable counters", () => {
metrics.recordCommand("test");
metrics.recordBytesSent(100);
metrics.recordBytesReceived(200);
metrics.recordPing(50);
metrics.recordHttpRequest(10, 200);
metrics.reset();
const snap = metrics.getSnapshot();
// Note: connection counters are cumulative and not cleared by reset()
expect(snap.commands.total).toBe(0);
expect(snap.bandwidth.sent).toBe(0);
expect(snap.bandwidth.received).toBe(0);
expect(snap.ping.count).toBe(0);
expect(snap.http.total).toBe(0);
});
});
});