Merge pull request #3730 from bluewave-labs/feat/additional-mongo-stats
feat: additional mongo stats
This commit is contained in:
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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user