Skip to content

Commit c99ce1c

Browse files
committed
feat: Include status code and body in alerts, style Slack
1 parent 5adfe1a commit c99ce1c

File tree

5 files changed

+104
-15
lines changed

5 files changed

+104
-15
lines changed

src/alerts/model.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,28 @@ pub struct WebhookNotification {
99
pub error_message: String,
1010
#[serde(skip_serializing_if = "Option::is_none")]
1111
pub trace_id: Option<String>,
12+
#[serde(skip_serializing_if = "Option::is_none")]
13+
pub status_code: Option<u32>,
14+
#[serde(skip_serializing_if = "Option::is_none")]
15+
pub body: Option<String>,
1216
}
1317

1418
#[derive(Debug, Clone, Serialize, Deserialize)]
1519
pub struct SlackNotification {
20+
pub blocks: Vec<SlackBlock>,
21+
}
22+
23+
#[derive(Debug, Clone, Serialize, Deserialize)]
24+
pub struct SlackBlock {
25+
pub r#type: String,
26+
#[serde(skip_serializing_if = "Option::is_none")]
27+
pub elements: Option<Vec<SlackTextBlock>>,
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
pub text: Option<SlackTextBlock>,
30+
}
31+
32+
#[derive(Debug, Clone, Serialize, Deserialize)]
33+
pub struct SlackTextBlock {
34+
pub r#type: String,
1635
pub text: String,
1736
}

src/alerts/outbound_webhook.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::time::Duration;
22

3-
use crate::alerts::model::WebhookNotification;
43
use crate::errors::MapToSendError;
54
use crate::probe::model::ProbeAlert;
5+
use crate::{alerts::model::WebhookNotification, probe::model::ProbeResponse};
66
use chrono::{DateTime, Utc};
77
use lazy_static::lazy_static;
88
use tracing::{info, warn};
99

10-
use super::model::SlackNotification;
10+
use super::model::{SlackBlock, SlackNotification, SlackTextBlock};
1111

1212
const REQUEST_TIMEOUT_SECS: u64 = 10;
1313

