diff --git a/awg-ui/mock-server.ts b/awg-ui/mock-server.ts index b617d4f..6ceee57 100644 --- a/awg-ui/mock-server.ts +++ b/awg-ui/mock-server.ts @@ -97,7 +97,7 @@ app.get("/awg/status", (_q, r) => r.json({ up: true, peers: users.length })); // Монитор: «установка» и «удаление» Prometheus с ходом процесса (как лог CLI на реальном сервере). const MON_LOGS = { - install: ["1/4 установка пакета prometheus", "Reading package lists…", "Setting up prometheus (2.45.0)…", "2/4 токен и конфиг", "3/4 слушаем только 127.0.0.1:9090, хранение 15d", "4/4 запуск", "✓ Prometheus работает на 127.0.0.1:9090, метрики панели скрейпятся каждые 15 с."], + install: ["1/4 установка пакета prometheus", "Reading package lists…", "Setting up prometheus (2.45.0)…", "2/4 токен и конфиг", "3/4 слушаем только 127.0.0.1:9090, хранение 31d", "4/4 запуск", "✓ Prometheus работает на 127.0.0.1:9090, метрики панели скрейпятся каждые 15 с."], remove: ["1/3 остановка сервиса", "2/3 токен", "3/3 удаление пакета и накопленных данных", "Removing prometheus (2.45.0)…", "Purging configuration files…", "✓ Prometheus удалён вместе с накопленными графиками."], }; let monJob: { action: "install" | "remove"; start: number } | null = null; @@ -118,7 +118,7 @@ for (const action of ["install", "remove"] as const) { app.post(`/ui/monitor/${action}`, (_q, r) => { if (!monRunning()) monJob = { action, start: Date.now() }; r.status(202).json({ running: true, action }); }); } app.get("/ui/monitor/overview", (q, r) => { - const R: Record = { "1h": [3600, 15], "6h": [21600, 60], "24h": [86400, 300] }; + const R: Record = { "1h": [3600, 15], "6h": [21600, 60], "24h": [86400, 300], "7d": [604800, 1800], "30d": [2592000, 7200] }; const [sec, step] = R[String(q.query.range ?? "1h")] ?? R["1h"]; const to = Math.floor(Date.now() / step) * step, from = to - sec; const pts = (f: (i: number, t: number) => number): [number, number][] => Array.from({ length: Math.floor(sec / step) + 1 }, (_, i) => [from + i * step, Math.max(0, f(i, from + i * step))]); @@ -136,11 +136,13 @@ app.get("/ui/monitor/overview", (q, r) => { const id = Number(q.query.server ?? 0); const k = id === 0 ? 0.7 : 0.3; const down1 = scale(down, k), up1 = scale(up, k); + const used = (a: [number, number][]) => a.reduce((n, [, v]) => n + v, 0) * step / 8; // бит/с × с → байты if (id === 3) return r.json({ range: q.query.range ?? "1h", step, from, to, servers: [], total: { down: [], up: [], online: [], heavy: [], drops: [], cpu: [], mem: [] } }); r.json({ range: q.query.range ?? "1h", step, from, to, servers: [{ id, name: id === 0 ? "VPN (mock)" : "Frankfurt", ok: true, down: down1.at(-1)![1], up: up1.at(-1)![1], online: id === 0 ? 7 : 3, heavy: id === 0 ? heavy.at(-1)![1] : 0, congested: id === 0 && heavy.at(-1)![1] > 0, - cpu: id === 0 ? cpu.at(-1)![1] : cpu.at(-1)![1] * 0.5, mem: id === 0 ? mem.at(-1)![1] : 31, cores: id === 0 ? 4 : 2, mem_total: (id === 0 ? 4 : 2) * 2 ** 30 }], + cpu: id === 0 ? cpu.at(-1)![1] : cpu.at(-1)![1] * 0.5, mem: id === 0 ? mem.at(-1)![1] : 31, cores: id === 0 ? 4 : 2, mem_total: (id === 0 ? 4 : 2) * 2 ** 30, + used_down: used(down1), used_up: used(up1) }], total: { down: down1, up: up1, online: scale(online, k), heavy: id === 0 ? heavy : scale(heavy, 0), drops: id === 0 ? drops : scale(drops, 0), cpu: id === 0 ? cpu : scale(cpu, 0.5), mem: id === 0 ? mem : scale(mem, 0.55) } }); }); diff --git a/awg-ui/monitor.ts b/awg-ui/monitor.ts index dbdef08..a8ba859 100644 --- a/awg-ui/monitor.ts +++ b/awg-ui/monitor.ts @@ -16,11 +16,18 @@ type Ctrl = (method: string, urlPath: string, body?: unknown) => Promise<{ statu const PROM_URL = process.env.PROM_URL ?? "http://127.0.0.1:9090"; // Шаг подбираем так, чтобы на графике было ~250–360 точек; окно rate — минимум два скрейпа (15 с). +// Дальше 30 дней не заглядываем: столько Prometheus хранит (RETENTION в cli/src/monitor.ts). const RANGES: Record = { - "1h": { seconds: 3600, step: 15 }, - "6h": { seconds: 21600, step: 60 }, - "24h": { seconds: 86400, step: 300 }, + "1h": { seconds: 3600, step: 15 }, + "6h": { seconds: 21600, step: 60 }, + "24h": { seconds: 86400, step: 300 }, + "7d": { seconds: 604800, step: 1800 }, + "30d": { seconds: 2592000, step: 7200 }, }; +// Окно rate() — не короче шага: иначе на неделе/месяце точка была бы случайной двухминутной выборкой +// из двух часов, а не средним за них. Значения «сейчас» при этом считаются отдельно, по окну 2m. +const RATE_WINDOW = "2m"; +const withWindow = (q: string, w: string) => q.split(`[${RATE_WINDOW}]`).join(`[${w}]`); type Point = [number, number]; type Series = Map; // server_id → ряд @@ -43,6 +50,16 @@ const QUERIES = { } as const; type Key = keyof typeof QUERIES; +async function instantQuery(query: string, time: number): Promise> { + const r = await axios.get(`${PROM_URL}/api/v1/query`, { params: { query, time }, timeout: 8000 }); + const out = new Map(); + for (const m of (r.data?.data?.result ?? []) as { metric: Record; value: [number, string] }[]) { + const id = Number(m.metric.server_id), v = Number(m.value[1]); + if (Number.isInteger(id) && Number.isFinite(v)) out.set(id, v); + } + return out; +} + async function rangeQuery(query: string, start: number, end: number, step: number): Promise { const r = await axios.get(`${PROM_URL}/api/v1/query_range`, { params: { query, start, end, step }, timeout: 8000, @@ -116,12 +133,17 @@ export function createMonitor(deps: { ctrlLocal: Ctrl; localEnabled: boolean }) router.get("/overview", async (req: Request, res: Response) => { const range = RANGES[String(req.query.range ?? "1h")]; - if (!range) { res.status(400).json({ error: "range: 1h, 6h или 24h" }); return; } + if (!range) { res.status(400).json({ error: "range: 1h, 6h, 24h, 7d или 30d" }); return; } const end = Math.floor(Date.now() / 1000); const start = end - range.seconds; try { const keys = Object.keys(QUERIES) as Key[]; - const got = await Promise.all(keys.map(k => rangeQuery(QUERIES[k], start, end, range.step))); + const window = range.step > 120 ? `${range.step}s` : RATE_WINDOW; + const got = await Promise.all(keys.map(k => rangeQuery(withWindow(QUERIES[k], window), start, end, range.step))); + // Окно расширено — «сейчас» с конца ряда было бы средним за шаг; берём мгновенные значения по 2m. + const rated: Key[] = ["down", "up", "cpu"]; + const now = window === RATE_WINDOW ? null + : new Map(await Promise.all(rated.map(async k => [k, await instantQuery(QUERIES[k], end)] as const))); // Вкладка показывает один сервер — тот, что выбран в сервер-баре (?server=). const only = Number.isInteger(Number(req.query.server)) && req.query.server !== undefined ? Number(req.query.server) : null; const s = Object.fromEntries(keys.map((k, i) => [k, @@ -130,19 +152,27 @@ export function createMonitor(deps: { ctrlLocal: Ctrl; localEnabled: boolean }) // Список серверов — из любого ряда, где они есть; «последнее значение» берём с конца окна. const ids = new Set(); for (const k of keys) for (const id of s[k].keys()) ids.add(id); + // Объём за весь период: increase() по тем же монотонным счётчикам интерфейса (сбросы счётчика он учитывает) + const [usedDown, usedUp] = await Promise.all([ + instantQuery(`sum by (server_id, server) (increase(awg_link_transmit_bytes_total[${range.seconds}s]))`, end), + instantQuery(`sum by (server_id, server) (increase(awg_link_receive_bytes_total[${range.seconds}s]))`, end), + ]); + const cur = (k: Key, id: number) => now?.has(k) ? (now.get(k)!.get(id) ?? null) : last(s[k].get(id)); const servers = [...ids].sort((a, b) => a - b).map(id => ({ id, name: (s.scrape_ok.get(id) ?? s.down.get(id) ?? s.online.get(id))?.name ?? String(id), ok: last(s.scrape_ok.get(id)) === 1, - down: last(s.down.get(id)), - up: last(s.up.get(id)), + down: cur("down", id), + up: cur("up", id), online: last(s.online.get(id)), heavy: last(s.heavy.get(id)), congested: last(s.congested.get(id)) === 1, - cpu: last(s.cpu.get(id)), + cpu: cur("cpu", id), mem: last(s.mem.get(id)), cores: last(s.cores.get(id)), mem_total: last(s.mem_total.get(id)), + used_down: usedDown.get(id) ?? null, + used_up: usedUp.get(id) ?? null, })); diff --git a/awg-ui/src/tabs/monitor/index.tsx b/awg-ui/src/tabs/monitor/index.tsx index 59e9d78..8357f80 100644 --- a/awg-ui/src/tabs/monitor/index.tsx +++ b/awg-ui/src/tabs/monitor/index.tsx @@ -21,12 +21,13 @@ interface Overview { servers: { id: number; name: string; ok: boolean; down: number | null; up: number | null; online: number | null; heavy: number | null; congested: boolean; cpu: number | null; mem: number | null; cores: number | null; mem_total: number | null; + used_down: number | null; used_up: number | null; // байт за весь период }[]; total: Series; } interface Series { down: Pts; up: Pts; online: Pts; heavy: Pts; drops: Pts; cpu: Pts; mem: Pts } -const RANGES: [string, string][] = [['1h', '1 час'], ['6h', '6 часов'], ['24h', '24 часа']]; +const RANGES: [string, string][] = [['1h', '1 час'], ['6h', '6 часов'], ['24h', '24 часа'], ['7d', '7 дней'], ['30d', '30 дней']]; const nf = (v: number, d = 1) => v.toLocaleString('ru-RU', { maximumFractionDigits: d }); // Скорость в битах/с → «12,4 Мбит/с» @@ -72,6 +73,12 @@ function JobProgress({ job, starting }: { job: Status['job']; starting: boolean ); } const gb = (b: number) => `${nf(b / 2 ** 30, 1)} ГБ`; +// Объём трафика — десятичные единицы, как считают провайдеры: 1 ТБ = 10¹² байт +function vol(b: number): string { + if (b >= 1e12) return `${nf(b / 1e12, 2)} ТБ`; + if (b >= 1e9) return `${nf(b / 1e9, 1)} ГБ`; + return `${nf(b / 1e6, 0)} МБ`; +} export default function MonitorPage({ token, showMsg }: PageProps) { const [status, setStatus] = useState(null); @@ -130,7 +137,7 @@ export default function MonitorPage({ token, showMsg }: PageProps) { }, [pending, status?.job?.running, loadStatus]); const run = async (action: 'install' | 'remove') => { - if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 15 дней) сотрутся. Панель продолжит отдавать /metrics.')) return; + if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 31 дня) сотрутся. Панель продолжит отдавать /metrics.')) return; setPending(action); try { await apiFetch('POST', `/ui/monitor/${action}`, token); await loadStatus(); } catch (e: any) { setPending(null); showMsg(e?.response?.data?.error ?? (action === 'install' ? 'Не удалось запустить установку' : 'Не удалось запустить удаление')); } @@ -203,7 +210,7 @@ export default function MonitorPage({ token, showMsg }: PageProps) { работает

