Admin: leases, pin, release, drain, usage, metrics
cmd/crossbar/main.go calls admin.Handler with the new 6-arg signature, passing nil for the not-yet-wired leases/limiter/store/drainer (task 07 wires them) so go vet and go test ./... pass on cmd/crossbar. This is a compile fix, not the task-07 wiring; noted in the implementer-log. Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
+148
-54
@@ -1,15 +1,19 @@
|
||||
// Package admin serves the operator's view of crossbar: the health table and the routes as JSON,
|
||||
// mounted at /_crossbar/ on the same listener as the proxy. The shape of /_crossbar/hosts is
|
||||
// fixed so operators can read why a request went where it went.
|
||||
// Package admin serves the operator's view of crossbar: the hosts view with
|
||||
// slots and drain state, the routes view with leases and pins, the pin/release
|
||||
// and drain controls, usage accounting, and Prometheus metrics, all under
|
||||
// /_crossbar/.
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/config"
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/health"
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/lease"
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/limiter"
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/store"
|
||||
)
|
||||
|
||||
// Hosts is what the admin handler needs from the health table.
|
||||
@@ -17,59 +21,93 @@ type Hosts interface {
|
||||
All() map[string]health.Status
|
||||
}
|
||||
|
||||
// Drainer is what the admin handler needs to steer draining; *proxy.Hosts
|
||||
// satisfies it.
|
||||
type Drainer interface {
|
||||
Draining(name string) bool
|
||||
SetDraining(name string, on bool)
|
||||
}
|
||||
|
||||
// HostView is one host's row in the hosts view.
|
||||
type HostView struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Loaded []string `json:"loaded"` // never null: an empty slice when nothing is loaded
|
||||
LastOK string `json:"last_ok"` // time.RFC3339 in UTC, or "" if never
|
||||
LastErr string `json:"last_err"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Loaded []string `json:"loaded"` // never null
|
||||
LastOK string `json:"last_ok"` // RFC 3339 UTC or ""
|
||||
LastErr string `json:"last_err"`
|
||||
FreeSlots int `json:"free_slots"` // lim.FreeSlots(host)
|
||||
InFlight int `json:"in_flight"` // sum over the host's configured models
|
||||
Queued int `json:"queued"` // same
|
||||
Draining bool `json:"draining"`
|
||||
}
|
||||
|
||||
// LeaseView is one lease's row in a route's leases.
|
||||
type LeaseView struct {
|
||||
FP string `json:"fp"`
|
||||
Model string `json:"model"`
|
||||
Host string `json:"host"`
|
||||
State string `json:"state"`
|
||||
Created string `json:"created"` // RFC 3339 UTC
|
||||
LastUsed string `json:"last_used"` // RFC 3339 UTC
|
||||
}
|
||||
|
||||
// RouteView is one route's row in the routes view.
|
||||
type RouteView struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
Hosts []string `json:"hosts"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
Pinned string `json:"pinned"` // "" when not pinned
|
||||
Leases []LeaseView `json:"leases"` // never null
|
||||
}
|
||||
|
||||
// Handler serves GET /_crossbar/hosts and GET /_crossbar/routes. Any other method on those paths is
|
||||
// a 405 with an Allow: GET header; anything else under the handler is a 404.
|
||||
func Handler(cfg *config.Config, h Hosts) http.Handler {
|
||||
// handler implements the operator's endpoints under /_crossbar/.
|
||||
type handler struct {
|
||||
cfg *config.Config
|
||||
h Hosts
|
||||
lt *lease.Table
|
||||
lim *limiter.Limiter
|
||||
st *store.Store
|
||||
d Drainer
|
||||
}
|
||||
|
||||
// Handler builds the operator's HTTP handler.
|
||||
func Handler(cfg *config.Config, h Hosts, lt *lease.Table, lim *limiter.Limiter, st *store.Store, d Drainer) http.Handler {
|
||||
hx := &handler{cfg: cfg, h: h, lt: lt, lim: lim, st: st, d: d}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/_crossbar/hosts", hostsHandler(h))
|
||||
mux.HandleFunc("/_crossbar/routes", routesHandler(cfg))
|
||||
mux.HandleFunc("/", notFound)
|
||||
mux.HandleFunc("/_crossbar/hosts", hx.hostsGet)
|
||||
mux.HandleFunc("/_crossbar/hosts/{host}", hx.hostDrain)
|
||||
mux.HandleFunc("/_crossbar/routes", hx.routesGet)
|
||||
mux.HandleFunc("/_crossbar/routes/{route}", hx.routePin)
|
||||
mux.HandleFunc("/_crossbar/usage", hx.usageGet)
|
||||
mux.HandleFunc("/_crossbar/metrics", hx.metricsGet)
|
||||
mux.HandleFunc("/_crossbar/", hx.unknown)
|
||||
return mux
|
||||
}
|
||||
|
||||
func hostsHandler(h Hosts) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
wrongMethod(w)
|
||||
return
|
||||
}
|
||||
views := make(map[string]HostView, len(h.All()))
|
||||
for name, s := range h.All() {
|
||||
views[name] = hostView(s)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, views)
|
||||
func (hx *handler) hostsGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
wrongMethod(w, "GET")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, hx.hostViews())
|
||||
}
|
||||
|
||||
func routesHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
wrongMethod(w)
|
||||
return
|
||||
}
|
||||
views := make(map[string]RouteView, len(cfg.Routes))
|
||||
for name, route := range cfg.Routes {
|
||||
hosts := make([]string, len(route.Hosts))
|
||||
copy(hosts, route.Hosts)
|
||||
views[name] = RouteView{Hosts: hosts, DefaultModel: route.DefaultModel}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, views)
|
||||
// hostViews builds every host's row, keyed by host name.
|
||||
func (hx *handler) hostViews() map[string]HostView {
|
||||
all := hx.h.All()
|
||||
out := make(map[string]HostView, len(all))
|
||||
for name, s := range all {
|
||||
out[name] = hx.hostView(name, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hostView(s health.Status) HostView {
|
||||
// hostView builds one host's row: concurrency from the limiter summed over the
|
||||
// models the host serves, draining from the drainer, and the health snapshot.
|
||||
func (hx *handler) hostView(name string, s health.Status) HostView {
|
||||
inflight, queued := 0, 0
|
||||
for _, m := range configuredModels(hx.cfg, name) {
|
||||
inflight += hx.lim.InFlight(name, m)
|
||||
queued += hx.lim.Queued(name, m)
|
||||
}
|
||||
loaded := s.Loaded
|
||||
if loaded == nil {
|
||||
loaded = []string{}
|
||||
@@ -79,24 +117,80 @@ func hostView(s health.Status) HostView {
|
||||
lastOK = s.LastOK.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return HostView{
|
||||
Healthy: s.Healthy,
|
||||
Loaded: loaded,
|
||||
LastOK: lastOK,
|
||||
LastErr: s.LastErr,
|
||||
Healthy: s.Healthy,
|
||||
Loaded: loaded,
|
||||
LastOK: lastOK,
|
||||
LastErr: s.LastErr,
|
||||
FreeSlots: hx.lim.FreeSlots(name),
|
||||
InFlight: inflight,
|
||||
Queued: queued,
|
||||
Draining: hx.d.Draining(name),
|
||||
}
|
||||
}
|
||||
|
||||
func wrongMethod(w http.ResponseWriter) {
|
||||
w.Header().Set("Allow", "GET")
|
||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
// configuredModels returns the sorted model ids the host serves, or nil when
|
||||
// the host is unknown.
|
||||
func configuredModels(cfg *config.Config, name string) []string {
|
||||
h, ok := cfg.Hosts[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
models := make([]string, 0, len(h.Models))
|
||||
for m := range h.Models {
|
||||
models = append(models, m)
|
||||
}
|
||||
sort.Strings(models)
|
||||
return models
|
||||
}
|
||||
|
||||
func notFound(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
func (hx *handler) routesGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
wrongMethod(w, "GET")
|
||||
return
|
||||
}
|
||||
snap := hx.lt.Snapshot()
|
||||
views := make(map[string]RouteView, len(hx.cfg.Routes))
|
||||
for name, route := range hx.cfg.Routes {
|
||||
hosts := make([]string, len(route.Hosts))
|
||||
copy(hosts, route.Hosts)
|
||||
views[name] = hx.routeView(name, hosts, route.DefaultModel, snap)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, views)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
// routeView builds one route's row: the pinned host (empty if none) and the
|
||||
// active leases on it, in snapshot order.
|
||||
func (hx *handler) routeView(route string, hosts []string, defaultModel string, snap []lease.Lease) RouteView {
|
||||
leases := make([]LeaseView, 0)
|
||||
for _, l := range snap {
|
||||
if l.Route == route {
|
||||
leases = append(leases, leaseView(l))
|
||||
}
|
||||
}
|
||||
return RouteView{
|
||||
Hosts: hosts,
|
||||
DefaultModel: defaultModel,
|
||||
Pinned: hx.lt.Pinned(route),
|
||||
Leases: leases,
|
||||
}
|
||||
}
|
||||
|
||||
// leaseView maps a lease to its operator view.
|
||||
func leaseView(l lease.Lease) LeaseView {
|
||||
return LeaseView{
|
||||
FP: l.FP,
|
||||
Model: l.Model,
|
||||
Host: l.Host,
|
||||
State: string(l.State),
|
||||
Created: formatTime(l.Created),
|
||||
LastUsed: formatTime(l.LastUsed),
|
||||
}
|
||||
}
|
||||
|
||||
// formatTime renders t as RFC 3339 in UTC, or "" for the zero time.
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user