Skip to content

Commit

Permalink
Merge pull request #766 from SquirrelCorporation/feature-db-servers-s…
Browse files Browse the repository at this point in the history
…tatuses

[FEAT] Add server stats endpoints and client integration
  • Loading branch information
SquirrelDeveloper authored Feb 21, 2025
2 parents e96625d + 7e68dc1 commit d2b652c
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 3 deletions.
121 changes: 119 additions & 2 deletions client/src/pages/Admin/Settings/components/Information.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,131 @@
import {
getMongoDBServerStats,
getPrometheusServerStats,
getRedisServerStats,
} from '@/services/rest/settings';
import { useModel } from '@umijs/max';
import { Descriptions, Flex } from 'antd';
import React from 'react';
import { Descriptions, Flex, Popover, Spin } from 'antd';
import React, { useEffect, useState } from 'react';
import JsonFormatter from 'react-json-formatter';
import { API } from 'ssm-shared-lib';
import { version } from '../../../../../package.json';

const jsonStyle = {
propertyStyle: { color: '#3b6b87' },
stringStyle: { color: '#538091' },
numberStyle: { color: '#614b98' },
};

const Information: React.FC = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const [mongoDbStats, setMongoDbStats] = useState<
API.MongoDBServerStats | 'error'
>();
const [redisStats, setRedisStats] = useState<
API.RedisServerStats | 'error'
>();
const [prometheusStats, setPrometheusStats] = useState<
API.PrometheusServerStats | 'error'
>();

const getMongoDbStats = async () => {
await getMongoDBServerStats()
.then((res) => {
setMongoDbStats(res.data);
})
.catch(() => setMongoDbStats('error'));
};

const getRedisStats = async () => {
await getRedisServerStats()
.then((res) => {
setRedisStats(res.data);
})
.catch(() => setRedisStats('error'));
};

const getPrometheusStats = async () => {
await getPrometheusServerStats()
.then((res) => {
setPrometheusStats(res.data);
})
.catch(() => setPrometheusStats('error'));
};

useEffect(() => {
void getMongoDbStats();
void getRedisStats();
void getPrometheusStats();
}, []);

return (
<Flex vertical gap={32} style={{ width: '80%' }}>
<Descriptions bordered title="Databases">
<Descriptions.Item label="MongoDB" span={24}>
{(mongoDbStats && (
<>
{(mongoDbStats === 'error' && <>🔴</>) || (
<Popover
content={
<div style={{ overflowY: 'scroll', height: '400px' }}>
<JsonFormatter
json={mongoDbStats}
tabWith={4}
jsonStyle={jsonStyle}
/>
</div>
}
>
🟢
</Popover>
)}
</>
)) || <Spin size="small" />}
</Descriptions.Item>
<Descriptions.Item label="Redis" span={24}>
{(redisStats && (
<>
{(redisStats === 'error' && <>🔴</>) || (
<Popover
content={
<div style={{ overflowY: 'scroll', height: '400px' }}>
<JsonFormatter
json={redisStats}
tabWith={4}
jsonStyle={jsonStyle}
/>
</div>
}
>
🟢
</Popover>
)}
</>
)) || <Spin size="small" />}
</Descriptions.Item>
<Descriptions.Item label="Prometheus" span={24}>
{(prometheusStats && (
<>
{(prometheusStats === 'error' && <>🔴</>) || (
<Popover
content={
<div style={{ overflowY: 'scroll', height: '400px' }}>
<JsonFormatter
json={prometheusStats}
tabWith={4}
jsonStyle={jsonStyle}
/>
</div>
}
>
🟢
</Popover>
)}
</>
)) || <Spin size="small" />}
</Descriptions.Item>
</Descriptions>
<Descriptions title="Client Information">
<Descriptions.Item label="Version">{version}</Descriptions.Item>
</Descriptions>
Expand Down
33 changes: 33 additions & 0 deletions client/src/services/rest/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,36 @@ export async function postMasterNodeUrlValue(
...(options || {}),
});
}

export async function getMongoDBServerStats(options?: Record<string, any>) {
return request<API.Response<API.MongoDBServerStats>>(
`/api/settings/information/mongodb`,
{
method: 'GET',
...{},
...(options || {}),
},
);
}

export async function getRedisServerStats(options?: Record<string, any>) {
return request<API.Response<API.RedisServerStats>>(
`/api/settings/information/redis`,
{
method: 'GET',
...{},
...(options || {}),
},
);
}

export async function getPrometheusServerStats(options?: Record<string, any>) {
return request<API.Response<API.PrometheusServerStats>>(
`/api/settings/information/prometheus`,
{
method: 'GET',
...{},
...(options || {}),
},
);
}
73 changes: 73 additions & 0 deletions server/src/controllers/rest/settings/information.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import mongoose from 'mongoose';
import { API } from 'ssm-shared-lib';
import { getRedisClient } from '../../../data/cache';
import { prometheusServerStats } from '../../../data/statistics/server-stats';
import { parseRedisInfo } from '../../../helpers/redis/redis-info';
import logger from '../../../logger';
import { SuccessResponse } from '../../../middlewares/api/ApiResponse';

