53 lines
3.4 KiB
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
|
|
}
|