mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-09 07:54:03 +02:00
refactor, fix metrics and upgrade go to 1.24.0
This commit is contained in:
parent
c807b30c8f
commit
82042e0b99
19 changed files with 157 additions and 104 deletions
|
@ -1,5 +1,5 @@
|
||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM golang:1.23.6-alpine AS builder
|
FROM golang:1.24.0-alpine AS builder
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
# package version does not matter
|
# package version does not matter
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -69,5 +68,5 @@ Tips:
|
||||||
|
|
||||||
server.StartAgentServer(t, opts)
|
server.StartAgentServer(t, opts)
|
||||||
|
|
||||||
utils.WaitExit(3)
|
task.WaitExit(3)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package handler
|
package handler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/go-proxy/agent/pkg/handler"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
)
|
)
|
||||||
|
@ -75,7 +76,7 @@ func TestCheckHealthHTTP(t *testing.T) {
|
||||||
query.Set(key, value)
|
query.Set(key, value)
|
||||||
}
|
}
|
||||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
CheckHealth(recorder, request)
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ func TestCheckHealthFileServer(t *testing.T) {
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
CheckHealth(recorder, request)
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
@ -203,7 +204,7 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
request := httptest.NewRequest(http.MethodGet, agent.APIEndpointBase+agent.EndpointHealth+"?"+query.Encode(), nil)
|
||||||
CheckHealth(recorder, request)
|
handler.CheckHealth(recorder, request)
|
||||||
|
|
||||||
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
ExpectEqual(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ func main() {
|
||||||
|
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
utils.WaitExit(cfg.Value().TimeoutShutdown)
|
task.WaitExit(cfg.Value().TimeoutShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDirectory(dir string) {
|
func prepareDirectory(dir string) {
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module github.com/yusing/go-proxy
|
module github.com/yusing/go-proxy
|
||||||
|
|
||||||
go 1.23.6
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.1
|
github.com/PuerkitoBio/goquery v1.10.1
|
||||||
|
|
|
@ -37,13 +37,13 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
||||||
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
||||||
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
||||||
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(memlogger.LogsWS(cfg)))
|
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(memlogger.LogsWS(cfg.Value().MatchDomains)))
|
||||||
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||||
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||||
mux.HandleFunc("GET", "/v1/metrics/system_info", auth.RequireAuth(useCfg(cfg, v1.SystemInfo)))
|
mux.HandleFunc("GET", "/v1/metrics/system_info", auth.RequireAuth(useCfg(cfg, v1.SystemInfo)))
|
||||||
mux.HandleFunc("GET", "/v1/metrics/system_info/ws", auth.RequireAuth(useCfg(cfg, v1.SystemInfo)))
|
mux.HandleFunc("GET", "/v1/metrics/system_info/ws", auth.RequireAuth(useCfg(cfg, v1.SystemInfo)))
|
||||||
mux.HandleFunc("GET", "/v1/metrics/uptime", auth.RequireAuth(uptime.Poller.ServeHTTP))
|
mux.HandleFunc("GET", "/v1/metrics/uptime", auth.RequireAuth(uptime.Poller.ServeHTTP))
|
||||||
mux.HandleFunc("GET", "/v1/metrics/uptime/ws", auth.RequireAuth(useCfg(cfg, uptime.Poller.ServeWS)))
|
mux.HandleFunc("GET", "/v1/metrics/uptime/ws", auth.RequireAuth(useWS(cfg, uptime.Poller.ServeWS)))
|
||||||
|
|
||||||
if common.PrometheusEnabled {
|
if common.PrometheusEnabled {
|
||||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||||
|
@ -74,3 +74,9 @@ func useCfg(cfg config.ConfigInstance, handler func(cfg config.ConfigInstance, w
|
||||||
handler(cfg, w, r)
|
handler(cfg, w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func useWS(cfg config.ConfigInstance, handler func(allowedDomains []string, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handler(cfg.Value().MatchDomains, w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func HealthWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func HealthWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
U.PeriodicWS(cfg.Value().MatchDomains, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatsWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func StatsWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
U.PeriodicWS(cfg.Value().MatchDomains, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
return wsjson.Write(r.Context(), conn, getStats(cfg))
|
return wsjson.Write(r.Context(), conn, getStats(cfg))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
||||||
agentName := r.URL.Query().Get("agent_name")
|
agentName := r.URL.Query().Get("agent_name")
|
||||||
if agentName == "" {
|
if agentName == "" {
|
||||||
if isWS {
|
if isWS {
|
||||||
systeminfo.Poller.ServeWS(cfg, w, r)
|
systeminfo.Poller.ServeWS(cfg.Value().MatchDomains, w, r)
|
||||||
} else {
|
} else {
|
||||||
systeminfo.Poller.ServeHTTP(w, r)
|
systeminfo.Poller.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
U.WriteBody(w, respData)
|
U.WriteBody(w, respData)
|
||||||
} else {
|
} else {
|
||||||
clientConn, err := U.InitiateWS(cfg, w, r)
|
clientConn, err := U.InitiateWS(cfg.Value().MatchDomains, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
|
@ -16,10 +17,11 @@ import (
|
||||||
//
|
//
|
||||||
// The error is only logged but not returned to the client.
|
// The error is only logged but not returned to the client.
|
||||||
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||||
if err == nil {
|
switch {
|
||||||
return
|
case err == nil,
|
||||||
}
|
errors.Is(err, context.Canceled),
|
||||||
if errors.Is(err, context.Canceled) {
|
errors.Is(err, syscall.EPIPE),
|
||||||
|
errors.Is(err, syscall.ECONNRESET):
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
LogError(r).Msg(err.Error())
|
LogError(r).Msg(err.Error())
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,45 +16,49 @@ func warnNoMatchDomains() {
|
||||||
|
|
||||||
var warnNoMatchDomainOnce sync.Once
|
var warnNoMatchDomainOnce sync.Once
|
||||||
|
|
||||||
func InitiateWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
func InitiateWS(allowedDomains []string, w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||||
var originPats []string
|
var originPats []string
|
||||||
|
|
||||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||||
|
|
||||||
if cfg == nil || len(cfg.Value().MatchDomains) == 0 {
|
if len(allowedDomains) == 0 || common.IsDebug {
|
||||||
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
|
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
|
||||||
originPats = []string{"*"}
|
originPats = []string{"*"}
|
||||||
} else {
|
} else {
|
||||||
originPats = make([]string, len(cfg.Value().MatchDomains))
|
originPats = make([]string, len(allowedDomains))
|
||||||
for i, domain := range cfg.Value().MatchDomains {
|
for i, domain := range allowedDomains {
|
||||||
|
if domain[0] != '.' {
|
||||||
|
originPats[i] = "*." + domain
|
||||||
|
} else {
|
||||||
originPats[i] = "*" + domain
|
originPats[i] = "*" + domain
|
||||||
}
|
}
|
||||||
originPats = append(originPats, localAddresses...)
|
|
||||||
}
|
}
|
||||||
if common.IsDebug {
|
originPats = append(originPats, localAddresses...)
|
||||||
originPats = []string{"*"}
|
|
||||||
}
|
}
|
||||||
return websocket.Accept(w, r, &websocket.AcceptOptions{
|
return websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
OriginPatterns: originPats,
|
OriginPatterns: originPats,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeriodicWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
|
func PeriodicWS(allowedDomains []string, w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
|
||||||
conn, err := InitiateWS(cfg, w, r)
|
conn, err := InitiateWS(allowedDomains, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleErr(w, r, err)
|
HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/* trunk-ignore(golangci-lint/errcheck) */
|
//nolint:errcheck
|
||||||
defer conn.CloseNow()
|
defer conn.CloseNow()
|
||||||
|
|
||||||
|
if err := do(conn); err != nil {
|
||||||
|
HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cfg.Context().Done():
|
|
||||||
return
|
|
||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
@ -81,9 +80,9 @@ func init() {
|
||||||
logging.InitLogger(zerolog.MultiLevelWriter(os.Stderr, memLoggerInstance))
|
logging.InitLogger(zerolog.MultiLevelWriter(os.Stderr, memLoggerInstance))
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogsWS(config config.ConfigInstance) http.HandlerFunc {
|
func LogsWS(allowedDomains []string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
memLoggerInstance.ServeHTTP(config, w, r)
|
memLoggerInstance.ServeHTTP(allowedDomains, w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,8 +150,8 @@ func (m *memLogger) Write(p []byte) (n int, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func (m *memLogger) ServeHTTP(allowedDomains []string, w http.ResponseWriter, r *http.Request) {
|
||||||
conn, err := utils.InitiateWS(config, w, r)
|
conn, err := utils.InitiateWS(allowedDomains, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.HandleErr(w, r, err)
|
utils.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -161,7 +160,6 @@ func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWrite
|
||||||
logCh := make(chan *logEntryRange)
|
logCh := make(chan *logEntryRange)
|
||||||
m.connChans.Store(logCh, struct{}{})
|
m.connChans.Store(logCh, struct{}{})
|
||||||
|
|
||||||
/* trunk-ignore(golangci-lint/errcheck) */
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = conn.CloseNow()
|
_ = conn.CloseNow()
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
package period
|
package period
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Entries[T any] struct {
|
type Entries[T any] struct {
|
||||||
entries [maxEntries]*T
|
entries [maxEntries]*T
|
||||||
index int
|
index int
|
||||||
count int
|
count int
|
||||||
interval int64
|
interval time.Duration
|
||||||
lastAdd int64
|
lastAdd time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxEntries = 500
|
const maxEntries = 200
|
||||||
|
|
||||||
func newEntries[T any](interval int64) *Entries[T] {
|
func newEntries[T any](duration time.Duration) *Entries[T] {
|
||||||
return &Entries[T]{
|
return &Entries[T]{
|
||||||
interval: interval,
|
interval: duration / maxEntries,
|
||||||
lastAdd: time.Now().Unix(),
|
lastAdd: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Entries[T]) Add(now int64, info *T) {
|
func (e *Entries[T]) Add(now time.Time, info *T) {
|
||||||
if now-e.lastAdd < e.interval {
|
if now.Sub(e.lastAdd) < e.interval {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.entries[e.index] = info
|
e.entries[e.index] = info
|
||||||
|
|
|
@ -2,11 +2,11 @@ package period
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) lastResultHandler(w http.ResponseWriter, r *http.Request) {
|
func (p *Poller[T, AggregateT]) lastResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -42,8 +42,35 @@ func (p *Poller[T, AggregateT]) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) ServeWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func (p *Poller[T, AggregateT]) ServeWS(allowedDomains []string, w http.ResponseWriter, r *http.Request) {
|
||||||
utils.PeriodicWS(cfg, w, r, p.interval, func(conn *websocket.Conn) error {
|
query := r.URL.Query()
|
||||||
|
period := query.Get("period")
|
||||||
|
intervalStr := query.Get("interval")
|
||||||
|
interval, err := time.ParseDuration(intervalStr)
|
||||||
|
|
||||||
|
minInterval := p.interval()
|
||||||
|
if err != nil || interval < minInterval {
|
||||||
|
interval = minInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if period == "" {
|
||||||
|
utils.PeriodicWS(allowedDomains, w, r, interval, func(conn *websocket.Conn) error {
|
||||||
return wsjson.Write(r.Context(), conn, p.GetLastResult())
|
return wsjson.Write(r.Context(), conn, p.GetLastResult())
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
periodFilter := Filter(period)
|
||||||
|
if !periodFilter.IsValid() {
|
||||||
|
http.Error(w, "invalid period", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.aggregator != nil {
|
||||||
|
utils.PeriodicWS(allowedDomains, w, r, interval, func(conn *websocket.Conn) error {
|
||||||
|
return wsjson.Write(r.Context(), conn, p.aggregator(p.Get(periodFilter)...))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.PeriodicWS(allowedDomains, w, r, interval, func(conn *websocket.Conn) error {
|
||||||
|
return wsjson.Write(r.Context(), conn, p.Get(periodFilter))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,17 +24,17 @@ const (
|
||||||
|
|
||||||
func NewPeriod[T any]() *Period[T] {
|
func NewPeriod[T any]() *Period[T] {
|
||||||
return &Period[T]{
|
return &Period[T]{
|
||||||
FifteenMinutes: newEntries[T](15 * 60 / maxEntries),
|
FifteenMinutes: newEntries[T](15 * time.Minute),
|
||||||
OneHour: newEntries[T](60 * 60 / maxEntries),
|
OneHour: newEntries[T](1 * time.Hour),
|
||||||
OneDay: newEntries[T](24 * 60 * 60 / maxEntries),
|
OneDay: newEntries[T](24 * time.Hour),
|
||||||
OneMonth: newEntries[T](30 * 24 * 60 * 60 / maxEntries),
|
OneMonth: newEntries[T](30 * 24 * time.Hour),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Period[T]) Add(info *T) {
|
func (p *Period[T]) Add(info *T) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
now := time.Now().Unix()
|
now := time.Now()
|
||||||
p.FifteenMinutes.Add(now, info)
|
p.FifteenMinutes.Add(now, info)
|
||||||
p.OneHour.Add(now, info)
|
p.OneHour.Add(now, info)
|
||||||
p.OneDay.Add(now, info)
|
p.OneDay.Add(now, info)
|
||||||
|
|
|
@ -18,7 +18,6 @@ type (
|
||||||
poll PollFunc[T]
|
poll PollFunc[T]
|
||||||
aggregator AggregateFunc[T, AggregateT]
|
aggregator AggregateFunc[T, AggregateT]
|
||||||
period *Period[T]
|
period *Period[T]
|
||||||
interval time.Duration
|
|
||||||
lastResult *T
|
lastResult *T
|
||||||
errs []pollErr
|
errs []pollErr
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,6 @@ func NewPoller[T any](
|
||||||
name: name,
|
name: name,
|
||||||
poll: poll,
|
poll: poll,
|
||||||
period: NewPeriod[T](),
|
period: NewPeriod[T](),
|
||||||
interval: interval,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,10 +52,13 @@ func NewPollerWithAggregator[T, AggregateT any](
|
||||||
poll: poll,
|
poll: poll,
|
||||||
aggregator: aggregator,
|
aggregator: aggregator,
|
||||||
period: NewPeriod[T](),
|
period: NewPeriod[T](),
|
||||||
interval: interval,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Poller[T, AggregateT]) interval() time.Duration {
|
||||||
|
return p.period.FifteenMinutes.interval
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) appendErr(err error) {
|
func (p *Poller[T, AggregateT]) appendErr(err error) {
|
||||||
if len(p.errs) == 0 {
|
if len(p.errs) == 0 {
|
||||||
p.errs = []pollErr{
|
p.errs = []pollErr{
|
||||||
|
@ -87,32 +88,36 @@ func (p *Poller[T, AggregateT]) gatherErrs() (string, bool) {
|
||||||
return strings.Join(errs, "\n"), true
|
return strings.Join(errs, "\n"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) (*T, error) {
|
func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, p.interval)
|
ctx, cancel := context.WithTimeout(ctx, p.interval())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return p.poll(ctx)
|
data, err := p.poll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
p.appendErr(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.period.Add(data)
|
||||||
|
p.lastResult = data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) Start() {
|
func (p *Poller[T, AggregateT]) Start() {
|
||||||
go func() {
|
go func() {
|
||||||
ctx := task.RootContext()
|
ctx := task.RootContext()
|
||||||
ticker := time.NewTicker(p.interval)
|
ticker := time.NewTicker(p.interval())
|
||||||
gatherErrsTicker := time.NewTicker(gatherErrsInterval)
|
gatherErrsTicker := time.NewTicker(gatherErrsInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
defer gatherErrsTicker.Stop()
|
defer gatherErrsTicker.Stop()
|
||||||
|
|
||||||
|
logging.Debug().Msgf("Starting poller %s with interval %s", p.name, p.interval())
|
||||||
|
|
||||||
|
p.pollWithTimeout(ctx)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
data, err := p.pollWithTimeout(ctx)
|
p.pollWithTimeout(ctx)
|
||||||
if err != nil {
|
|
||||||
p.appendErr(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.period.Add(data)
|
|
||||||
p.lastResult = data
|
|
||||||
case <-gatherErrsTicker.C:
|
case <-gatherErrsTicker.C:
|
||||||
errs, ok := p.gatherErrs()
|
errs, ok := p.gatherErrs()
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
@ -2,21 +2,23 @@ package uptime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/metrics/period"
|
"github.com/yusing/go-proxy/internal/metrics/period"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Statuses struct {
|
Statuses struct {
|
||||||
Statuses map[string]health.Status
|
Statuses map[string]health.Status
|
||||||
Timestamp int64
|
Timestamp time.Time
|
||||||
}
|
}
|
||||||
Status struct {
|
Status struct {
|
||||||
Status health.Status
|
Status health.Status
|
||||||
Timestamp int64
|
Timestamp time.Time
|
||||||
}
|
}
|
||||||
Aggregated map[string][]Status
|
Aggregated map[string][]Status
|
||||||
)
|
)
|
||||||
|
@ -30,7 +32,7 @@ func init() {
|
||||||
func getStatuses(ctx context.Context) (*Statuses, error) {
|
func getStatuses(ctx context.Context) (*Statuses, error) {
|
||||||
return &Statuses{
|
return &Statuses{
|
||||||
Statuses: routequery.HealthStatuses(),
|
Statuses: routequery.HealthStatuses(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,3 +73,19 @@ func (a Aggregated) finalize() map[string]map[string]interface{} {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Status) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"status": s.Status.String(),
|
||||||
|
"timestamp": s.Timestamp.Unix(),
|
||||||
|
"tooltip": strutils.FormatTime(s.Timestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Statuses) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"statuses": s.Statuses,
|
||||||
|
"timestamp": s.Timestamp.Unix(),
|
||||||
|
"tooltip": strutils.FormatTime(s.Timestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
@ -73,3 +76,17 @@ func GracefulShutdown(timeout time.Duration) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WaitExit(shutdownTimeout int) {
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT)
|
||||||
|
signal.Notify(sig, syscall.SIGTERM)
|
||||||
|
signal.Notify(sig, syscall.SIGHUP)
|
||||||
|
|
||||||
|
// wait for signal
|
||||||
|
<-sig
|
||||||
|
|
||||||
|
// gracefully shutdown
|
||||||
|
logging.Info().Msg("shutting down")
|
||||||
|
_ = GracefulShutdown(time.Second * time.Duration(shutdownTimeout))
|
||||||
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WaitExit(shutdownTimeout int) {
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGINT)
|
|
||||||
signal.Notify(sig, syscall.SIGTERM)
|
|
||||||
signal.Notify(sig, syscall.SIGHUP)
|
|
||||||
|
|
||||||
// wait for signal
|
|
||||||
<-sig
|
|
||||||
|
|
||||||
// gracefully shutdown
|
|
||||||
logging.Info().Msg("shutting down")
|
|
||||||
_ = task.GracefulShutdown(time.Second * time.Duration(shutdownTimeout))
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue