Files
Prism/internal/health/checker.go
T
renjue fe8ea784d0
CI / docker (push) Successful in 2m54s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:58:28 +08:00

171 lines
3.8 KiB
Go

package health
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync/atomic"
"time"
"github.com/prism/proxy/internal/cache"
"github.com/prism/proxy/internal/store"
)
type ReloadFunc func(ctx context.Context) error
type Checker struct {
store *store.Store
cache *cache.Cache
apiBase string
secret string
client *http.Client
testing atomic.Bool
}
func NewChecker(s *store.Store, c *cache.Cache, apiBase, secret string) *Checker {
return &Checker{
store: s,
cache: c,
apiBase: stringsTrimRight(apiBase, "/"),
secret: secret,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func stringsTrimRight(s, cut string) string {
for len(s) > 0 && stringsHasSuffix(s, cut) {
s = s[:len(s)-len(cut)]
}
return s
}
func stringsHasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
func (h *Checker) TestNode(ctx context.Context, nodeID int64, reload ReloadFunc) (int, error) {
node, err := h.store.GetProxyNode(ctx, nodeID)
if err != nil {
return -1, err
}
latency, err := h.testRuntimeName(node.RuntimeName)
if isNotFound(err) && reload != nil {
if reloadErr := reload(ctx); reloadErr == nil {
latency, err = h.testRuntimeName(node.RuntimeName)
}
}
if isNotFound(err) {
err = fmt.Errorf("proxy %q not loaded in engine; reload engine in settings", node.RuntimeName)
}
alive := err == nil && latency >= 0
lat := latency
if !alive {
lat = -1
}
_ = h.store.UpdateProxyNodeHealth(ctx, nodeID, lat, alive)
h.cache.SetProxyLatency(node.RuntimeName, lat)
return lat, err
}
func (h *Checker) TestAll(ctx context.Context, _ ReloadFunc) error {
if !h.testing.CompareAndSwap(false, true) {
return nil
}
go func() {
defer h.testing.Store(false)
nodes, err := h.store.ListProxyNodes(context.Background(), "", nil)
if err != nil {
return
}
for _, n := range nodes {
if !n.Enabled {
continue
}
_, _ = h.TestNode(context.Background(), n.ID, nil)
}
}()
return nil
}
func (h *Checker) ProxyLoaded(name string) bool {
data, err := h.getJSON("/proxies")
if err != nil {
return false
}
proxies, ok := data["proxies"].(map[string]any)
if !ok {
return false
}
_, ok = proxies[name]
return ok
}
func (h *Checker) testRuntimeName(name string) (int, error) {
testURL := fmt.Sprintf("%s/proxies/%s/delay?timeout=5000&url=%s", h.apiBase, url.PathEscape(name), url.QueryEscape("http://www.gstatic.com/generate_204"))
req, err := http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
return -1, err
}
if h.secret != "" {
req.Header.Set("Authorization", "Bearer "+h.secret)
}
resp, err := h.client.Do(req)
if err != nil {
return -1, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return -1, fmt.Errorf("delay test failed: %s", string(body))
}
var result struct {
Delay int `json:"delay"`
}
if err := json.Unmarshal(body, &result); err != nil {
return -1, err
}
return result.Delay, nil
}
func (h *Checker) getJSON(path string) (map[string]any, error) {
req, err := http.NewRequest(http.MethodGet, h.apiBase+path, nil)
if err != nil {
return nil, err
}
if h.secret != "" {
req.Header.Set("Authorization", "Bearer "+h.secret)
}
resp, err := h.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(body))
}
var out map[string]any
if err := json.Unmarshal(body, &out); err != nil {
return nil, err
}
return out, nil
}
func isNotFound(err error) bool {
return err != nil && strings.Contains(err.Error(), "Resource not found")
}
func (h *Checker) UpdateAPIBase(apiBase, secret string) {
h.apiBase = stringsTrimRight(apiBase, "/")
h.secret = secret
}