Skip to content

Commit 74bba81

Browse files
committed
feat: Add support for marking steps as sensitive
This leads to logs and alerts being redacted so that the sensitive response bodies aren't included.
1 parent c99ce1c commit 74bba81

File tree

8 files changed

+119
-48
lines changed

8 files changed

+119
-48
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "prodzilla"
5-
version = "0.0.4"
5+
version = "0.0.3-rc.2"
66
edition = "2021"
77

88
[dependencies]

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ A complete Probe config looks as follows:
7676
- name: Your Post Url
7777
url: https://your.site/some/path
7878
http_method: POST
79+
sensitive: false
7980
with:
8081
headers:
8182
x-client-id: ClientId
@@ -162,14 +163,34 @@ The webhook looks as such:
162163
"probe_name": "Your Probe",
163164
"failure_timestamp": "2024-01-26T02:41:02.983025Z",
164165
"trace_id": "123456789abcdef",
165-
"error_message": "Failed to meet expectation for field 'StatusCode' with operation Equals \"200\". Received: status '500', body '\"Internal Server Error\"' (truncatated to 100 characters)."
166+
"error_message": "Failed to meet expectation for field 'StatusCode' with operation Equals \"200\".",
167+
"status_code": 500,
168+
"body": "Internal Server Error"
166169
}
167170

