Merge pull request #3675 from bluewave-labs/feat/mongo-diagnostics
Feat/mongo diagnostics
This commit is contained in:
15 files changed
+785
-190
No files matched your search
Generated
+234
-2
@@ -57,6 +57,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"lint-staged": "^16.2.7",
|
||||
"mongoose": "9.6.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.4.2"
|
||||
@@ -84,6 +85,7 @@
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -354,6 +356,7 @@
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.14.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -391,6 +394,7 @@
|
||||
"node_modules/@emotion/styled": {
|
||||
"version": "11.14.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -1107,6 +1111,16 @@
|
||||
"supercluster": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
|
||||
"integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
@@ -1160,6 +1174,7 @@
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/core-downloads-tracker": "^7.3.7",
|
||||
@@ -1264,6 +1279,7 @@
|
||||
"node_modules/@mui/system": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/private-theming": "^7.3.7",
|
||||
@@ -2304,6 +2320,7 @@
|
||||
"version": "24.5.2",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
@@ -2319,6 +2336,7 @@
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2352,6 +2370,23 @@
|
||||
"version": "0.0.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
|
||||
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
||||
@@ -2392,6 +2427,7 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@@ -2672,6 +2708,7 @@
|
||||
"version": "8.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3006,6 +3043,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3020,6 +3058,16 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
|
||||
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytewise": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
|
||||
@@ -3437,7 +3485,8 @@
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -3832,6 +3881,7 @@
|
||||
"version": "8.57.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -4627,6 +4677,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -5222,6 +5273,16 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz",
|
||||
"integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
@@ -5412,6 +5473,7 @@
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
||||
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
@@ -5451,6 +5513,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -5540,6 +5609,109 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
|
||||
"integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.3.0",
|
||||
"bson": "^7.2.0",
|
||||
"mongodb-connection-string-url": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.806.0",
|
||||
"@mongodb-js/zstd": "^7.0.0",
|
||||
"gcp-metadata": "^7.0.1",
|
||||
"kerberos": "^7.0.0",
|
||||
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
|
||||
"snappy": "^7.3.2",
|
||||
"socks": "^2.8.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
|
||||
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^13.0.0",
|
||||
"whatwg-url": "^14.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "9.6.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.2.tgz",
|
||||
"integrity": "sha512-7m8HntjkoRnwEmuPC0kdlwcZXJOQf4twumFj+PNzg/anqqZE2Er7hQslqyzy07mP3JcFjoTSgH5765PyqOXsxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kareem": "3.3.0",
|
||||
"mongodb": "~7.2",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "6.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mpath": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mquery": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
|
||||
"integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
@@ -6078,6 +6250,7 @@
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -6089,6 +6262,7 @@
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.71.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -6288,7 +6462,8 @@
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
@@ -6438,6 +6613,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -6713,6 +6889,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "17.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -6823,6 +7006,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/split-string": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
@@ -7136,6 +7329,19 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
@@ -7249,6 +7455,7 @@
|
||||
"version": "5.9.3",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7384,6 +7591,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -7472,6 +7680,30 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"lint-staged": "^16.2.7",
|
||||
"mongoose": "9.6.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.4.2"
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
import { BaseChart } from "@/Components/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Box, { type BoxProps } from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { forwardRef, useMemo, useState, useEffect } from "react";
|
||||
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
|
||||
export const Gauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}: {
|
||||
interface GaugeProps extends Omit<BoxProps, "color"> {
|
||||
isLoading?: boolean;
|
||||
progress?: number;
|
||||
radius?: number;
|
||||
strokeWidth?: number;
|
||||
precision?: number;
|
||||
unit?: string;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const Gauge = forwardRef<HTMLDivElement, GaugeProps>(function Gauge(
|
||||
{
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const theme = useTheme();
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
@@ -51,17 +57,19 @@ export const Gauge = ({
|
||||
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
|
||||
|
||||
if (isLoading) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
display={"inline-block"}
|
||||
position={"relative"}
|
||||
width={radius}
|
||||
height={radius}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
borderRadius={"50%"}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
@@ -105,7 +113,7 @@ export const Gauge = ({
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const DetailGauge = ({
|
||||
title,
|
||||
|
||||
@@ -6,7 +6,11 @@ import type { TooltipProps } from "@mui/material/Tooltip";
|
||||
|
||||
type StyledTooltipProps = TooltipProps;
|
||||
|
||||
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
|
||||
export const Tooltip = ({
|
||||
placement = "top",
|
||||
slotProps,
|
||||
...props
|
||||
}: StyledTooltipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -27,6 +31,7 @@ export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) =>
|
||||
boxShadow: theme.shadows[6],
|
||||
},
|
||||
},
|
||||
...slotProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Stats } from "@/Pages/Logs/components/Stats";
|
||||
import { MongoStats } from "@/Pages/Logs/components/MongoStats";
|
||||
import { MongoCollectionStats } from "./components/MongoCollectionStats";
|
||||
import { StatGauges } from "@/Pages/Logs/components/StatGauges";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
export const TabDiagnostics = () => {
|
||||
const theme = useTheme();
|
||||
@@ -19,12 +22,12 @@ export const TabDiagnostics = () => {
|
||||
} = useGet<Diagnostics>("/diagnostic/system", {}, { refreshInterval: 5000 });
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack gap={theme.spacing(1)}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XL)}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Stack gap={theme.spacing(SPACING.SM)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color="text.secondary"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.logs.diagnostics.sections.runtimeStats.title")}
|
||||
</Typography>
|
||||
@@ -34,11 +37,11 @@ export const TabDiagnostics = () => {
|
||||
</Stack>
|
||||
<Stats diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Stack gap={theme.spacing(1)}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Stack gap={theme.spacing(SPACING.SM)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color="text.secondary"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.logs.diagnostics.sections.memoryCpu.title")}
|
||||
</Typography>
|
||||
@@ -48,6 +51,30 @@ export const TabDiagnostics = () => {
|
||||
</Stack>
|
||||
<StatGauges diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.logs.diagnostics.sections.mongoDBStats.title")}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.logs.diagnostics.sections.mongoDBStats.description")}
|
||||
</Typography>
|
||||
<MongoStats diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.logs.diagnostics.sections.mongoDBCollectionStats.title")}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.logs.diagnostics.sections.mongoDBCollectionStats.description")}
|
||||
</Typography>
|
||||
<MongoCollectionStats diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { StatBox, Gauge, Tooltip } from "@/Components/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface MongoStatsProps {
|
||||
diagnostics: Diagnostics | null;
|
||||
}
|
||||
|
||||
export const MongoCollectionStats = ({ diagnostics }: MongoStatsProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mongoStats = diagnostics?.mongoStats;
|
||||
if (!mongoStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={"grid"}
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
sx={{
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(5, 1fr)" },
|
||||
"& > *": { width: "100% !important" },
|
||||
}}
|
||||
>
|
||||
{mongoStats.collections.map((colStat) => (
|
||||
<StatBox
|
||||
key={colStat.name}
|
||||
title={colStat.name}
|
||||
subtitle={""}
|
||||
>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Stack>
|
||||
{/* <Typography variant="body2">{`Documents: ${colStat.documentCount}`}</Typography> */}
|
||||
<Typography variant="body2">
|
||||
{t("pages.logs.diagnostics.mongoDBCollectionStats.documents", {
|
||||
count: colStat.documentCount,
|
||||
})}
|
||||
</Typography>
|
||||
{colStat.bucketCount !== undefined && (
|
||||
<Typography variant="body2">
|
||||
{t("pages.logs.diagnostics.mongoDBCollectionStats.bucketCount", {
|
||||
count: colStat.bucketCount,
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="body2">
|
||||
{t("pages.logs.diagnostics.mongoDBCollectionStats.storageSize", {
|
||||
size: prettyBytes(colStat.storageSize),
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{t("pages.logs.diagnostics.mongoDBCollectionStats.indexSize", {
|
||||
size: prettyBytes(colStat.totalIndexSize),
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{t("pages.logs.diagnostics.mongoDBCollectionStats.totalSize", {
|
||||
size: prettyBytes(colStat.totalSize),
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack alignItems={"center"}>
|
||||
<Tooltip
|
||||
title={t("pages.logs.diagnostics.mongoDBCollectionStats.dbUsageTooltip", {
|
||||
percentage: ((colStat.totalSize / mongoStats.totalSize) * 100).toFixed(
|
||||
2
|
||||
),
|
||||
})}
|
||||
placement="top"
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Gauge progress={(colStat.totalSize / mongoStats.totalSize) * 100} />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</StatBox>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { StatBox } from "@/Components/design-elements";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface MongoStatsProps {
|
||||
diagnostics: Diagnostics | null;
|
||||
}
|
||||
|
||||
export const MongoStats = ({ diagnostics }: MongoStatsProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mongoStats = diagnostics?.mongoStats;
|
||||
if (!mongoStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={"grid"}
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
sx={{
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(5, 1fr)" },
|
||||
"& > *": { width: "100% !important" },
|
||||
}}
|
||||
>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.readyState")}
|
||||
subtitle={mongoStats.readyState.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.host")}
|
||||
subtitle={mongoStats.host}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.port")}
|
||||
subtitle={mongoStats.port.toString()}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.dbName")}
|
||||
subtitle={mongoStats.dbName}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.logs.diagnostics.mongoDBStats.storageSize")}
|
||||
subtitle={prettyBytes(mongoStats.totalSize)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
const LogsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [activeTab, setActiveTab] = useState<number>(2);
|
||||
return (
|
||||
<BasePage headerKey="logs">
|
||||
<Tabs
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
export interface Diagnostics {
|
||||
osStats: OsStats;
|
||||
memoryUsage: MemoryUsage;
|
||||
cpuUsage: CpuUsage;
|
||||
v8HeapStats: V8HeapStats;
|
||||
eventLoopDelayMs: number;
|
||||
uptimeMs: number;
|
||||
mongoStats: MongoStats;
|
||||
}
|
||||
|
||||
export interface OsStats {
|
||||
freeMemoryBytes: number;
|
||||
totalMemoryBytes: number;
|
||||
}
|
||||
|
||||
export interface MemoryUsage {
|
||||
rss: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
external: number;
|
||||
arrayBuffers: number;
|
||||
}
|
||||
|
||||
export interface CpuUsage {
|
||||
userUsageMs: number;
|
||||
systemUsageMs: number;
|
||||
usagePercentage: number;
|
||||
}
|
||||
|
||||
@@ -31,3 +20,21 @@ export interface V8HeapStats {
|
||||
usedHeapSizeBytes: number;
|
||||
heapSizeLimitBytes: number;
|
||||
}
|
||||
|
||||
export interface MongoStats {
|
||||
readyState: number;
|
||||
host: string;
|
||||
port: number;
|
||||
dbName: string;
|
||||
totalSize: number;
|
||||
collections: CollectionStats[];
|
||||
}
|
||||
|
||||
export interface CollectionStats {
|
||||
name: string;
|
||||
documentCount: number;
|
||||
storageSize: number;
|
||||
totalIndexSize: number;
|
||||
totalSize: number;
|
||||
bucketCount?: number;
|
||||
}
|
||||
@@ -952,6 +952,14 @@
|
||||
"memoryCpu": {
|
||||
"title": "Memory & CPU",
|
||||
"description": "How the process is using its allocated memory and host CPU."
|
||||
},
|
||||
"mongoDBStats": {
|
||||
"title": "MongoDB stats",
|
||||
"description": "Snapshot of MongoDB metrics"
|
||||
},
|
||||
"mongoDBCollectionStats": {
|
||||
"title": "MongoDB collection stats",
|
||||
"description": "Stats for MongoDB collections"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
@@ -961,6 +969,21 @@
|
||||
"totalHeapSize": "Total heap size",
|
||||
"osMemoryLimit": "OS memory limit"
|
||||
},
|
||||
"mongoDBStats": {
|
||||
"readyState": "Ready state",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"dbName": "Database name",
|
||||
"storageSize": "Storage size"
|
||||
},
|
||||
"mongoDBCollectionStats": {
|
||||
"documents": "Documents: {{count}}",
|
||||
"bucketCount": "Bucket count: {{count}}",
|
||||
"storageSize": "Storage size: {{size}}",
|
||||
"indexSize": "Index size: {{size}}",
|
||||
"totalSize": "Total size: {{size}}",
|
||||
"dbUsageTooltip": "{{percentage}}% of DB storage"
|
||||
},
|
||||
"gauges": {
|
||||
"heapAllocation": "Heap allocation",
|
||||
"heapUsage": "Heap usage",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Mongoose } from "mongoose";
|
||||
import MongoDB from "../db/MongoDB.js";
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import {
|
||||
@@ -121,11 +122,11 @@ import {
|
||||
IMaintenanceWindowsRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
// import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
export type InitializedServices = {
|
||||
settingsService: ISettingsService;
|
||||
db: IDb;
|
||||
db: IDb<Mongoose>;
|
||||
networkService: INetworkService;
|
||||
emailService: IEmailService;
|
||||
bufferService: IBufferService;
|
||||
@@ -173,55 +174,76 @@ export const initializeServices = async ({
|
||||
}): Promise<InitializedServices> => {
|
||||
// Create DB
|
||||
|
||||
const dbType = envSettings.dbType;
|
||||
|
||||
let db: IDb | null = null;
|
||||
|
||||
if (dbType === "mongodb") {
|
||||
db = new MongoDB(logger, envSettings);
|
||||
}
|
||||
|
||||
if (!db) {
|
||||
throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
}
|
||||
|
||||
let db: IDb<Mongoose> | null = null;
|
||||
db = new MongoDB(logger, envSettings);
|
||||
await db.connect();
|
||||
|
||||
let monitorsRepository: IMonitorsRepository;
|
||||
let checksRepository: IChecksRepository;
|
||||
let geoChecksRepository: IGeoChecksRepository;
|
||||
let monitorStatsRepository: IMonitorStatsRepository;
|
||||
let statusPagesRepository: IStatusPagesRepository;
|
||||
let usersRepository: IUsersRepository;
|
||||
let invitesRepository: IInvitesRepository;
|
||||
let recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
let settingsRepository: ISettingsRepository;
|
||||
let notificationsRepository: INotificationsRepository;
|
||||
let tagsRepository: ITagsRepository;
|
||||
let incidentsRepository: IIncidentsRepository;
|
||||
let teamsRepository: ITeamsRepository;
|
||||
let maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
// ** NOTE **
|
||||
// const dbType = envSettings.dbType;
|
||||
// DB type has been fixed to MongoDB for now
|
||||
|
||||
// if (dbType === "mongodb") {
|
||||
// db = new MongoDB(logger, envSettings);
|
||||
// }
|
||||
|
||||
// if (!db) {
|
||||
// throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
// }
|
||||
|
||||
// await db.connect();
|
||||
|
||||
// Repositories
|
||||
|
||||
if (dbType === "mongodb") {
|
||||
monitorsRepository = new MongoMonitorsRepository();
|
||||
checksRepository = new MongoChecksRepository(logger);
|
||||
geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
statusPagesRepository = new MongoStatusPagesRepository();
|
||||
usersRepository = new MongoUsersRepository();
|
||||
invitesRepository = new MongoInvitesRepository();
|
||||
recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
settingsRepository = new MongoSettingsRepository();
|
||||
notificationsRepository = new MongoNotificationsRepository();
|
||||
tagsRepository = new MongoTagsRepository();
|
||||
incidentsRepository = new MongoIncidentsRepository();
|
||||
teamsRepository = new MongoTeamsRepository();
|
||||
maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
} else {
|
||||
throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
}
|
||||
// ** NOTE **
|
||||
// DB Type fixed to MongoDB for now
|
||||
// let monitorsRepository: IMonitorsRepository;
|
||||
// let checksRepository: IChecksRepository;
|
||||
// let geoChecksRepository: IGeoChecksRepository;
|
||||
// let monitorStatsRepository: IMonitorStatsRepository;
|
||||
// let statusPagesRepository: IStatusPagesRepository;
|
||||
// let usersRepository: IUsersRepository;
|
||||
// let invitesRepository: IInvitesRepository;
|
||||
// let recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
// let settingsRepository: ISettingsRepository;
|
||||
// let notificationsRepository: INotificationsRepository;
|
||||
// let tagsRepository: ITagsRepository;
|
||||
// let incidentsRepository: IIncidentsRepository;
|
||||
// let teamsRepository: ITeamsRepository;
|
||||
// let maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
|
||||
// if (dbType === "mongodb") {
|
||||
// monitorsRepository = new MongoMonitorsRepository();
|
||||
// checksRepository = new MongoChecksRepository(logger);
|
||||
// geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
// monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
// statusPagesRepository = new MongoStatusPagesRepository();
|
||||
// usersRepository = new MongoUsersRepository();
|
||||
// invitesRepository = new MongoInvitesRepository();
|
||||
// recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
// settingsRepository = new MongoSettingsRepository();
|
||||
// notificationsRepository = new MongoNotificationsRepository();
|
||||
// tagsRepository = new MongoTagsRepository();
|
||||
// incidentsRepository = new MongoIncidentsRepository();
|
||||
// teamsRepository = new MongoTeamsRepository();
|
||||
// maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
// } else {
|
||||
// throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
// }
|
||||
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository(logger);
|
||||
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||
const usersRepository = new MongoUsersRepository();
|
||||
const invitesRepository = new MongoInvitesRepository();
|
||||
const recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
const settingsRepository = new MongoSettingsRepository();
|
||||
const notificationsRepository = new MongoNotificationsRepository();
|
||||
const tagsRepository = new MongoTagsRepository();
|
||||
const incidentsRepository = new MongoIncidentsRepository();
|
||||
const teamsRepository = new MongoTeamsRepository();
|
||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
|
||||
// Inject settings repository into settings service (now that DB is connected)
|
||||
(settingsService as SettingsService).setRepository(settingsRepository);
|
||||
@@ -340,7 +362,7 @@ export const initializeServices = async ({
|
||||
teamsRepository,
|
||||
});
|
||||
|
||||
const diagnosticService = new DiagnosticService();
|
||||
const diagnosticService = new DiagnosticService(db);
|
||||
const inviteService = new InviteService({
|
||||
invitesRepository,
|
||||
settingsService,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface IDb {
|
||||
export interface IDb<TConnection = unknown> {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
getConnection(): Promise<TConnection>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Mongoose } from "mongoose";
|
||||
import mongoose from "mongoose";
|
||||
import AppSettings from "@/db/models/AppSettings.js";
|
||||
import { runMigrations } from "@/db/migration/index.js";
|
||||
@@ -5,16 +6,19 @@ import { ILogger } from "@/utils/logger.js";
|
||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||
const SERVICE_NAME = "MongoDB";
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
class MongoDB implements IDb {
|
||||
class MongoDB implements IDb<Mongoose> {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: ILogger;
|
||||
private envSettings: EnvConfig;
|
||||
private db: Mongoose | null;
|
||||
|
||||
constructor(logger: ILogger, envSettings: EnvConfig) {
|
||||
this.logger = logger;
|
||||
this.envSettings = envSettings;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -24,7 +28,7 @@ class MongoDB implements IDb {
|
||||
connect = async () => {
|
||||
try {
|
||||
const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
|
||||
await mongoose.connect(connectionString);
|
||||
this.db = await mongoose.connect(connectionString);
|
||||
// If there are no AppSettings, create one // TODO why is this here?
|
||||
await AppSettings.findOneAndUpdate(
|
||||
{}, // empty filter to match any document
|
||||
@@ -63,6 +67,13 @@ class MongoDB implements IDb {
|
||||
}
|
||||
};
|
||||
|
||||
getConnection = async (): Promise<Mongoose> => {
|
||||
if (this.db === null) {
|
||||
throw new AppError({ message: "Database not connected", service: SERVICE_NAME, method: "getConnection" });
|
||||
}
|
||||
return this.db;
|
||||
};
|
||||
|
||||
disconnect = async () => {
|
||||
try {
|
||||
this.logger.info({ message: "Disconnecting from MongoDB", service: SERVICE_NAME, method: "disconnect" });
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import v8 from "v8";
|
||||
import os from "os";
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import { Mongoose } from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "diagnosticService";
|
||||
|
||||
export interface CollectionDiagnostics {
|
||||
name: string;
|
||||
documentCount: number;
|
||||
storageSize: number;
|
||||
totalIndexSize: number;
|
||||
totalSize: number;
|
||||
bucketCount?: number;
|
||||
}
|
||||
|
||||
export interface MongoDiagnostics {
|
||||
readyState: number;
|
||||
host: string;
|
||||
port: number;
|
||||
dbName: string;
|
||||
totalSize: number;
|
||||
collections: CollectionDiagnostics[];
|
||||
}
|
||||
|
||||
export interface Diagnostics {
|
||||
osStats: { totalMemoryBytes: number };
|
||||
cpuUsage: { usagePercentage: number };
|
||||
v8HeapStats: {
|
||||
totalHeapSizeBytes: number;
|
||||
usedHeapSizeBytes: number;
|
||||
heapSizeLimitBytes: number;
|
||||
};
|
||||
eventLoopDelayMs: number;
|
||||
uptimeMs: number;
|
||||
mongoStats: MongoDiagnostics;
|
||||
}
|
||||
|
||||
export interface IDiagnosticService {
|
||||
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>;
|
||||
getSystemStats(): Promise<{
|
||||
osStats: { freeMemoryBytes: number; totalMemoryBytes: number };
|
||||
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>;
|
||||
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number };
|
||||
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
|
||||
eventLoopDelayMs: number;
|
||||
uptimeMs: number;
|
||||
}>;
|
||||
getSystemStats(): Promise<Diagnostics>;
|
||||
}
|
||||
|
||||
export class DiagnosticService implements IDiagnosticService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor() {
|
||||
constructor(private db: IDb<Mongoose>) {
|
||||
/**
|
||||
* Performance Observer for monitoring system performance metrics.
|
||||
* Clears performance marks after each measurement to prevent memory leaks.
|
||||
@@ -34,75 +60,86 @@ export class DiagnosticService implements IDiagnosticService {
|
||||
return DiagnosticService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getCPUUsage = async () => {
|
||||
private getCPUUsagePercentage = async (): Promise<number> => {
|
||||
const startUsage = process.cpuUsage();
|
||||
const timingPeriod = 1000; // measured in ms
|
||||
await new Promise((resolve) => setTimeout(resolve, timingPeriod));
|
||||
const endUsage = process.cpuUsage(startUsage);
|
||||
const cpuUsage = {
|
||||
userUsageMs: endUsage.user / 1000,
|
||||
systemUsageMs: endUsage.system / 1000,
|
||||
usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100,
|
||||
};
|
||||
return cpuUsage;
|
||||
return ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100;
|
||||
};
|
||||
|
||||
getSystemStats = async () => {
|
||||
// Memory Usage
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
private getMongoDBStats = async (): Promise<MongoDiagnostics> => {
|
||||
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 osStats = {
|
||||
freeMemoryBytes: freeMemory, // bytes
|
||||
totalMemoryBytes: totalMemory, // bytes
|
||||
const dbStats = await db.stats();
|
||||
const rawCollections = await db.collections();
|
||||
|
||||
const collections: CollectionDiagnostics[] = await Promise.all(
|
||||
rawCollections
|
||||
.filter((collection) => !collection.collectionName.startsWith("system"))
|
||||
.map(async (collection) => {
|
||||
const stats = await db.command({ collStats: collection.collectionName });
|
||||
const documentCount = await db.collection(collection.collectionName).countDocuments({});
|
||||
const entry: CollectionDiagnostics = {
|
||||
name: collection.collectionName,
|
||||
documentCount,
|
||||
storageSize: stats.storageSize,
|
||||
totalIndexSize: stats.totalIndexSize,
|
||||
totalSize: stats.totalSize,
|
||||
};
|
||||
if (stats.timeseries?.bucketCount !== undefined) {
|
||||
entry.bucketCount = stats.timeseries.bucketCount;
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
readyState: mongo.connection.readyState,
|
||||
host: mongo.connection.host,
|
||||
port: mongo.connection.port,
|
||||
dbName: mongo.connection.name,
|
||||
totalSize: dbStats.totalSize,
|
||||
collections,
|
||||
};
|
||||
};
|
||||
|
||||
const used = process.memoryUsage();
|
||||
getSystemStats = async (): Promise<Diagnostics> => {
|
||||
const osStats = { totalMemoryBytes: os.totalmem() };
|
||||
|
||||
// In MB
|
||||
const memoryUsage: Record<keyof NodeJS.MemoryUsage, number> = {
|
||||
rss: Math.round((used.rss / 1024 / 1024) * 100) / 100,
|
||||
heapTotal: Math.round((used.heapTotal / 1024 / 1024) * 100) / 100,
|
||||
heapUsed: Math.round((used.heapUsed / 1024 / 1024) * 100) / 100,
|
||||
external: Math.round((used.external / 1024 / 1024) * 100) / 100,
|
||||
arrayBuffers: Math.round((used.arrayBuffers / 1024 / 1024) * 100) / 100,
|
||||
};
|
||||
const cpuUsage = { usagePercentage: await this.getCPUUsagePercentage() };
|
||||
|
||||
// CPU Usage
|
||||
const cpuMetrics = await this.getCPUUsage();
|
||||
|
||||
// V8 Heap Statistics
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const v8Metrics = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size, // bytes
|
||||
usedHeapSizeBytes: heapStats.used_heap_size, // bytes
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit, // bytes
|
||||
const v8HeapStats = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size,
|
||||
usedHeapSizeBytes: heapStats.used_heap_size,
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit,
|
||||
};
|
||||
|
||||
// Event Loop Delay
|
||||
let eventLoopDelay = 0;
|
||||
let eventLoopDelayMs = 0;
|
||||
performance.mark("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
performance.mark("end");
|
||||
performance.measure("eventLoopDelay", "start", "end");
|
||||
const entries = performance.getEntriesByName("eventLoopDelay");
|
||||
if (entries.length > 0 && entries[0] !== undefined) {
|
||||
eventLoopDelay = entries[0].duration;
|
||||
eventLoopDelayMs = entries[0].duration;
|
||||
}
|
||||
|
||||
// Uptime
|
||||
const uptimeMs = process.uptime() * 1000; // ms
|
||||
const uptimeMs = process.uptime() * 1000;
|
||||
const mongoStats = await this.getMongoDBStats();
|
||||
|
||||
// Combine Metrics
|
||||
const diagnostics = {
|
||||
return {
|
||||
osStats,
|
||||
memoryUsage,
|
||||
cpuUsage: cpuMetrics,
|
||||
v8HeapStats: v8Metrics,
|
||||
eventLoopDelayMs: eventLoopDelay,
|
||||
cpuUsage,
|
||||
v8HeapStats,
|
||||
eventLoopDelayMs,
|
||||
uptimeMs,
|
||||
mongoStats,
|
||||
};
|
||||
|
||||
return diagnostics;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,49 @@
|
||||
import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals";
|
||||
import { DiagnosticService } from "../../../src/service/business/diagnosticService.ts";
|
||||
import type { IDb } from "../../../src/db/IDb.ts";
|
||||
import type { Mongoose } from "mongoose";
|
||||
|
||||
// ── Test helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
const makeDb = (overrides?: {
|
||||
collections?: Array<{ collectionName: string }>;
|
||||
collStats?: Record<string, unknown>;
|
||||
dbStats?: Record<string, unknown>;
|
||||
countDocuments?: number;
|
||||
noDb?: boolean;
|
||||
}): IDb<Mongoose> => {
|
||||
const collections = overrides?.collections ?? [];
|
||||
const collStats = overrides?.collStats ?? {
|
||||
storageSize: 100,
|
||||
totalIndexSize: 50,
|
||||
totalSize: 150,
|
||||
};
|
||||
const dbStats = overrides?.dbStats ?? { totalSize: 1000 };
|
||||
const countDocuments = overrides?.countDocuments ?? 42;
|
||||
|
||||
const dbApi = overrides?.noDb
|
||||
? null
|
||||
: {
|
||||
stats: jest.fn<() => Promise<Record<string, unknown>>>().mockResolvedValue(dbStats),
|
||||
collections: jest.fn<() => Promise<Array<{ collectionName: string }>>>().mockResolvedValue(collections),
|
||||
command: jest.fn<() => Promise<Record<string, unknown>>>().mockResolvedValue(collStats),
|
||||
collection: jest.fn().mockReturnValue({
|
||||
countDocuments: jest.fn<() => Promise<number>>().mockResolvedValue(countDocuments),
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
getConnection: jest.fn<() => Promise<Mongoose>>().mockResolvedValue({
|
||||
connection: {
|
||||
db: dbApi,
|
||||
readyState: 1,
|
||||
host: "localhost",
|
||||
port: 27017,
|
||||
name: "uptime_db",
|
||||
},
|
||||
} as unknown as Mongoose),
|
||||
} as unknown as IDb<Mongoose>;
|
||||
};
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -18,7 +62,7 @@ describe("DiagnosticService", () => {
|
||||
});
|
||||
|
||||
it("returns diagnosticService from instance getter", () => {
|
||||
const service = new DiagnosticService();
|
||||
const service = new DiagnosticService(makeDb());
|
||||
expect(service.serviceName).toBe("diagnosticService");
|
||||
});
|
||||
});
|
||||
@@ -37,12 +81,11 @@ describe("DiagnosticService", () => {
|
||||
}) as any;
|
||||
|
||||
const clearMarksSpy = jest.spyOn(performance, "clearMarks");
|
||||
const service = new DiagnosticService();
|
||||
const service = new DiagnosticService(makeDb());
|
||||
|
||||
expect(mockObserve).toHaveBeenCalledWith({ entryTypes: ["measure"] });
|
||||
expect(capturedCallback).not.toBeNull();
|
||||
|
||||
// Invoke the callback manually
|
||||
const mockEntryList = { getEntries: jest.fn().mockReturnValue([]) } as unknown as PerformanceObserverEntryList;
|
||||
capturedCallback!(mockEntryList);
|
||||
|
||||
@@ -55,68 +98,88 @@ describe("DiagnosticService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── getCPUUsage ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("getCPUUsage", () => {
|
||||
it("returns cpu usage metrics after timing period", async () => {
|
||||
const service = new DiagnosticService();
|
||||
const promise = service.getCPUUsage();
|
||||
jest.advanceTimersByTime(1000);
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toHaveProperty("userUsageMs");
|
||||
expect(result).toHaveProperty("systemUsageMs");
|
||||
expect(result).toHaveProperty("usagePercentage");
|
||||
expect(typeof result.userUsageMs).toBe("number");
|
||||
expect(typeof result.systemUsageMs).toBe("number");
|
||||
expect(typeof result.usagePercentage).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ── getSystemStats ──────────────────────────────────────────────────────
|
||||
|
||||
describe("getSystemStats", () => {
|
||||
it("returns all diagnostic sections", async () => {
|
||||
const service = new DiagnosticService();
|
||||
it("returns only the slimmed diagnostic shape (no serverStatus/serverInfo/memoryUsage)", async () => {
|
||||
const service = new DiagnosticService(makeDb());
|
||||
const promise = service.getSystemStats();
|
||||
// getCPUUsage has 1s timeout, then event loop delay has 0ms timeout
|
||||
// getCPUUsagePercentage waits 1s, then event loop delay waits 0ms
|
||||
jest.advanceTimersByTime(1000);
|
||||
// Need to flush the microtask queue and advance again for the nested setTimeout(0)
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toHaveProperty("osStats");
|
||||
expect(result.osStats).toHaveProperty("freeMemoryBytes");
|
||||
expect(result.osStats).toHaveProperty("totalMemoryBytes");
|
||||
|
||||
expect(result).toHaveProperty("memoryUsage");
|
||||
expect(result.memoryUsage).toHaveProperty("rss");
|
||||
expect(result.memoryUsage).toHaveProperty("heapTotal");
|
||||
expect(result.memoryUsage).toHaveProperty("heapUsed");
|
||||
expect(result.memoryUsage).toHaveProperty("external");
|
||||
expect(result.memoryUsage).toHaveProperty("arrayBuffers");
|
||||
|
||||
expect(result).toHaveProperty("cpuUsage");
|
||||
expect(result.cpuUsage).toHaveProperty("userUsageMs");
|
||||
|
||||
expect(result).toHaveProperty("v8HeapStats");
|
||||
expect(result.v8HeapStats).toHaveProperty("totalHeapSizeBytes");
|
||||
expect(result.v8HeapStats).toHaveProperty("usedHeapSizeBytes");
|
||||
expect(result.v8HeapStats).toHaveProperty("heapSizeLimitBytes");
|
||||
expect(Object.keys(result).sort()).toEqual(["cpuUsage", "eventLoopDelayMs", "mongoStats", "osStats", "uptimeMs", "v8HeapStats"].sort());
|
||||
|
||||
expect(result.osStats).toEqual({ totalMemoryBytes: expect.any(Number) });
|
||||
expect(result.cpuUsage).toEqual({ usagePercentage: expect.any(Number) });
|
||||
expect(result.v8HeapStats).toEqual({
|
||||
totalHeapSizeBytes: expect.any(Number),
|
||||
usedHeapSizeBytes: expect.any(Number),
|
||||
heapSizeLimitBytes: expect.any(Number),
|
||||
});
|
||||
expect(typeof result.eventLoopDelayMs).toBe("number");
|
||||
expect(result.eventLoopDelayMs).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof result.uptimeMs).toBe("number");
|
||||
expect(result.uptimeMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("returns slimmed mongoStats with `collections` (not `collectionStats`) and no serverStatus/serverInfo", async () => {
|
||||
const db = makeDb({
|
||||
collections: [{ collectionName: "monitors" }, { collectionName: "checks" }],
|
||||
collStats: { storageSize: 200, totalIndexSize: 80, totalSize: 280 },
|
||||
dbStats: { totalSize: 9999 },
|
||||
countDocuments: 7,
|
||||
});
|
||||
const service = new DiagnosticService(db);
|
||||
const promise = service.getSystemStats();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
const result = await promise;
|
||||
|
||||
expect(Object.keys(result.mongoStats).sort()).toEqual(["collections", "dbName", "host", "port", "readyState", "totalSize"].sort());
|
||||
expect(result.mongoStats.totalSize).toBe(9999);
|
||||
expect(result.mongoStats.collections).toHaveLength(2);
|
||||
expect(result.mongoStats.collections[0]).toEqual({
|
||||
name: "monitors",
|
||||
documentCount: 7,
|
||||
storageSize: 200,
|
||||
totalIndexSize: 80,
|
||||
totalSize: 280,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes bucketCount on timeseries collections, omits it elsewhere", async () => {
|
||||
const db = makeDb({
|
||||
collections: [{ collectionName: "checks" }],
|
||||
collStats: { storageSize: 1, totalIndexSize: 2, totalSize: 3, timeseries: { bucketCount: 12 } },
|
||||
});
|
||||
const service = new DiagnosticService(db);
|
||||
const promise = service.getSystemStats();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
const result = await promise;
|
||||
|
||||
expect(result.mongoStats.collections[0]?.bucketCount).toBe(12);
|
||||
|
||||
const db2 = makeDb({ collections: [{ collectionName: "monitors" }] });
|
||||
const service2 = new DiagnosticService(db2);
|
||||
const promise2 = service2.getSystemStats();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
const result2 = await promise2;
|
||||
expect(result2.mongoStats.collections[0]).not.toHaveProperty("bucketCount");
|
||||
});
|
||||
|
||||
it("throws AppError when the mongo connection has no db handle", async () => {
|
||||
jest.useRealTimers();
|
||||
const service = new DiagnosticService(makeDb({ noDb: true }));
|
||||
await expect(service.getSystemStats()).rejects.toThrow("Database connection is not available");
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it("reads eventLoopDelay from real performance entries", async () => {
|
||||
jest.useRealTimers();
|
||||
const service = new DiagnosticService();
|
||||
|
||||
// Call getSystemStats with real timers — slow but guarantees real perf entries
|
||||
const service = new DiagnosticService(makeDb());
|
||||
const result = await service.getSystemStats();
|
||||
|
||||
expect(typeof result.eventLoopDelayMs).toBe("number");
|
||||
expect(result.eventLoopDelayMs).toBeGreaterThanOrEqual(0);
|
||||
jest.useFakeTimers();
|
||||
@@ -126,7 +189,7 @@ describe("DiagnosticService", () => {
|
||||
{ label: "empty entries array", entries: [] },
|
||||
{ label: "undefined entry in array", entries: [undefined] },
|
||||
])("defaults eventLoopDelayMs to 0 when $label", async ({ entries }) => {
|
||||
const service = new DiagnosticService();
|
||||
const service = new DiagnosticService(makeDb());
|
||||
const originalGetEntries = performance.getEntriesByName;
|
||||
const originalMeasure = performance.measure;
|
||||
const originalMark = performance.mark;
|
||||
|
||||
Reference in new issue
Block a user