| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use chrono::{DateTime, Utc}; |
| 8 |
use serde::Deserialize; |
| 9 |
use sqlx::PgPool; |
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
#[derive(Debug, Clone, Copy, Deserialize)] |
| 22 |
#[serde(rename_all = "snake_case")] |
| 23 |
pub enum AlertKind { |
| 24 |
Health, |
| 25 |
Tls, |
| 26 |
Dns, |
| 27 |
Whois, |
| 28 |
Latency, |
| 29 |
Cors, |
| 30 |
Backup, |
| 31 |
Peer, |
| 32 |
Route, |
| 33 |
Scan, |
| 34 |
Monitoring, |
| 35 |
|
| 36 |
|
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
|
| 41 |
Security, |
| 42 |
|
| 43 |
|
| 44 |
|
| 45 |
|
| 46 |
Mail, |
| 47 |
} |
| 48 |
|
| 49 |
impl AlertKind { |
| 50 |
|
| 51 |
pub fn as_str(self) -> &'static str { |
| 52 |
match self { |
| 53 |
Self::Health => "health", |
| 54 |
Self::Tls => "tls", |
| 55 |
Self::Dns => "dns", |
| 56 |
Self::Whois => "whois", |
| 57 |
Self::Latency => "latency", |
| 58 |
Self::Cors => "cors", |
| 59 |
Self::Backup => "backup", |
| 60 |
Self::Peer => "peer", |
| 61 |
Self::Route => "route", |
| 62 |
Self::Scan => "scan", |
| 63 |
Self::Monitoring => "monitoring", |
| 64 |
Self::Security => "security", |
| 65 |
Self::Mail => "mail", |
| 66 |
} |
| 67 |
} |
| 68 |
} |
| 69 |
|
| 70 |
|
| 71 |
#[derive(Debug, Clone, Copy, Deserialize)] |
| 72 |
#[serde(rename_all = "snake_case")] |
| 73 |
pub enum AlertSeverity { |
| 74 |
Critical, |
| 75 |
Warning, |
| 76 |
Info, |
| 77 |
} |
| 78 |
|
| 79 |
impl AlertSeverity { |
| 80 |
|
| 81 |
pub fn as_str(self) -> &'static str { |
| 82 |
match self { |
| 83 |
Self::Critical => "critical", |
| 84 |
Self::Warning => "warning", |
| 85 |
Self::Info => "info", |
| 86 |
} |
| 87 |
} |
| 88 |
} |
| 89 |
|
| 90 |
|
| 91 |
pub struct NewAlert<'a> { |
| 92 |
pub source: &'a str, |
| 93 |
pub kind: AlertKind, |
| 94 |
pub severity: AlertSeverity, |
| 95 |
pub title: &'a str, |
| 96 |
pub body: &'a str, |
| 97 |
pub dedup_key: Option<&'a str>, |
| 98 |
pub details: Option<&'a serde_json::Value>, |
| 99 |
} |
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
|
| 104 |
#[tracing::instrument(skip_all, fields(source = alert.source, kind = alert.kind.as_str(), severity = alert.severity.as_str()))] |
| 105 |
pub async fn insert_alert(pool: &PgPool, alert: &NewAlert<'_>) -> Result<i64, sqlx::Error> { |
| 106 |
let id: i64 = sqlx::query_scalar( |
| 107 |
"INSERT INTO admin_alerts (source, kind, severity, title, body, dedup_key, details) |
| 108 |
VALUES ($1, $2, $3, $4, $5, $6, $7) |
| 109 |
RETURNING id", |
| 110 |
) |
| 111 |
.bind(alert.source) |
| 112 |
.bind(alert.kind.as_str()) |
| 113 |
.bind(alert.severity.as_str()) |
| 114 |
.bind(alert.title) |
| 115 |
.bind(alert.body) |
| 116 |
.bind(alert.dedup_key) |
| 117 |
.bind(alert.details) |
| 118 |
.fetch_one(pool) |
| 119 |
.await?; |
| 120 |
Ok(id) |
| 121 |
} |
| 122 |
|
| 123 |
|
| 124 |
#[tracing::instrument(skip_all)] |
| 125 |
pub async fn mark_emailed(pool: &PgPool, id: i64) -> Result<(), sqlx::Error> { |
| 126 |
sqlx::query("UPDATE admin_alerts SET emailed = true WHERE id = $1") |
| 127 |
.bind(id) |
| 128 |
.execute(pool) |
| 129 |
.await?; |
| 130 |
Ok(()) |
| 131 |
} |
| 132 |
|
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
|
| 139 |
|
| 140 |
|
| 141 |
#[tracing::instrument(skip_all)] |
| 142 |
pub async fn alerted_since( |
| 143 |
pool: &PgPool, |
| 144 |
dedup_key: &str, |
| 145 |
since: DateTime<Utc>, |
| 146 |
) -> Result<bool, sqlx::Error> { |
| 147 |
let exists = sqlx::query_scalar::<_, bool>( |
| 148 |
"SELECT EXISTS(SELECT 1 FROM admin_alerts WHERE dedup_key = $1 AND received_at >= $2)", |
| 149 |
) |
| 150 |
.bind(dedup_key) |
| 151 |
.bind(since) |
| 152 |
.fetch_one(pool) |
| 153 |
.await?; |
| 154 |
Ok(exists) |
| 155 |
} |
| 156 |
|