coil-observability 0.1.0

Observability primitives for the Coil framework.
Documentation
use crate::ObservabilityError;
use crate::validation::CustomerAppId;
use std::collections::BTreeSet;
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogSeverity {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ErrorCategory {
    Validation,
    AuthorizationDenied,
    StateConflict,
    DependencyFailure,
    Timeout,
    Capacity,
    InvariantViolation,
    ExtensionTrap,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HealthProbeKind {
    Liveness,
    Readiness,
    Synthetic,
}

impl fmt::Display for HealthProbeKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Liveness => f.write_str("liveness"),
            Self::Readiness => f.write_str("readiness"),
            Self::Synthetic => f.write_str("synthetic"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DependencyKind {
    Database,
    DistributedCache,
    Queue,
    ExtensionRegistry,
    ObjectStore,
    Secrets,
    Tls,
}

impl fmt::Display for DependencyKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Database => f.write_str("database"),
            Self::DistributedCache => f.write_str("distributed_cache"),
            Self::Queue => f.write_str("queue"),
            Self::ExtensionRegistry => f.write_str("extension_registry"),
            Self::ObjectStore => f.write_str("object_store"),
            Self::Secrets => f.write_str("secrets"),
            Self::Tls => f.write_str("tls"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyStatus {
    Healthy,
    Degraded,
    Unhealthy,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProbeDependency {
    pub kind: DependencyKind,
    pub required: bool,
    pub status: DependencyStatus,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HealthReport {
    pub kind: HealthProbeKind,
    pub dependencies: Vec<ProbeDependency>,
}

impl HealthReport {
    pub fn new(kind: HealthProbeKind) -> Self {
        Self {
            kind,
            dependencies: Vec::new(),
        }
    }

    pub fn with_dependency(
        mut self,
        kind: DependencyKind,
        required: bool,
        status: DependencyStatus,
    ) -> Result<Self, ObservabilityError> {
        if self
            .dependencies
            .iter()
            .any(|dependency| dependency.kind == kind)
        {
            return Err(ObservabilityError::DuplicateDependency {
                probe: self.kind,
                dependency: kind,
            });
        }

        self.dependencies.push(ProbeDependency {
            kind,
            required,
            status,
        });
        Ok(self)
    }

    pub fn overall_status(&self) -> DependencyStatus {
        if self.dependencies.iter().any(|dependency| {
            dependency.required && dependency.status == DependencyStatus::Unhealthy
        }) {
            return DependencyStatus::Unhealthy;
        }

        if self.dependencies.iter().any(|dependency| {
            dependency.required
                && matches!(
                    dependency.status,
                    DependencyStatus::Degraded | DependencyStatus::Unknown
                )
        }) {
            return DependencyStatus::Degraded;
        }

        DependencyStatus::Healthy
    }

    pub fn dependency(&self, kind: DependencyKind) -> Option<ProbeDependency> {
        self.dependencies
            .iter()
            .find(|dependency| dependency.kind == kind)
            .copied()
    }

    pub fn set_dependency_status(&mut self, kind: DependencyKind, status: DependencyStatus) -> bool {
        let Some(dependency) = self
            .dependencies
            .iter_mut()
            .find(|dependency| dependency.kind == kind)
        else {
            return false;
        };
        dependency.status = status;
        true
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BackgroundWorkClass {
    QueueDrain,
    TlsRenewal,
    StorageSync,
    WebhookRetry,
    SearchMaintenance,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MaintenanceAudience {
    Deployment,
    CustomerApp(CustomerAppId),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaintenanceImpact {
    AllTraffic,
    MutatingTrafficOnly,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaintenanceMode {
    pub enabled: bool,
    pub audience: MaintenanceAudience,
    pub impact: MaintenanceImpact,
    pub bypass_token: Option<String>,
    pub allowed_background_work: BTreeSet<BackgroundWorkClass>,
}

impl MaintenanceMode {
    pub fn disabled() -> Self {
        Self {
            enabled: false,
            audience: MaintenanceAudience::Deployment,
            impact: MaintenanceImpact::AllTraffic,
            bypass_token: None,
            allowed_background_work: BTreeSet::new(),
        }
    }

    pub fn blocks_request(
        &self,
        customer_app: Option<&CustomerAppId>,
        method_is_mutating: bool,
        bypass_token: Option<&str>,
    ) -> bool {
        if !self.enabled {
            return false;
        }

        if self
            .bypass_token
            .as_deref()
            .is_some_and(|expected| Some(expected) == bypass_token)
        {
            return false;
        }

        let applies_to_app = match (&self.audience, customer_app) {
            (MaintenanceAudience::Deployment, _) => true,
            (MaintenanceAudience::CustomerApp(expected), Some(actual)) => expected == actual,
            (MaintenanceAudience::CustomerApp(_), None) => false,
        };

        if !applies_to_app {
            return false;
        }

        match self.impact {
            MaintenanceImpact::AllTraffic => true,
            MaintenanceImpact::MutatingTrafficOnly => method_is_mutating,
        }
    }
}