Skip to main content

better_uptime/
lib.rs

1use serde::{Deserialize, Serialize};
2
3/// A `Result` alias where the `Err` case is `better_uptime::Error`.
4pub type Result<T> = std::result::Result<T, Error>;
5
6const API_URL: &str = "https://uptime.betterstack.com/api";
7
8/// The Errors that may occur while using this crate
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11    #[error("reqwest: {0}")]
12    Reqwest(#[from] reqwest::Error),
13
14    #[error("Uptime API: {0}")]
15    UptimeApi(String),
16
17    #[error("serde_json: {0}")]
18    SerdeJson(#[from] serde_json::Error),
19}
20
21fn maybe_uptime_api_error<T>(value: serde_json::Value) -> Result<T>
22where
23    T: serde::de::DeserializeOwned,
24{
25    #[derive(Deserialize)]
26    struct ErrorResponse {
27        error: String,
28    }
29    if let Ok(ErrorResponse { error }) = serde_json::from_value::<ErrorResponse>(value.clone()) {
30        Err(Error::UptimeApi(error))
31    } else {
32        serde_json::from_value(value).map_err(|err| err.into())
33    }
34}
35
36pub struct Uptime {
37    pub token: String,
38}
39
40#[derive(Debug, Serialize, Default)]
41pub struct IncidentRequest {
42    pub requester_email: String,
43    pub name: String,
44    pub summary: String,
45    pub description: String,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub call: Option<bool>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub sms: Option<bool>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub email: Option<bool>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub push: Option<bool>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub team_wait: Option<u64>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub poliy_id: Option<String>,
58}
59
60#[derive(Clone, Debug, Deserialize)]
61pub struct Incident {
62    pub id: String,
63}
64
65impl Uptime {
66    /// Ping the Uptime API to make sure that the service is up: https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/
67    pub async fn heartbeat(&self, identifier: String) -> Result<()> {
68        let url = format!("{API_URL}/v1/heartbeat/{identifier}");
69        match reqwest::get(url).await?.error_for_status() {
70            Ok(_) => Ok(()),
71            Err(err) => Err(err.into()),
72        }
73    }
74
75    /// Create a new incident: https://betterstack.com/docs/uptime/api/create-a-new-incident/
76    pub async fn create_incident(&self, request: IncidentRequest) -> Result<Incident> {
77        let url = format!("{API_URL}/v2/incidents");
78
79        #[derive(Debug, Deserialize)]
80        struct IncidentResponse {
81            data: Incident,
82        }
83
84        let response = maybe_uptime_api_error::<IncidentResponse>(
85            reqwest::Client::builder()
86                .build()?
87                .post(url)
88                .header("Content-Type", "application/json")
89                .header("Authorization", format!("Bearer {}", self.token))
90                .json(&request)
91                .send()
92                .await?
93                .error_for_status()?
94                .json()
95                .await?,
96        )?;
97
98        Ok(response.data)
99    }
100}