From 6b51e5459618fb9fca45f76012d8b59fe2d01233 Mon Sep 17 00:00:00 2001
From: awu0403 <76416779+awu0403@users.noreply.github.com>
Date: Fri, 11 Apr 2025 11:09:25 +0800
Subject: [PATCH] System metrics (#7700)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* add metric ui
* Update statistic-metrics.js
* handle metrics
* optimize ui
* update
* optimize ui
* update ui
---------
Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
---
frontend/src/pages/sys-admin/index.js | 4 +-
.../sys-admin/statistic/statistic-metrics.js | 300 ++++++++++++++++++
.../sys-admin/statistic/statistic-nav.js | 1 +
frontend/src/utils/system-admin-api.js | 5 +
seahub/api2/endpoints/admin/statistics.py | 107 +++++++
seahub/urls.py | 4 +-
6 files changed, 419 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/pages/sys-admin/statistic/statistic-metrics.js
diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js
index 4e261587e8..63bee3bfe0 100644
--- a/frontend/src/pages/sys-admin/index.js
+++ b/frontend/src/pages/sys-admin/index.js
@@ -18,6 +18,7 @@ import StatisticStorage from './statistic/statistic-storage';
import StatisticTraffic from './statistic/statistic-traffic';
import StatisticUsers from './statistic/statistic-users';
import StatisticReport from './statistic/statistic-reports';
+import StatisticMetrics from './statistic/statistic-metrics';
import DesktopDevices from './devices/desktop-devices';
import MobileDevices from './devices/mobile-devices';
@@ -112,7 +113,7 @@ class SysAdmin extends React.Component {
},
{
tab: 'statistic',
- urlPartList: ['statistics/file', 'statistics/storage', 'statistics/user', 'statistics/traffic', 'statistics/reports']
+ urlPartList: ['statistics/file', 'statistics/storage', 'statistics/user', 'statistics/traffic', 'statistics/reports', 'statistics/metrics']
},
{
tab: 'users',
@@ -222,6 +223,7 @@ class SysAdmin extends React.Component {
+
diff --git a/frontend/src/pages/sys-admin/statistic/statistic-metrics.js b/frontend/src/pages/sys-admin/statistic/statistic-metrics.js
new file mode 100644
index 0000000000..6922c96595
--- /dev/null
+++ b/frontend/src/pages/sys-admin/statistic/statistic-metrics.js
@@ -0,0 +1,300 @@
+import React, { Component } from 'react';
+import dayjs from 'dayjs';
+import { systemAdminAPI } from '../../../utils/system-admin-api';
+import MainPanelTopbar from '../main-panel-topbar';
+import StatisticNav from './statistic-nav';
+import { gettext } from '../../../utils/constants';
+import { Tooltip } from 'reactstrap';
+
+class ComponentMetricsTable extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ hoveredRow: null,
+ tooltipOpen: null
+ };
+ }
+
+ toggleTooltip = (id) => {
+ this.setState(prevState => ({
+ tooltipOpen: prevState.tooltipOpen === id ? null : id
+ }));
+ };
+
+ handleRowHover = (id) => {
+ this.setState({ hoveredRow: id });
+ };
+
+ handleRowLeave = () => {
+ this.setState({ hoveredRow: null });
+ };
+
+ render() {
+ const { componentName, metrics } = this.props;
+ const { hoveredRow, tooltipOpen } = this.state;
+ return (
+ <>
+
+
+
+ {componentName}
+
+ |
+
+ {metrics.map((metric) => (
+ metric.data_points.map((point, pointIndex) => {
+ const rowId = `${metric.name}-${point.labels.node}-${pointIndex}`;
+ return (
+
+ this.handleRowHover(rowId)}
+ onMouseLeave={this.handleRowLeave}
+ >
+
+
+ {metric.name.substring(metric.name.indexOf('_') + 1)}
+ {metric.help && (
+ <>
+
+
+ this.toggleTooltip(rowId)}
+ target={rowId}
+ delay={{ show: 200, hide: 0 }}
+ fade={true}
+ className="metric-tooltip"
+ hideArrow={true}
+ >
+ {metric.help}
+
+ >
+ )}
+
+
+ |
+ {point.labels.node} |
+ {point.value} |
+
+
+ {dayjs(point.labels.collected_at).format('YYYY-MM-DD HH:mm:ss')}
+
+ |
+
+ );
+ })
+ ))}
+ >
+ );
+ }
+}
+
+class StatisticMetrics extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ metrics: [],
+ loading: true,
+ error: null,
+ groupedMetrics: {}
+ };
+ }
+
+ componentDidMount() {
+ this.getMetrics();
+ }
+
+ groupMetricsByComponent = (metrics) => {
+ const groups = {};
+ metrics.forEach(metric => {
+ if (metric.data_points && metric.data_points.length > 0) {
+ metric.data_points.forEach(point => {
+ const component = point.labels.component || 'Other';
+ if (!groups[component]) {
+ groups[component] = [];
+ }
+ const existingMetric = groups[component].find(m => m.name === metric.name);
+ if (existingMetric) {
+ existingMetric.data_points.push(point);
+ } else {
+ groups[component].push({
+ ...metric,
+ data_points: [point]
+ });
+ }
+ });
+ }
+ });
+ return groups;
+ };
+
+ getMetrics = async () => {
+ this.setState({ loading: true });
+ try {
+ const res = await systemAdminAPI.sysAdminStatisticMetrics();
+ const groupedMetrics = this.groupMetricsByComponent(res.data.metrics);
+ this.setState({
+ metrics: res.data.metrics,
+ groupedMetrics,
+ loading: false
+ });
+ } catch (error) {
+ this.setState({
+ error: 'Failed to get metric data',
+ loading: false
+ });
+ }
+ };
+
+ render() {
+ const { groupedMetrics, loading, error } = this.state;
+
+ return (
+ <>
+
+
+
+
+ {loading ? (
+
+ ) : error ? (
+
{error}
+ ) : (
+
+
+
+
+
+
+ {gettext('Metrics')} |
+ {gettext('Node')} |
+ {gettext('Value')} |
+ {gettext('Collected time')} |
+
+
+
+ {Object.entries(groupedMetrics).map(([component, metrics]) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+
+ >
+ );
+ }
+}
+
+const style = `
+
+`;
+
+document.head.insertAdjacentHTML('beforeend', style);
+
+export default StatisticMetrics;
diff --git a/frontend/src/pages/sys-admin/statistic/statistic-nav.js b/frontend/src/pages/sys-admin/statistic/statistic-nav.js
index 51f04e352e..f3c5e3a0f4 100644
--- a/frontend/src/pages/sys-admin/statistic/statistic-nav.js
+++ b/frontend/src/pages/sys-admin/statistic/statistic-nav.js
@@ -17,6 +17,7 @@ class Nav extends React.Component {
{ name: 'usersStatistic', urlPart: 'statistics/user', text: gettext('Users') },
{ name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic') },
{ name: 'reportsStatistic', urlPart: 'statistics/reports', text: gettext('Reports') },
+ { name: 'metricsStatistic', urlPart: 'statistics/metrics', text: gettext('Metrics') },
];
}
diff --git a/frontend/src/utils/system-admin-api.js b/frontend/src/utils/system-admin-api.js
index 31fa1cf76b..5757825839 100644
--- a/frontend/src/utils/system-admin-api.js
+++ b/frontend/src/utils/system-admin-api.js
@@ -1236,6 +1236,11 @@ class SystemAdminAPI {
return this.req.post(url, formData);
}
+ sysAdminStatisticMetrics() {
+ const url = this.server + '/api/v2.1/admin/statistics/system-metrics/';
+ return this.req.get(url);
+ }
+
sysAdminStatisticFiles(startTime, endTime, groupBy) {
const url = this.server + '/api/v2.1/admin/statistics/file-operations/';
let params = {
diff --git a/seahub/api2/endpoints/admin/statistics.py b/seahub/api2/endpoints/admin/statistics.py
index d9db2e35dc..cb9f385906 100644
--- a/seahub/api2/endpoints/admin/statistics.py
+++ b/seahub/api2/endpoints/admin/statistics.py
@@ -28,6 +28,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname, \
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
+from seahub.api2.endpoints.utils import get_seafevents_metrics
logger = logging.getLogger(__name__)
@@ -474,3 +475,109 @@ class SystemUserStorageExcelView(APIView):
wb.save(response)
return response
+
+
+def parse_prometheus_metrics(metrics_raw):
+ """
+ Parse prometheus metrics and format metric names
+ """
+ formatted_metrics_dict = {}
+
+ def ensure_metric_exists(raw_name):
+ """
+ Ensure metric entry exists in formatted metrics dict
+ """
+ if raw_name not in formatted_metrics_dict:
+ formatted_metrics_dict[raw_name] = {
+ 'name': raw_name,
+ 'help': '',
+ 'type': '',
+ 'data_points': []
+ }
+ return raw_name
+
+ def parse_labels(line):
+ """
+ Parse labels from metric line
+ """
+ labels = {}
+ if '{' in line and '}' in line:
+ labels_str = line[line.index('{')+1:line.index('}')]
+ for label in labels_str.split(','):
+ key, value = [part.strip() for part in label.split('=', 1)]
+ labels[key] = value.strip('"')
+ return labels
+
+ def parse_metric_line(line):
+ """
+ Parse a single metric line
+ """
+ if '{' in line:
+ name_part = line.split('{')[0]
+ value_part = line.split('}')[1].strip()
+ else:
+ name_part, value_part = line.split(' ', 1)
+
+ return (
+ name_part.strip(),
+ parse_labels(line),
+ float(value_part)
+ )
+
+ for line in metrics_raw.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+
+ if line.startswith('# HELP'):
+ parts = line.split(' ', 3)
+ if len(parts) > 3:
+ metric_name, help_text = parts[2], parts[3]
+ ensure_metric_exists(metric_name)
+ formatted_metrics_dict[metric_name]['help'] = help_text
+
+ elif line.startswith('# TYPE'):
+ parts = line.split(' ')
+ if len(parts) > 3:
+ metric_name, metric_type = parts[2], parts[3]
+ ensure_metric_exists(metric_name)
+ formatted_metrics_dict[metric_name]['type'] = metric_type
+
+ elif not line.startswith('#'):
+ # handle metric data
+ parsed_data = parse_metric_line(line)
+ if parsed_data:
+ metric_name, labels, value = parsed_data
+ ensure_metric_exists(metric_name)
+ formatted_metrics_dict[metric_name]['data_points'].append({
+ 'labels': labels,
+ 'value': value
+ })
+ # check data
+ result = []
+ for metric in formatted_metrics_dict.values():
+ if metric['name'] and metric['type']:
+ result.append(metric)
+
+ return result
+
+class SystemMetricsView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ throttle_classes = (UserRateThrottle,)
+ permission_classes = (IsAdminUser,)
+
+
+ def get(self, request):
+ if not request.user.admin_permissions.can_view_statistic():
+ return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
+
+ try:
+ res = get_seafevents_metrics()
+ metrics_raw = res.content.decode('utf-8')
+ metrics_data = parse_prometheus_metrics(metrics_raw)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({ 'metrics': metrics_data })
diff --git a/seahub/urls.py b/seahub/urls.py
index 26dc826074..b0f1a2a0eb 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -141,7 +141,7 @@ from seahub.api2.endpoints.admin.web_settings import AdminWebSettings
from seahub.api2.endpoints.admin.statistics import (
FileOperationsView, TotalStorageView, ActiveUsersView, SystemTrafficView, \
SystemUserTrafficExcelView, SystemUserStorageExcelView, SystemUserTrafficView, \
- SystemOrgTrafficView
+ SystemOrgTrafficView, SystemMetricsView
)
from seahub.api2.endpoints.admin.devices import AdminDevices
from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors
@@ -627,6 +627,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/statistics/system-org-traffic/$', SystemOrgTrafficView.as_view(), name='api-v2.1-admin-statistics-system-org-traffic'),
re_path(r'^api/v2.1/admin/statistics/system-user-traffic/excel/$', SystemUserTrafficExcelView.as_view(), name='api-v2.1-admin-statistics-system-user-traffic-excel'),
re_path(r'^api/v2.1/admin/statistics/system-user-storage/excel/$', SystemUserStorageExcelView.as_view(), name='api-v2.1-admin-statistics-system-user-storage-excel'),
+ re_path(r'^api/v2.1/admin/statistics/system-metrics/$', SystemMetricsView.as_view(), name='api-v2.1-admin-statistics-system-metrics'),
## admin::users
re_path(r'^api/v2.1/admin/users/$', AdminUsers.as_view(), name='api-v2.1-admin-users'),
re_path(r'^api/v2.1/admin/ldap-users/$', AdminLDAPUsers.as_view(), name='api-v2.1-admin-ldap-users'),
@@ -841,6 +842,7 @@ urlpatterns = [
path('sys/statistics/user/', sysadmin_react_fake_view, name="sys_statistics_user"),
path('sys/statistics/traffic/', sysadmin_react_fake_view, name="sys_statistics_traffic"),
path('sys/statistics/reports/', sysadmin_react_fake_view, name="sys_statistics_reports"),
+ path('sys/statistics/metrics/', sysadmin_react_fake_view, name="sys_statistics_metrics"),
path('sys/desktop-devices/', sysadmin_react_fake_view, name="sys_desktop_devices"),
path('sys/mobile-devices/', sysadmin_react_fake_view, name="sys_mobile_devices"),
path('sys/device-errors/', sysadmin_react_fake_view, name="sys_device_errors"),