Update On Mon Aug 26 20:34:39 CEST 2024

This commit is contained in:
github-action[bot]
2024-08-26 20:34:40 +02:00
parent 2e77f8eb45
commit 4afce62e47
112 changed files with 1924 additions and 896 deletions

View File

@@ -16,6 +16,11 @@ const (
ConnectionTypeClosed = "closed"
)
type QueryNodeMetricsReq struct {
TimeRange string `json:"time_range"` // 15min/30min/1h/6h/12h/24h
Num int `json:"num"` // number of nodes to query
}
// connection manager interface/
// TODO support closed connection
type Cmgr interface {
@@ -34,17 +39,21 @@ type Cmgr interface {
// Start starts the connection manager.
Start(ctx context.Context, errCH chan error)
QueryNodeMetrics(ctx context.Context, req *QueryNodeMetricsReq) ([]metric_reader.NodeMetrics, error)
}
type cmgrImpl struct {
lock sync.RWMutex
cfg *Config
l *zap.SugaredLogger
mr metric_reader.Reader
// k: relay label, v: connection list
activeConnectionsMap map[string][]conn.RelayConn
closedConnectionsMap map[string][]conn.RelayConn
mr metric_reader.Reader
ms []*metric_reader.NodeMetrics // TODO gc this
}
func NewCmgr(cfg *Config) Cmgr {
@@ -171,6 +180,12 @@ func (cm *cmgrImpl) Start(ctx context.Context, errCH chan error) {
cm.l.Infof("Start Cmgr sync interval=%d", cm.cfg.SyncInterval)
ticker := time.NewTicker(time.Second * time.Duration(cm.cfg.SyncInterval))
defer ticker.Stop()
// sync once at the beginning
if err := cm.syncOnce(ctx); err != nil {
cm.l.Errorf("meet non retry error: %s ,exit now", err)
errCH <- err
return
}
for {
select {
@@ -185,3 +200,38 @@ func (cm *cmgrImpl) Start(ctx context.Context, errCH chan error) {
}
}
}
func (cm *cmgrImpl) QueryNodeMetrics(ctx context.Context, req *QueryNodeMetricsReq) ([]metric_reader.NodeMetrics, error) {
cm.lock.RLock()
defer cm.lock.RUnlock()
var startTime time.Time
switch req.TimeRange {
case "15min":
startTime = time.Now().Add(-15 * time.Minute)
case "30min":
startTime = time.Now().Add(-30 * time.Minute)
case "1h":
startTime = time.Now().Add(-1 * time.Hour)
case "6h":
startTime = time.Now().Add(-6 * time.Hour)
case "12h":
startTime = time.Now().Add(-12 * time.Hour)
case "24h":
startTime = time.Now().Add(-24 * time.Hour)
default:
// default to 15min
startTime = time.Now().Add(-15 * time.Minute)
}
res := []metric_reader.NodeMetrics{}
for _, metrics := range cm.ms {
if metrics.SyncTime.After(startTime) {
res = append(res, *metrics)
}
if req.Num > 0 && len(res) >= req.Num {
break
}
}
return res, nil
}

View File

@@ -35,13 +35,13 @@ func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
// todo: opt lock
cm.lock.Lock()
shorCommit := constant.GitRevision
shortCommit := constant.GitRevision
if len(constant.GitRevision) > 7 {
shorCommit = constant.GitRevision[:7]
shortCommit = constant.GitRevision[:7]
}
req := syncReq{
Stats: []StatsPerRule{},
Version: VersionInfo{Version: constant.Version, ShortCommit: shorCommit},
Version: VersionInfo{Version: constant.Version, ShortCommit: shortCommit},
}
if cm.cfg.NeedMetrics() {
@@ -50,6 +50,7 @@ func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
cm.l.Errorf("read metrics failed: %v", err)
} else {
req.Node = *metrics
cm.ms = append(cm.ms, metrics)
}
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/Ehco1996/ehco/internal/relay/conf"
"github.com/Ehco1996/ehco/internal/tls"
myhttp "github.com/Ehco1996/ehco/pkg/http"
"github.com/Ehco1996/ehco/pkg/sub"
xConf "github.com/xtls/xray-core/infra/conf"
"go.uber.org/zap"
)
@@ -33,18 +32,15 @@ type Config struct {
RelaySyncURL string `json:"relay_sync_url,omitempty"`
RelaySyncInterval int `json:"relay_sync_interval,omitempty"`
SubConfigs []*SubConfig `json:"sub_configs,omitempty"`
XRayConfig *xConf.Config `json:"xray_config,omitempty"`
SyncTrafficEndPoint string `json:"sync_traffic_endpoint,omitempty"`
lastLoadTime time.Time
l *zap.SugaredLogger
cachedClashSubMap map[string]*sub.ClashSub // key: clash sub name
}
func NewConfig(path string) *Config {
return &Config{PATH: path, l: zap.S().Named("cfg"), cachedClashSubMap: make(map[string]*sub.ClashSub)}
return &Config{PATH: path, l: zap.S().Named("cfg")}
}
func (c *Config) NeedSyncFromServer() bool {
@@ -93,21 +89,6 @@ func (c *Config) Adjust() error {
c.WebHost = "0.0.0.0"
}
clashSubList, err := c.GetClashSubList()
if err != nil {
return err
}
for _, clashSub := range clashSubList {
if err := clashSub.Refresh(); err != nil {
return err
}
relayConfigs, err := clashSub.ToRelayConfigs(c.WebHost)
if err != nil {
return err
}
c.RelayConfigs = append(c.RelayConfigs, relayConfigs...)
}
for _, r := range c.RelayConfigs {
if err := r.Validate(); err != nil {
return err
@@ -160,32 +141,3 @@ func (c *Config) GetMetricURL() string {
}
return url
}
func (c *Config) GetClashSubList() ([]*sub.ClashSub, error) {
clashSubList := make([]*sub.ClashSub, 0, len(c.SubConfigs))
for _, subCfg := range c.SubConfigs {
clashSub, err := c.getOrCreateClashSub(subCfg)
if err != nil {
return nil, err
}
clashSubList = append(clashSubList, clashSub)
}
return clashSubList, nil
}
func (c *Config) getOrCreateClashSub(subCfg *SubConfig) (*sub.ClashSub, error) {
if clashSub, ok := c.cachedClashSubMap[subCfg.Name]; ok {
return clashSub, nil
}
clashSub, err := sub.NewClashSubByURL(subCfg.URL, subCfg.Name)
if err != nil {
return nil, err
}
c.cachedClashSubMap[subCfg.Name] = clashSub
return clashSub, nil
}
type SubConfig struct {
Name string `json:"name"`
URL string `json:"url"`
}

View File

@@ -21,9 +21,7 @@ const (
shortHashLength = 7
)
var (
ErrIdleTimeout = errors.New("connection closed due to idle timeout")
)
var ErrIdleTimeout = errors.New("connection closed due to idle timeout")
// RelayConn is the interface that represents a relay connection.
// it contains two connections: clientConn and remoteConn

View File

@@ -107,7 +107,7 @@ func (r *Config) Adjust() error {
zap.S().Debugf("label is empty, set default label:%s", r.Label)
}
if len(r.Remotes) == 0 && len(r.TCPRemotes) != 0 {
zap.S().Warnf("tcp remotes is deprecated, use remotes instead")
zap.S().Warnf("tcp remotes is deprecated, please use remotes instead")
r.Remotes = r.TCPRemotes
}

View File

@@ -71,7 +71,7 @@ func (s *Server) Start(ctx context.Context) error {
go s.startOneRelay(ctx, r)
}
if s.cfg.PATH != "" && (s.cfg.ReloadInterval > 0 || len(s.cfg.SubConfigs) > 0) {
if s.cfg.PATH != "" && (s.cfg.ReloadInterval > 0) {
s.l.Infof("Start to watch relay config %s ", s.cfg.PATH)
go s.WatchAndReload(ctx)
}

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"strconv"
"github.com/Ehco1996/ehco/internal/config"
"github.com/Ehco1996/ehco/internal/cmgr"
"github.com/Ehco1996/ehco/internal/constant"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
@@ -28,60 +28,16 @@ func (s *Server) index(c echo.Context) error {
GitRevision string
BuildTime string
StartTime string
SubConfigs []*config.SubConfig
}{
Version: constant.Version,
GitBranch: constant.GitBranch,
GitRevision: constant.GitRevision,
BuildTime: constant.BuildTime,
StartTime: constant.StartTime.Format("2006-01-02 15:04:05"),
SubConfigs: s.cfg.SubConfigs,
}
return c.Render(http.StatusOK, "index.html", data)
}
func (s *Server) HandleClashProxyProvider(c echo.Context) error {
subName := c.QueryParam("sub_name")
if subName == "" {
return c.String(http.StatusBadRequest, "sub_name is empty")
}
grouped, _ := strconv.ParseBool(c.QueryParam("grouped")) // defaults to false if parameter is missing or invalid
return s.handleClashProxyProvider(c, subName, grouped)
}
func (s *Server) handleClashProxyProvider(c echo.Context, subName string, grouped bool) error {
if s.Reloader != nil {
if err := s.Reloader.Reload(true); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
} else {
s.l.Debugf("Reloader is nil this should not happen")
return echo.NewHTTPError(http.StatusBadRequest, "should not happen error happen :)")
}
clashSubList, err := s.cfg.GetClashSubList()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
for _, clashSub := range clashSubList {
if clashSub.Name == subName {
var clashCfgBuf []byte
if grouped {
clashCfgBuf, err = clashSub.ToGroupedClashConfigYaml()
} else {
clashCfgBuf, err = clashSub.ToClashConfigYaml()
}
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"message": err.Error()})
}
return c.String(http.StatusOK, string(clashCfgBuf))
}
}
msg := fmt.Sprintf("sub_name=%s not found", subName)
return c.JSON(http.StatusBadRequest, map[string]string{"message": msg})
}
func (s *Server) HandleReload(c echo.Context) error {
if s.Reloader == nil {
return echo.NewHTTPError(http.StatusBadRequest, "reload not support")
@@ -165,3 +121,20 @@ func (s *Server) ListRules(c echo.Context) error {
"Configs": s.cfg.RelayConfigs,
})
}
func (s *Server) GetNodeMetrics(c echo.Context) error {
req := &cmgr.QueryNodeMetricsReq{TimeRange: c.QueryParam("time_range")}
num := c.QueryParam("num")
if num != "" {
n, err := strconv.Atoi(num)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
req.Num = n
}
metrics, err := s.connMgr.QueryNodeMetrics(c.Request().Context(), req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, metrics)
}

View File

@@ -103,13 +103,13 @@ func NewServer(
e.GET("/", s.index)
e.GET("/connections/", s.ListConnections)
e.GET("/rules/", s.ListRules)
e.GET("/clash_proxy_provider/", s.HandleClashProxyProvider)
// api group
api := e.Group("/api/v1")
api.GET("/config/", s.CurrentConfig)
api.POST("/config/reload/", s.HandleReload)
api.GET("/health_check/", s.HandleHealthCheck)
api.GET("/node_metrics/", s.GetNodeMetrics)
return s, nil
}

View File

@@ -1,167 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.0/css/bulma.min.css"
/>
<title>Ehco</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<section class="hero is-fullheight is-light">
<div class="hero-body">
<div class="container">
<!-- Title -->
<h1 class="title has-text-centered">
ehco is a network relay tool and a typo :)
</h1>
<!-- Build Info Card -->
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Build Info
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>Version: {{.Version}}</li>
<li>GitBranch: {{.GitBranch}}</li>
<li>GitRevision: {{.GitRevision}}</li>
<li>BuildTime: {{.BuildTime}}</li>
<li>StartTime: {{.StartTime}}</li>
</ul>
</div>
</div>
</div>
<!-- Stylish Links Card -->
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Quick Links
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>
<a
href="/metrics/"
class="button is-info is-light"
>Metrics</a
>
</li>
<li>
<a
href="/debug/pprof/"
class="button is-info is-light"
>Debug</a
>
</li>
<li>
<a
href="/connections/?conn_type=active"
class="button is-info is-light"
>Connections</a
>
</li>
<li>
<a
href="/rules/"
class="button is-info is-light"
>Rule List</a
>
</li>
<li>
<a
href="/api/v1/config/"
class="button is-info is-light"
>Config</a
>
</li>
</ul>
</div>
</div>
</div>
<!-- Clash Providers card -->
{{ if .SubConfigs }}
<div class="card">
<header class="card-header">
<p class="card-header-title has-text-centered">
Clash Providers
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
{{ range .SubConfigs }}
<li>
<a
class="button is-info is-light"
href="/clash_proxy_provider/?sub_name={{.Name}}"
>{{.Name}}</a
>
</li>
<li>
<a
class="button is-info is-light"
href="/clash_proxy_provider/?sub_name={{.Name}}&grouped=true"
>{{.Name}}-lb</a
>
</li>
</ul>
{{ end }}
</div>
</div>
</div>
{{ end }}
<!-- Reload Config Button -->
<div class="has-text-centered">
<button
class="button is-danger is-outlined"
id="reloadButton"
>
Reload Config
</button>
</div>
<head>
<title>Ehco Web</title>
<meta charset="UTF-8" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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" />
<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>
</head>
<body>
<section class="hero is-fullheight is-light">
<div class="hero-body">
<div class="container">
<h1 class="title has-text-centered">Ehco Relay</h1>
<div class="columns is-variable is-8 is-flex-grow">
<div class="column is-flex">
<!-- Build Info Card -->
<div class="card is-flex is-flex-direction-column is-flex-grow-1">
<header class="card-header">
<p class="card-header-title has-text-centered">Build Info</p>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<ul>
<li>Version: {{.Version}}</li>
<li>GitBranch: {{.GitBranch}}</li>
<li>GitRevision: {{.GitRevision}}</li>
<li>BuildTime: {{.BuildTime}}</li>
<li>StartTime: {{.StartTime}}</li>
</ul>
</div>
</div>
</div>
<!-- Footer -->
<div class="hero-foot">
<footer class="footer">
<div class="content has-text-centered">
<a href="https://github.com/Ehco1996/ehco"
>Source code</a
>
</div>
<footer class="card-footer">
<button class="button is-danger is-outlined card-footer-item" id="reloadButton">
<span class="icon"><i class="fas fa-sync-alt"></i></span>
<span>Reload Config</span>
</button>
</footer>
</div>
</div>
</section>
<script>
$(document).ready(function () {
$("#reloadButton").click(function () {
$.ajax({
type: "POST",
url: "/api/v1/config/reload/",
success: function (response) {
alert(
"Reload config success. Response: " + response
);
},
error: function (response) {
alert(
"Failed to reload config. Response: " +
response.responseText
);
},
});
});
});
</script>
</body>
<div class="column is-flex">
<!-- Stylish Links Card -->
<div class="card is-flex is-flex-direction-column is-flex-grow-1">
<header class="card-header">
<p class="card-header-title has-text-centered">Quick Links</p>
</header>
<div class="card-content is-flex-grow-1">
<div class="content">
<ul>
<li>
<a href="/metrics/" class="button is-info is-light">
<span class="icon"><i class="fas fa-chart-bar"></i></span>
<span>Metrics</span>
</a>
</li>
<li>
<a href="/debug/pprof/" class="button is-info is-light">
<span class="icon"><i class="fas fa-bug"></i></span>
<span>Debug</span>
</a>
</li>
<li>
<a href="/connections/?conn_type=active" class="button is-info is-light">
<span class="icon"><i class="fas fa-link"></i></span>
<span>Connections</span>
</a>
</li>
<li>
<a href="/rules/" class="button is-info is-light">
<span class="icon"><i class="fas fa-list"></i></span>
<span>Rule List</span>
</a>
</li>
<li>
<a href="/api/v1/config/" class="button is-info is-light">
<span class="icon"><i class="fas fa-cog"></i></span>
<span>Config</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- metrics -->
{{template "metrics.html"}}
</div>
</div>
<!-- Footer -->
<div class="hero-foot">
<footer class="footer">
<div class="content has-text-centered">
<a href="https://github.com/Ehco1996/ehco"><i class="fab fa-github"></i> Source code</a>
</div>
</footer>
</div>
</section>
<script>
$(document).ready(function () {
// Reload config button click event
$('#reloadButton').click(function () {
$.ajax({
type: 'POST',
url: '/api/v1/config/reload/',
success: function (response) {
alert('Reload config success. Response: ' + response);
},
error: function (response) {
alert('Failed to reload config. Response: ' + response.responseText);
},
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,284 @@
<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 is-hoverable">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<i class="fas fa-clock"></i>
</span>
<span>Time</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-time="15min">Last 15 Min</a>
<a href="#" class="dropdown-item" data-time="30min">Last 30 Min</a>
<a href="#" class="dropdown-item" data-time="1h">Last 1 hour</a>
<a href="#" class="dropdown-item" data-time="6h">Last 6 hours</a>
<a href="#" class="dropdown-item" data-time="12h">Last 12 hours</a>
<a href="#" class="dropdown-item" data-time="24h">Last 24 hours</a>
</div>
</div>
</div>
</div>
</header>
<div class="card-content">
<div class="content">
<div class="columns is-multiline">
<div class="column is-4">
<h2 class="subtitle is-5">CPU</h2>
<canvas id="cpuChart"></canvas>
</div>
<div class="column is-4">
<h2 class="subtitle is-5">Memory</h2>
<canvas id="memoryChart"></canvas>
</div>
<div class="column is-4">
<h2 class="subtitle is-5">Disk</h2>
<canvas id="diskChart"></canvas>
</div>
<div class="column is-6">
<h2 class="subtitle is-5">Network</h2>
<canvas id="networkChart"></canvas>
</div>
<div class="column is-6">
<h2 class="subtitle is-5">Ping</h2>
<canvas id="pingChart"></canvas>
</div>
</div>
</div>
</div>
<script>
// Constants
const API_BASE_URL = '/api/v1/node_metrics/';
const BYTE_TO_MB = 1024 * 1024;
const BYTE_TO_GB = BYTE_TO_MB * 1024;
// Utility functions
const handleError = (error) => {
console.error('Error:', error);
// You can add user notifications here
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
};
const formatBytes = (bytes, decimals = 2) => {
return (bytes / BYTE_TO_GB).toFixed(decimals);
};
// Chart functions
const initChart = (canvasId, type, datasets, legendPosition = '', yDisplayText = '', additionalInfo = '', unit = '') => {
const ctx = document.getElementById(canvasId).getContext('2d');
const data = {
labels: [],
datasets: Array.isArray(datasets) ? datasets.map((dataset) => ({ ...dataset, data: [] })) : [{ ...datasets, data: [] }],
};
return new Chart(ctx, {
type,
data,
options: {
responsive: true,
plugins: {
legend: { position: legendPosition },
title: {
display: !!additionalInfo,
text: additionalInfo,
position: 'bottom',
font: { size: 12 },
},
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 },
},
},
elements: { line: { tension: 0.2 } },
downsample: {
enabled: true,
samples: 100,
threshold: 50,
},
},
});
};
const updateChart = (chart, newData, labels) => {
if (!newData || !labels) {
console.error('Invalid data or labels provided');
return;
}
const formattedLabels = labels.map(formatDate);
if (Array.isArray(newData) && Array.isArray(newData[0])) {
chart.data.datasets.forEach((dataset, index) => {
if (newData[index]) {
dataset.data = newData[index];
}
});
} else {
chart.data.datasets[0].data = newData;
}
chart.data.labels = formattedLabels;
chart.update();
};
// Data fetching functions
const fetchLatestMetric = async () => {
try {
const response = await fetch(`${API_BASE_URL}?time_range=15min&num=1`);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data[0];
} catch (error) {
handleError(error);
}
};
const fetchMetrics = async (timeRange) => {
try {
const response = await fetch(`${API_BASE_URL}?time_range=${timeRange}`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
handleError(error);
}
};
// Chart initialization
const initializeCharts = async () => {
const metric = await fetchLatestMetric();
if (!metric) return null;
const pingTargets = metric.ping_metrics.map((ping) => ping.target);
return {
cpu: initChart(
'cpuChart',
'line',
{ label: 'CPU' },
'top',
'Usage (%)',
`Load: ${metric.cpu_load_info} | Cores: ${metric.cpu_core_count}`,
'%',
),
memory: initChart(
'memoryChart',
'line',
{ label: 'Memory' },
'top',
'Usage (%)',
`Total: ${formatBytes(metric.memory_total_bytes)} GB`,
'%',
),
disk: initChart(
'diskChart',
'line',
{ label: 'Disk' },
'top',
'Usage (%)',
`Total: ${formatBytes(metric.disk_total_bytes)} GB`,
'%',
),
network: initChart('networkChart', 'line', [{ label: 'Receive' }, { label: 'Transmit' }], 'top', 'Rate (MB/s)', '', 'MB/s'),
ping: initChart(
'pingChart',
'line',
pingTargets.map((target) => ({ label: target })),
'right',
'Latency (ms)',
'',
'ms',
),
};
};
// Update functions
const updateCharts = (charts, metrics) => {
const timestamps = metrics.map((data) => data.SyncTime);
updateChart(
charts.cpu,
metrics.map((data) => data.cpu_usage_percent),
timestamps,
);
updateChart(
charts.memory,
metrics.map((data) => data.memory_usage_percent),
timestamps,
);
updateChart(
charts.disk,
metrics.map((data) => data.disk_usage_percent),
timestamps,
);
updateChart(
charts.network,
[
metrics.map((data) => data.network_receive_bytes_rate / BYTE_TO_MB),
metrics.map((data) => data.network_transmit_bytes_rate / BYTE_TO_MB),
],
timestamps,
);
const pingTargets = [...new Set(metrics.flatMap((data) => data.ping_metrics.map((ping) => ping.target)))];
const pingData = pingTargets.map((target) =>
metrics.map((data) => {
const pingMetric = data.ping_metrics.find((ping) => ping.target === target);
return pingMetric ? pingMetric.latency : null;
}),
);
updateChart(charts.ping, pingData, timestamps);
const latestMetric = metrics[metrics.length - 1];
updateAdditionalInfo(charts, latestMetric);
};
const updateAdditionalInfo = (charts, metric) => {
charts.cpu.options.plugins.title.text = `Load: ${metric.cpu_load_info} | Cores: ${metric.cpu_core_count}`;
charts.memory.options.plugins.title.text = `Total: ${formatBytes(metric.memory_total_bytes)} GB`;
charts.disk.options.plugins.title.text = `Total: ${formatBytes(metric.disk_total_bytes)} GB`;
charts.cpu.update();
charts.memory.update();
charts.disk.update();
};
// Main execution
$(document).ready(async function () {
let charts = await initializeCharts();
if (!charts) return;
$('.dropdown-item').click(async function () {
const timeRange = $(this).data('time');
const metrics = await fetchMetrics(timeRange);
if (metrics) updateCharts(charts, metrics);
});
});
</script>
</div>

View File

@@ -1,86 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css"
/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<title>Rules</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rules</h1>
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Label</th>
<th>Listen</th>
<th>Listen Type</th>
<th>Transport Type</th>
<th>Remote</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Configs}}
<tr>
<td>{{.Label}}</td>
<td>{{.Listen}}</td>
<td>{{.ListenType}}</td>
<td>{{.TransportType}}</td>
<td>{{.GetTCPRemotes}}</td>
<td>
<button
class="button is-small is-primary health-check"
data-label="{{.Label}}"
onclick="checkHealth('{{.Label}}')"
>
Check Health
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ehco web" />
<meta name="keywords" content="ehco-relay" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.1/css/bulma.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<title>Rules</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rules</h1>
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Label</th>
<th>Listen</th>
<th>Listen Type</th>
<th>Transport Type</th>
<th>Remote</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Configs}}
<tr>
<td>{{.Label}}</td>
<td>{{.Listen}}</td>
<td>{{.ListenType}}</td>
<td>{{.TransportType}}</td>
<td>{{.Remotes}}</td>
<td>
<button class="button is-small is-primary health-check" data-label="{{.Label}}" onclick="checkHealth('{{.Label}}')">
Check Health
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<script>
function checkHealth(label) {
$.ajax({
url: "/api/v1/health_check/?relay_label=" + label,
method: "GET",
success: function (response) {
// Check if the response includes an error code
if (response.error_code === 0) {
// If no error, show success message with latency
alert(
"Health Check for " +
label +
": " +
response.msg + // Use 'msg' as per Go struct
" (Latency: " +
response.latency + // Ensure this matches the Go struct field name
"ms)"
);
} else {
// If error code is not 0, show error message
alert("Error for " + label + ": " + response.msg);
}
},
error: function (xhr) {
// Parse the response JSON in case of HTTP error
var response = JSON.parse(xhr.responseText);
alert("Error: " + response.msg); // Use 'msg' as per Go struct
},
});
<script>
function checkHealth(label) {
$.ajax({
url: '/api/v1/health_check/?relay_label=' + label,
method: 'GET',
success: function (response) {
// Check if the response includes an error code
if (response.error_code === 0) {
// If no error, show success message with latency
alert(
'Health Check for ' +
label +
': ' +
response.msg + // Use 'msg' as per Go struct
' (Latency: ' +
response.latency + // Ensure this matches the Go struct field name
'ms)'
);
} else {
// If error code is not 0, show error message
alert('Error for ' + label + ': ' + response.msg);
}
</script>
</body>
},
error: function (xhr) {
// Parse the response JSON in case of HTTP error
var response = JSON.parse(xhr.responseText);
alert('Error: ' + response.msg); // Use 'msg' as per Go struct
},
});
}
</script>
</body>
</html>