Skip to main content

coil_observability/
health.rs

1use crate::ObservabilityError;
2use crate::validation::CustomerAppId;
3use std::collections::BTreeSet;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum LogSeverity {
8    Error,
9    Warn,
10    Info,
11    Debug,
12    Trace,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum ErrorCategory {
17    Validation,
18    AuthorizationDenied,
19    StateConflict,
20    DependencyFailure,
21    Timeout,
22    Capacity,
23    InvariantViolation,
24    ExtensionTrap,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum HealthProbeKind {
29    Liveness,
30    Readiness,
31    Synthetic,
32}
33
34impl fmt::Display for HealthProbeKind {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::Liveness => f.write_str("liveness"),
38            Self::Readiness => f.write_str("readiness"),
39            Self::Synthetic => f.write_str("synthetic"),
40        }
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub enum DependencyKind {
46    Database,
47    DistributedCache,
48    Queue,
49    ExtensionRegistry,
50    ObjectStore,
51    Secrets,
52    Tls,
53}
54
55impl fmt::Display for DependencyKind {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Database => f.write_str("database"),
59            Self::DistributedCache => f.write_str("distributed_cache"),
60            Self::Queue => f.write_str("queue"),
61            Self::ExtensionRegistry => f.write_str("extension_registry"),
62            Self::ObjectStore => f.write_str("object_store"),
63            Self::Secrets => f.write_str("secrets"),
64            Self::Tls => f.write_str("tls"),
65        }
66    }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum DependencyStatus {
71    Healthy,
72    Degraded,
73    Unhealthy,
74    Unknown,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct ProbeDependency {
79    pub kind: DependencyKind,
80    pub required: bool,
81    pub status: DependencyStatus,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct HealthReport {
86    pub kind: HealthProbeKind,
87    pub dependencies: Vec<ProbeDependency>,
88}
89
90impl HealthReport {
91    pub fn new(kind: HealthProbeKind) -> Self {
92        Self {
93            kind,
94            dependencies: Vec::new(),
95        }
96    }
97
98    pub fn with_dependency(
99        mut self,
100        kind: DependencyKind,
101        required: bool,
102        status: DependencyStatus,
103    ) -> Result<Self, ObservabilityError> {
104        if self
105            .dependencies
106            .iter()
107            .any(|dependency| dependency.kind == kind)
108        {
109            return Err(ObservabilityError::DuplicateDependency {
110                probe: self.kind,
111                dependency: kind,
112            });
113        }
114
115        self.dependencies.push(ProbeDependency {
116            kind,
117            required,
118            status,
119        });
120        Ok(self)
121    }
122
123    pub fn overall_status(&self) -> DependencyStatus {
124        if self.dependencies.iter().any(|dependency| {
125            dependency.required && dependency.status == DependencyStatus::Unhealthy
126        }) {
127            return DependencyStatus::Unhealthy;
128        }
129
130        if self.dependencies.iter().any(|dependency| {
131            dependency.required
132                && matches!(
133                    dependency.status,
134                    DependencyStatus::Degraded | DependencyStatus::Unknown
135                )
136        }) {
137            return DependencyStatus::Degraded;
138        }
139
140        DependencyStatus::Healthy
141    }
142
143    pub fn dependency(&self, kind: DependencyKind) -> Option<ProbeDependency> {
144        self.dependencies
145            .iter()
146            .find(|dependency| dependency.kind == kind)
147            .copied()
148    }
149
150    pub fn set_dependency_status(&mut self, kind: DependencyKind, status: DependencyStatus) -> bool {
151        let Some(dependency) = self
152            .dependencies
153            .iter_mut()
154            .find(|dependency| dependency.kind == kind)
155        else {
156            return false;
157        };
158        dependency.status = status;
159        true
160    }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
164pub enum BackgroundWorkClass {
165    QueueDrain,
166    TlsRenewal,
167    StorageSync,
168    WebhookRetry,
169    SearchMaintenance,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum MaintenanceAudience {
174    Deployment,
175    CustomerApp(CustomerAppId),
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum MaintenanceImpact {
180    AllTraffic,
181    MutatingTrafficOnly,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct MaintenanceMode {
186    pub enabled: bool,
187    pub audience: MaintenanceAudience,
188    pub impact: MaintenanceImpact,
189    pub bypass_token: Option<String>,
190    pub allowed_background_work: BTreeSet<BackgroundWorkClass>,
191}
192
193impl MaintenanceMode {
194    pub fn disabled() -> Self {
195        Self {
196            enabled: false,
197            audience: MaintenanceAudience::Deployment,
198            impact: MaintenanceImpact::AllTraffic,
199            bypass_token: None,
200            allowed_background_work: BTreeSet::new(),
201        }
202    }
203
204    pub fn blocks_request(
205        &self,
206        customer_app: Option<&CustomerAppId>,
207        method_is_mutating: bool,
208        bypass_token: Option<&str>,
209    ) -> bool {
210        if !self.enabled {
211            return false;
212        }
213
214        if self
215            .bypass_token
216            .as_deref()
217            .is_some_and(|expected| Some(expected) == bypass_token)
218        {
219            return false;
220        }
221
222        let applies_to_app = match (&self.audience, customer_app) {
223            (MaintenanceAudience::Deployment, _) => true,
224            (MaintenanceAudience::CustomerApp(expected), Some(actual)) => expected == actual,
225            (MaintenanceAudience::CustomerApp(_), None) => false,
226        };
227
228        if !applies_to_app {
229            return false;
230        }
231
232        match self.impact {
233            MaintenanceImpact::AllTraffic => true,
234            MaintenanceImpact::MutatingTrafficOnly => method_is_mutating,
235        }
236    }
237}