1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use std::{error::Error, fmt::Display};

use serde::{Deserialize, Serialize, Serializer};
use uuid::Uuid;

use crate::crontab_validator;

/// Error type for errors with parsing a crontab schedule
#[derive(Debug)]
pub struct CrontabParseError {
    invalid_crontab: String,
}

impl Display for CrontabParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "\"{}\" is not a valid crontab schedule.\n\t \
            For help determining why this schedule is invalid, you can use this site: \
            https://crontab.guru/#{}",
            self.invalid_crontab,
            self.invalid_crontab
                .split_whitespace()
                .collect::<Vec<_>>()
                .join("_"),
        )
    }
}

impl Error for CrontabParseError {}

impl CrontabParseError {
    /// Constructs a new CrontabParseError from a given invalid crontab string
    ///
    /// ## Example
    /// ```
    /// use sentry_types::protocol::v7::CrontabParseError;
    ///
    /// let error = CrontabParseError::new("* * * *");
    /// ```
    pub fn new(invalid_crontab: &str) -> Self {
        Self {
            invalid_crontab: String::from(invalid_crontab),
        }
    }
}

/// Represents the status of the monitor check-in
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MonitorCheckInStatus {
    /// Check-in had no issues during execution.
    Ok,
    /// Check-in failed or otherwise had some issues.
    Error,
    /// Check-in is expectred to complete.
    InProgress,
    /// Monitor did not check in on time.
    Missed,
    /// No status was passed.
    #[serde(other)]
    Unknown,
}

/// Configuration object of the monitor schedule.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum MonitorSchedule {
    /// A Crontab schedule allows you to use a standard UNIX crontab style schedule string to
    /// configure when a monitor check-in will be expected on Sentry.
    Crontab {
        /// The crontab syntax string defining the schedule.
        value: String,
    },
    /// A Interval schedule allows you to configure a periodic check-in, that will occur at an
    /// interval after the most recent check-in.
    Interval {
        /// The interval value.
        value: u64,
        /// The interval unit of the value.
        unit: MonitorIntervalUnit,
    },
}

impl MonitorSchedule {
    /// Attempts to create a MonitorSchedule from a provided crontab_str. If the crontab_str is a
    /// valid crontab schedule, we return a Result containing the MonitorSchedule; otherwise, we
    /// return a Result containing a CrontabParseError.
    ///
    /// ## Example with valid crontab
    /// ```
    /// use sentry_types::protocol::v7::MonitorSchedule;
    ///
    /// // Create a crontab that runs every other day of the month at midnight.
    /// let result = MonitorSchedule::from_crontab("0 0 */2 * *");
    /// assert!(result.is_ok())
    /// ```
    ///
    /// ## Example with an invalid crontab
    /// ```
    /// use sentry_types::protocol::v7::MonitorSchedule;
    ///
    /// // Invalid crontab.
    /// let result = MonitorSchedule::from_crontab("invalid");
    /// assert!(result.is_err());
    /// ```
    pub fn from_crontab(crontab_str: &str) -> Result<Self, CrontabParseError> {
        if crontab_validator::validate(crontab_str) {
            Ok(Self::Crontab {
                value: String::from(crontab_str),
            })
        } else {
            Err(CrontabParseError::new(crontab_str))
        }
    }
}

/// The unit for the interval schedule type
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MonitorIntervalUnit {
    /// Year Interval.
    Year,
    /// Month Interval.
    Month,
    /// Week Interval.
    Week,
    /// Day Interval.
    Day,
    /// Hour Interval.
    Hour,
    /// Minute Interval.
    Minute,
}

/// The monitor configuration playload for upserting monitors during check-in
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct MonitorConfig {
    /// The monitor schedule configuration.
    pub schedule: MonitorSchedule,

    /// How long (in minutes) after the expected check-in time will we wait until we consider the
    /// check-in to have been missed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub checkin_margin: Option<u64>,

    /// How long (in minutes) is the check-in allowed to run for in
    /// [`MonitorCheckInStatus::InProgress`] before it is considered failed.in_rogress
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_runtime: Option<u64>,

    /// tz database style timezone string
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timezone: Option<String>,

    /// The number of consecutive failed/error check-ins that triggers issue creation.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub failure_issue_threshold: Option<u64>,

    /// The number of consecutive successful check-ins that triggers issue resolution.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recovery_threshold: Option<u64>,
}

fn serialize_id<S: Serializer>(uuid: &Uuid, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.serialize_some(&uuid.as_simple())
}

/// The monitor check-in payload.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct MonitorCheckIn {
    /// Unique identifier of this check-in.
    #[serde(serialize_with = "serialize_id")]
    pub check_in_id: Uuid,

    /// Identifier of the monitor for this check-in.
    pub monitor_slug: String,

    /// Status of this check-in. Defaults to `"unknown"`.
    pub status: MonitorCheckInStatus,

    /// The environment to associate the check-in with.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub environment: Option<String>,

    /// Duration of this check-in since it has started in seconds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration: Option<f64>,

    /// Monitor configuration to support upserts. When provided a monitor will be created on Sentry
    /// upon receiving the first check-in.
    ///
    /// If the monitor already exists the configuration will be updated with the values provided in
    /// this object.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub monitor_config: Option<MonitorConfig>,
}