- Стоит на этом сервере и слушает только 127.0.0.1:9090, графики хранит 15 дней. Удаление остановит его, снимет + Стоит на этом сервере и слушает только 127.0.0.1:9090, графики хранит 31 день. Удаление остановит его, снимет пакет и сотрёт накопленные данные.

{status.can_install ? ( @@ -228,9 +235,11 @@ export default function MonitorPage({ token, showMsg }: PageProps) { const one = ov?.servers[0]; const data = ov?.total ?? null; const cur = data && { - down: lastOf(data.down), up: lastOf(data.up), online: lastOf(data.online), heavy: lastOf(data.heavy), + // Скорость «сейчас» — от сервера: на неделе/месяце последняя точка ряда — среднее за шаг, а не текущее + down: one?.down ?? lastOf(data.down), up: one?.up ?? lastOf(data.up), online: lastOf(data.online), heavy: lastOf(data.heavy), busy: !!one?.congested, }; + const used = one?.used_down != null && one.used_up != null ? one.used_down + one.used_up : null; // Одна единица на всю ось (Мбит/с, а не «50 Мбит» у каждой риски): выбирается по максимуму ряда const peak = data ? Math.max(0, ...data.down.map(p => p[1]), ...data.up.map(p => p[1])) : 0; const unit = peak >= 1e9 ? { name: 'Гбит/с', div: 1e9 } : peak >= 1e6 ? { name: 'Мбит/с', div: 1e6 } : peak >= 1e3 ? { name: 'Кбит/с', div: 1e3 } : { name: 'бит/с', div: 1 }; @@ -251,33 +260,57 @@ export default function MonitorPage({ token, showMsg }: PageProps) {
- Загрузка сейчас - {cur?.down != null ? rate(cur.down) : '—'} + Трафик сейчас + {/* Стрелки окрашены как серии графика «Трафик» ниже */} +
+ + ↓ + {cur?.down != null ? rate(cur.down) : '—'} + + + ↑ + {cur?.up != null ? rate(cur.up) : '—'} + +
+ + {used != null ? vol(used) : '—'} + потрачено за {RANGES.find(([id]) => id === range)?.[1]} +
- Отдача сейчас - {cur?.up != null ? rate(cur.up) : '—'} + Устройства + {/* Точки окрашены как серии графика «Устройства» ниже */} +
+ + + + +
- Устройств онлайн - {cur?.online != null ? nf(cur.online, 0) : '—'} -
-
- В фоне - {cur?.heavy != null ? nf(cur.heavy, 0) : '—'} - {cur?.busy ? 'канал занят' : 'канал свободен'} -
-
- Процессор - {one?.cpu != null ? `${nf(one.cpu, 0)} %` : '—'} - - {one?.cores ? `${nf(one.cores, 0)} ядер` : ''} -
-
- Память - {one?.mem != null ? `${nf(one.mem, 0)} %` : '—'} - - {one?.mem_total ? `из ${gb(one.mem_total)}` : ''} + Нагрузка +
+
+ + {one?.cpu != null ? `${nf(one.cpu, 0)} %` : '—'} + процессор{one?.cores ? ` · ${nf(one.cores, 0)} ядер` : ''} + + +
+
+ + {one?.mem != null ? `${nf(one.mem, 0)} %` : '—'} + память{one?.mem_total ? ` · из ${gb(one.mem_total)}` : ''} + + +
+
diff --git a/awg-ui/src/tabs/monitor/monitor.css b/awg-ui/src/tabs/monitor/monitor.css index a06f52a..6d643bc 100644 --- a/awg-ui/src/tabs/monitor/monitor.css +++ b/awg-ui/src/tabs/monitor/monitor.css @@ -56,6 +56,12 @@ LICENSE file in the root directory of this source tree. .mon-tile-label { font-size: 12px; color: var(--neutral-text); } .mon-tile-value { font-size: 20px; font-weight: 500; color: var(--on-surface); } .mon-tile-sub { font-size: 12px; color: var(--neutral-text); } +.mon-tile-rates { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; } +.mon-tile-rate { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; white-space: nowrap; } +.mon-tile-dir { font-size: 16px; } +.mon-tile-total { margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--outline-variant); } +.mon-tile-meter { display: flex; flex-direction: column; gap: 4px; } +.mon-tile-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; align-self: center; } .mon-charts { display: grid; grid-template-columns: 1fr; gap: 16px; margin-bottom: 16px; } @media (min-width: 1000px) { diff --git a/cli/src/monitor.ts b/cli/src/monitor.ts index c37a175..5bedd68 100644 --- a/cli/src/monitor.ts +++ b/cli/src/monitor.ts @@ -20,7 +20,7 @@ export const MONITOR_HASH_FILE = `${AWG_DIR}/monitor_token.sha256`; const DEFAULTS_FILE = "/etc/default/prometheus"; const MARKER = "# managed by forgetting"; const LISTEN = "127.0.0.1:9090"; -const RETENTION = "15d"; +const RETENTION = "31d"; // с запасом на вид «30 дней» во вкладке «Монитор» const log = (s: string) => console.log(` ${s}`); function fail(s: string): never { console.error(`\n ✗ ${s}\n`); process.exit(1); } diff --git a/internal/cli/monitor.go b/internal/cli/monitor.go index da11bbb..2ff3b0d 100644 --- a/internal/cli/monitor.go +++ b/internal/cli/monitor.go @@ -29,7 +29,7 @@ const ( promDefaults = "/etc/default/prometheus" promMarker = "# managed by forgetting" promListen = "127.0.0.1:9090" - promRetention = "15d" + promRetention = "31d" // с запасом на вид «30 дней» во вкладке «Монитор» ) func mlog(s string) { fmt.Println(" " + s) } diff --git a/internal/ui/monitor.go b/internal/ui/monitor.go index 7c83e26..011a7f2 100644 --- a/internal/ui/monitor.go +++ b/internal/ui/monitor.go @@ -4,7 +4,7 @@ package ui // GET /ui/monitor — готов ли Prometheus, можно ли поставить его кнопкой, ход установки // POST /ui/monitor/install — попросить локальный awg-ctrl выполнить `awg-ctrl monitor install` (root) // POST /ui/monitor/remove — то же для `awg-ctrl monitor remove` -// GET /ui/monitor/overview — серии для графиков за 1 ч / 6 ч / 24 ч +// GET /ui/monitor/overview — серии для графиков за 1 ч / 6 ч / 24 ч / 7 дн / 30 дн // Панель сама пакетов не ставит и в Prometheus ходит только фиксированными запросами. import ( @@ -25,7 +25,20 @@ import ( type promRange struct{ seconds, step int64 } // Шаг — чтобы на графике было ~250–360 точек; окно rate — минимум два скрейпа (15 с). -var promRanges = map[string]promRange{"1h": {3600, 15}, "6h": {21600, 60}, "24h": {86400, 300}} +// Дальше 30 дней не заглядываем: столько Prometheus хранит (promRetention в internal/cli/monitor.go). +var promRanges = map[string]promRange{ + "1h": {3600, 15}, "6h": {21600, 60}, "24h": {86400, 300}, + "7d": {604800, 1800}, "30d": {2592000, 7200}, +} + +// Окно rate() — не короче шага: иначе на неделе/месяце точка была бы случайной двухминутной выборкой +// из двух часов, а не средним за них. Значения «сейчас» при этом считаются отдельно, по окну 2m. +const promRateWindow = "2m" + +func withWindow(q, w string) string { return strings.ReplaceAll(q, "["+promRateWindow+"]", "["+w+"]") } + +// rated — ряды, чьё «сейчас» показывает вкладка; при расширенном окне их берём мгновенным запросом. +var rated = []string{"down", "up", "cpu"} // Запросы по серверам. Трафик — со счётчиков интерфейса (монотонны), а не с сумм по пирам. var promQueries = []struct{ key, q string }{ @@ -132,6 +145,39 @@ func (m *Monitor) rangeQuery(query string, start, end, step int64) (*series, err return out, nil } +// instantQuery — значение на момент time по каждому server_id. +func (m *Monitor) instantQuery(query string, time_ int64) (map[int]float64, error) { + q := url.Values{} + q.Set("query", query) + q.Set("time", strconv.FormatInt(time_, 10)) + c := &http.Client{Transport: m.http.Transport, Timeout: 8 * time.Second} + resp, err := c.Get(m.promURL + "/api/v1/query?" + q.Encode()) + if err != nil { + return nil, errProm + } + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil || resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, errProm + } + out := map[int]float64{} + for _, r := range arr(field(decodeLikeAxios(raw), "data", "result")) { + metric := field(r, "metric") + if field(metric, "server_id") == nil { + continue + } + idf := jsToNumberAny(field(metric, "server_id")) + pair := arr(field(r, "value")) + if idf != float64(int(idf)) || len(pair) < 2 { + continue + } + if v := jsToNumberAny(pair[1]); !math.IsNaN(v) && !math.IsInf(v, 0) { + out[int(idf)] = v + } + } + return out, nil +} + // total — сумма (или максимум) рядов по меткам времени. func total(s *series, max bool) []any { acc := map[float64]float64{} @@ -246,16 +292,54 @@ func (m *Monitor) router() *Router { } rg, ok := promRanges[rangeKey] if !ok { - return c.Error(400, "range: 1h, 6h или 24h") + return c.Error(400, "range: 1h, 6h, 24h, 7d или 30d") } end := now() start := end - rg.seconds + window := promRateWindow + if rg.step > 120 { + window = strconv.FormatInt(rg.step, 10) + "s" + } got := make([]*series, len(promQueries)) - errs := make([]error, len(promQueries)) + errs := make([]error, len(promQueries)+2+len(rated)) + // Объём за весь период: increase() по тем же монотонным счётчикам (сбросы счётчика он учитывает). + // Окно расширено — «сейчас» с конца ряда было бы средним за шаг; берём мгновенные значения по 2m. + var usedDown, usedUp map[int]float64 + nowVals := map[string]map[int]float64{} + var mu sync.Mutex var wg sync.WaitGroup for i, pq := range promQueries { wg.Add(1) - go func() { defer wg.Done(); got[i], errs[i] = m.rangeQuery(pq.q, start, end, rg.step) }() + go func() { defer wg.Done(); got[i], errs[i] = m.rangeQuery(withWindow(pq.q, window), start, end, rg.step) }() + } + span := strconv.FormatInt(rg.seconds, 10) + "s" + n := len(promQueries) + wg.Add(2) + go func() { + defer wg.Done() + usedDown, errs[n] = m.instantQuery(`sum by (server_id, server) (increase(awg_link_transmit_bytes_total[`+span+`]))`, end) + }() + go func() { + defer wg.Done() + usedUp, errs[n+1] = m.instantQuery(`sum by (server_id, server) (increase(awg_link_receive_bytes_total[`+span+`]))`, end) + }() + if window != promRateWindow { + for j, k := range rated { + for _, pq := range promQueries { + if pq.key != k { + continue + } + wg.Add(1) + go func() { + defer wg.Done() + v, err := m.instantQuery(pq.q, end) + errs[n+2+j] = err + mu.Lock() + nowVals[k] = v + mu.Unlock() + }() + } + } } wg.Wait() for _, err := range errs { @@ -263,6 +347,12 @@ func (m *Monitor) router() *Router { return c.Error(502, err.Error()) } } + used := func(v map[int]float64, id int) any { + if x, ok := v[id]; ok { + return x + } + return nil + } // Вкладка показывает один сервер — выбранный в сервер-баре (?server=). if _, has := qs["server"]; has { if f := jsToNumber(strings.Join(qs["server"], ",")); f == float64(int(f)) { @@ -280,6 +370,15 @@ func (m *Monitor) router() *Router { for i, pq := range promQueries { s[pq.key] = got[i] } + cur := func(k string, id int) any { + if v, ok := nowVals[k]; ok { + if x, ok := v[id]; ok { + return x + } + return nil + } + return last(s[k].get(id)) + } ids := map[int]bool{} for _, x := range got { for _, id := range x.ids { @@ -302,11 +401,12 @@ func (m *Monitor) router() *Router { } servers = append(servers, jsjson.Object{ {Key: "id", Val: id}, {Key: "name", Val: name}, {Key: "ok", Val: last(s["scrape_ok"].get(id)) == 1.0}, - {Key: "down", Val: last(s["down"].get(id))}, {Key: "up", Val: last(s["up"].get(id))}, + {Key: "down", Val: cur("down", id)}, {Key: "up", Val: cur("up", id)}, {Key: "online", Val: last(s["online"].get(id))}, {Key: "heavy", Val: last(s["heavy"].get(id))}, - {Key: "congested", Val: last(s["congested"].get(id)) == 1.0}, {Key: "cpu", Val: last(s["cpu"].get(id))}, + {Key: "congested", Val: last(s["congested"].get(id)) == 1.0}, {Key: "cpu", Val: cur("cpu", id)}, {Key: "mem", Val: last(s["mem"].get(id))}, {Key: "cores", Val: last(s["cores"].get(id))}, {Key: "mem_total", Val: last(s["mem_total"].get(id))}, + {Key: "used_down", Val: used(usedDown, id)}, {Key: "used_up", Val: used(usedUp, id)}, }) } var rangeOut any = "1h"