mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Mon Aug 26 20:34:39 CEST 2024
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
284
echo/internal/web/templates/metrics.html
Normal file
284
echo/internal/web/templates/metrics.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user