Merge pull request #3730 from bluewave-labs/feat/additional-mongo-stats

feat: additional mongo stats
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-06-17 11:09:30 -07:00
commit 1e2a4b4a84
6 files changed
+126 -17

No files matched your search

@@ -33,6 +33,26 @@ export const MongoStats = ({ diagnostics }: MongoStatsProps) => {
title={t("pages.logs.diagnostics.mongoDBStats.readyState")}
subtitle={mongoStats.readyState.toString()}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.readsPerSecond")}
subtitle={mongoStats.readsPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.insertsPerSecond")}
subtitle={mongoStats.insertsPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.updatesPerSecond")}
subtitle={mongoStats.updatesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.deletesPerSecond")}
subtitle={mongoStats.deletesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.writesPerSecond")}
subtitle={mongoStats.writesPerSecond.toFixed(0)}
/>
<StatBox
title={t("pages.logs.diagnostics.mongoDBStats.host")}
subtitle={mongoStats.host}
+5
View File
@@ -23,6 +23,11 @@ export interface V8HeapStats {
export interface MongoStats {
readyState: number;
readsPerSecond: number;
insertsPerSecond: number;
updatesPerSecond: number;
deletesPerSecond: number;
writesPerSecond: number;
host: string;
port: number;
dbName: string;
+5
View File
@@ -975,6 +975,11 @@
},
"mongoDBStats": {
"readyState": "Ready state",
"readsPerSecond": "Reads/s",
"insertsPerSecond": "Inserts/s",
"updatesPerSecond": "Updates/s",
"deletesPerSecond": "Deletes/s",
"writesPerSecond": "Total writes/s",
"host": "Host",
"port": "Port",
"dbName": "Database name",
+1 -1
View File
@@ -14,7 +14,7 @@ import { UserModel } from "../dist/domain/users/user.model.js";
// NOTE: the running queue only schedules monitors it finds at init(), so restart
// the server after seeding for the new monitors to actually start checking.
const DEFAULT_COUNT = 5_000;
const DEFAULT_COUNT = 500;
const DEFAULT_BATCH = 1_000;
const DEFAULT_INTERVAL = 60_000;
const NAME_PREFIX = "Seed";
@@ -6,6 +6,22 @@ import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "diagnosticService";
export interface MongoStats {
reads: number;
inserts: number;
updates: number;
deletes: number;
timestamp: number;
}
export interface MongoOpsPerSecond {
readsPerSecond: number;
insertsPerSecond: number;
updatesPerSecond: number;
deletesPerSecond: number;
writesPerSecond: number;
}
export interface CollectionDiagnostics {
name: string;
documentCount: number;
@@ -17,6 +33,11 @@ export interface CollectionDiagnostics {
export interface MongoDiagnostics {
readyState: number;
readsPerSecond: number;
insertsPerSecond: number;
updatesPerSecond: number;
deletesPerSecond: number;
writesPerSecond: number;
host: string;
port: number;
dbName: string;
@@ -44,6 +65,8 @@ export interface IDiagnosticService {
export class DiagnosticService implements IDiagnosticService {
static SERVICE_NAME = SERVICE_NAME;
private lastMongoOpsPerSecond: MongoStats | null = null;
constructor(private db: IDb<Mongoose>) {
/**
* Performance Observer for monitoring system performance metrics.
@@ -68,6 +91,48 @@ export class DiagnosticService implements IDiagnosticService {
return ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100;
};
private getMongoOpsPerSecond = async (): Promise<MongoOpsPerSecond | null> => {
try {
const mongo = await this.db.getConnection();
const db = mongo.connection.db;
if (!db) {
throw new AppError({ message: "Database connection is not available", service: SERVICE_NAME, method: "getMongoDBStats" });
}
const sample = async (): Promise<MongoStats> => {
const res = await db.command({ serverStatus: 1 });
return {
reads: (res.opcounters?.query ?? 0) + (res.opcounters?.getmore ?? 0),
inserts: res.opcounters?.insert ?? 0,
updates: res.opcounters?.update ?? 0,
deletes: res.opcounters?.delete ?? 0,
timestamp: Date.now(),
};
};
// First call has no baseline, seed one so we always have a rate
if (this.lastMongoOpsPerSecond === null) {
this.lastMongoOpsPerSecond = await sample();
await new Promise((resolve) => setTimeout(resolve, 1000));
}
const previous = this.lastMongoOpsPerSecond;
const current = await sample();
this.lastMongoOpsPerSecond = current;
const elapsedSeconds = (current.timestamp - previous.timestamp) / 1000;
return {
readsPerSecond: (current.reads - previous.reads) / elapsedSeconds,
insertsPerSecond: (current.inserts - previous.inserts) / elapsedSeconds,
updatesPerSecond: (current.updates - previous.updates) / elapsedSeconds,
deletesPerSecond: (current.deletes - previous.deletes) / elapsedSeconds,
writesPerSecond:
(current.inserts - previous.inserts + current.updates - previous.updates + current.deletes - previous.deletes) / elapsedSeconds,
};
} catch {
return null;
}
};
private getMongoDBStats = async (): Promise<MongoDiagnostics> => {
const mongo = await this.db.getConnection();
const db = mongo.connection.db;
@@ -75,6 +140,8 @@ export class DiagnosticService implements IDiagnosticService {
throw new AppError({ message: "Database connection is not available", service: SERVICE_NAME, method: "getMongoDBStats" });
}
const opsPerSecond = await this.getMongoOpsPerSecond();
const dbStats = await db.stats();
const rawCollections = await db.collections();
@@ -100,6 +167,11 @@ export class DiagnosticService implements IDiagnosticService {
return {
readyState: mongo.connection.readyState,
readsPerSecond: opsPerSecond?.readsPerSecond ?? 0,
insertsPerSecond: opsPerSecond?.insertsPerSecond ?? 0,
updatesPerSecond: opsPerSecond?.updatesPerSecond ?? 0,
deletesPerSecond: opsPerSecond?.deletesPerSecond ?? 0,
writesPerSecond: opsPerSecond?.writesPerSecond ?? 0,
host: mongo.connection.host,
port: mongo.connection.port,
dbName: mongo.connection.name,
@@ -104,9 +104,8 @@ describe("DiagnosticService", () => {
it("returns only the slimmed diagnostic shape (no serverStatus/serverInfo/memoryUsage)", async () => {
const service = new DiagnosticService(makeDb());
const promise = service.getSystemStats();
// getCPUUsagePercentage waits 1s, then event loop delay waits 0ms
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
// getCPUUsagePercentage waits 1s, the first mongo ops sample waits another 1s, then event loop delay waits 0ms
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
expect(Object.keys(result).sort()).toEqual(["cpuUsage", "eventLoopDelayMs", "mongoStats", "osStats", "uptimeMs", "v8HeapStats"].sort());
@@ -131,11 +130,24 @@ describe("DiagnosticService", () => {
});
const service = new DiagnosticService(db);
const promise = service.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
expect(Object.keys(result.mongoStats).sort()).toEqual(["collections", "dbName", "host", "port", "readyState", "totalSize"].sort());
expect(Object.keys(result.mongoStats).sort()).toEqual(
[
"collections",
"dbName",
"host",
"port",
"readyState",
"totalSize",
"readsPerSecond",
"insertsPerSecond",
"updatesPerSecond",
"deletesPerSecond",
"writesPerSecond",
].sort()
);
expect(result.mongoStats.totalSize).toBe(9999);
expect(result.mongoStats.collections).toHaveLength(2);
expect(result.mongoStats.collections[0]).toEqual({
@@ -154,8 +166,7 @@ describe("DiagnosticService", () => {
});
const service = new DiagnosticService(db);
const promise = service.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
expect(result.mongoStats.totalSize).toBe(376832 + 1155072);
@@ -169,8 +180,7 @@ describe("DiagnosticService", () => {
});
const service = new DiagnosticService(db);
const promise = service.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
const coll = result.mongoStats.collections[0];
@@ -190,8 +200,7 @@ describe("DiagnosticService", () => {
});
const service = new DiagnosticService(db);
const promise = service.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
expect(result.mongoStats.collections[0]?.bucketCount).toBe(12);
@@ -199,8 +208,7 @@ describe("DiagnosticService", () => {
const db2 = makeDb({ collections: [{ collectionName: "monitors" }] });
const service2 = new DiagnosticService(db2);
const promise2 = service2.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result2 = await promise2;
expect(result2.mongoStats.collections[0]).not.toHaveProperty("bucketCount");
});
@@ -235,8 +243,7 @@ describe("DiagnosticService", () => {
performance.getEntriesByName = jest.fn().mockReturnValue(entries) as any;
const promise = service.getSystemStats();
jest.advanceTimersByTime(1000);
await jest.advanceTimersByTimeAsync(10);
await jest.advanceTimersByTimeAsync(2500);
const result = await promise;
expect(result.eventLoopDelayMs).toBe(0);