Skip to main content

opssignal_core/
delivery.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4pub enum DeliveryStatus {
5    /// Recorded, delivery not finished yet. A row left in this state
6    /// means the process exited before delivery completed.
7    Pending,
8    Delivered,
9    /// A fan-out reached some of its targets but not all of them.
10    Partial,
11    Failed,
12    /// Not sent: every route it matched was held back by its `dedup` or
13    /// `rate_limit`.
14    Skipped,
15}
16
17impl std::fmt::Display for DeliveryStatus {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.write_str(match self {
20            DeliveryStatus::Pending => "pending",
21            DeliveryStatus::Delivered => "delivered",
22            DeliveryStatus::Partial => "partial",
23            DeliveryStatus::Failed => "failed",
24            DeliveryStatus::Skipped => "skipped",
25        })
26    }
27}
28
29/// The overall outcome of delivering one signal.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DeliveryResult {
32    pub status: DeliveryStatus,
33    /// The providers that delivered, comma separated in plan order
34    /// (`slack,pagerduty`), or `None` if none did.
35    pub provider: Option<String>,
36    /// Why delivery failed. With more than one target, each failed
37    /// target's last error, prefixed with its provider.
38    pub error: Option<String>,
39    /// Attempts made on the provider that decided the outcome; the most
40    /// any target needed when there are several. `deliveries` has the
41    /// count for every provider.
42    pub attempts: u32,
43    /// One entry per provider tried, in plan order.
44    #[serde(default)]
45    pub deliveries: Vec<ProviderOutcome>,
46    /// Matching routes held back by their `dedup` or `rate_limit`, one
47    /// line per route with the reason. A signal can be delivered on one
48    /// route and suppressed on another.
49    #[serde(default)]
50    pub suppressed: Vec<String>,
51}
52
53impl DeliveryResult {
54    /// What the sink stores before delivery starts.
55    pub fn pending() -> Self {
56        DeliveryResult {
57            status: DeliveryStatus::Pending,
58            provider: None,
59            error: None,
60            attempts: 0,
61            deliveries: Vec::new(),
62            suppressed: Vec::new(),
63        }
64    }
65}
66
67/// How delivery to one provider went: `pending` while it runs, then
68/// `delivered` or `failed`.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ProviderOutcome {
71    pub provider: String,
72    pub status: DeliveryStatus,
73    pub error: Option<String>,
74    pub attempts: u32,
75}
76
77/// Every provider (Slack, Webhook, Console, ...) implements this. Adding
78/// a provider never requires touching routing or retry logic — that's
79/// the abstraction boundary that keeps the core's "don't become an
80/// event bus" promise honest.
81#[async_trait::async_trait]
82pub trait Provider: Send + Sync {
83    fn name(&self) -> &'static str;
84    async fn send(&self, signal: &crate::signal::Signal) -> Result<(), crate::signal::SignalError>;
85
86    /// What `send` would deliver for `signal`, without sending it, for
87    /// checking templates (`signal preview`). `None` when the provider
88    /// can't show it.
89    fn preview(&self, _signal: &crate::signal::Signal) -> Option<serde_json::Value> {
90        None
91    }
92}