@@ -21,6 +21,7 @@ lazy_static! {
2121
pub async fn alert_if_failure(
2222
success: bool,
2323
error: Option<&str>,
24+
probe_response: Option<&ProbeResponse>,
2425
probe_name: &str,
2526
failure_timestamp: DateTime<Utc>,
2627
alerts: &Option<Vec<ProbeAlert>>,
@@ -30,16 +31,20 @@ pub async fn alert_if_failure(
3031
return Ok(());
3132
}
3233
let error_message = error.unwrap_or("No error message");
34+
let truncated_body = probe_response.map(|r| r.truncated_body(500));
3335
warn!(
34-
"Probe {probe_name} failed at {failure_timestamp} with trace ID {}. Error: {error_message}",
35-
trace_id.as_ref().unwrap_or(&"N/A".to_owned())
36+
"Probe {probe_name} failed at {failure_timestamp} with trace ID {}. Status code: {}. Error: {error_message}. Body: {}",
37+
trace_id.as_ref().unwrap_or(&"N/A".to_owned()),
38+
probe_response.map_or("N/A".to_owned(), |r| r.status_code.to_string()),
39+
truncated_body.unwrap_or("N/A".to_owned()),
3640
);
3741
let mut errors = Vec::new();
3842
if let Some(alerts_vec) = alerts {
3943
for alert in alerts_vec {
4044
if let Err(e) = send_alert(
4145
alert,
4246
probe_name.to_owned(),
47+
probe_response,
4348
error_message,
4449
failure_timestamp,
4550
trace_id.clone(),
@@ -81,6 +86,7 @@ pub async fn send_generic_webhook(
8186
pub async fn send_webhook_alert(
8287
url: &String,
8388
probe_name: String,
89+
probe_response: Option<&ProbeResponse>,
8490
error_message: &str,
8591
failure_timestamp: DateTime<Utc>,
8692
trace_id: Option<String>,
@@ -91,6 +97,8 @@ pub async fn send_webhook_alert(
9197
error_message: error_message.to_owned(),
9298
failure_timestamp,
9399
trace_id,
100+
body: probe_response.map(|r| r.truncated_body(500)),
101+
status_code: probe_response.map(|r| r.status_code),
94102
};
95103

96104
let json = serde_json::to_string(&request_body).map_to_send_err()?;
@@ -100,26 +108,77 @@ pub async fn send_webhook_alert(
100108
pub async fn send_slack_alert(
101109
webhook_url: &String,
102110
probe_name: String,
111+
probe_response: Option<&ProbeResponse>,
103112
error_message: &str,
104113
failure_timestamp: DateTime<Utc>,
105114
trace_id: Option<String>,
106115
) -> Result<(), Box<dyn std::error::Error + Send>> {
107-
let request_body = SlackNotification {
108-
text: format!(
109-
"Probe {} failed at {}. Trace ID: {}. Error: {}",
110-
probe_name,
111-
failure_timestamp,
112-
trace_id.unwrap_or_else(|| "N/A".to_owned()),
113-
error_message,
114-
),
115-
};
116+
// Uses Slack's Block Kit UI to make the message prettier
117+
let mut blocks = vec![
118+
SlackBlock {
119+
r#type: "header".to_owned(),
120+
text: Some(SlackTextBlock {
121+
r#type: "plain_text".to_owned(),
122+
text: format!("\"{}\" failed.", probe_name),
123+
}),
124+
elements: None,
125+
},
126+
SlackBlock {
127+
r#type: "section".to_owned(),
128+
text: Some(SlackTextBlock {
129+
r#type: "mrkdwn".to_owned(),
130+
text: format!("Error message:\n\n> {}", error_message),
131+
}),
132+
elements: None,
133+
},
134+
];
135+
136+
if let Some(response) = probe_response {
137+
blocks.extend([
138+
SlackBlock {
139+
r#type: "divider".to_owned(),
140+
elements: None,
141+
text: None,
142+
},
143+
SlackBlock {
144+
r#type: "section".to_owned(),
145+
elements: None,
146+
text: Some(SlackTextBlock {
147+
r#type: "mrkdwn".to_owned(),
148+
text: format!(
149+
"Received status code *{}* and (truncated) body:\n```\n{}\n```",
150+
response.status_code,
151+
response.body.chars().take(500).collect::<String>()
152+
),
153+
}),
154+
},
155+
])
156+
}
157+
158+
blocks.push(SlackBlock {
159+
r#type: "context".to_owned(),
160+
elements: Some(vec![
161+
SlackTextBlock {
162+
r#type: "mrkdwn".to_owned(),
163+
text: format!("Time: *{}*", failure_timestamp),
164+
},
165+
SlackTextBlock {
166+
r#type: "mrkdwn".to_owned(),
167+
text: format!("Trace ID: *{}*", trace_id.unwrap_or("N/A".to_owned())),
168+
},
169+
]),
170+
text: None,
171+
});
172+
let request_body = SlackNotification { blocks };
116173
let json = serde_json::to_string(&request_body).map_to_send_err()?;
174+
println!("{}", json);
117175
send_generic_webhook(webhook_url, json).await
118176
}
119177

120178
pub async fn send_alert(
121179
alert: &ProbeAlert,
122180
probe_name: String,
181+
probe_response: Option<&ProbeResponse>,
123182
error_message: &str,
124183
failure_timestamp: DateTime<Utc>,
125184
trace_id: Option<String>,
@@ -130,6 +189,7 @@ pub async fn send_alert(
130189
send_slack_alert(
131190
&alert.url,
132191
probe_name.clone(),
192+
probe_response,
133193
error_message,
134194
failure_timestamp,
135195
trace_id.clone(),
@@ -140,6 +200,7 @@ pub async fn send_alert(
140200
send_webhook_alert(
141201
&alert.url,
142202
probe_name.clone(),
203+
probe_response,
143204
error_message,
144205
failure_timestamp,
145206
trace_id.clone(),
@@ -181,6 +242,7 @@ mod webhook_tests {
181242
let alert_result = alert_if_failure(
182243
false,
183244
Some("Test error"),
245+
None,
184246
&probe_name,
185247
failure_timestamp,
186248
&alerts,

src/errors.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ impl std::fmt::Display for ExpectationFailedError {
2929
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3030
write!(
3131
f,
32-
"Failed to meet expectation for field '{:?}' with operation {:?} {:?}. Received: status '{}', body '{}' (truncatated to 100 characters).",
33-
self.field, self.operation, self.expected, self.status_code, self.body.chars().take(100).collect::<String>()
32+
"Failed to meet expectation for field '{:?}' with operation {:?} {:?}.",
33+
self.field, self.operation, self.expected,
3434
)
3535
}
3636
}

src/probe/model.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ pub struct ProbeResponse {
7575
pub body: String,
7676
}
7777

78+
impl ProbeResponse {
79+
pub fn truncated_body(&self, n: usize) -> String {
80+
self.body.chars().take(n).collect()
81+
}
82+
}
83+
7884
#[derive(Debug, Clone, Serialize, Deserialize)]
7985
pub struct Story {
8086
pub name: String,

src/probe/probe_logic.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ impl Monitorable for Story {
175175
let send_alert_result = alert_if_failure(
176176
story_success,
177177
last_step.error_message.as_deref(),
178+
last_step.response.as_ref(),
178179
&self.name,
179180
timestamp_started,
180181
&self.alerts,
@@ -270,6 +271,7 @@ impl Monitorable for Probe {
270271
let send_alert_result = alert_if_failure(
271272
probe_result.success,
272273
probe_result.error_message.as_deref(),
274+
probe_result.response.as_ref(),
273275
&self.name,
274276
timestamp,
275277
&self.alerts,

0 commit comments

Comments
 (0)