export const getMongoDBServerStats = async (req, res) => {
const serverStatus = await mongoose.connection.db?.admin().serverStatus();
try {
const data: API.MongoDBServerStats = {
// Memory metrics
memory: {
resident: serverStatus?.mem?.resident, // Resident memory in MB
virtual: serverStatus?.mem?.virtual, // Virtual memory in MB
mapped: serverStatus?.mem?.mapped, // Memory mapped size
},

// Connections
connections: serverStatus?.connections, // Current, available, total connections

// CPU metrics
cpu: {
userPercent: serverStatus?.cpu?.user,
systemPercent: serverStatus?.cpu?.sys,
idlePercent: serverStatus?.cpu?.idle,
},

// Operations metrics
operations: serverStatus?.opcounters, // Insert, query, update, delete counts
};
new SuccessResponse(`Got MongoDB server stats`, data).send(res);
} catch (error) {
logger.error(error);
throw error;
}
};

// Get detailed database storage stats
export const getStorageStats = async (req, res) => {
const dbStats = await mongoose.connection.db?.stats();
const data = {
dataSize: dbStats?.dataSize, // Size of all documents
storageSize: dbStats?.storageSize, // Total storage allocated
indexSize: dbStats?.indexSize, // Total size of all indexes
totalSize: dbStats?.totalSize, // Total size (data + indexes)
scaleFactor: dbStats?.scaleFactor, // Scaling factor for sizes
};
new SuccessResponse(`Got MongoDB server storage stats`, data).send(res);
};

export const getRedisServerStats = async (req, res) => {
const client = await getRedisClient();

const memory = await client.info('memory');
const cpu = await client.info('cpu');
const stats = await client.info('stats');
const server = await client.info('server');
const data = {
memory: parseRedisInfo(memory), // Memory usage, peak memory, etc.
cpu: parseRedisInfo(cpu), // CPU statistics
stats: parseRedisInfo(stats), // General statistics
server: parseRedisInfo(server), // Server information
};

new SuccessResponse(`Got Redis server stats`, data).send(res);
};

export const getPrometheusServerStats = async (req, res) => {
const data = await prometheusServerStats();
new SuccessResponse(`Got Prometheus server stats`, data).send(res);
};
2 changes: 1 addition & 1 deletion server/src/data/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function createRedisClient(): Promise<any> {
return redisClient;
}

async function getRedisClient(): Promise<RedisClientType> {
export async function getRedisClient(): Promise<RedisClientType> {
if (!isReady) {
redisClient = await createRedisClient();
}
Expand Down
30 changes: 30 additions & 0 deletions server/src/data/statistics/server-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import axios from 'axios';
import { prometheusConf } from '../../config';
import logger from '../../logger';

export async function prometheusServerStats() {
const { host, baseURL, user, password } = prometheusConf;
const auth = { username: user, password: password };

try {
// Get runtime information and build information
const runtimeInfo = await axios.get(`${host}${baseURL}/status/runtimeinfo`, { auth });
const buildInfo = await axios.get(`${host}${baseURL}/status/buildinfo`, { auth });

// Get targets status (up/down)
const targets = await axios.get(`${host}${baseURL}/targets`, { auth });

// Get TSDB stats (time series database statistics)
const tsdbStats = await axios.get(`${host}${baseURL}/status/tsdb`, { auth });

return {
runtime: runtimeInfo.data.data,
build: buildInfo.data.data,
targets: targets.data.data,
tsdb: tsdbStats.data.data,
};
} catch (error) {
logger.error(error, 'Failed to fetch Prometheus stats');
throw error;
}
}
16 changes: 16 additions & 0 deletions server/src/helpers/redis/redis-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Helper to parse Redis INFO output into an object
export function parseRedisInfo(info: string) {
return info
.split('\n')
.filter((line) => line && !line.startsWith('#'))
.reduce(
(acc, line) => {
const [key, value] = line.split(':');
if (key && value) {
acc[key.trim()] = value.trim();
}
return acc;
},
{} as Record<string, string>,
);
}
8 changes: 8 additions & 0 deletions server/src/routes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import { postDevicesSettings } from '../controllers/rest/settings/devices';
import { postDevicesSettingsValidator } from '../controllers/rest/settings/devices.validator';
import { postDeviceStatsSettings } from '../controllers/rest/settings/devicestats';
import { postDeviceStatsSettingsValidator } from '../controllers/rest/settings/devicestats.validator';
import {
getMongoDBServerStats,
getPrometheusServerStats,
getRedisServerStats,
} from '../controllers/rest/settings/information';
import { postMasterNodeUrlValue } from '../controllers/rest/settings/keys';
import { postMasterNodeUrlValueValidator } from '../controllers/rest/settings/keys.validator';
import { postLogsSettings } from '../controllers/rest/settings/logs';
Expand All @@ -30,4 +35,7 @@ router.post('/advanced/restart', postRestartServer);
router.delete('/advanced/logs', deleteLogs);
router.delete('/advanced/ansible-logs', deleteAnsibleLogs);
router.delete('/advanced/playbooks-and-resync', deletePlaybooksModelAndResync);
router.get('/information/mongodb', getMongoDBServerStats);
router.get('/information/redis', getRedisServerStats);
router.get('/information/prometheus', getPrometheusServerStats);
export default router;
Loading

0 comments on commit d2b652c

Please sign in to comment.