Skip to main content

stackpatrol_core/
event.rs

1use serde::{Deserialize, Serialize};
2
3/// Diagnostic context attached to a critical event so the control-plane
4/// rule library has something to match against. See ADR 009.
5///
6/// The agent collects per-probe context (last journald lines, `docker logs`,
7/// `df -h`, top processes, etc.), runs every item through the redaction module,
8/// caps total size to 64 KiB, and ships the result as part of the envelope.
9/// Backward-compatible: old agents emit envelopes without this field, and the
10/// backend treats absent `context` as "no diagnosis input."
11#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
12pub struct ContextBundle {
13    /// Independent context items — typically 1–4 per bundle. Order matters
14    /// for the per-bundle truncation policy (drop from the end first).
15    pub items: Vec<ContextItem>,
16    /// Aggregate redaction stats across all items.
17    #[serde(default)]
18    pub redactions: RedactionStats,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct ContextItem {
23    /// Stable identifier for the source — `"journalctl"`, `"docker_logs"`,
24    /// `"df_h"`, `"ps_top10_rss"`, `"dmesg"`, etc. Rule-library matchers key
25    /// off this for source-aware patterns.
26    pub source: String,
27    /// Plain text. Always post-redaction. Trimmed to the per-item soft cap.
28    pub content: String,
29    /// True if `content` was truncated below what the collector originally produced.
30    pub truncated: bool,
31}
32
33/// Per-category redaction counts. Useful for two things: telling the control
34/// plane what kinds of secrets the bundle had (which is itself a diagnostic
35/// signal — *"this log has JWTs in it, suggesting auth attempts"*), and for
36/// trust-page reporting (*"we redacted 12,847 secrets last month"*).
37#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
38pub struct RedactionStats {
39    #[serde(default)]
40    pub bearer_tokens: u32,
41    #[serde(default)]
42    pub api_keys: u32,
43    #[serde(default)]
44    pub jwts: u32,
45    #[serde(default)]
46    pub emails: u32,
47    #[serde(default)]
48    pub db_credentials: u32,
49    #[serde(default)]
50    pub env_secrets: u32,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "snake_case", tag = "kind")]
55pub enum Event {
56    Heartbeat {
57        uptime_secs: u64,
58    },
59    ServiceDown {
60        name: String,
61    },
62    ServiceUp {
63        name: String,
64    },
65    /// Disk usage crossed the warning threshold (default 10pp below `disk_high`).
66    /// Heads-up alert before things go critical. Suppressed if the value is
67    /// already in critical range — `DiskHigh` covers that case.
68    DiskWarning {
69        mount: String,
70        percent: u8,
71    },
72    DiskHigh {
73        mount: String,
74        percent: u8,
75    },
76    /// Memory usage crossed the warning threshold (default 10pp below `memory_high`).
77    MemoryWarning {
78        percent: u8,
79    },
80    MemoryHigh {
81        percent: u8,
82    },
83    /// 1-minute load crossed the warning threshold (default 75% of `load_1m_high`).
84    LoadWarning {
85        load_1m: f32,
86    },
87    LoadHigh {
88        load_1m: f32,
89    },
90    HostUnreachable,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct EventEnvelope {
95    pub server_name: String,
96    pub timestamp: i64,
97    pub event: Event,
98    /// Optional diagnostic bundle attached by the agent for critical events,
99    /// per ADR 009. Old agents (pre-Phase-1.5) emit envelopes without this
100    /// field; the field is also `None` for events the operator opted out of
101    /// (`[diagnosis]` config) or for non-critical events that don't get
102    /// bundles by default. The control plane treats absent or empty bundles
103    /// as "no rule-library input" and ships the alert without a diagnosis
104    /// paragraph — never invents one.
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    pub context: Option<ContextBundle>,
107}