Update On Mon Sep 2 20:34:11 CEST 2024

This commit is contained in:
github-action[bot]
2024-09-02 20:34:11 +02:00
parent e2e637c916
commit 144e46a9ef
121 changed files with 2249 additions and 1278 deletions

View File

@@ -64,12 +64,11 @@ type relayConnImpl struct {
EndTime time.Time `json:"end_time,omitempty"`
// options set those fields
l *zap.SugaredLogger
remote *lb.Node
HandshakeDuration time.Duration
RelayLabel string `json:"relay_label"`
ConnType string `json:"conn_type"`
Options *conf.Options
l *zap.SugaredLogger
remote *lb.Node
RelayLabel string `json:"relay_label"`
ConnType string `json:"conn_type"`
Options *conf.Options
}
func WithRelayLabel(relayLabel string) RelayConnOption {
@@ -78,12 +77,6 @@ func WithRelayLabel(relayLabel string) RelayConnOption {
}
}
func WithHandshakeDuration(duration time.Duration) RelayConnOption {
return func(rci *relayConnImpl) {
rci.HandshakeDuration = duration
}
}
func WithConnType(connType string) RelayConnOption {
return func(rci *relayConnImpl) {
rci.ConnType = connType
@@ -93,6 +86,7 @@ func WithConnType(connType string) RelayConnOption {
func WithRemote(remote *lb.Node) RelayConnOption {
return func(rci *relayConnImpl) {
rci.remote = remote
rci.Stats.HandShakeLatency = remote.HandShakeDuration
}
}

View File

@@ -44,9 +44,6 @@ func newBaseRelayServer(cfg *conf.Config, cmgr cmgr.Cmgr) (*BaseRelayServer, err
}
func (b *BaseRelayServer) RelayTCPConn(ctx context.Context, c net.Conn, remote *lb.Node) error {
if remote == nil {
remote = b.remotes.Next().Clone()
}
metrics.CurConnectionCount.WithLabelValues(remote.Label, metrics.METRIC_CONN_TYPE_TCP).Inc()
defer metrics.CurConnectionCount.WithLabelValues(remote.Label, metrics.METRIC_CONN_TYPE_TCP).Dec()
@@ -66,15 +63,11 @@ func (b *BaseRelayServer) RelayTCPConn(ctx context.Context, c net.Conn, remote *
return fmt.Errorf("handshake error: %w", err)
}
defer rc.Close()
b.l.Infof("RelayTCPConn from %s to %s", c.LocalAddr(), remote.Address)
return b.handleRelayConn(c, rc, remote, metrics.METRIC_CONN_TYPE_TCP)
}
func (b *BaseRelayServer) RelayUDPConn(ctx context.Context, c net.Conn, remote *lb.Node) error {
if remote == nil {
remote = b.remotes.Next().Clone()
}
metrics.CurConnectionCount.WithLabelValues(remote.Label, metrics.METRIC_CONN_TYPE_UDP).Inc()
defer metrics.CurConnectionCount.WithLabelValues(remote.Label, metrics.METRIC_CONN_TYPE_UDP).Dec()
@@ -144,7 +137,6 @@ func (b *BaseRelayServer) handleRelayConn(c, rc net.Conn, remote *lb.Node, connT
conn.WithConnType(connType),
conn.WithRelayLabel(b.cfg.Label),
conn.WithRelayOptions(b.cfg.Options),
conn.WithHandshakeDuration(remote.HandShakeDuration),
}
relayConn := conn.NewRelayConn(c, rc, opts...)
if b.cmgr != nil {

View File

@@ -99,7 +99,7 @@ func (s *RawServer) ListenAndServe(ctx context.Context) error {
}
go func(c net.Conn) {
defer c.Close()
if err := s.RelayTCPConn(ctx, c, nil); err != nil {
if err := s.RelayTCPConn(ctx, c, s.remotes.Next()); err != nil {
s.l.Errorf("RelayTCPConn meet error: %s", err.Error())
}
}(c)
@@ -118,7 +118,7 @@ func (s *RawServer) listenUDP(ctx context.Context) error {
return err
}
go func() {
if err := s.RelayUDPConn(ctx, c, nil); err != nil {
if err := s.RelayUDPConn(ctx, c, s.remotes.Next()); err != nil {
s.l.Errorf("RelayUDPConn meet error: %s", err.Error())
}
}()

View File

@@ -98,6 +98,8 @@ func (s *WsServer) handleRequest(w http.ResponseWriter, req *http.Request) {
var remote *lb.Node
if addr := req.URL.Query().Get(conf.WS_QUERY_REMOTE_ADDR); addr != "" {
remote = &lb.Node{Address: addr, Label: addr}
} else {
remote = s.remotes.Next()
}
if req.URL.Query().Get("type") == "udp" {

View File

@@ -0,0 +1,393 @@
const MetricsModule = (function () {
// Constants
const API_BASE_URL = '/api/v1';
const NODE_METRICS_PATH = '/node_metrics/';
const BYTE_TO_MB = 1024 * 1024;
const handleError = (error) => {
console.error('Error:', error);
};
// API functions
const fetchData = async (path, params = {}) => {
const url = new URL(API_BASE_URL + path, window.location.origin);
Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
try {
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
handleError(error);
return null;
}
};
const fetchLatestMetric = () => fetchData(NODE_METRICS_PATH, { latest: true }).then((data) => data?.data[0]);
const fetchMetrics = (startTs, endTs) => fetchData(NODE_METRICS_PATH, { start_ts: startTs, end_ts: endTs }).then((data) => data?.data);
// Chart functions
const initChart = (canvasId, type, datasets, legendPosition = '', yDisplayText = '', title = '', unit = '') => {
const ctx = $(`#${canvasId}`)[0].getContext('2d');
const colors = {
cpu: 'rgba(255, 99, 132, 1)',
memory: 'rgba(54, 162, 235, 1)',
disk: 'rgba(255, 206, 86, 1)',
receive: 'rgba(0, 150, 255, 1)',
transmit: 'rgba(255, 140, 0, 1)',
};
const getDatasetConfig = (label) => {
const color = colors[label.toLowerCase()] || 'rgba(0, 0, 0, 1)';
return {
label,
borderColor: color,
backgroundColor: color.replace('1)', '0.2)'),
borderWidth: 2,
pointRadius: 2,
pointHoverRadius: 2,
fill: true,
data: [],
};
};
const data = {
labels: [],
datasets: $.isArray(datasets) ? datasets.map((dataset) => getDatasetConfig(dataset.label)) : [getDatasetConfig(datasets.label)],
};
return new Chart(ctx, {
type,
data,
options: {
line: {
spanGaps: false, // 设置为 false不连接空值
},
responsive: true,
plugins: {
legend: { position: legendPosition },
title: {
display: !!title,
text: title,
position: 'bottom',
font: { size: 14, weight: 'bold' },
},
tooltip: {
callbacks: {
title: function (tooltipItems) {
return new Date(tooltipItems[0].label).toLocaleString();
},
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(2) + ' ' + unit;
}
return label;
},
},
},
},
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
displayFormats: {
minute: 'HH:mm',
},
},
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 10,
},
adapters: {
date: {
locale: 'en',
},
},
},
y: {
beginAtZero: true,
title: { display: true, text: yDisplayText, font: { weight: 'bold' } },
},
},
elements: { line: { tension: 0.4 } },
downsample: {
enabled: true,
threshold: 200,
},
},
});
};
const updateChart = (chart, newData, labels) => {
if (!newData || !labels) {
console.error('Invalid data or labels provided');
return;
}
if ($.isArray(newData) && $.isArray(newData[0])) {
$.each(chart.data.datasets, (index, dataset) => {
if (newData[index]) {
dataset.data = newData[index].map((value, i) => ({ x: moment(labels[i]), y: value }));
}
});
} else {
chart.data.datasets[0].data = newData.map((value, i) => ({ x: moment(labels[i]), y: value }));
}
chart.options.scales.x.min = moment(labels[0]);
chart.options.scales.x.max = moment(labels[labels.length - 1]);
chart.update();
};
const updateCharts = (charts, metrics, startTs, endTs) => {
console.log('Raw metrics data:', metrics);
const generateTimestamps = (start, end) => {
const timestamps = [];
let current = moment.unix(start);
const endMoment = moment.unix(end);
while (current.isSameOrBefore(endMoment)) {
timestamps.push(current.toISOString());
current.add(1, 'minute');
}
return timestamps;
};
const timestamps = generateTimestamps(startTs, endTs);
const processData = (dataKey) => {
const data = new Array(timestamps.length).fill(null);
metrics.forEach((metric) => {
const index = Math.floor((metric.timestamp - startTs) / 60);
if (index >= 0 && index < data.length) {
data[index] = metric[dataKey];
}
});
return data;
};
updateChart(charts.cpu, processData('cpu_usage'), timestamps);
updateChart(charts.memory, processData('memory_usage'), timestamps);
updateChart(charts.disk, processData('disk_usage'), timestamps);
updateChart(
charts.network,
[
processData('network_in').map((v) => (v === null ? null : v / BYTE_TO_MB)),
processData('network_out').map((v) => (v === null ? null : v / BYTE_TO_MB)),
],
timestamps
);
};
const addLatestDataToCharts = (charts, latestMetric) => {
console.log('Raw latestMetric data:', latestMetric);
const timestamp = moment.unix(latestMetric.timestamp);
$.each(charts, (key, chart) => {
// 检查是否已经有这个时间戳的数据
const existingDataIndex = chart.data.labels.findIndex((label) => label.isSame(timestamp));
if (existingDataIndex === -1) {
// 如果是新数据,添加到末尾
chart.data.labels.push(timestamp);
if (key === 'network') {
chart.data.datasets[0].data.push({ x: timestamp, y: latestMetric.network_in / BYTE_TO_MB });
chart.data.datasets[1].data.push({ x: timestamp, y: latestMetric.network_out / BYTE_TO_MB });
} else {
chart.data.datasets[0].data.push({ x: timestamp, y: latestMetric[`${key}_usage`] });
}
// 更新x轴范围但保持一定的时间窗口
const timeWindow = moment.duration(30, 'minutes'); // 设置显示的时间窗口例如30分钟
const oldestAllowedTime = moment(timestamp).subtract(timeWindow);
chart.options.scales.x.min = oldestAllowedTime;
chart.options.scales.x.max = timestamp;
// 开启图表的平移和缩放功能
chart.options.plugins.zoom = {
pan: {
enabled: true,
mode: 'x',
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true,
},
mode: 'x',
},
};
chart.update();
}
// 如果数据已存在,我们不做任何操作,保持现有数据
});
};
// Chart initialization
const initializeCharts = async () => {
const metric = await fetchLatestMetric();
if (!metric) return null;
return {
cpu: initChart('cpuChart', 'line', { label: 'CPU' }, 'top', 'Usage (%)', `CPU`, '%'),
memory: initChart('memoryChart', 'line', { label: 'Memory' }, 'top', 'Usage (%)', `Memory`, '%'),
disk: initChart('diskChart', 'line', { label: 'Disk' }, 'top', 'Usage (%)', `Disk`, '%'),
network: initChart(
'networkChart',
'line',
[{ label: 'Receive' }, { label: 'Transmit' }],
'top',
'Rate (MB/s)',
'Network Rate',
'MB/s'
),
};
};
// Date range functions
const setupDateRangeDropdown = (charts) => {
const $dateRangeDropdown = $('#dateRangeDropdown');
const $dateRangeButton = $('#dateRangeButton');
const $dateRangeText = $('#dateRangeText');
const $dateRangeInput = $('#dateRangeInput');
$dateRangeDropdown.find('.dropdown-item[data-range]').on('click', function (e) {
e.preventDefault();
const range = $(this).data('range');
const now = new Date();
let start, end;
switch (range) {
case '30m':
start = new Date(now - 30 * 60 * 1000);
break;
case '1h':
start = new Date(now - 60 * 60 * 1000);
break;
case '3h':
start = new Date(now - 3 * 60 * 60 * 1000);
break;
case '6h':
start = new Date(now - 6 * 60 * 60 * 1000);
break;
case '12h':
start = new Date(now - 12 * 60 * 60 * 1000);
break;
case '24h':
start = new Date(now - 24 * 60 * 60 * 1000);
break;
case '7d':
start = new Date(now - 7 * 24 * 60 * 60 * 1000);
break;
}
end = now;
const startTs = Math.floor(start.getTime() / 1000);
const endTs = Math.floor(end.getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
$dateRangeText.text($(this).text());
$dateRangeDropdown.removeClass('is-active');
});
$dateRangeButton.on('click', (event) => {
event.stopPropagation();
$dateRangeDropdown.toggleClass('is-active');
});
$(document).on('click', (event) => {
if (!$dateRangeDropdown.has(event.target).length) {
$dateRangeDropdown.removeClass('is-active');
}
});
const picker = flatpickr($dateRangeInput[0], {
mode: 'range',
enableTime: true,
dateFormat: 'Y-m-d H:i',
onChange: function (selectedDates) {
if (selectedDates.length === 2) {
const startTs = Math.floor(selectedDates[0].getTime() / 1000);
const endTs = Math.floor(selectedDates[1].getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
const formattedStart = selectedDates[0].toLocaleString();
const formattedEnd = selectedDates[1].toLocaleString();
$dateRangeText.text(`${formattedStart} - ${formattedEnd}`);
// 关闭下拉菜单
$dateRangeDropdown.removeClass('is-active');
}
},
onClose: function () {
// 确保在日期选择器关闭时也关闭下拉菜单
$dateRangeDropdown.removeClass('is-active');
},
});
// 防止点击日期选择器时关闭下拉菜单
$dateRangeInput.on('click', (event) => {
event.stopPropagation();
});
};
const fetchDataForRange = async (charts, startTs, endTs) => {
const metrics = await fetchMetrics(startTs, endTs);
if (metrics) {
console.log('Raw metrics data:', metrics);
updateCharts(charts, metrics, startTs, endTs);
}
};
// Auto refresh functions
const setupAutoRefresh = (charts) => {
let autoRefreshInterval;
let isAutoRefreshing = false;
$('#refreshButton').click(function () {
if (isAutoRefreshing) {
clearInterval(autoRefreshInterval);
$(this).removeClass('is-info');
$(this).find('span:last').text('Auto Refresh');
isAutoRefreshing = false;
} else {
$(this).addClass('is-info');
$(this).find('span:last').text('Stop Refresh');
isAutoRefreshing = true;
refreshData(charts);
autoRefreshInterval = setInterval(() => refreshData(charts), 5000);
}
});
};
const refreshData = async (charts) => {
const latestMetric = await fetchLatestMetric();
if (latestMetric) {
addLatestDataToCharts(charts, latestMetric);
}
};
// Main initialization function
const init = async () => {
const charts = await initializeCharts();
if (charts) {
setupDateRangeDropdown(charts);
setupAutoRefresh(charts);
}
};
// Public API
return {
init: init,
};
})();
// Initialize when the DOM is ready
document.addEventListener('DOMContentLoaded', MetricsModule.init);

View File

@@ -22,7 +22,7 @@ import (
"github.com/Ehco1996/ehco/internal/metrics"
)
//go:embed templates/*.html
//go:embed templates/*.html js/*.js
var templatesFS embed.FS
const (
@@ -70,7 +70,7 @@ func NewServer(
return nil, errors.Wrap(err, "failed to setup middleware")
}
if err := setupTemplates(e, l); err != nil {
if err := setupTemplates(e, l, cfg); err != nil {
return nil, errors.Wrap(err, "failed to setup templates")
}
@@ -128,10 +128,13 @@ func setupMiddleware(e *echo.Echo, cfg *config.Config, l *zap.SugaredLogger) err
return nil
}
func setupTemplates(e *echo.Echo, l *zap.SugaredLogger) error {
func setupTemplates(e *echo.Echo, l *zap.SugaredLogger, cfg *config.Config) error {
funcMap := template.FuncMap{
"sub": func(a, b int) int { return a - b },
"add": func(a, b int) int { return a + b },
"CurrentCfg": func() *config.Config {
return cfg
},
}
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/*.html")
if err != nil {
@@ -158,6 +161,7 @@ func setupMetrics(cfg *config.Config) error {
func setupRoutes(s *Server) {
e := s.e
e.StaticFS("/js", echo.MustSubFS(templatesFS, "js"))
e.GET(metricsPath, echo.WrapHandler(promhttp.Handler()))
e.GET("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux))

View File

@@ -1,5 +1,5 @@
<head>
<title>Ehco Web</title>
<title>Ehco Web({{ (CurrentCfg).NodeLabel}})</title>
<meta charset="UTF-8" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
@@ -7,7 +7,9 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.1/chartjs-adapter-moment.min.js"></script>
</head>

View File

@@ -1,374 +1,57 @@
<div class="card" id="metrics-card">
<header class="card-header is-flex is-flex-wrap-wrap">
<p class="card-header-title has-text-centered">Node Metrics</p>
<div class="card-header-icon is-flex-grow-1 is-flex is-justify-content-flex-end">
<div class="dropdown" id="dateRangeDropdown">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" id="dateRangeButton">
<span id="dateRangeText">Select date range</span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" data-range="30m">Last 30 minutes</a>
<a href="#" class="dropdown-item" data-range="1h">Last 1 hour</a>
<a href="#" class="dropdown-item" data-range="3h">Last 3 hours</a>
<a href="#" class="dropdown-item" data-range="6h">Last 6 hours</a>
<a href="#" class="dropdown-item" data-range="12h">Last 12 hours</a>
<a href="#" class="dropdown-item" data-range="24h">Last 24 hours</a>
<a href="#" class="dropdown-item" data-range="7d">Last 7 days</a>
<hr class="dropdown-divider" />
<a href="#" class="dropdown-item" id="dateRangeInput">Select date range</a>
</div>
</div>
</div>
<button id="refreshButton" class="button ml-2">
<span class="icon">
<i class="fas fa-sync-alt"></i>
</span>
<span>Auto Refresh</span>
</button>
<header class="card-header is-flex is-flex-wrap-wrap">
<p class="card-header-title has-text-centered">Node Metrics</p>
<div class="card-header-icon is-flex-grow-1 is-flex is-justify-content-flex-end">
<div class="dropdown" id="dateRangeDropdown">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu" id="dateRangeButton">
<span id="dateRangeText">Select date range</span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
</header>
<div class="card-content">
<div class="content">
<div class="columns is-multiline">
<div class="column is-4">
<canvas id="cpuChart"></canvas>
</div>
<div class="column is-4">
<canvas id="memoryChart"></canvas>
</div>
<div class="column is-4">
<canvas id="diskChart"></canvas>
</div>
<div class="column is-12">
<canvas id="networkChart"></canvas>
</div>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" data-range="30m">Last 30 minutes</a>
<a href="#" class="dropdown-item" data-range="1h">Last 1 hour</a>
<a href="#" class="dropdown-item" data-range="3h">Last 3 hours</a>
<a href="#" class="dropdown-item" data-range="6h">Last 6 hours</a>
<a href="#" class="dropdown-item" data-range="12h">Last 12 hours</a>
<a href="#" class="dropdown-item" data-range="24h">Last 24 hours</a>
<a href="#" class="dropdown-item" data-range="7d">Last 7 days</a>
<hr class="dropdown-divider" />
<a href="#" class="dropdown-item" id="dateRangeInput">Select date range</a>
</div>
</div>
</div>
<button id="refreshButton" class="button ml-2">
<span class="icon">
<i class="fas fa-sync-alt"></i>
</span>
<span>Auto Refresh</span>
</button>
</div>
<script>
$(document).ready(async function () {
// Constants
const API_BASE_URL = '/api/v1';
const NODE_METRICS_PATH = '/node_metrics/';
const BYTE_TO_MB = 1024 * 1024;
const BYTE_TO_GB = BYTE_TO_MB * 1024;
</header>
<div class="card-content">
<div class="content">
<div class="columns is-multiline">
<div class="column is-4">
<canvas id="cpuChart"></canvas>
</div>
<div class="column is-4">
<canvas id="memoryChart"></canvas>
</div>
<div class="column is-4">
<canvas id="diskChart"></canvas>
</div>
// Utility functions
const handleError = (error) => {
console.error('Error:', error);
// You can add user notifications here
};
const formatDate = (timeStamp) => {
const date = new Date(timeStamp * 1000);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
};
const formatBytes = (bytes, decimals = 2) => {
return (bytes / BYTE_TO_GB).toFixed(decimals);
};
// API functions
const fetchData = async (path, params = {}) => {
const url = new URL(API_BASE_URL + path, window.location.origin);
$.each(params, (key, value) => url.searchParams.append(key, value));
try {
const response = await $.ajax({
url: url.toString(),
method: 'GET',
dataType: 'json',
});
return response;
} catch (error) {
handleError(error);
return null;
}
};
const fetchLatestMetric = () => fetchData(NODE_METRICS_PATH, { latest: true }).then((data) => data?.data[0]);
const fetchMetrics = (startTs, endTs) =>
fetchData(NODE_METRICS_PATH, { start_ts: startTs, end_ts: endTs }).then((data) => data?.data);
// Chart functions
const initChart = (canvasId, type, datasets, legendPosition = '', yDisplayText = '', title = '', unit = '') => {
const ctx = $(`#${canvasId}`)[0].getContext('2d');
const colors = {
cpu: 'rgba(255, 99, 132, 1)',
memory: 'rgba(54, 162, 235, 1)',
disk: 'rgba(255, 206, 86, 1)',
receive: 'rgba(0, 150, 255, 1)',
transmit: 'rgba(255, 140, 0, 1)',
};
const getDatasetConfig = (label) => {
const color = colors[label.toLowerCase()] || 'rgba(0, 0, 0, 1)'; // 默认颜色
console.log(label, color);
return {
label,
borderColor: color,
backgroundColor: color.replace('1)', '0.2)'),
borderWidth: 2,
pointRadius: 0,
fill: true,
data: [],
};
};
const data = {
labels: [],
datasets: $.isArray(datasets)
? datasets.map((dataset) => getDatasetConfig(dataset.label))
: [getDatasetConfig(datasets.label)],
};
return new Chart(ctx, {
type,
data,
options: {
responsive: true,
plugins: {
legend: { position: legendPosition },
title: {
display: !!title,
text: title,
position: 'bottom',
font: { size: 14, weight: 'bold' },
},
tooltip: {
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(2) + ' ' + unit;
}
return label;
},
},
},
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: yDisplayText, font: { weight: 'bold' } },
},
x: {
ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 10 },
},
},
elements: { line: { tension: 0.4 } },
downsample: {
enabled: true,
threshold: 200,
},
},
});
};
const updateChart = (chart, newData, labels) => {
if (!newData || !labels) {
console.error('Invalid data or labels provided');
return;
}
const formattedLabels = labels.map(formatDate);
if ($.isArray(newData) && $.isArray(newData[0])) {
$.each(chart.data.datasets, (index, dataset) => {
if (newData[index]) {
dataset.data = newData[index];
}
});
} else {
chart.data.datasets[0].data = newData;
}
chart.data.labels = formattedLabels;
chart.update();
};
const updateCharts = (charts, metrics) => {
console.log('Raw metrics data:', metrics);
const timestamps = metrics.map((data) => data.timestamp);
updateChart(
charts.cpu,
metrics.map((data) => data.cpu_usage),
timestamps,
);
updateChart(
charts.memory,
metrics.map((data) => data.memory_usage),
timestamps,
);
updateChart(
charts.disk,
metrics.map((data) => data.disk_usage),
timestamps,
);
updateChart(
charts.network,
[metrics.map((data) => data.network_in / BYTE_TO_MB), metrics.map((data) => data.network_out / BYTE_TO_MB)],
timestamps,
);
};
const addLatestDataToCharts = (charts, latestMetric) => {
console.log('Raw latestMetric data:', latestMetric);
const timestamp = formatDate(latestMetric.timestamp);
$.each(charts, (key, chart) => {
chart.data.labels.push(timestamp);
if (key === 'network') {
chart.data.datasets[0].data.push(latestMetric.network_in / BYTE_TO_MB);
chart.data.datasets[1].data.push(latestMetric.network_out / BYTE_TO_MB);
} else {
chart.data.datasets[0].data.push(latestMetric[`${key}_usage`]);
}
chart.update();
});
};
// Chart initialization
const initializeCharts = async () => {
const metric = await fetchLatestMetric();
if (!metric) return null;
return {
cpu: initChart('cpuChart', 'line', { label: 'CPU' }, 'top', 'Usage (%)', `CPU`, '%'),
memory: initChart('memoryChart', 'line', { label: 'Memory' }, 'top', 'Usage (%)', `Memory`, '%'),
disk: initChart('diskChart', 'line', { label: 'Disk' }, 'top', 'Usage (%)', `Disk`, '%'),
network: initChart(
'networkChart',
'line',
[{ label: 'Receive' }, { label: 'Transmit' }],
'top',
'Rate (MB/s)',
'Network Rate',
'MB/s',
),
};
};
// Date range functions
const setupDateRangeDropdown = (charts) => {
const $dateRangeDropdown = $('#dateRangeDropdown');
const $dateRangeButton = $('#dateRangeButton');
const $dateRangeText = $('#dateRangeText');
const $dateRangeInput = $('#dateRangeInput');
$dateRangeDropdown.find('.dropdown-item[data-range]').on('click', function (e) {
e.preventDefault();
const range = $(this).data('range');
const now = new Date();
let start, end;
switch (range) {
case '30m':
start = new Date(now - 30 * 60 * 1000);
break;
case '1h':
start = new Date(now - 60 * 60 * 1000);
break;
case '3h':
start = new Date(now - 3 * 60 * 60 * 1000);
break;
case '6h':
start = new Date(now - 6 * 60 * 60 * 1000);
break;
case '12h':
start = new Date(now - 12 * 60 * 60 * 1000);
break;
case '24h':
start = new Date(now - 24 * 60 * 60 * 1000);
break;
case '7d':
start = new Date(now - 7 * 24 * 60 * 60 * 1000);
break;
}
end = now;
const startTs = Math.floor(start.getTime() / 1000);
const endTs = Math.floor(end.getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
$dateRangeText.text($(this).text());
$dateRangeDropdown.removeClass('is-active');
});
$dateRangeButton.on('click', (event) => {
event.stopPropagation();
$dateRangeDropdown.toggleClass('is-active');
});
$(document).on('click', (event) => {
if (!$dateRangeDropdown.has(event.target).length) {
$dateRangeDropdown.removeClass('is-active');
}
});
const picker = flatpickr($dateRangeInput[0], {
mode: 'range',
enableTime: true,
dateFormat: 'Y-m-d H:i',
onChange: function (selectedDates) {
if (selectedDates.length === 2) {
const startTs = Math.floor(selectedDates[0].getTime() / 1000);
const endTs = Math.floor(selectedDates[1].getTime() / 1000);
fetchDataForRange(charts, startTs, endTs);
const formattedStart = selectedDates[0].toLocaleString();
const formattedEnd = selectedDates[1].toLocaleString();
$dateRangeText.text(`${formattedStart} - ${formattedEnd}`);
}
},
});
};
const fetchDataForRange = async (charts, startTs, endTs) => {
const metrics = await fetchMetrics(startTs, endTs);
if (metrics) {
console.log('Raw metrics data:', metrics);
updateCharts(charts, metrics);
}
};
// Auto refresh functions
const setupAutoRefresh = (charts) => {
let autoRefreshInterval;
let isAutoRefreshing = false;
$('#refreshButton').click(function () {
if (isAutoRefreshing) {
clearInterval(autoRefreshInterval);
$(this).removeClass('is-info');
$(this).find('span:last').text('Auto Refresh');
isAutoRefreshing = false;
} else {
$(this).addClass('is-info');
$(this).find('span:last').text('Stop Refresh');
isAutoRefreshing = true;
refreshData(charts);
autoRefreshInterval = setInterval(() => refreshData(charts), 5000);
}
});
};
const refreshData = async (charts) => {
const latestMetric = await fetchLatestMetric();
if (latestMetric) {
addLatestDataToCharts(charts, latestMetric);
}
};
// Main execution
const charts = await initializeCharts();
if (charts) {
setupDateRangeDropdown(charts);
setupAutoRefresh(charts);
}
});
</script>
<div class="column is-12">
<canvas id="networkChart"></canvas>
</div>
</div>
</div>
</div>
<!-- </div> -->
<script src="js/metrics.js"></script>
</div>

View File

@@ -4,7 +4,7 @@
<a class="navbar-item" href="/">
<h1 class="title is-4">Ehco Relay</h1>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
@@ -12,7 +12,7 @@
</a>
</div>
<div class="navbar-menu">
<div id="navbarMenu" class="navbar-menu">
<div class="navbar-start">
<a href="/rules/" class="navbar-item">
<span class="icon"><i class="fas fa-list"></i></span>
@@ -56,3 +56,28 @@
</div>
</nav>
<hr />
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach((el) => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Check if the target element exists
if ($target) {
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
} else {
console.error(`Target element with id "${target}" not found`);
}
});
});
});
</script>