1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-11 11:52:08 +00:00

System metrics (#7700)

* 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>
This commit is contained in:
awu0403 2025-04-11 11:09:25 +08:00 committed by GitHub
parent 1134469495
commit 6b51e54596
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 419 additions and 2 deletions

View File

@ -18,6 +18,7 @@ import StatisticStorage from './statistic/statistic-storage';
import StatisticTraffic from './statistic/statistic-traffic'; import StatisticTraffic from './statistic/statistic-traffic';
import StatisticUsers from './statistic/statistic-users'; import StatisticUsers from './statistic/statistic-users';
import StatisticReport from './statistic/statistic-reports'; import StatisticReport from './statistic/statistic-reports';
import StatisticMetrics from './statistic/statistic-metrics';
import DesktopDevices from './devices/desktop-devices'; import DesktopDevices from './devices/desktop-devices';
import MobileDevices from './devices/mobile-devices'; import MobileDevices from './devices/mobile-devices';
@ -112,7 +113,7 @@ class SysAdmin extends React.Component {
}, },
{ {
tab: 'statistic', 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', tab: 'users',
@ -222,6 +223,7 @@ class SysAdmin extends React.Component {
<StatisticUsers path={siteRoot + 'sys/statistics/user'} {...commonProps} /> <StatisticUsers path={siteRoot + 'sys/statistics/user'} {...commonProps} />
<StatisticTraffic path={siteRoot + 'sys/statistics/traffic'} {...commonProps} /> <StatisticTraffic path={siteRoot + 'sys/statistics/traffic'} {...commonProps} />
<StatisticReport path={siteRoot + 'sys/statistics/reports'} {...commonProps} /> <StatisticReport path={siteRoot + 'sys/statistics/reports'} {...commonProps} />
<StatisticMetrics path={siteRoot + 'sys/statistics/metrics'} {...commonProps} />
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} {...commonProps} /> <DesktopDevices path={siteRoot + 'sys/desktop-devices'} {...commonProps} />
<MobileDevices path={siteRoot + 'sys/mobile-devices'} {...commonProps} /> <MobileDevices path={siteRoot + 'sys/mobile-devices'} {...commonProps} />
<DeviceErrors path={siteRoot + 'sys/device-errors'} {...commonProps} /> <DeviceErrors path={siteRoot + 'sys/device-errors'} {...commonProps} />

View File

@ -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 (
<>
<tr className="component-header">
<td colSpan="4">
<div className="component-title">
<span>{componentName}</span>
</div>
</td>
</tr>
{metrics.map((metric) => (
metric.data_points.map((point, pointIndex) => {
const rowId = `${metric.name}-${point.labels.node}-${pointIndex}`;
return (
<tr key={rowId} className="metric-row">
<td
onMouseEnter={() => this.handleRowHover(rowId)}
onMouseLeave={this.handleRowLeave}
>
<div className="metric-info">
<div className="metric-name">
{metric.name.substring(metric.name.indexOf('_') + 1)}
{metric.help && (
<>
<i
className="sf3-font-help sf3-font"
id={rowId}
aria-hidden="true"
style={{
visibility: hoveredRow === rowId ? 'visible' : 'hidden',
}}
>
</i>
<Tooltip
placement='right'
isOpen={tooltipOpen === rowId && hoveredRow === rowId}
toggle={() => this.toggleTooltip(rowId)}
target={rowId}
delay={{ show: 200, hide: 0 }}
fade={true}
className="metric-tooltip"
hideArrow={true}
>
{metric.help}
</Tooltip>
</>
)}
</div>
</div>
</td>
<td>{point.labels.node}</td>
<td className="metric-value">{point.value}</td>
<td>
<span className="collected-time">
{dayjs(point.labels.collected_at).format('YYYY-MM-DD HH:mm:ss')}
</span>
</td>
</tr>
);
})
))}
</>
);
}
}
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 (
<>
<MainPanelTopbar {...this.props} />
<div className="">
<StatisticNav currentItem="metricsStatistic" />
<div className="cur-metrics-content">
{loading ? (
<div className="loading-icon loading-tip"></div>
) : error ? (
<div className="error text-danger">{error}</div>
) : (
<div className="metrics-container">
<div className="card">
<div className="card-body">
<table className="table table-striped mb-0">
<thead>
<tr>
<th width="40%">{gettext('Metrics')}</th>
<th width="20%">{gettext('Node')}</th>
<th width="15%">{gettext('Value')}</th>
<th width="25%">{gettext('Collected time')}</th>
</tr>
</thead>
<tbody>
{Object.entries(groupedMetrics).map(([component, metrics]) => (
<ComponentMetricsTable
key={component}
componentName={component}
metrics={metrics}
/>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
</div>
</>
);
}
}
const style = `
<style>
.cur-metrics-content {
padding: 10px 16px;
border: none;
}
.cur-metrics-content .card {
margin-bottom: 20px;
}
.component-metrics-card .card-header {
background-color: #fff;
padding: 16px 20px;
}
.metric-info {
display: flex;
flex-direction: column;
}
.metric-name {
font-size: 14px;
}
.metrics-container {
padding: 0;
}
.loading-tip {
margin: 100px auto;
text-align: center;
}
.card {
box-shadow: none;
border: none;
}
.card-body {
border: none;
padding: 0;
}
.component-header {
background-color: #fafafa !important;
}
.component-header td {
padding-top: 24px !important;
padding-bottom: 2px !important;
}
.component-title {
color: #212529;
font-size: 16px;
font-weight: 500;
}
.metric-row td {
padding: 0;
font-size: 14px;
}
.metrics-container .table {
margin-bottom: 0;
border: none;
}
.metrics-container .table td {
vertical-align: middle;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
padding-left: 8px;
}
.metrics-container .table th {
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
color: #666;
font-size: 14px;
}
.sf3-font-help {
font-size: 14px;
color: #666;
margin-left: 4px;
opacity: 0.7;
transition: opacity 0.2s;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', style);
export default StatisticMetrics;

View File

@ -17,6 +17,7 @@ class Nav extends React.Component {
{ name: 'usersStatistic', urlPart: 'statistics/user', text: gettext('Users') }, { name: 'usersStatistic', urlPart: 'statistics/user', text: gettext('Users') },
{ name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic') }, { name: 'trafficStatistic', urlPart: 'statistics/traffic', text: gettext('Traffic') },
{ name: 'reportsStatistic', urlPart: 'statistics/reports', text: gettext('Reports') }, { name: 'reportsStatistic', urlPart: 'statistics/reports', text: gettext('Reports') },
{ name: 'metricsStatistic', urlPart: 'statistics/metrics', text: gettext('Metrics') },
]; ];
} }

View File

@ -1236,6 +1236,11 @@ class SystemAdminAPI {
return this.req.post(url, formData); 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) { sysAdminStatisticFiles(startTime, endTime, groupBy) {
const url = this.server + '/api/v2.1/admin/statistics/file-operations/'; const url = this.server + '/api/v2.1/admin/statistics/file-operations/';
let params = { let params = {

View File

@ -28,6 +28,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname, \
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.api2.endpoints.utils import get_seafevents_metrics
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -474,3 +475,109 @@ class SystemUserStorageExcelView(APIView):
wb.save(response) wb.save(response)
return 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 })

View File

@ -141,7 +141,7 @@ from seahub.api2.endpoints.admin.web_settings import AdminWebSettings
from seahub.api2.endpoints.admin.statistics import ( from seahub.api2.endpoints.admin.statistics import (
FileOperationsView, TotalStorageView, ActiveUsersView, SystemTrafficView, \ FileOperationsView, TotalStorageView, ActiveUsersView, SystemTrafficView, \
SystemUserTrafficExcelView, SystemUserStorageExcelView, SystemUserTrafficView, \ SystemUserTrafficExcelView, SystemUserStorageExcelView, SystemUserTrafficView, \
SystemOrgTrafficView SystemOrgTrafficView, SystemMetricsView
) )
from seahub.api2.endpoints.admin.devices import AdminDevices from seahub.api2.endpoints.admin.devices import AdminDevices
from seahub.api2.endpoints.admin.device_errors import AdminDeviceErrors 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-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-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-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 ## 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/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'), 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/user/', sysadmin_react_fake_view, name="sys_statistics_user"),
path('sys/statistics/traffic/', sysadmin_react_fake_view, name="sys_statistics_traffic"), 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/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/desktop-devices/', sysadmin_react_fake_view, name="sys_desktop_devices"),
path('sys/mobile-devices/', sysadmin_react_fake_view, name="sys_mobile_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"), path('sys/device-errors/', sysadmin_react_fake_view, name="sys_device_errors"),