Monitor draws CPU, memory, GPUs, requests, tokens, latency, network and disk as Braille charts over 5m/1h/24h/7d, then per node, card and model sparklines; node and model pages have their own. History is read from monitoring.prometheus through POST /api/v1/metrics/range, or without it from what the console saw. The control plane serves the cluster's machines for Prometheus http_sd at /prometheus/targets on its metrics port, and node add installs prometheus-node-exporter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
231 lines
6.1 KiB
Go
231 lines
6.1 KiB
Go
package cli
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// Charts drawn in a terminal the way btop draws them: Braille characters,
|
||
// each cell two dots across and four down, so a chart forty cells wide holds
|
||
// eighty points and five rows twenty levels. Filled below the line, and
|
||
// coloured by height — green low, yellow, red near the top — so how hard
|
||
// something is working reads before any number does.
|
||
|
||
// Braille dot bits by column (0 left, 1 right) and row (0 top .. 3 bottom).
|
||
var brailleBit = [2][4]rune{{0x01, 0x02, 0x04, 0x40}, {0x08, 0x10, 0x20, 0x80}}
|
||
|
||
// gradient is the colour at a fraction of a chart's height, 0 bottom, 1 top.
|
||
func gradient(f float64) lipgloss.Color {
|
||
stops := [][3]float64{{80, 200, 120}, {230, 200, 60}, {230, 80, 70}}
|
||
f = math.Max(0, math.Min(1, f))
|
||
seg := f * float64(len(stops)-1)
|
||
i := int(seg)
|
||
if i >= len(stops)-1 {
|
||
i, seg = len(stops)-2, float64(len(stops)-1)
|
||
}
|
||
t := seg - float64(i)
|
||
c := [3]float64{}
|
||
for k := range c {
|
||
c[k] = stops[i][k] + (stops[i+1][k]-stops[i][k])*t
|
||
}
|
||
return lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", int(c[0]), int(c[1]), int(c[2])))
|
||
}
|
||
|
||
// lineColors are the colours of the lines of a chart with several.
|
||
var lineColors = []lipgloss.Color{"#61afef", "#e5c07b", "#c678dd", "#56b6c2", "#e06c75", "#98c379", "#d19a66", "#abb2bf"}
|
||
|
||
// chartSeries is one line: its values, oldest first, and a name for the
|
||
// legend. NaN is no sample.
|
||
type chartSeries struct {
|
||
name string
|
||
values []float64
|
||
}
|
||
|
||
// brailleChart draws series into width×height cells. top is the value at the
|
||
// top of the chart; 0 takes the largest value. One series is filled and
|
||
// coloured by height; several are lines, each its own colour.
|
||
func brailleChart(series []chartSeries, width, height int, top float64) []string {
|
||
width, height = max(width, 4), max(height, 1)
|
||
if top <= 0 {
|
||
for _, s := range series {
|
||
for _, v := range s.values {
|
||
if !math.IsNaN(v) && v > top {
|
||
top = v
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if top <= 0 {
|
||
top = 1
|
||
}
|
||
cols, rows := width*2, height*4
|
||
dots := make([][]rune, height)
|
||
owner := make([][]int, height)
|
||
for r := range dots {
|
||
dots[r] = make([]rune, width)
|
||
owner[r] = make([]int, width)
|
||
for c := range owner[r] {
|
||
owner[r][c] = -1
|
||
}
|
||
}
|
||
set := func(x, y, who int) { // y from the bottom
|
||
if x < 0 || x >= cols || y < 0 || y >= rows {
|
||
return
|
||
}
|
||
r := height - 1 - y/4
|
||
dots[r][x/2] |= brailleBit[x%2][3-y%4]
|
||
owner[r][x/2] = who
|
||
}
|
||
area := len(series) == 1
|
||
for si, s := range series {
|
||
vals := resample(s.values, cols)
|
||
prev := -1
|
||
for x, v := range vals {
|
||
if math.IsNaN(v) {
|
||
prev = -1
|
||
continue
|
||
}
|
||
h := int(math.Round(v / top * float64(rows-1)))
|
||
h = max(0, min(rows-1, h))
|
||
if area {
|
||
for y := 0; y <= h; y++ {
|
||
set(x, y, si)
|
||
}
|
||
continue
|
||
}
|
||
// A line: this point, and the rise or fall from the last.
|
||
lo, hi := h, h
|
||
if prev >= 0 {
|
||
lo, hi = min(prev, h), max(prev, h)
|
||
}
|
||
for y := lo; y <= hi; y++ {
|
||
set(x, y, si)
|
||
}
|
||
prev = h
|
||
}
|
||
}
|
||
out := make([]string, height)
|
||
for r := range dots {
|
||
var b strings.Builder
|
||
for c, d := range dots[r] {
|
||
if owner[r][c] < 0 {
|
||
b.WriteString(" ")
|
||
continue
|
||
}
|
||
color := lineColors[owner[r][c]%len(lineColors)]
|
||
if area {
|
||
color = gradient(float64(height-1-r) / float64(max(1, height-1)))
|
||
}
|
||
b.WriteString(lipgloss.NewStyle().Foreground(color).Render(string(0x2800 + d)))
|
||
}
|
||
out[r] = b.String()
|
||
}
|
||
return out
|
||
}
|
||
|
||
// resample fits values into n points: the most recent n, each the average of
|
||
// what falls in it, the left padded with no sample when there are fewer.
|
||
func resample(vals []float64, n int) []float64 {
|
||
out := make([]float64, n)
|
||
for i := range out {
|
||
out[i] = math.NaN()
|
||
}
|
||
if len(vals) == 0 {
|
||
return out
|
||
}
|
||
if len(vals) <= n {
|
||
copy(out[n-len(vals):], vals)
|
||
return out
|
||
}
|
||
per := float64(len(vals)) / float64(n)
|
||
for i := range out {
|
||
from, to := int(float64(i)*per), int(float64(i+1)*per)
|
||
sum, k := 0.0, 0
|
||
for _, v := range vals[from:max(to, from+1)] {
|
||
if !math.IsNaN(v) {
|
||
sum += v
|
||
k++
|
||
}
|
||
}
|
||
if k > 0 {
|
||
out[i] = sum / float64(k)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
var sparkRunes = []rune("▁▂▃▄▅▆▇█")
|
||
|
||
// sparkline is values in one row of block characters, coloured by height.
|
||
func sparkline(vals []float64, width int, top float64) string {
|
||
vals = resample(vals, width)
|
||
if top <= 0 {
|
||
for _, v := range vals {
|
||
if !math.IsNaN(v) && v > top {
|
||
top = v
|
||
}
|
||
}
|
||
}
|
||
if top <= 0 {
|
||
top = 1
|
||
}
|
||
var b strings.Builder
|
||
for _, v := range vals {
|
||
if math.IsNaN(v) {
|
||
b.WriteString(" ")
|
||
continue
|
||
}
|
||
f := math.Max(0, math.Min(1, v/top))
|
||
r := sparkRunes[int(math.Round(f*float64(len(sparkRunes)-1)))]
|
||
b.WriteString(lipgloss.NewStyle().Foreground(gradient(f)).Render(string(r)))
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// meter is a fraction as a bar of width cells, btop's ■, coloured by level.
|
||
func meter(f float64, width int) string {
|
||
f = math.Max(0, math.Min(1, f))
|
||
n := int(math.Round(f * float64(width)))
|
||
var b strings.Builder
|
||
for i := 0; i < width; i++ {
|
||
if i < n {
|
||
b.WriteString(lipgloss.NewStyle().Foreground(gradient(float64(i) / float64(max(1, width-1)))).Render("■"))
|
||
} else {
|
||
b.WriteString(sDim.Render("■"))
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// chartBox is a chart with its title and current value above it, and the
|
||
// window's span below: what a panel of the monitor shows.
|
||
func chartBox(title, now string, series []chartSeries, width, height int, top float64, span string) string {
|
||
var b strings.Builder
|
||
head := mdBold.Render(title)
|
||
if now != "" {
|
||
head += " " + now
|
||
}
|
||
if len(series) > 1 {
|
||
var leg []string
|
||
for i, s := range series {
|
||
if i == 6 {
|
||
leg = append(leg, sDim.Render(fmt.Sprintf("+%d", len(series)-6)))
|
||
break
|
||
}
|
||
leg = append(leg, lipgloss.NewStyle().Foreground(lineColors[i%len(lineColors)]).Render("━ ")+truncate(s.name, 18))
|
||
}
|
||
head += " " + strings.Join(leg, " ")
|
||
}
|
||
b.WriteString(truncateWidth(head, width) + "\n")
|
||
for _, l := range brailleChart(series, width, height, top) {
|
||
b.WriteString(l + "\n")
|
||
}
|
||
left, right := "-"+span, "now"
|
||
gap := max(1, width-len(left)-len(right))
|
||
b.WriteString(sDim.Render(left + strings.Repeat("─", gap) + right))
|
||
return b.String()
|
||
}
|