refactor, fix metrics and upgrade go to 1.24.0

This commit is contained in:
yusing 2025-02-12 11:15:45 +08:00
parent c807b30c8f
commit 82042e0b99
19 changed files with 157 additions and 104 deletions

View file

@ -1,5 +1,5 @@
# Stage 1: Builder
FROM golang:1.23.6-alpine AS builder
FROM golang:1.24.0-alpine AS builder
HEALTHCHECK NONE
# package version does not matter

View file

@ -10,7 +10,6 @@ import (
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/pkg"
"gopkg.in/yaml.v3"
)
@ -69,5 +68,5 @@ Tips:
server.StartAgentServer(t, opts)
utils.WaitExit(3)
task.WaitExit(3)
}

View file

@ -1,4 +1,4 @@
package handler
package handler_test
import (
"encoding/json"
@ -10,6 +10,7 @@ import (
"testing"
"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/watcher/health"
)
@ -75,7 +76,7 @@ func TestCheckHealthHTTP(t *testing.T) {
query.Set(key, value)
}
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)
@ -120,7 +121,7 @@ func TestCheckHealthFileServer(t *testing.T) {
recorder := httptest.NewRecorder()
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)
@ -203,7 +204,7 @@ func TestCheckHealthTCPUDP(t *testing.T) {
recorder := httptest.NewRecorder()
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)

View file

@ -16,7 +16,7 @@ import (
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/middleware"
"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"
)
@ -129,7 +129,7 @@ func main() {
config.WatchChanges()
utils.WaitExit(cfg.Value().TimeoutShutdown)
task.WaitExit(cfg.Value().TimeoutShutdown)
}
func prepareDirectory(dir string) {

2
go.mod
View file

@ -1,6 +1,6 @@
module github.com/yusing/go-proxy
go 1.23.6
go 1.24.0
require (
github.com/PuerkitoBio/goquery v1.10.1

View file

@ -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/ws", useCfg(cfg, v1.StatsWS))
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("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/ws", auth.RequireAuth(useCfg(cfg, v1.SystemInfo)))
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 {
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)
}
}
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)
}
}

View file

@ -12,7 +12,7 @@ import (
)
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())
})
}

View file

@ -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) {
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))
})
}

View file

@ -16,7 +16,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
agentName := r.URL.Query().Get("agent_name")
if agentName == "" {
if isWS {
systeminfo.Poller.ServeWS(cfg, w, r)
systeminfo.Poller.ServeWS(cfg.Value().MatchDomains, w, r)
} else {
systeminfo.Poller.ServeHTTP(w, r)
}
@ -40,7 +40,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
}
U.WriteBody(w, respData)
} else {
clientConn, err := U.InitiateWS(cfg, w, r)
clientConn, err := U.InitiateWS(cfg.Value().MatchDomains, w, r)
if err != nil {
U.HandleErr(w, r, err)
return

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"net/http"
"syscall"
E "github.com/yusing/go-proxy/internal/error"
"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.
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
if err == nil {
return
}
if errors.Is(err, context.Canceled) {
switch {
case err == nil,
errors.Is(err, context.Canceled),
errors.Is(err, syscall.EPIPE),
errors.Is(err, syscall.ECONNRESET):
return
}
LogError(r).Msg(err.Error())

View file

@ -7,7 +7,6 @@ import (
"github.com/coder/websocket"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/logging"
)
@ -17,45 +16,49 @@ func warnNoMatchDomains() {
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
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)
originPats = []string{"*"}
} else {
originPats = make([]string, len(cfg.Value().MatchDomains))
for i, domain := range cfg.Value().MatchDomains {
originPats[i] = "*" + domain
originPats = make([]string, len(allowedDomains))
for i, domain := range allowedDomains {
if domain[0] != '.' {
originPats[i] = "*." + domain
} else {
originPats[i] = "*" + domain
}
}
originPats = append(originPats, localAddresses...)
}
if common.IsDebug {
originPats = []string{"*"}
}
return websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: originPats,
})
}
func PeriodicWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
conn, err := InitiateWS(cfg, w, r)
func PeriodicWS(allowedDomains []string, w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
conn, err := InitiateWS(allowedDomains, w, r)
if err != nil {
HandleErr(w, r, err)
return
}
/* trunk-ignore(golangci-lint/errcheck) */
//nolint:errcheck
defer conn.CloseNow()
if err := do(conn); err != nil {
HandleErr(w, r, err)
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-cfg.Context().Done():
return
case <-r.Context().Done():
return
case <-ticker.C:

View file

@ -13,7 +13,6 @@ import (
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/api/v1/utils"
"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/task"
F "github.com/yusing/go-proxy/internal/utils/functional"
@ -81,9 +80,9 @@ func init() {
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) {
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
}
func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
conn, err := utils.InitiateWS(config, w, r)
func (m *memLogger) ServeHTTP(allowedDomains []string, w http.ResponseWriter, r *http.Request) {
conn, err := utils.InitiateWS(allowedDomains, w, r)
if err != nil {
utils.HandleErr(w, r, err)
return
@ -161,7 +160,6 @@ func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWrite
logCh := make(chan *logEntryRange)
m.connChans.Store(logCh, struct{}{})
/* trunk-ignore(golangci-lint/errcheck) */
defer func() {
_ = conn.CloseNow()

View file

@ -1,26 +1,28 @@
package period
import "time"
import (
"time"
)
type Entries[T any] struct {
entries [maxEntries]*T
index int
count int
interval int64
lastAdd int64
interval time.Duration
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]{
interval: interval,
lastAdd: time.Now().Unix(),
interval: duration / maxEntries,
lastAdd: time.Now(),
}
}
func (e *Entries[T]) Add(now int64, info *T) {
if now-e.lastAdd < e.interval {
func (e *Entries[T]) Add(now time.Time, info *T) {
if now.Sub(e.lastAdd) < e.interval {
return
}
e.entries[e.index] = info

View file

@ -2,11 +2,11 @@ package period
import (
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
"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) {
@ -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) {
utils.PeriodicWS(cfg, w, r, p.interval, func(conn *websocket.Conn) error {
return wsjson.Write(r.Context(), conn, p.GetLastResult())
})
func (p *Poller[T, AggregateT]) ServeWS(allowedDomains []string, w http.ResponseWriter, r *http.Request) {
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())
})
} 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))
})
}
}
}

