Skip to content

Commit

Permalink
Add server stats endpoints and client integration
Browse files Browse the repository at this point in the history
Introduce endpoints to fetch MongoDB, Redis, and Prometheus server stats, along with corresponding client-side integration. Implement data parsing helpers, API types, and admin settings page updates to display server statistics.
  • Loading branch information
SquirrelDevelopper committed Feb 21, 2025
1 parent d499a50 commit 7e68dc1
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 7e68dc1

Please sign in to comment.