The full switch: Monitors and Disks read only the sensor API; /api/metrics retired
- stock Monitors modules fed by one sensor stream, the catalog and one batch of histories (GET /api/sensors/history?ids=…), through sampleAdapter.ts - the stream closes while the page is hidden and refills its curves on return - Disks tab live rates from the stream - new sensors: hamlab.sample_interval, ups.last_event - stream: asking for about the sampling rate means every sample - NUT runtime rounded at the source Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f01175ceca
commit
4ddf10be51
@@ -136,16 +136,6 @@ type Sample struct {
|
||||
fsT, upsT int64 // when the slow sections were really read; sensors report these, not T
|
||||
}
|
||||
|
||||
// Series is the compact history the UI seeds its sparklines from.
|
||||
type Series struct {
|
||||
T []int64 `json:"t"`
|
||||
CPU []float64 `json:"cpu"`
|
||||
Mem []uint64 `json:"mem"`
|
||||
Net map[string]map[string][]float64 `json:"net"` // iface -> {rx,tx}
|
||||
Disk map[string]map[string][]float64 `json:"disk"` // name -> {r,w,u}
|
||||
Temp map[string][]float64 `json:"temp"` // "chip/label" -> °C
|
||||
}
|
||||
|
||||
type Collector struct {
|
||||
mu sync.RWMutex
|
||||
ring []Sample
|
||||
@@ -236,44 +226,6 @@ func (c *Collector) Latest() (Sample, bool) {
|
||||
return c.ring[len(c.ring)-1], true
|
||||
}
|
||||
|
||||
func (c *Collector) History() Series {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
n := len(c.ring)
|
||||
s := Series{T: make([]int64, n), CPU: make([]float64, n), Mem: make([]uint64, n),
|
||||
Net: map[string]map[string][]float64{}, Disk: map[string]map[string][]float64{}, Temp: map[string][]float64{}}
|
||||
series := func(m map[string]map[string][]float64, key string, keys ...string) map[string][]float64 {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
v = map[string][]float64{}
|
||||
for _, k := range keys {
|
||||
v[k] = make([]float64, n)
|
||||
}
|
||||
m[key] = v
|
||||
}
|
||||
return v
|
||||
}
|
||||
for i, smp := range c.ring {
|
||||
s.T[i], s.CPU[i], s.Mem[i] = smp.T, smp.CPU, smp.Mem.Used
|
||||
for _, x := range smp.Net {
|
||||
v := series(s.Net, x.Iface, "rx", "tx")
|
||||
v["rx"][i], v["tx"][i] = x.RxBps, x.TxBps
|
||||
}
|
||||
for _, d := range smp.Disks {
|
||||
v := series(s.Disk, d.Name, "r", "w", "u")
|
||||
v["r"][i], v["w"][i], v["u"][i] = d.ReadBps, d.WriteBps, d.UtilPct
|
||||
}
|
||||
for _, t := range smp.Temps {
|
||||
k := t.Chip + "/" + t.Label
|
||||
if s.Temp[k] == nil {
|
||||
s.Temp[k] = make([]float64, n)
|
||||
}
|
||||
s.Temp[k][i] = t.C
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ---- sampling ----
|
||||
|
||||
func (c *Collector) sample() {
|
||||
@@ -853,7 +805,7 @@ func upsNUT() *UPS {
|
||||
u.InV = leadingNum(kv["input.voltage"])
|
||||
u.OutV = leadingNum(kv["output.voltage"])
|
||||
u.BatteryPct = leadingNum(kv["battery.charge"])
|
||||
u.RuntimeMin = leadingNum(kv["battery.runtime"]) / 60
|
||||
u.RuntimeMin = round1(leadingNum(kv["battery.runtime"]) / 60)
|
||||
u.LoadPct = leadingNum(kv["ups.load"])
|
||||
u.LoadW = leadingNum(kv["ups.realpower"])
|
||||
if u.LoadW == 0 {
|
||||
|
||||
+32
-23
@@ -11,10 +11,10 @@ import (
|
||||
// IDs are built from names the hardware reports (chip, label, interface, device, mount),
|
||||
// never from enumeration order like hwmon3, which moves between boots.
|
||||
|
||||
// flattener turns one Sample into sensors. With only set it builds just that one, so
|
||||
// walking the whole ring for a history costs string compares, not allocations.
|
||||
// flattener turns one Sample into sensors. With want set it builds only those, so walking
|
||||
// the whole ring for a history costs string compares, not allocations.
|
||||
type flattener struct {
|
||||
only string
|
||||
want func(id string) bool
|
||||
seen map[string]int
|
||||
out []sensors.Sensor
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (f *flattener) add(id, name, kind, unit string, v any, t int64, b bounds, l
|
||||
if n := f.seen[id]; n > 1 { // two identical chips: the second is coretemp_2
|
||||
id = fmt.Sprintf("%s_%d", id, n)
|
||||
}
|
||||
if f.only != "" && id != f.only {
|
||||
if f.want != nil && !f.want(id) {
|
||||
return
|
||||
}
|
||||
s := sensors.Sensor{ID: id, Name: name, Kind: kind, Unit: unit, Value: v, T: t, Min: b.min, Max: b.max, Warn: b.warn, Crit: b.crit}
|
||||
@@ -50,8 +50,8 @@ func ratio(part, whole uint64) float64 {
|
||||
return round1(float64(part) / float64(whole) * 100)
|
||||
}
|
||||
|
||||
func flatten(s Sample, only string) []sensors.Sensor {
|
||||
f := &flattener{only: only, seen: map[string]int{}}
|
||||
func flatten(s Sample, want func(id string) bool) []sensors.Sensor {
|
||||
f := &flattener{want: want, seen: map[string]int{}}
|
||||
t, none := s.T, bounds{}
|
||||
|
||||
f.add("cpu.usage", "CPU", "usage", "%", s.CPU, t, pct)
|
||||
@@ -66,6 +66,7 @@ func flatten(s Sample, only string) []sensors.Sensor {
|
||||
f.add("load."+w, "Load "+w+" min", "load", "", s.Load[i], t, none)
|
||||
}
|
||||
f.add("uptime", "Uptime", "duration", "s", s.Uptime, t, none)
|
||||
f.add("hamlab.sample_interval", "Sampling interval", "duration", "ms", float64(s.Interval), t, none)
|
||||
|
||||
for _, m := range []struct {
|
||||
id, name string
|
||||
@@ -132,6 +133,7 @@ func flatten(s Sample, only string) []sensors.Sensor {
|
||||
f.add("ups.on_battery", "UPS on battery", "state", "", u.OnBattery, ut, none, "model", u.Model)
|
||||
f.add("ups.state", "UPS state", "state", "", u.State, ut, none, "model", u.Model)
|
||||
f.add("ups.source", "UPS power source", "state", "", u.Source, ut, none, "model", u.Model)
|
||||
f.add("ups.last_event", "UPS last event", "state", "", u.LastEvent, ut, none, "model", u.Model, "driver", u.Driver)
|
||||
}
|
||||
return f.out
|
||||
}
|
||||
@@ -142,28 +144,35 @@ func (c *Collector) Sensors() []sensors.Sensor {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return flatten(s, "")
|
||||
return flatten(s, nil)
|
||||
}
|
||||
|
||||
// SensorHistory implements sensors.Historian: the ring, seen through one sensor. Samples
|
||||
// where the sensor didn't exist (a disk plugged in since) are left out, not zero-filled.
|
||||
func (c *Collector) SensorHistory(id string) (sensors.Series, bool) {
|
||||
// SensorHistories implements sensors.Historian: the ring, seen through the sensors that
|
||||
// match. A sensor that didn't exist in a sample (a disk plugged in since) simply has a
|
||||
// shorter series; nothing is zero-filled. States have no curve and are left out.
|
||||
func (c *Collector) SensorHistories(match func(id string) bool) []sensors.Series {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
out := sensors.Series{ID: id, T: []int64{}, V: []float64{}}
|
||||
found := false
|
||||
by := map[string]*sensors.Series{}
|
||||
var order []string
|
||||
for _, smp := range c.ring {
|
||||
got := flatten(smp, id)
|
||||
if len(got) == 0 {
|
||||
continue
|
||||
for _, x := range flatten(smp, match) {
|
||||
v, ok := x.Value.(float64)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s := by[x.ID]
|
||||
if s == nil {
|
||||
s = &sensors.Series{ID: x.ID, Unit: x.Unit, T: []int64{}, V: []float64{}}
|
||||
by[x.ID] = s
|
||||
order = append(order, x.ID)
|
||||
}
|
||||
s.T, s.V = append(s.T, smp.T), append(s.V, v)
|
||||
}
|
||||
found = true
|
||||
v, ok := got[0].Value.(float64)
|
||||
if !ok {
|
||||
return sensors.Series{}, false // states have no curve
|
||||
}
|
||||
out.Unit = got[0].Unit
|
||||
out.T, out.V = append(out.T, smp.T), append(out.V, v)
|
||||
}
|
||||
return out, found
|
||||
out := make([]sensors.Series, 0, len(order))
|
||||
for _, id := range order {
|
||||
out = append(out, *by[id])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ func TestFlatten(t *testing.T) {
|
||||
Fans: []Fan{{Chip: "nct6775", Label: "fan2", RPM: 1100}},
|
||||
FS: []FS{{Mount: "/", Size: 100, Used: 25}}, fsT: 900}
|
||||
got := map[string]any{}
|
||||
for _, x := range flatten(s, "") {
|
||||
for _, x := range flatten(s, nil) {
|
||||
if _, dup := got[x.ID]; dup {
|
||||
t.Errorf("duplicate id %s", x.ID)
|
||||
}
|
||||
@@ -20,31 +20,37 @@ func TestFlatten(t *testing.T) {
|
||||
t.Errorf("%s = %v, want %v", id, got[id], want)
|
||||
}
|
||||
}
|
||||
one := flatten(s, "temp.nvme.composite_2")
|
||||
one := flatten(s, func(id string) bool { return id == "temp.nvme.composite_2" })
|
||||
if len(one) != 1 || one[0].Value != 45.0 {
|
||||
t.Errorf("only: got %+v", one)
|
||||
}
|
||||
for _, x := range flatten(s, "fs.root.used") {
|
||||
for _, x := range flatten(s, func(id string) bool { return id == "fs.root.used" }) {
|
||||
if x.T != 900 {
|
||||
t.Errorf("fs sensor should carry the fs read time, got %d", x.T)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSensorHistory(t *testing.T) {
|
||||
func TestSensorHistories(t *testing.T) {
|
||||
c := NewCollector()
|
||||
c.ring = []Sample{{T: 1, CPU: 10}, {T: 2, CPU: 20, Net: []Net{{Iface: "eth0", Up: true, RxBps: 5}}}}
|
||||
h, ok := c.SensorHistory("cpu.usage")
|
||||
if !ok || len(h.V) != 2 || h.V[1] != 20 || h.Unit != "%" {
|
||||
t.Fatalf("cpu: %+v %v", h, ok)
|
||||
got := map[string][]float64{}
|
||||
for _, h := range c.SensorHistories(func(id string) bool { return id == "cpu.usage" || id == "net.eth0.rx" || id == "net.eth0.up" }) {
|
||||
if len(h.T) != len(h.V) {
|
||||
t.Errorf("%s: t and v differ in length", h.ID)
|
||||
}
|
||||
got[h.ID] = h.V
|
||||
}
|
||||
if h, ok := c.SensorHistory("net.eth0.rx"); !ok || len(h.V) != 1 || h.T[0] != 2 {
|
||||
t.Fatalf("net: a sensor that appeared later has a shorter history, got %+v %v", h, ok)
|
||||
if len(got["cpu.usage"]) != 2 || got["cpu.usage"][1] != 20 {
|
||||
t.Errorf("cpu: %v", got["cpu.usage"])
|
||||
}
|
||||
if _, ok := c.SensorHistory("net.eth0.up"); ok {
|
||||
if len(got["net.eth0.rx"]) != 1 {
|
||||
t.Errorf("a sensor that appeared later has a shorter history, got %v", got["net.eth0.rx"])
|
||||
}
|
||||
if _, ok := got["net.eth0.up"]; ok {
|
||||
t.Error("a state has no history")
|
||||
}
|
||||
if _, ok := c.SensorHistory("nope"); ok {
|
||||
t.Error("unknown id")
|
||||
if len(c.SensorHistories(func(string) bool { return false })) != 0 {
|
||||
t.Error("nothing matched, nothing returned")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user