View file

@ -24,17 +24,17 @@ const (
func NewPeriod[T any]() *Period[T] {
return &Period[T]{
FifteenMinutes: newEntries[T](15 * 60 / maxEntries),
OneHour: newEntries[T](60 * 60 / maxEntries),
OneDay: newEntries[T](24 * 60 * 60 / maxEntries),
OneMonth: newEntries[T](30 * 24 * 60 * 60 / maxEntries),
FifteenMinutes: newEntries[T](15 * time.Minute),
OneHour: newEntries[T](1 * time.Hour),
OneDay: newEntries[T](24 * time.Hour),
OneMonth: newEntries[T](30 * 24 * time.Hour),
}
}
func (p *Period[T]) Add(info *T) {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now().Unix()
now := time.Now()
p.FifteenMinutes.Add(now, info)
p.OneHour.Add(now, info)
p.OneDay.Add(now, info)

View file

@ -18,7 +18,6 @@ type (
poll PollFunc[T]
aggregator AggregateFunc[T, AggregateT]
period *Period[T]
interval time.Duration
lastResult *T
errs []pollErr
}
@ -36,10 +35,9 @@ func NewPoller[T any](
poll PollFunc[T],
) *Poller[T, T] {
return &Poller[T, T]{
name: name,
poll: poll,
period: NewPeriod[T](),
interval: interval,
name: name,
poll: poll,
period: NewPeriod[T](),
}
}
@ -54,10 +52,13 @@ func NewPollerWithAggregator[T, AggregateT any](
poll: poll,
aggregator: aggregator,
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) {
if len(p.errs) == 0 {
p.errs = []pollErr{
@ -87,32 +88,36 @@ func (p *Poller[T, AggregateT]) gatherErrs() (string, bool) {
return strings.Join(errs, "\n"), true
}
func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) (*T, error) {
ctx, cancel := context.WithTimeout(ctx, p.interval)
func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, p.interval())
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() {
go func() {
ctx := task.RootContext()
ticker := time.NewTicker(p.interval)
ticker := time.NewTicker(p.interval())
gatherErrsTicker := time.NewTicker(gatherErrsInterval)
defer ticker.Stop()
defer gatherErrsTicker.Stop()
logging.Debug().Msgf("Starting poller %s with interval %s", p.name, p.interval())
p.pollWithTimeout(ctx)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
data, err := p.pollWithTimeout(ctx)
if err != nil {
p.appendErr(err)
continue
}
p.period.Add(data)
p.lastResult = data
p.pollWithTimeout(ctx)
case <-gatherErrsTicker.C:
errs, ok := p.gatherErrs()
if ok {

View file

@ -2,21 +2,23 @@ package uptime
import (
"context"
"encoding/json"
"time"
"github.com/yusing/go-proxy/internal/metrics/period"
"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"
)
type (
Statuses struct {
Statuses map[string]health.Status
Timestamp int64
Timestamp time.Time
}
Status struct {
Status health.Status
Timestamp int64
Timestamp time.Time
}
Aggregated map[string][]Status
)
@ -30,7 +32,7 @@ func init() {
func getStatuses(ctx context.Context) (*Statuses, error) {
return &Statuses{
Statuses: routequery.HealthStatuses(),
Timestamp: time.Now().Unix(),
Timestamp: time.Now(),
}, nil
}
@ -71,3 +73,19 @@ func (a Aggregated) finalize() map[string]map[string]interface{} {
}
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),
})
}

View file

@ -4,6 +4,9 @@ import (
"context"
"encoding/json"
"errors"
"os"
"os/signal"
"syscall"
"time"
"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))
}

View file

@ -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))
}