168171
```
169172

173+
Response bodies are truncated to 500 characters. If a step or probe is marked as sensitive, the request body will be redacted from logs and alerts.
174+
170175
Prodzilla will also recognize the Slack webhook domain `hooks.slack.com` and produce messages like:
171176

172-
> Probe Your Probe failed at 2024-06-10 08:16:33.935659994 UTC. Trace ID: 123456789abcdef. Error: Failed to meet expectation for field 'StatusCode' with operation Equals "200". Received: status '500', body '"Internal Server Error"' (truncatated to 100 characters).
177+
> **"Your Probe" failed.**
178+
>
179+
> Error message:
180+
>
181+
> > Failed to meet expectation for field 'StatusCode' with operation Equals "429".
182+
>
183+
> Received status code **500**
184+
>
185+
> Received body:
186+
>
187+
> ```
188+
> Internal Server Error
189+
> ```
190+
>
191+
> Time: **2024-06-26 14:36:30.094126 UTC**
192+
>
193+
> Trace ID: **e03cc9b03185db8004400049264331de**
173194
174195
OpsGenie, and PagerDuty notification integrations are planned.
175196

src/alerts/outbound_webhook.rs

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,26 @@ pub async fn alert_if_failure(
3131
return Ok(());
3232
}
3333
let error_message = error.unwrap_or("No error message");
34-
let truncated_body = probe_response.map(|r| r.truncated_body(500));
34+
let status_code = probe_response.map(|r| r.status_code);
35+
let truncated_body = match probe_response {
36+
Some(r) if !r.sensitive => Some(r.truncated_body(500)),
37+
Some(_) => Some("Redacted".to_owned()),
38+
None => None,
39+
};
3540
warn!(
3641
"Probe {probe_name} failed at {failure_timestamp} with trace ID {}. Status code: {}. Error: {error_message}. Body: {}",
3742
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()),
43+
status_code.map_or("N/A".to_owned(), |code| code.to_string()),
44+
truncated_body.as_ref().unwrap_or(&"N/A".to_owned()),
4045
);
4146
let mut errors = Vec::new();
4247
if let Some(alerts_vec) = alerts {
4348
for alert in alerts_vec {
4449
if let Err(e) = send_alert(
4550
alert,
4651
probe_name.to_owned(),
47-
probe_response,
52+
status_code,
53+
truncated_body.as_deref(),
4854
error_message,
4955
failure_timestamp,
5056
trace_id.clone(),
@@ -86,7 +92,8 @@ pub async fn send_generic_webhook(
8692
pub async fn send_webhook_alert(
8793
url: &String,
8894
probe_name: String,
89-
probe_response: Option<&ProbeResponse>,
95+
status_code: Option<u32>,
96+
body: Option<&str>,
9097
error_message: &str,
9198
failure_timestamp: DateTime<Utc>,
9299
trace_id: Option<String>,
@@ -97,8 +104,8 @@ pub async fn send_webhook_alert(
97104
error_message: error_message.to_owned(),
98105
failure_timestamp,
99106
trace_id,
100-
body: probe_response.map(|r| r.truncated_body(500)),
101-
status_code: probe_response.map(|r| r.status_code),
107+
body: body.map(|s| s.to_owned()),
108+
status_code,
102109
};
103110

104111
let json = serde_json::to_string(&request_body).map_to_send_err()?;
@@ -108,7 +115,8 @@ pub async fn send_webhook_alert(
108115
pub async fn send_slack_alert(
109116
webhook_url: &String,
110117
probe_name: String,
111-
probe_response: Option<&ProbeResponse>,
118+
status_code: Option<u32>,
119+
body: Option<&str>,
112120
error_message: &str,
113121
failure_timestamp: DateTime<Utc>,
114122
trace_id: Option<String>,
@@ -133,26 +141,26 @@ pub async fn send_slack_alert(
133141
},
134142
];
135143

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-
])
144+
if let Some(code) = status_code {
145+
blocks.push(SlackBlock {
146+
r#type: "section".to_owned(),
147+
elements: None,
148+
text: Some(SlackTextBlock {
149+
r#type: "mrkdwn".to_owned(),
150+
text: format!("Received status code *{}*", code,),
151+
}),
152+
})
153+
}
154+
155+
if let Some(s) = body {
156+
blocks.push(SlackBlock {
157+
r#type: "section".to_owned(),
158+
elements: None,
159+
text: Some(SlackTextBlock {
160+
r#type: "mrkdwn".to_owned(),
161+
text: format!("Received body:\n```\n{}\n```", s,),
162+
}),
163+
})
156164
}
157165

158166
blocks.push(SlackBlock {
@@ -178,7 +186,8 @@ pub async fn send_slack_alert(
178186
pub async fn send_alert(
179187
alert: &ProbeAlert,
180188
probe_name: String,
181-
probe_response: Option<&ProbeResponse>,
189+
status_code: Option<u32>,
190+
body: Option<&str>,
182191
error_message: &str,
183192
failure_timestamp: DateTime<Utc>,
184193
trace_id: Option<String>,
@@ -189,7 +198,8 @@ pub async fn send_alert(
189198
send_slack_alert(
190199
&alert.url,
191200
probe_name.clone(),
192-
probe_response,
201+
status_code,
202+
body,
193203
error_message,
194204
failure_timestamp,
195205
trace_id.clone(),
@@ -200,7 +210,8 @@ pub async fn send_alert(
200210
send_webhook_alert(
201211
&alert.url,
202212
probe_name.clone(),
203-
probe_response,
213+
status_code,
214+
body,
204215
error_message,
205216
failure_timestamp,
206217
trace_id.clone(),

src/probe/http_probe.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use std::time::Duration;
44
use crate::errors::MapToSendError;
55
use chrono::Utc;
66
use lazy_static::lazy_static;
7-
use opentelemetry::global::ObjectSafeSpan;
7+
use opentelemetry::KeyValue;
8+
use opentelemetry_semantic_conventions::trace as semconv;
89

910
use opentelemetry::trace::FutureExt;
11+
use opentelemetry::trace::Span;
1012
use opentelemetry::trace::SpanId;
1113
use opentelemetry::trace::TraceId;
1214

@@ -32,6 +34,7 @@ pub async fn call_endpoint(
3234
http_method: &str,
3335
url: &String,
3436
input_parameters: &Option<ProbeInputParameters>,
37+
sensitive: bool,
3538
) -> Result<EndpointResult, Box<dyn std::error::Error + Send>> {
3639
let timestamp_start = Utc::now();
3740
let (otel_headers, cx, span_id, trace_id) =
@@ -41,20 +44,41 @@ pub async fn call_endpoint(
4144
let response = request
4245
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
4346
.send()
44-
.with_context(cx)
47+
.with_context(cx.clone())
4548
.await
4649
.map_to_send_err()?;
4750

4851
let timestamp_response = Utc::now();
4952

50-
Ok(EndpointResult {
53+
let result = EndpointResult {
5154
timestamp_request_started: timestamp_start,
5255
timestamp_response_received: timestamp_response,
5356
status_code: response.status().as_u16() as u32,
5457
body: response.text().await.map_to_send_err()?,
58+
sensitive,
5559
trace_id: trace_id.to_string(),
5660
span_id: span_id.to_string(),
57-
})
61+
};
62+
let span = cx.span();
63+
span.set_attributes(vec![
64+
KeyValue::new(semconv::HTTP_METHOD, http_method.to_owned()),
65+
KeyValue::new(semconv::HTTP_URL, url.clone()),
66+
]);
67+
span.set_attribute(KeyValue::new(
68+
semconv::HTTP_STATUS_CODE,
69+
result.status_code.to_string(),
70+
));
71+
if !sensitive {
72+
span.add_event(
73+
"response",
74+
vec![KeyValue::new(
75+
"body",
76+
result.body.chars().take(500).collect::<String>(),
77+
)],
78+
)
79+
}
80+
81+
Ok(result)
5882
}
5983

6084
fn get_otel_headers(span_name: String) -> (HeaderMap, Context, SpanId, TraceId) {
@@ -130,7 +154,7 @@ mod http_tests {
130154
format!("{}/test", mock_server.uri()),
131155
"".to_owned(),
132156
);
133-
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with)
157+
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false)
134158
.await
135159
.unwrap();
136160
let check_expectations_result = validate_response(
@@ -160,7 +184,8 @@ mod http_tests {
160184
format!("{}/test", mock_server.uri()),
161185
body.to_string(),
162186
);
163-
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with).await;
187+
let endpoint_result =
188+
call_endpoint(&probe.http_method, &probe.url, &probe.with, false).await;
164189

165190
assert!(endpoint_result.is_err());
166191
}
@@ -183,7 +208,7 @@ mod http_tests {
183208
format!("{}/test", mock_server.uri()),
184209
body.to_string(),
185210
);
186-
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with)
211+
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false)
187212
.await
188213
.unwrap();
189214
let check_expectations_result = validate_response(
@@ -220,7 +245,7 @@ mod http_tests {
220245
format!("{}/test", mock_server.uri()),
221246
request_body.to_owned(),
222247
);
223-
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with)
248+
let endpoint_result = call_endpoint(&probe.http_method, &probe.url, &probe.with, false)
224249
.await
225250
.unwrap();
226251
let check_expectations_result = validate_response(

src/probe/model.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub struct Probe {
1212
pub expectations: Option<Vec<ProbeExpectation>>,
1313
pub schedule: ProbeScheduleParameters,
1414
pub alerts: Option<Vec<ProbeAlert>>,
15+
#[serde(default)] // default to false
16+
pub sensitive: bool,
1517
}
1618

1719
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -73,6 +75,7 @@ pub struct ProbeResponse {
7375
pub timestamp_received: DateTime<Utc>,
7476
pub status_code: u32,
7577
pub body: String,
78+
pub sensitive: bool,
7679
}
7780

7881
impl ProbeResponse {
@@ -96,6 +99,8 @@ pub struct Step {
9699
pub http_method: String,
97100
pub with: Option<ProbeInputParameters>,
98101
pub expectations: Option<Vec<ProbeExpectation>>,
102+
#[serde(default)] // default to false
103+
pub sensitive: bool,
99104
}
100105

101106
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -128,6 +133,7 @@ pub struct EndpointResult {
128133
pub body: String,
129134
pub trace_id: String,
130135
pub span_id: String,
136+
pub sensitive: bool,
131137
}
132138

133139
impl EndpointResult {
@@ -136,6 +142,7 @@ impl EndpointResult {
136142
timestamp_received: self.timestamp_response_received,
137143
status_code: self.status_code,
138144
body: self.body.clone(),
145+
sensitive: self.sensitive,
139146
}
140147
}
141148
}

0 commit comments

Comments
 (0)