refactor(api): share monitoring route registration

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 16:45:13 +00:00
parent 140e66ac64
commit e523fd0740
4 changed files with 27 additions and 158 deletions

View file

@ -1,23 +1,15 @@
// Package api wires the monitoring endpoints onto an HTTP router.
//
// api.RegisterRoutes(http.NewServeMux(), p)
// mux := http.NewServeMux()
// api.RegisterRoutes(mux, p)
package api
import (
"encoding/json"
"net/http"
"strings"
"dappco.re/go/core/proxy"
)
// Router is the minimal route-registration surface used by RegisterRoutes.
//
// mux := http.NewServeMux()
// api.RegisterRoutes(mux, p)
type Router interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
type Router = proxy.RouteRegistrar
type SummaryResponse = proxy.SummaryResponse
type HashrateResponse = proxy.HashrateResponse
@ -30,145 +22,5 @@ type ResultsResponse = proxy.ResultsResponse
// mux := http.NewServeMux()
// api.RegisterRoutes(mux, p)
func RegisterRoutes(router Router, proxyValue *proxy.Proxy) {
if router == nil || proxyValue == nil {
return
}
router.HandleFunc("/1/summary", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
summary := proxyValue.Summary()
response := SummaryResponse{
Version: "1.0.0",
Mode: proxyValue.Mode(),
Hashrate: HashrateResponse{
Total: summary.Hashrate,
},
Miners: MinersCountResponse{
Now: proxyValue.CurrentMiners(),
Max: proxyValue.MaxMiners(),
},
Workers: uint64(len(proxyValue.Workers())),
Upstreams: func() UpstreamResponse {
upstreams := proxyValue.Upstreams()
ratio := 0.0
if upstreams.Total > 0 {
ratio = float64(proxyValue.CurrentMiners()) / float64(upstreams.Total)
}
return UpstreamResponse{
Active: upstreams.Active,
Sleep: upstreams.Sleep,
Error: upstreams.Error,
Total: upstreams.Total,
Ratio: ratio,
}
}(),
Results: ResultsResponse{
Accepted: summary.Accepted,
Rejected: summary.Rejected,
Invalid: summary.Invalid,
Expired: summary.Expired,
AvgTime: summary.AvgTime,
Latency: summary.AvgLatency,
HashesTotal: summary.Hashes,
Best: summary.TopDiff,
},
}
writeJSON(writer, response)
})
router.HandleFunc("/1/workers", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
type responseBody struct {
Mode string `json:"mode"`
Workers [][]interface{} `json:"workers"`
}
records := proxyValue.Workers()
rows := make([][]interface{}, 0, len(records))
for _, record := range records {
rows = append(rows, []interface{}{
record.Name,
record.LastIP,
record.Connections,
record.Accepted,
record.Rejected,
record.Invalid,
record.Hashes,
record.LastHashAt.Unix(),
record.Hashrate(60),
record.Hashrate(600),
record.Hashrate(3600),
record.Hashrate(43200),
record.Hashrate(86400),
})
}
writeJSON(writer, responseBody{
Mode: proxyValue.WorkersMode(),
Workers: rows,
})
})
router.HandleFunc("/1/miners", func(writer http.ResponseWriter, request *http.Request) {
if !allowRequest(writer, request, proxyValue.HTTPConfig()) {
return
}
miners := proxyValue.Miners()
rows := make([][]interface{}, 0, len(miners))
for _, miner := range miners {
ip := ""
if remote := miner.RemoteAddr(); remote != nil {
ip = remote.String()
}
rows = append(rows, []interface{}{
miner.ID(),
ip,
miner.TX(),
miner.RX(),
miner.State(),
miner.Diff(),
miner.User(),
"********",
miner.RigID(),
miner.Agent(),
})
}
writeJSON(writer, map[string]interface{}{
"format": []string{"id", "ip", "tx", "rx", "state", "diff", "user", "password", "rig_id", "agent"},
"miners": rows,
})
})
}
func allowRequest(writer http.ResponseWriter, request *http.Request, config proxy.HTTPConfig) bool {
if request == nil {
return false
}
if config.AccessToken != "" {
header := request.Header.Get("Authorization")
prefix := "Bearer "
if !strings.HasPrefix(header, prefix) || strings.TrimSpace(strings.TrimPrefix(header, prefix)) != config.AccessToken {
writer.Header().Set("WWW-Authenticate", "Bearer")
http.Error(writer, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return false
}
}
if config.Restricted && request.Method != http.MethodGet {
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return false
}
return true
}
func writeJSON(writer http.ResponseWriter, value interface{}) {
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(value)
proxy.RegisterMonitoringRoutes(router, proxyValue)
}

View file

@ -35,7 +35,10 @@ func LoadConfig(path string) (*Config, error) {
// Validate checks that `bind` and `pools` are present and every enabled pool has a URL.
//
// cfg := &proxy.Config{Bind: []proxy.BindAddr{{Host: "127.0.0.1", Port: 3333}}, Pools: []proxy.PoolConfig{{URL: "pool-a:3333", Enabled: true}}}
// cfg := &proxy.Config{
// Bind: []proxy.BindAddr{{Host: "127.0.0.1", Port: 3333}},
// Pools: []proxy.PoolConfig{{URL: "pool-a:3333", Enabled: true}},
// }
// if errorValue := cfg.Validate(); errorValue != nil {
// return
// }
@ -61,7 +64,9 @@ func (c *Config) Validate() error {
// NewConfigWatcher watches a config file and reloads the proxy on modification.
//
// w := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) { p.Reload(cfg) })
// w := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) {
// p.Reload(cfg)
// })
func NewConfigWatcher(path string, onChange func(*Config)) *ConfigWatcher {
return newConfigWatcher(path, onChange, true)
}

View file

@ -12,6 +12,14 @@ import (
const proxyAPIVersion = "1.0.0"
// RouteRegistrar is the minimal route-registration surface used by RegisterMonitoringRoutes.
//
// mux := http.NewServeMux()
// RegisterMonitoringRoutes(mux, p)
type RouteRegistrar interface {
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
// SummaryResponse is the /1/summary JSON body.
type SummaryResponse struct {
Version string `json:"version"`
@ -61,7 +69,7 @@ func startHTTPServer(p *Proxy) {
}
mux := http.NewServeMux()
registerMonitoringRoutes(mux, p)
RegisterMonitoringRoutes(mux, p)
address := net.JoinHostPort(p.config.HTTP.Host, strconv.Itoa(int(p.config.HTTP.Port)))
listener, errorValue := net.Listen("tcp", address)
@ -92,7 +100,11 @@ func stopHTTPServer(p *Proxy) {
_ = server.Shutdown(shutdownContext)
}
func registerMonitoringRoutes(router *http.ServeMux, proxyValue *Proxy) {
// RegisterMonitoringRoutes mounts the monitoring endpoints on any router with HandleFunc.
//
// mux := http.NewServeMux()
// RegisterMonitoringRoutes(mux, p)
func RegisterMonitoringRoutes(router RouteRegistrar, proxyValue *Proxy) {
if router == nil || proxyValue == nil {
return
}

View file

@ -8,7 +8,7 @@ import (
// NewRateLimiter creates a per-IP limiter, for example:
//
// rl := proxy.NewRateLimiter(cfg.RateLimit)
// rl := proxy.NewRateLimiter(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
func NewRateLimiter(config RateLimit) *RateLimiter {
return &RateLimiter{
limitConfig: config,
@ -17,7 +17,7 @@ func NewRateLimiter(config RateLimit) *RateLimiter {
}
}
// SetConfig swaps in a live reload value such as `proxy.RateLimit{MaxConnectionsPerMinute: 30}`.
// SetConfig swaps in a live reload value such as:
//
// rl.SetConfig(proxy.RateLimit{MaxConnectionsPerMinute: 30, BanDurationSeconds: 300})
func (rateLimiter *RateLimiter) SetConfig(config RateLimit) {