mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Sun Sep 1 20:31:00 CEST 2024
This commit is contained in:
1
echo/.gitignore
vendored
1
echo/.gitignore
vendored
@@ -28,3 +28,4 @@ cmd/test/
|
||||
localdev/
|
||||
|
||||
.vscode/settings.json
|
||||
.zed/
|
||||
|
||||
12
echo/go.mod
12
echo/go.mod
@@ -12,6 +12,7 @@ require (
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.20.0
|
||||
github.com/prometheus/client_model v0.6.1
|
||||
github.com/prometheus/common v0.55.0
|
||||
@@ -27,6 +28,7 @@ require (
|
||||
golang.org/x/time v0.6.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -43,6 +45,7 @@ require (
|
||||
github.com/dennwc/btrfs v0.0.0-20240418142341-0167142bde7a // indirect
|
||||
github.com/dennwc/ioctl v1.0.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ema/qdisc v1.0.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
@@ -62,6 +65,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-envparse v0.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hodgesds/perf-utils v0.7.0 // indirect
|
||||
github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
@@ -81,6 +85,7 @@ require (
|
||||
github.com/mdlayher/wifi v0.2.0 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
@@ -92,6 +97,7 @@ require (
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/quic-go v0.45.1 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
@@ -127,4 +133,10 @@ require (
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
34
echo/go.sum
34
echo/go.sum
@@ -47,6 +47,8 @@ github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fp
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ema/qdisc v1.0.0 h1:EHLG08FVRbWLg8uRICa3xzC9Zm0m7HyMHfXobWFnXYg=
|
||||
github.com/ema/qdisc v1.0.0/go.mod h1:FhIc0fLYi7f+lK5maMsesDqwYojIOh3VfRs8EVd5YJQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
@@ -128,6 +130,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hodgesds/perf-utils v0.7.0 h1:7KlHGMuig4FRH5fNw68PV6xLmgTe7jKs9hgAcEAbioU=
|
||||
github.com/hodgesds/perf-utils v0.7.0/go.mod h1:LAklqfDadNKpkxoAJNHpD5tkY0rkZEVdnCEWN5k4QJY=
|
||||
github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973 h1:hk4LPqXIY/c9XzRbe7dA6qQxaT6Axcbny0L/G5a4owQ=
|
||||
@@ -192,6 +196,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
@@ -238,6 +244,8 @@ github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjC
|
||||
github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
@@ -458,5 +466,31 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
|
||||
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
@@ -31,7 +31,7 @@ var RootFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "lt,listen_type",
|
||||
Value: "raw",
|
||||
Usage: "监听类型,可选项有 raw,ws,wss,mwss",
|
||||
Usage: "监听类型,可选项有 raw,ws,wss",
|
||||
EnvVars: []string{"EHCO_LISTEN_TYPE"},
|
||||
Destination: (*string)(&ListenType),
|
||||
Required: false,
|
||||
@@ -45,7 +45,7 @@ var RootFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "tt,transport_type",
|
||||
Value: "raw",
|
||||
Usage: "传输类型,可选选有 raw,ws,wss,mwss",
|
||||
Usage: "传输类型,可选选有 raw,ws,wss",
|
||||
EnvVars: []string{"EHCO_TRANSPORT_TYPE"},
|
||||
Destination: (*string)(&TransportType),
|
||||
},
|
||||
|
||||
@@ -2,10 +2,13 @@ package cmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/cmgr/ms"
|
||||
"github.com/Ehco1996/ehco/internal/conn"
|
||||
"github.com/Ehco1996/ehco/pkg/metric_reader"
|
||||
"go.uber.org/zap"
|
||||
@@ -35,7 +38,8 @@ 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)
|
||||
// Metrics related
|
||||
QueryNodeMetrics(ctx context.Context, req *ms.QueryNodeMetricsReq) (*ms.QueryNodeMetricsResp, error)
|
||||
}
|
||||
|
||||
type cmgrImpl struct {
|
||||
@@ -47,11 +51,11 @@ type cmgrImpl struct {
|
||||
activeConnectionsMap map[string][]conn.RelayConn
|
||||
closedConnectionsMap map[string][]conn.RelayConn
|
||||
|
||||
ms *ms.MetricsStore
|
||||
mr metric_reader.Reader
|
||||
ms *MetricsStore
|
||||
}
|
||||
|
||||
func NewCmgr(cfg *Config) Cmgr {
|
||||
func NewCmgr(cfg *Config) (Cmgr, error) {
|
||||
cmgr := &cmgrImpl{
|
||||
cfg: cfg,
|
||||
l: zap.S().Named("cmgr"),
|
||||
@@ -60,12 +64,16 @@ func NewCmgr(cfg *Config) Cmgr {
|
||||
}
|
||||
if cfg.NeedMetrics() {
|
||||
cmgr.mr = metric_reader.NewReader(cfg.MetricsURL)
|
||||
// 当前只能存储 24h 的 metrics,之后再优化
|
||||
bufSize := 60 * 60 * 24 / cfg.SyncInterval
|
||||
cmgr.l.Infof("metrics buffer size: %d", bufSize)
|
||||
cmgr.ms = NewMetricsStore(bufSize, time.Hour*24)
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
dbPath := filepath.Join(homeDir, ".ehco", "metrics.db")
|
||||
ms, err := ms.NewMetricsStore(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmgr.ms = ms
|
||||
}
|
||||
return cmgr
|
||||
return cmgr, nil
|
||||
}
|
||||
|
||||
func (cm *cmgrImpl) ListConnections(connType string, page, pageSize int) []conn.RelayConn {
|
||||
@@ -192,3 +200,21 @@ func (cm *cmgrImpl) Start(ctx context.Context, errCH chan error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *cmgrImpl) QueryNodeMetrics(ctx context.Context, req *ms.QueryNodeMetricsReq) (*ms.QueryNodeMetricsResp, error) {
|
||||
num := -1 // default to return all metrics
|
||||
if req.Latest {
|
||||
m, err := cm.mr.ReadOnce(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cm.ms.AddNodeMetric(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
num = 1
|
||||
}
|
||||
|
||||
startTime := time.Unix(req.StartTimestamp, 0)
|
||||
endTime := time.Unix(req.EndTimestamp, 0)
|
||||
return cm.ms.QueryNodeMetric(startTime, endTime, num)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cmgr
|
||||
|
||||
var DummyConfig = &Config{}
|
||||
|
||||
type Config struct {
|
||||
SyncURL string
|
||||
MetricsURL string
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package cmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/conn"
|
||||
"github.com/Ehco1996/ehco/internal/constant"
|
||||
myhttp "github.com/Ehco1996/ehco/pkg/http"
|
||||
"github.com/Ehco1996/ehco/pkg/metric_reader"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type StatsPerRule struct {
|
||||
RelayLabel string `json:"relay_label"`
|
||||
|
||||
Up int64 `json:"up_bytes"`
|
||||
Down int64 `json:"down_bytes"`
|
||||
ConnectionCnt int `json:"connection_count"`
|
||||
HandShakeLatency int64 `json:"latency_in_ms"`
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
ShortCommit string `json:"short_commit"`
|
||||
}
|
||||
|
||||
type syncReq struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
Node metric_reader.NodeMetrics `json:"node"`
|
||||
Stats []StatsPerRule `json:"stats"`
|
||||
}
|
||||
|
||||
type MetricsStore struct {
|
||||
mutex sync.RWMutex
|
||||
|
||||
metrics []metric_reader.NodeMetrics
|
||||
|
||||
bufSize int
|
||||
clearDuration time.Duration
|
||||
}
|
||||
|
||||
func NewMetricsStore(bufSize int, clearDuration time.Duration) *MetricsStore {
|
||||
return &MetricsStore{
|
||||
metrics: make([]metric_reader.NodeMetrics, bufSize),
|
||||
clearDuration: clearDuration,
|
||||
bufSize: bufSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MetricsStore) Add(m *metric_reader.NodeMetrics) {
|
||||
ms.mutex.Lock()
|
||||
defer ms.mutex.Unlock()
|
||||
|
||||
// 直接添加新的 metric,假设它是最新的
|
||||
ms.metrics = append(ms.metrics, *m)
|
||||
|
||||
// 清理旧数据
|
||||
cutoffTime := time.Now().Add(-ms.clearDuration)
|
||||
for i, metric := range ms.metrics {
|
||||
if metric.SyncTime.After(cutoffTime) {
|
||||
ms.metrics = ms.metrics[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MetricsStore) Query(startTime, endTime time.Time) []metric_reader.NodeMetrics {
|
||||
ms.mutex.RLock()
|
||||
defer ms.mutex.RUnlock()
|
||||
|
||||
var result []metric_reader.NodeMetrics
|
||||
for i := len(ms.metrics) - 1; i >= 0; i-- {
|
||||
if ms.metrics[i].SyncTime.Before(startTime) {
|
||||
break
|
||||
}
|
||||
if !ms.metrics[i].SyncTime.After(endTime) {
|
||||
result = append(result, ms.metrics[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 反转结果,使其按时间升序排列
|
||||
for i := 0; i < len(result)/2; i++ {
|
||||
j := len(result) - 1 - i
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type QueryNodeMetricsReq struct {
|
||||
TimeRange string `json:"time_range"` // 15min/30min/1h/6h/12h/24h
|
||||
Latest bool `json:"latest"` // whether to refresh the cache and get the latest data
|
||||
}
|
||||
|
||||
func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
|
||||
cm.l.Infof("sync once total closed connections: %d", cm.countClosedConnection())
|
||||
// todo: opt lock
|
||||
cm.lock.Lock()
|
||||
|
||||
shortCommit := constant.GitRevision
|
||||
if len(constant.GitRevision) > 7 {
|
||||
shortCommit = constant.GitRevision[:7]
|
||||
}
|
||||
req := syncReq{
|
||||
Stats: []StatsPerRule{},
|
||||
Version: VersionInfo{Version: constant.Version, ShortCommit: shortCommit},
|
||||
}
|
||||
|
||||
if cm.cfg.NeedMetrics() {
|
||||
metrics, err := cm.mr.ReadOnce(ctx)
|
||||
if err != nil {
|
||||
cm.l.Errorf("read metrics failed: %v", err)
|
||||
} else {
|
||||
req.Node = *metrics
|
||||
cm.ms.Add(metrics)
|
||||
}
|
||||
}
|
||||
|
||||
for label, conns := range cm.closedConnectionsMap {
|
||||
s := StatsPerRule{
|
||||
RelayLabel: label,
|
||||
}
|
||||
var totalLatency int64
|
||||
for _, c := range conns {
|
||||
s.ConnectionCnt++
|
||||
s.Up += c.GetStats().Up
|
||||
s.Down += c.GetStats().Down
|
||||
totalLatency += c.GetStats().HandShakeLatency.Milliseconds()
|
||||
}
|
||||
if s.ConnectionCnt > 0 {
|
||||
s.HandShakeLatency = totalLatency / int64(s.ConnectionCnt)
|
||||
}
|
||||
req.Stats = append(req.Stats, s)
|
||||
}
|
||||
cm.closedConnectionsMap = make(map[string][]conn.RelayConn)
|
||||
cm.lock.Unlock()
|
||||
|
||||
if cm.cfg.NeedSync() {
|
||||
cm.l.Debug("syncing data to server", zap.Any("data", req))
|
||||
return myhttp.PostJSONWithRetry(cm.cfg.SyncURL, &req)
|
||||
} else {
|
||||
cm.l.Debugf("remove %d closed connections", len(req.Stats))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTimeRangeDuration(timeRange string) time.Duration {
|
||||
switch timeRange {
|
||||
case "15min":
|
||||
return 15 * time.Minute
|
||||
case "30min":
|
||||
return 30 * time.Minute
|
||||
case "1h":
|
||||
return 1 * time.Hour
|
||||
case "6h":
|
||||
return 6 * time.Hour
|
||||
case "12h":
|
||||
return 12 * time.Hour
|
||||
case "24h":
|
||||
return 24 * time.Hour
|
||||
default:
|
||||
return 15 * time.Minute
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *cmgrImpl) QueryNodeMetrics(ctx context.Context, req *QueryNodeMetricsReq) ([]metric_reader.NodeMetrics, error) {
|
||||
if req.Latest {
|
||||
m, err := cm.mr.ReadOnce(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cm.ms.Add(m)
|
||||
return []metric_reader.NodeMetrics{*m}, nil
|
||||
}
|
||||
|
||||
startTime := time.Now().Add(-getTimeRangeDuration(req.TimeRange))
|
||||
return cm.ms.Query(startTime, time.Now()), nil
|
||||
}
|
||||
118
echo/internal/cmgr/ms/ms.go
Normal file
118
echo/internal/cmgr/ms/ms.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package ms
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/Ehco1996/ehco/pkg/metric_reader"
|
||||
)
|
||||
|
||||
type NodeMetrics struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
DiskUsage float64 `json:"disk_usage"`
|
||||
NetworkIn float64 `json:"network_in"`
|
||||
NetworkOut float64 `json:"network_out"`
|
||||
}
|
||||
|
||||
type QueryNodeMetricsReq struct {
|
||||
StartTimestamp int64 `json:"start_ts"`
|
||||
EndTimestamp int64 `json:"end_ts"`
|
||||
|
||||
Latest bool `json:"latest"` // whether to refresh the cache and get the latest data
|
||||
}
|
||||
type QueryNodeMetricsResp struct {
|
||||
TOTAL int `json:"total"`
|
||||
Data []NodeMetrics `json:"data"`
|
||||
}
|
||||
|
||||
type MetricsStore struct {
|
||||
db *sql.DB
|
||||
dbPath string
|
||||
|
||||
l *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewMetricsStore(dbPath string) (*MetricsStore, error) {
|
||||
// ensure the directory exists
|
||||
dirPath := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// create db file if not exists
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
f, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ms := &MetricsStore{dbPath: dbPath, db: db, l: zap.S().Named("ms")}
|
||||
if err := ms.initDB(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ms, nil
|
||||
}
|
||||
|
||||
func (ms *MetricsStore) initDB() error {
|
||||
// init NodeMetrics table
|
||||
_, err := ms.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS node_metrics (
|
||||
timestamp INTEGER,
|
||||
cpu_usage REAL,
|
||||
memory_usage REAL,
|
||||
disk_usage REAL,
|
||||
network_in REAL,
|
||||
network_out REAL,
|
||||
PRIMARY KEY (timestamp)
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ms *MetricsStore) AddNodeMetric(m *metric_reader.NodeMetrics) error {
|
||||
_, err := ms.db.Exec(`
|
||||
INSERT OR REPLACE INTO node_metrics (timestamp, cpu_usage, memory_usage, disk_usage, network_in, network_out)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, m.SyncTime.Unix(), m.CpuUsagePercent, m.MemoryUsagePercent, m.DiskUsagePercent, m.NetworkReceiveBytesRate, m.NetworkTransmitBytesRate)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ms *MetricsStore) QueryNodeMetric(startTime, endTime time.Time, num int) (*QueryNodeMetricsResp, error) {
|
||||
rows, err := ms.db.Query(`
|
||||
SELECT timestamp, cpu_usage, memory_usage, disk_usage, network_in, network_out
|
||||
FROM node_metrics
|
||||
WHERE timestamp >= ? AND timestamp <= ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`, startTime.Unix(), endTime.Unix(), num)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var resp QueryNodeMetricsResp
|
||||
for rows.Next() {
|
||||
var m NodeMetrics
|
||||
if err := rows.Scan(&m.Timestamp, &m.CPUUsage, &m.MemoryUsage, &m.DiskUsage, &m.NetworkIn, &m.NetworkOut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Data = append(resp.Data, m)
|
||||
}
|
||||
resp.TOTAL = len(resp.Data)
|
||||
return &resp, nil
|
||||
}
|
||||
85
echo/internal/cmgr/syncer.go
Normal file
85
echo/internal/cmgr/syncer.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package cmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/conn"
|
||||
"github.com/Ehco1996/ehco/internal/constant"
|
||||
myhttp "github.com/Ehco1996/ehco/pkg/http"
|
||||
"github.com/Ehco1996/ehco/pkg/metric_reader"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type StatsPerRule struct {
|
||||
RelayLabel string `json:"relay_label"`
|
||||
|
||||
Up int64 `json:"up_bytes"`
|
||||
Down int64 `json:"down_bytes"`
|
||||
ConnectionCnt int `json:"connection_count"`
|
||||
HandShakeLatency int64 `json:"latency_in_ms"`
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
ShortCommit string `json:"short_commit"`
|
||||
}
|
||||
|
||||
type syncReq struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
Node metric_reader.NodeMetrics `json:"node"`
|
||||
Stats []StatsPerRule `json:"stats"`
|
||||
}
|
||||
|
||||
func (cm *cmgrImpl) syncOnce(ctx context.Context) error {
|
||||
cm.l.Infof("sync once total closed connections: %d", cm.countClosedConnection())
|
||||
// todo: opt lock
|
||||
cm.lock.Lock()
|
||||
|
||||
shortCommit := constant.GitRevision
|
||||
if len(constant.GitRevision) > 7 {
|
||||
shortCommit = constant.GitRevision[:7]
|
||||
}
|
||||
req := syncReq{
|
||||
Stats: []StatsPerRule{},
|
||||
Version: VersionInfo{Version: constant.Version, ShortCommit: shortCommit},
|
||||
}
|
||||
|
||||
if cm.cfg.NeedMetrics() {
|
||||
metrics, err := cm.mr.ReadOnce(ctx)
|
||||
if err != nil {
|
||||
cm.l.Errorf("read metrics failed: %v", err)
|
||||
} else {
|
||||
req.Node = *metrics
|
||||
if err := cm.ms.AddNodeMetric(metrics); err != nil {
|
||||
cm.l.Errorf("add metrics to store failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for label, conns := range cm.closedConnectionsMap {
|
||||
s := StatsPerRule{
|
||||
RelayLabel: label,
|
||||
}
|
||||
var totalLatency int64
|
||||
for _, c := range conns {
|
||||
s.ConnectionCnt++
|
||||
s.Up += c.GetStats().Up
|
||||
s.Down += c.GetStats().Down
|
||||
totalLatency += c.GetStats().HandShakeLatency.Milliseconds()
|
||||
}
|
||||
if s.ConnectionCnt > 0 {
|
||||
s.HandShakeLatency = totalLatency / int64(s.ConnectionCnt)
|
||||
}
|
||||
req.Stats = append(req.Stats, s)
|
||||
}
|
||||
cm.closedConnectionsMap = make(map[string][]conn.RelayConn)
|
||||
cm.lock.Unlock()
|
||||
|
||||
if cm.cfg.NeedSync() {
|
||||
cm.l.Debug("syncing data to server", zap.Any("data", req))
|
||||
return myhttp.PostJSONWithRetry(cm.cfg.SyncURL, &req)
|
||||
} else {
|
||||
cm.l.Debugf("remove %d closed connections", len(req.Stats))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
PATH string
|
||||
PATH string `json:"-"`
|
||||
|
||||
NodeLabel string `json:"node_label,omitempty"`
|
||||
WebHost string `json:"web_host,omitempty"`
|
||||
WebPort int `json:"web_port,omitempty"`
|
||||
WebToken string `json:"web_token,omitempty"`
|
||||
@@ -105,7 +106,7 @@ func (c *Config) Adjust() error {
|
||||
}
|
||||
// init tls when need
|
||||
for _, r := range c.RelayConfigs {
|
||||
if r.ListenType == constant.RelayTypeWSS {
|
||||
if r.ListenType == constant.RelayTypeWSS || r.TransportType == constant.RelayTypeWSS {
|
||||
if err := tls.InitTlsCfg(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -190,10 +190,10 @@ func (s *Stats) Record(up, down int64) {
|
||||
}
|
||||
|
||||
func (s *Stats) String() string {
|
||||
return fmt.Sprintf("up: %s, down: %s, handshake latency: %s",
|
||||
return fmt.Sprintf("up: %s, down: %s, latency: %s",
|
||||
bytes.PrettyByteSize(float64(s.Up)),
|
||||
bytes.PrettyByteSize(float64(s.Down)),
|
||||
s.HandShakeLatency.String(),
|
||||
fmt.Sprintf("%d ms", s.HandShakeLatency.Milliseconds()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,10 @@ type Options struct {
|
||||
SniffTimeoutSec int `json:"sniff_timeout_sec,omitempty"`
|
||||
|
||||
// timeout in duration
|
||||
DialTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
SniffTimeout time.Duration
|
||||
DialTimeout time.Duration `json:"-"`
|
||||
IdleTimeout time.Duration `json:"-"`
|
||||
ReadTimeout time.Duration `json:"-"`
|
||||
SniffTimeout time.Duration `json:"-"`
|
||||
}
|
||||
|
||||
func (o *Options) Clone() *Options {
|
||||
@@ -78,9 +78,6 @@ type Config struct {
|
||||
Remotes []string `json:"remotes"`
|
||||
|
||||
Options *Options `json:"options,omitempty"`
|
||||
|
||||
// deprecated
|
||||
TCPRemotes []string `json:"tcp_remotes"`
|
||||
}
|
||||
|
||||
func (r *Config) GetWSHandShakePath() string {
|
||||
@@ -106,10 +103,6 @@ func (r *Config) Adjust() error {
|
||||
r.Label = r.DefaultLabel()
|
||||
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, please use remotes instead")
|
||||
r.Remotes = r.TCPRemotes
|
||||
}
|
||||
|
||||
if r.Options == nil {
|
||||
r.Options = newDefaultOptions()
|
||||
@@ -126,21 +119,17 @@ func (r *Config) Validate() error {
|
||||
if err := r.Adjust(); err != nil {
|
||||
return errors.New("adjust config failed")
|
||||
}
|
||||
|
||||
if err := r.validateType(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Listen == "" {
|
||||
return fmt.Errorf("invalid listen: %s", r.Listen)
|
||||
}
|
||||
|
||||
for _, addr := range r.Remotes {
|
||||
if addr == "" {
|
||||
return fmt.Errorf("invalid remote addr: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, protocol := range r.Options.BlockedProtocols {
|
||||
if protocol != ProtocolHTTP && protocol != ProtocolTLS {
|
||||
return fmt.Errorf("invalid blocked protocol: %s", protocol)
|
||||
@@ -227,10 +216,17 @@ func getDuration(seconds int, defaultDuration time.Duration) time.Duration {
|
||||
func newDefaultOptions() *Options {
|
||||
return &Options{
|
||||
EnableMultipathTCP: true,
|
||||
WSConfig: &WSConfig{},
|
||||
DialTimeout: constant.DefaultDialTimeOut,
|
||||
IdleTimeout: constant.DefaultIdleTimeOut,
|
||||
ReadTimeout: constant.DefaultReadTimeOut,
|
||||
SniffTimeout: constant.DefaultSniffTimeOut,
|
||||
|
||||
DialTimeout: constant.DefaultDialTimeOut,
|
||||
DialTimeoutSec: int(constant.DefaultDialTimeOut.Seconds()),
|
||||
|
||||
IdleTimeout: constant.DefaultIdleTimeOut,
|
||||
IdleTimeoutSec: int(constant.DefaultIdleTimeOut.Seconds()),
|
||||
|
||||
ReadTimeout: constant.DefaultReadTimeOut,
|
||||
ReadTimeoutSec: int(constant.DefaultReadTimeOut.Seconds()),
|
||||
|
||||
SniffTimeout: constant.DefaultSniffTimeOut,
|
||||
SniffTimeoutSec: int(constant.DefaultSniffTimeOut.Seconds()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,17 @@ func NewServer(cfg *config.Config) (*Server, error) {
|
||||
MetricsURL: cfg.GetMetricURL(),
|
||||
}
|
||||
cmgrCfg.Adjust()
|
||||
cmgr, err := cmgr.NewCmgr(cmgrCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
l: l,
|
||||
relayM: &sync.Map{},
|
||||
errCH: make(chan error, 1),
|
||||
reloadCH: make(chan struct{}, 1),
|
||||
Cmgr: cmgr.NewCmgr(cmgrCfg),
|
||||
Cmgr: cmgr,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ func (b *BaseRelayServer) RelayUDPConn(ctx context.Context, c net.Conn, remote *
|
||||
}
|
||||
|
||||
func (b *BaseRelayServer) checkConnectionLimit() error {
|
||||
if b.cmgr == nil {
|
||||
return nil
|
||||
}
|
||||
if b.cfg.Options.MaxConnection > 0 && b.cmgr.CountConnection(cmgr.ConnectionTypeActive) >= b.cfg.Options.MaxConnection {
|
||||
return fmt.Errorf("relay:%s active connection count exceed limit %d", b.cfg.Label, b.cfg.Options.MaxConnection)
|
||||
}
|
||||
@@ -144,8 +147,11 @@ func (b *BaseRelayServer) handleRelayConn(c, rc net.Conn, remote *lb.Node, connT
|
||||
conn.WithHandshakeDuration(remote.HandShakeDuration),
|
||||
}
|
||||
relayConn := conn.NewRelayConn(c, rc, opts...)
|
||||
b.cmgr.AddConnection(relayConn)
|
||||
defer b.cmgr.RemoveConnection(relayConn)
|
||||
if b.cmgr != nil {
|
||||
b.cmgr.AddConnection(relayConn)
|
||||
defer b.cmgr.RemoveConnection(relayConn)
|
||||
}
|
||||
|
||||
return relayConn.Transport()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,21 +24,19 @@ var (
|
||||
)
|
||||
|
||||
type WsClient struct {
|
||||
dialer *ws.Dialer
|
||||
cfg *conf.Config
|
||||
netDialer *net.Dialer
|
||||
l *zap.SugaredLogger
|
||||
dialer *ws.Dialer
|
||||
cfg *conf.Config
|
||||
l *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func newWsClient(cfg *conf.Config) (*WsClient, error) {
|
||||
s := &WsClient{
|
||||
cfg: cfg,
|
||||
netDialer: NewNetDialer(cfg),
|
||||
l: zap.S().Named(string(cfg.TransportType)),
|
||||
dialer: &ws.DefaultDialer, // todo config buffer size
|
||||
}
|
||||
s.dialer.NetDial = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return s.netDialer.Dial(network, addr)
|
||||
cfg: cfg,
|
||||
l: zap.S().Named(string(cfg.TransportType)),
|
||||
// todo config buffer size
|
||||
dialer: &ws.Dialer{
|
||||
Timeout: cfg.Options.DialTimeout,
|
||||
},
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -64,7 +62,6 @@ func (s *WsClient) HandShake(ctx context.Context, remote *lb.Node, isTCP bool) (
|
||||
if !isTCP {
|
||||
addr = s.addUDPQueryParam(addr)
|
||||
}
|
||||
|
||||
wsc, _, _, err := s.dialer.Dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,6 +24,7 @@ func newWssClient(cfg *conf.Config) (*WssClient, error) {
|
||||
}
|
||||
// insert tls config
|
||||
wc.dialer.TLSConfig = mytls.DefaultTLSConfig
|
||||
wc.dialer.TLSConfig.InsecureSkipVerify = true
|
||||
return &WssClient{WsClient: wc}, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +45,8 @@ func (s *WssServer) ListenAndServe(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsCfg := mytls.DefaultTLSConfig
|
||||
tlsCfg.InsecureSkipVerify = true
|
||||
tlsListener := tls.NewListener(listener, mytls.DefaultTLSConfig)
|
||||
return s.httpServer.Serve(tlsListener)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/cmgr"
|
||||
"github.com/Ehco1996/ehco/internal/cmgr/ms"
|
||||
"github.com/Ehco1996/ehco/internal/config"
|
||||
"github.com/Ehco1996/ehco/internal/constant"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
@@ -28,12 +30,14 @@ func (s *Server) index(c echo.Context) error {
|
||||
GitRevision string
|
||||
BuildTime string
|
||||
StartTime string
|
||||
Cfg config.Config
|
||||
}{
|
||||
Version: constant.Version,
|
||||
GitBranch: constant.GitBranch,
|
||||
GitRevision: constant.GitRevision,
|
||||
BuildTime: constant.BuildTime,
|
||||
StartTime: constant.StartTime.Format("2006-01-02 15:04:05"),
|
||||
Cfg: *s.cfg,
|
||||
}
|
||||
return c.Render(http.StatusOK, "index.html", data)
|
||||
}
|
||||
@@ -123,7 +127,23 @@ func (s *Server) ListRules(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (s *Server) GetNodeMetrics(c echo.Context) error {
|
||||
req := &cmgr.QueryNodeMetricsReq{TimeRange: c.QueryParam("time_range")}
|
||||
startTS := time.Now().Unix() - 60
|
||||
if c.QueryParam("start_ts") != "" {
|
||||
star, err := strconv.ParseInt(c.QueryParam("start_ts"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
startTS = star
|
||||
}
|
||||
endTS := time.Now().Unix()
|
||||
if c.QueryParam("end_ts") != "" {
|
||||
end, err := strconv.ParseInt(c.QueryParam("end_ts"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
endTS = end
|
||||
}
|
||||
req := &ms.QueryNodeMetricsReq{StartTimestamp: startTS, EndTimestamp: endTS}
|
||||
latest := c.QueryParam("latest")
|
||||
if latest != "" {
|
||||
r, err := strconv.ParseBool(latest)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -24,6 +25,14 @@ import (
|
||||
//go:embed templates/*.html
|
||||
var templatesFS embed.FS
|
||||
|
||||
const (
|
||||
metricsPath = "/metrics/"
|
||||
indexPath = "/"
|
||||
connectionsPath = "/connections/"
|
||||
rulesPath = "/rules/"
|
||||
apiPrefix = "/api/v1"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
glue.Reloader
|
||||
glue.HealthChecker
|
||||
@@ -50,41 +59,25 @@ func NewServer(
|
||||
healthChecker glue.HealthChecker,
|
||||
connMgr cmgr.Cmgr,
|
||||
) (*Server, error) {
|
||||
if err := validateConfig(cfg); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid configuration")
|
||||
}
|
||||
|
||||
l := zap.S().Named("web")
|
||||
|
||||
templates := template.Must(template.ParseFS(templatesFS, "templates/*.html"))
|
||||
for _, temp := range templates.Templates() {
|
||||
l.Debug("template name: ", temp.Name())
|
||||
}
|
||||
e := NewEchoServer()
|
||||
e.Use(NginxLogMiddleware(l))
|
||||
e.Renderer = &echoTemplate{templates: templates}
|
||||
if cfg.WebToken != "" {
|
||||
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
|
||||
KeyLookup: "query:token",
|
||||
Validator: func(key string, c echo.Context) (bool, error) {
|
||||
return key == cfg.WebToken, nil
|
||||
},
|
||||
}))
|
||||
if err := setupMiddleware(e, cfg, l); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to setup middleware")
|
||||
}
|
||||
|
||||
if cfg.WebAuthUser != "" && cfg.WebAuthPass != "" {
|
||||
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
// Be careful to use constant time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(username), []byte(cfg.WebAuthUser)) == 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(password), []byte(cfg.WebAuthPass)) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
if err := setupTemplates(e, l); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to setup templates")
|
||||
}
|
||||
|
||||
if err := metrics.RegisterEhcoMetrics(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metrics.RegisterNodeExporterMetrics(cfg); err != nil {
|
||||
return nil, err
|
||||
if err := setupMetrics(cfg); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to setup metrics")
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
Reloader: relayReloader,
|
||||
HealthChecker: healthChecker,
|
||||
@@ -96,21 +89,87 @@ func NewServer(
|
||||
addr: net.JoinHostPort(cfg.WebHost, fmt.Sprintf("%d", cfg.WebPort)),
|
||||
}
|
||||
|
||||
// register handler
|
||||
e.GET("/metrics/", echo.WrapHandler(promhttp.Handler()))
|
||||
setupRoutes(s)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func validateConfig(cfg *config.Config) error {
|
||||
// Add validation logic here
|
||||
if cfg.WebPort <= 0 || cfg.WebPort > 65535 {
|
||||
return errors.New("invalid web port")
|
||||
}
|
||||
// Add more validations as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupMiddleware(e *echo.Echo, cfg *config.Config, l *zap.SugaredLogger) error {
|
||||
e.Use(NginxLogMiddleware(l))
|
||||
|
||||
if cfg.WebToken != "" {
|
||||
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
|
||||
KeyLookup: "query:token",
|
||||
Validator: func(key string, c echo.Context) (bool, error) {
|
||||
return key == cfg.WebToken, nil
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.WebAuthUser != "" && cfg.WebAuthPass != "" {
|
||||
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
if subtle.ConstantTimeCompare([]byte(username), []byte(cfg.WebAuthUser)) == 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(password), []byte(cfg.WebAuthPass)) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupTemplates(e *echo.Echo, l *zap.SugaredLogger) error {
|
||||
funcMap := template.FuncMap{
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"add": func(a, b int) int { return a + b },
|
||||
}
|
||||
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/*.html")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse templates")
|
||||
}
|
||||
templates := template.Must(tmpl, nil)
|
||||
for _, temp := range templates.Templates() {
|
||||
l.Debug("template name: ", temp.Name())
|
||||
}
|
||||
e.Renderer = &echoTemplate{templates: templates}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupMetrics(cfg *config.Config) error {
|
||||
if err := metrics.RegisterEhcoMetrics(cfg); err != nil {
|
||||
return errors.Wrap(err, "failed to register Ehco metrics")
|
||||
}
|
||||
if err := metrics.RegisterNodeExporterMetrics(cfg); err != nil {
|
||||
return errors.Wrap(err, "failed to register Node Exporter metrics")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupRoutes(s *Server) {
|
||||
e := s.e
|
||||
|
||||
e.GET(metricsPath, echo.WrapHandler(promhttp.Handler()))
|
||||
e.GET("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux))
|
||||
|
||||
e.GET("/", s.index)
|
||||
e.GET("/connections/", s.ListConnections)
|
||||
e.GET("/rules/", s.ListRules)
|
||||
e.GET(indexPath, s.index)
|
||||
e.GET(connectionsPath, s.ListConnections)
|
||||
e.GET(rulesPath, s.ListRules)
|
||||
|
||||
// api group
|
||||
api := e.Group("/api/v1")
|
||||
api := e.Group(apiPrefix)
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
|
||||
13
echo/internal/web/templates/_head.html
Normal file
13
echo/internal/web/templates/_head.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<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" />
|
||||
<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/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>
|
||||
</head>
|
||||
374
echo/internal/web/templates/_metrics.html
Normal file
374
echo/internal/web/templates/_metrics.html
Normal file
@@ -0,0 +1,374 @@
|
||||
<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>
|
||||
</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>
|
||||
</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;
|
||||
|
||||
// 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>
|
||||
58
echo/internal/web/templates/_navbar.html
Normal file
58
echo/internal/web/templates/_navbar.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<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">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a href="/rules/" class="navbar-item">
|
||||
<span class="icon"><i class="fas fa-list"></i></span>
|
||||
<span>Rule List</span>
|
||||
</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
<span class="icon"><i class="fas fa-link"></i></span>
|
||||
<span>Connections</span>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a href="/connections/?conn_type=active" class="navbar-item">Active</a>
|
||||
<a href="/connections/?conn_type=closed" class="navbar-item">Closed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
<span class="icon"><i class="fas fa-external-link-alt"></i></span>
|
||||
<span>Quick Links</span>
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a href="/metrics/" class="navbar-item">
|
||||
<span class="icon"><i class="fas fa-chart-line"></i></span>
|
||||
<span>Metrics</span>
|
||||
</a>
|
||||
<a href="/debug/pprof/" class="navbar-item">
|
||||
<span class="icon"><i class="fas fa-bug"></i></span>
|
||||
<span>Debug</span>
|
||||
</a>
|
||||
<a href="/api/v1/config/" class="navbar-item">
|
||||
<span class="icon"><i class="fas fa-cog"></i></span>
|
||||
<span>Config</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<hr />
|
||||
@@ -1,33 +1,30 @@
|
||||
<!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">
|
||||
<title>Connections</title>
|
||||
</head>
|
||||
{{template "_head.html" .}}
|
||||
<body>
|
||||
{{ template "_navbar.html" . }}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">ALL Connections: {{.AllCount}}</h1>
|
||||
<h1 class="title">Connections</h1>
|
||||
<h2 class="subtitle">Total: {{.AllCount}}</h2>
|
||||
|
||||
<div class="tabs is-boxed is-centered">
|
||||
<ul>
|
||||
<li class="{{if eq .ConnType "active"}}is-active{{end}}">
|
||||
<a href="?conn_type=active&page=1&page_size={{.PageSize}}">Active({{.ActiveCount}})</a>
|
||||
<a href="?conn_type=active&page=1&page_size={{.PageSize}}">Active ({{.ActiveCount}})</a>
|
||||
</li>
|
||||
<li class="{{if eq .ConnType "closed"}}is-active{{end}}">
|
||||
<a href="?conn_type=closed&page=1&page_size={{.PageSize}}">Closed({{.ClosedCount}})</a>
|
||||
<a href="?conn_type=closed&page=1&page_size={{.PageSize}}">Closed ({{.ClosedCount}})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{if gt (len .ConnectionList) 0}}
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Relay Label</th>
|
||||
<th>Label</th>
|
||||
<th>Type</th>
|
||||
<th>Flow</th>
|
||||
<th>Stats</th>
|
||||
@@ -37,39 +34,70 @@
|
||||
<tbody>
|
||||
{{range .ConnectionList}}
|
||||
<tr>
|
||||
<td>{{.RelayLabel}}</td>
|
||||
<td>{{.ConnType}}</td>
|
||||
<td>{{.GetFlow}}</td>
|
||||
<td>{{.Stats}}</td>
|
||||
<td>{{.GetTime}}</td>
|
||||
<td data-label="Label">{{.RelayLabel}}</td>
|
||||
<td data-label="Type">{{.ConnType}}</td>
|
||||
<td data-label="Flow">{{.GetFlow}}</td>
|
||||
<td data-label="Stats">{{.Stats}}</td>
|
||||
<td data-label="Time">{{.GetTime}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="notification is-info">
|
||||
<p>No connections available.</p>
|
||||
<div class="notification is-info is-light">
|
||||
<p class="has-text-centered">No {{.ConnType}} connections available.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .TotalPage 1}}
|
||||
<nav class="pagination is-centered" role="navigation" aria-label="pagination" style="margin-top: 20px;">
|
||||
<a class="pagination-previous" {{if eq .Prev 0}}disabled{{end}}
|
||||
<a class="pagination-previous" {{if le .CurrentPage 1}}disabled{{end}}
|
||||
href="?conn_type={{.ConnType}}&page={{.Prev}}&page_size={{.PageSize}}">Previous</a>
|
||||
<a class="pagination-next" {{if eq .Next 0}}disabled{{end}}
|
||||
<a class="pagination-next" {{if ge .CurrentPage .TotalPage}}disabled{{end}}
|
||||
href="?conn_type={{.ConnType}}&page={{.Next}}&page_size={{.PageSize}}">Next page</a>
|
||||
<ul class="pagination-list">
|
||||
{{if gt .CurrentPage 1}}
|
||||
{{if gt .CurrentPage 2}}
|
||||
<li><a class="pagination-link" href="?conn_type={{.ConnType}}&page=1&page_size={{.PageSize}}">1</a></li>
|
||||
{{if gt .CurrentPage 3}}<li><span class="pagination-ellipsis">…</span></li>{{end}}
|
||||
{{end}}
|
||||
{{if gt .CurrentPage 1}}
|
||||
<li><a class="pagination-link" href="?conn_type={{.ConnType}}&page={{sub .CurrentPage 1}}&page_size={{.PageSize}}">{{sub .CurrentPage 1}}</a></li>
|
||||
{{end}}
|
||||
<li><span class="pagination-ellipsis">…</span></li>
|
||||
<li><a class="pagination-link is-current" aria-label="Page {{.CurrentPage}}" aria-current="page">{{.CurrentPage}}</a></li>
|
||||
<li><span class="pagination-ellipsis">…</span></li>
|
||||
{{if lt .CurrentPage .TotalPage}}
|
||||
<li><a class="pagination-link" href="?conn_type={{.ConnType}}&page={{.TotalPage}}&page_size={{.PageSize}}">{{.TotalPage}}</a></li>
|
||||
<li><a class="pagination-link" href="?conn_type={{.ConnType}}&page={{add .CurrentPage 1}}&page_size={{.PageSize}}">{{add .CurrentPage 1}}</a></li>
|
||||
{{if lt .CurrentPage (sub .TotalPage 1)}}<li><span class="pagination-ellipsis">…</span></li>{{end}}
|
||||
{{end}}
|
||||
{{if lt .CurrentPage (sub .TotalPage 1)}}
|
||||
<li><a class="pagination-link" href="?conn_type={{.ConnType}}&page={{.TotalPage}}&page_size={{.PageSize}}">{{.TotalPage}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="loading" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="loader is-loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const links = document.querySelectorAll('a:not([target="_blank"])');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
if (!link.classList.contains('disabled')) {
|
||||
loading.classList.add('is-active');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,108 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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>
|
||||
|
||||
{{template "_head.html" .}}
|
||||
<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>
|
||||
<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>
|
||||
{{ template "_navbar.html" . }}
|
||||
|
||||
<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 class="container">
|
||||
<div class="columns is-variable is-8 is-flex-grow">
|
||||
<div class="column is-flex">
|
||||
<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>
|
||||
<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>
|
||||
<!-- metrics -->
|
||||
{{template "metrics.html"}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- metrics -->
|
||||
{{template "_metrics.html" .}}
|
||||
</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>
|
||||
<!-- 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>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
<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>
|
||||
<button id="refreshButton" class="button ml-2">
|
||||
<span class="icon">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</span>
|
||||
<span>Auto Refresh</span>
|
||||
</button>
|
||||
</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}?latest=true`);
|
||||
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`;
|
||||
|
||||
// 更新图表
|
||||
for (const chart of Object.values(charts)) {
|
||||
chart.update();
|
||||
}
|
||||
};
|
||||
|
||||
const addLatestDataToCharts = (charts, latestMetric) => {
|
||||
const timestamp = formatDate(latestMetric.SyncTime);
|
||||
|
||||
// 更新 CPU 图表
|
||||
charts.cpu.data.labels.push(timestamp);
|
||||
charts.cpu.data.datasets[0].data.push(latestMetric.cpu_usage_percent);
|
||||
|
||||
// 更新内存图表
|
||||
charts.memory.data.labels.push(timestamp);
|
||||
charts.memory.data.datasets[0].data.push(latestMetric.memory_usage_percent);
|
||||
|
||||
// 更新磁盘图表
|
||||
charts.disk.data.labels.push(timestamp);
|
||||
charts.disk.data.datasets[0].data.push(latestMetric.disk_usage_percent);
|
||||
|
||||
// 更新网络图表
|
||||
charts.network.data.labels.push(timestamp);
|
||||
charts.network.data.datasets[0].data.push(latestMetric.network_receive_bytes_rate / BYTE_TO_MB);
|
||||
charts.network.data.datasets[1].data.push(latestMetric.network_transmit_bytes_rate / BYTE_TO_MB);
|
||||
|
||||
// 更新 ping 图表
|
||||
charts.ping.data.labels.push(timestamp);
|
||||
charts.ping.data.datasets.forEach((dataset) => {
|
||||
const pingMetric = latestMetric.ping_metrics.find((ping) => ping.target === dataset.label);
|
||||
dataset.data.push(pingMetric ? pingMetric.latency : null);
|
||||
});
|
||||
// 更新额外信息
|
||||
updateAdditionalInfo(charts, latestMetric);
|
||||
};
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
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();
|
||||
autoRefreshInterval = setInterval(refreshData, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshData() {
|
||||
const latestMetric = await fetchLatestMetric();
|
||||
if (latestMetric) {
|
||||
addLatestDataToCharts(charts, latestMetric);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
@@ -1,18 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
{{template "_head.html" .}}
|
||||
<body>
|
||||
{{ template "_navbar.html" . }}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Rules</h1>
|
||||
<h2 class="subtitle">Rules</h2>
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -60,7 +53,7 @@
|
||||
response.msg + // Use 'msg' as per Go struct
|
||||
' (Latency: ' +
|
||||
response.latency + // Ensure this matches the Go struct field name
|
||||
'ms)'
|
||||
'ms)',
|
||||
);
|
||||
} else {
|
||||
// If error code is not 0, show error message
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/cmgr"
|
||||
"github.com/Ehco1996/ehco/internal/config"
|
||||
|
||||
"github.com/Ehco1996/ehco/internal/constant"
|
||||
@@ -121,7 +120,7 @@ func startRelayServers() []*relay.Relay {
|
||||
var servers []*relay.Relay
|
||||
for _, c := range cfg.RelayConfigs {
|
||||
c.Adjust()
|
||||
r, err := relay.NewRelay(c, cmgr.NewCmgr(cmgr.DummyConfig))
|
||||
r, err := relay.NewRelay(c, nil)
|
||||
if err != nil {
|
||||
zap.S().Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user