Files
3x-ui-agent/internal/web/service/probe/read.go
T

174 lines
5.6 KiB
Go

package probe
import (
"database/sql"
"strconv"
)
// Span is one chart window as the read side answers it (hub db::Span):
// the window start in unix seconds, the seconds each returned point
// covers, and whether the window reaches past the minute tier and is
// read from the hourly one.
type Span struct {
Since int64
Step int64
Hourly bool
}
// ComputeSpan reproduces hub api::span(): the point budget defaults to
// 1440 (a day of minutes) and the caller's points may only lower it,
// clamped to [60, 1440]; windows longer than DetailDays read hourly
// rows, so the step is a whole number of minutes — or of hours past the
// detail window, so no hourly row straddles two points; the window
// start is aligned down to a step boundary.
func ComputeSpan(hours int64, points *int64, now int64) Span {
const samples = 1440
budget := int64(samples)
if points != nil {
budget = *points
}
if budget < 60 {
budget = 60
}
if budget > samples {
budget = samples
}
hourly := hours > DetailDays*24
unit := int64(60)
if hourly {
unit = 3600
}
// Rounded up, or the budget would not be one.
q := (hours*3600/unit + budget - 1) / budget
if q < 1 {
q = 1
}
step := unit * q
return Span{
Since: euclidDiv(now-hours*3600, step) * step,
Step: step,
Hourly: hourly,
}
}
func euclidDiv(a, b int64) int64 {
q := a / b
if a%b != 0 && (a < 0) != (b < 0) {
q--
}
return q
}
// MetricRow is one thinned history point, in the hub's key order and
// with the hub's value semantics: means for the gauge columns (byte
// columns truncated, count columns rounded — the asymmetry is the
// hub's, kept), maxima for the peak columns, and `minutes` counting
// the minute rows folded into the point.
type MetricRow struct {
Ts int64 `json:"ts"`
Cpu float64 `json:"cpu"`
MemUsed int64 `json:"mem_used"`
DiskUsed int64 `json:"disk_used"`
NetRx int64 `json:"net_rx"`
NetTx int64 `json:"net_tx"`
NetRxMax int64 `json:"net_rx_max"`
NetTxMax int64 `json:"net_tx_max"`
CpuMax float64 `json:"cpu_max"`
Minutes int64 `json:"minutes"`
SwapUsed int64 `json:"swap_used"`
Tcp int64 `json:"tcp"`
Udp int64 `json:"udp"`
Procs int64 `json:"procs"`
}
// RolledWatermark reads the roll-up watermark (unix seconds of the
// first hour not yet folded). ok is false when no pass has run yet.
func (s *Store) RolledWatermark() (int64, bool, error) {
var raw string
err := s.db.QueryRow(`SELECT value FROM probe_setting WHERE key = ?`, rolledKey).Scan(&raw)
if err == sql.ErrNoRows {
return 0, false, nil
}
if err != nil {
return 0, false, err
}
n, perr := strconv.ParseInt(raw, 10, 64)
if perr != nil {
return 0, false, nil
}
return n, true, nil
}
const metricColumns = `(MIN(ts)/?3)*?3, AVG(cpu), CAST(AVG(mem_used) AS INTEGER),
CAST(AVG(disk_used) AS INTEGER),
CAST(AVG(net_rx) AS INTEGER), CAST(AVG(net_tx) AS INTEGER),
MAX(MAX(net_rx, net_rx_max)), MAX(MAX(net_tx, net_tx_max)),
MAX(MAX(cpu, cpu_max)), COUNT(*), CAST(AVG(swap_used) AS INTEGER),
CAST(ROUND(AVG(tcp)) AS INTEGER), CAST(ROUND(AVG(udp)) AS INTEGER),
CAST(ROUND(AVG(procs)) AS INTEGER)`
// MetricHistory answers one agent's history window (hub Db::metrics).
// Minute windows group minute rows directly. Hourly windows merge the
// folded hourly tier (up to the watermark) with the not-yet-folded
// minute rows and the newest week of minute rows, weighted by each
// row's minute count. Both statements run in one read snapshot so a
// roll-up committing in between cannot double-count an hour.
func (s *Store) MetricHistory(agentID int64, sp Span) ([]MetricRow, error) {
tx, err := s.db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
var rows *sql.Rows
if !sp.Hourly {
rows, err = tx.Query(
`SELECT `+metricColumns+`
FROM probe_metric WHERE agent_id=?1 AND ts>=?2 GROUP BY ts/?3 ORDER BY ts/?3`,
agentID, sp.Since, sp.Step)
} else {
rolled := int64(-1) << 62 // hub: unwrap_or(i64::MIN)
var raw sql.NullString
if qerr := tx.QueryRow(`SELECT value FROM probe_setting WHERE key = ?`, rolledKey).Scan(&raw); qerr != nil && qerr != sql.ErrNoRows {
return nil, qerr
} else if raw.Valid {
if n, perr := strconv.ParseInt(raw.String, 10, 64); perr == nil {
rolled = n
}
}
rows, err = tx.Query(
`SELECT (MIN(ts)/?3)*?3, SUM(cpu*w)/SUM(w), SUM(mem_used*w)/SUM(w), SUM(disk_used*w)/SUM(w),
SUM(net_rx*w)/SUM(w), SUM(net_tx*w)/SUM(w), MAX(rx_max), MAX(tx_max), MAX(cpu_top), SUM(w),
SUM(swap_used*w)/SUM(w), CAST(ROUND(SUM(tcp*w)*1.0/SUM(w)) AS INTEGER),
CAST(ROUND(SUM(udp*w)*1.0/SUM(w)) AS INTEGER),
CAST(ROUND(SUM(procs*w)*1.0/SUM(w)) AS INTEGER)
FROM (SELECT ts, minutes AS w, cpu, mem_used, disk_used, net_rx, net_tx,
net_rx_max AS rx_max, net_tx_max AS tx_max, cpu_max AS cpu_top,
swap_used, tcp, udp, procs
FROM probe_metric_hour WHERE agent_id=?1 AND ts>=?2 AND ts<?4
UNION ALL
SELECT ts, 1, cpu, mem_used, disk_used, net_rx, net_tx,
MAX(net_rx, net_rx_max), MAX(net_tx, net_tx_max), MAX(cpu, cpu_max),
swap_used, tcp, udp, procs
FROM probe_metric WHERE agent_id=?1
AND ts>=MAX(?2, ?4, (SELECT MAX(ts) FROM probe_metric WHERE agent_id=?1) - ?5))
GROUP BY ts/?3 ORDER BY ts/?3`,
agentID, sp.Since, sp.Step, rolled, DetailDays*86400)
}
if err != nil {
return nil, err
}
defer rows.Close()
out := []MetricRow{}
for rows.Next() {
var r MetricRow
if err := rows.Scan(&r.Ts, &r.Cpu, &r.MemUsed, &r.DiskUsed, &r.NetRx, &r.NetTx,
&r.NetRxMax, &r.NetTxMax, &r.CpuMax, &r.Minutes, &r.SwapUsed, &r.Tcp, &r.Udp, &r.Procs); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}