mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-11 03:42:16 +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:
parent
1134469495
commit
6b51e54596
@ -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 {
|
||||
<StatisticUsers path={siteRoot + 'sys/statistics/user'} {...commonProps} />
|
||||
<StatisticTraffic path={siteRoot + 'sys/statistics/traffic'} {...commonProps} />
|
||||
<StatisticReport path={siteRoot + 'sys/statistics/reports'} {...commonProps} />
|
||||
<StatisticMetrics path={siteRoot + 'sys/statistics/metrics'} {...commonProps} />
|
||||
<DesktopDevices path={siteRoot + 'sys/desktop-devices'} {...commonProps} />
|
||||
<MobileDevices path={siteRoot + 'sys/mobile-devices'} {...commonProps} />
|
||||
<DeviceErrors path={siteRoot + 'sys/device-errors'} {...commonProps} />
|
||||
|
300
frontend/src/pages/sys-admin/statistic/statistic-metrics.js
Normal file
300
frontend/src/pages/sys-admin/statistic/statistic-metrics.js
Normal 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;
|
@ -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') },
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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 })
|
||||
|
@ -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"),
|
||||
|
Loading…
Reference in New Issue
Block a user