vdi-cloud/platform/internal/server/terminal_alerts.go

53 lines
3.4 KiB
Go

package server
import (
"context"
"github.com/jmoiron/sqlx"
"vdi-cloud/platform/internal/store"
)
// Column names are exclusively from this allowlist, never request text.
func terminalAlertColumn(metric string) string {
return map[string]string{"terminal_cpu": "cpu_percent", "terminal_memory": "memory_percent", "terminal_temperature": "soc_temperature_c"}[metric]
}
func terminalAlertQuery(column string) string {
return `SELECT t.id,t.name,false unavailable,m.sampled_at,
CASE WHEN m.supported AND t.last_seen>now()-interval '90 seconds' AND m.sampled_at>now()-interval '90 seconds'
AND m.sampled_at<=now() THEN m.` + column + ` ELSE NULL END value
FROM terminals t LEFT JOIN terminal_metrics_current m ON m.terminal_id=t.id
WHERE t.enabled AND t.token_hash IS NOT NULL AND t.status!='unregistered'`
}
// Non-terminal metrics keep a null freshness flag for backward compatibility.
// A value becomes unknown on missing/old samples, unsupported agents or revocation.
const terminalAlertFreshSQL = `CASE WHEN metric IN ('terminal_cpu','terminal_memory','terminal_temperature') THEN EXISTS(
SELECT 1 FROM terminals t JOIN terminal_metrics_current m ON m.terminal_id=t.id
WHERE t.id=alerts.target_id AND t.enabled AND t.token_hash IS NOT NULL AND t.status!='unregistered'
AND t.last_seen>now()-interval '90 seconds' AND m.supported AND m.sampled_at>now()-interval '90 seconds' AND m.sampled_at<=now()
AND CASE alerts.metric WHEN 'terminal_cpu' THEN m.cpu_percent WHEN 'terminal_memory' THEN m.memory_percent
WHEN 'terminal_temperature' THEN m.soc_temperature_c END IS NOT NULL) ELSE NULL END`
func evaluateTerminalHigh(ctx context.Context, tx *sqlx.Tx, rule alertRule, target observation, column string) error {
// Count source time, not repeated evaluator ticks. A source gap over 90s or
// an intervening null/normal history sample starts a new continuous segment.
// The history range is at most 90 seconds in the non-gap case and uses its PK.
_, err := tx.ExecContext(ctx, `INSERT INTO alert_states(rule_id,target_id,first_seen,last_sample_at) VALUES($1,$2,GREATEST($3::timestamptz,$5::timestamptz),$3)
ON CONFLICT(rule_id,target_id) DO UPDATE SET first_seen=CASE
WHEN alert_states.last_sample_at IS NULL OR EXCLUDED.last_sample_at-alert_states.last_sample_at>interval '90 seconds'
OR EXISTS(SELECT 1 FROM terminal_metric_samples s WHERE s.terminal_id=$2
AND s.sampled_at>GREATEST(alert_states.last_sample_at,EXCLUDED.last_sample_at-interval '90 seconds') AND s.sampled_at<=EXCLUDED.last_sample_at
AND (s.`+column+` IS NULL OR s.`+column+`<$4)) THEN EXCLUDED.first_seen ELSE alert_states.first_seen END,
last_sample_at=EXCLUDED.last_sample_at
WHERE alert_states.last_sample_at IS NULL OR EXCLUDED.last_sample_at>alert_states.last_sample_at`, rule.ID, target.ID, target.SampledAt, rule.Threshold, rule.UpdatedAt)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO alerts(id,rule_id,rule_name,metric,target_id,target_name,value,threshold,started_at)
SELECT $1,$2,$3,$4,$5,$6,$7,$8,first_seen FROM alert_states WHERE rule_id=$2 AND target_id=$5
AND ($9=0 OR last_sample_at-first_seen>=make_interval(secs=>$9) OR EXISTS(SELECT 1 FROM alerts WHERE rule_id=$2 AND target_id=$5 AND status='firing'))
ON CONFLICT(rule_id,target_id) WHERE status='firing' DO UPDATE SET value=EXCLUDED.value`,
store.ID("alert_"), rule.ID, rule.Name, rule.Metric, target.ID, target.Name, *target.Value, rule.Threshold, rule.HoldSeconds)
return err
}