diff --git a/client/src/Pages/Logs/components/MongoStats.tsx b/client/src/Pages/Logs/components/MongoStats.tsx index ed7b9cc53..8dc2f0231 100644 --- a/client/src/Pages/Logs/components/MongoStats.tsx +++ b/client/src/Pages/Logs/components/MongoStats.tsx @@ -33,6 +33,26 @@ export const MongoStats = ({ diagnostics }: MongoStatsProps) => { title={t("pages.logs.diagnostics.mongoDBStats.readyState")} subtitle={mongoStats.readyState.toString()} /> + + + + + ) { /** * 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 => { + 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 => { + 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 => { 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, diff --git a/server/test/unit/services/diagnosticService.test.ts b/server/test/unit/services/diagnosticService.test.ts index b44de1986..07962a68e 100644 --- a/server/test/unit/services/diagnosticService.test.ts +++ b/server/test/unit/services/diagnosticService.test.ts @@ -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);