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}
+ ) : ( +
+
+
+ + + + + + + + + + + {Object.entries(groupedMetrics).map(([component, metrics]) => ( + + ))} + +
{gettext('Metrics')}{gettext('Node')}{gettext('Value')}{gettext('Collected time')}
+
+
+
+ )} +
+
+ + ); + } +} + +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"),