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}