Skip to content

Commit bfd4bb5

Browse files
committed
Report error when process fails to start
When starting the child process fails and error reporting is enabled, report the failure to start the process as an error to AppSignal. Fixes #22.
1 parent 3ba1e28 commit bfd4bb5

File tree

3 files changed

+141
-26
lines changed

3 files changed

+141
-26
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
bump: patch
3+
type: add
4+
---
5+
6+
Report exit failures as errors to AppSignal. Use the `--error` command-line option to report an error to AppSignal when the command exits with a non-zero status code, or when the command fails to start:
7+
8+
```
9+
appsignal-wrap --error backup -- ./backup.sh
10+
```
11+
12+
The name given as the value to the `--error` command-line option will be used to group the errors in AppSignal.

src/error.rs

Lines changed: 108 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,41 @@ pub struct ErrorConfig {
1919
}
2020

2121
impl ErrorConfig {
22-
pub fn request(
23-
&self,
24-
timestamp: &mut impl Timestamp,
25-
exit: &ExitStatus,
26-
lines: impl IntoIterator<Item = String>,
27-
) -> Result<reqwest::Request, reqwest::Error> {
22+
pub fn request(&self, body: impl Into<Body>) -> Result<reqwest::Request, reqwest::Error> {
2823
let url = format!("{}/errors", self.endpoint);
2924

3025
client()
3126
.post(url)
3227
.query(&[("api_key", &self.api_key)])
3328
.header("Content-Type", "application/json")
34-
.body(ErrorBody::from_config(self, timestamp, exit, lines))
29+
.body(body)
3530
.build()
3631
}
32+
33+
pub fn request_from_spawn(
34+
&self,
35+
timestamp: &mut impl Timestamp,
36+
error: &std::io::Error,
37+
) -> Result<reqwest::Request, reqwest::Error> {
38+
self.request(ErrorBody::from_spawn(self, timestamp, error))
39+
}
40+
41+
pub fn request_from_exit(
42+
&self,
43+
timestamp: &mut impl Timestamp,
44+
exit: &ExitStatus,
45+
lines: impl IntoIterator<Item = String>,
46+
) -> Result<reqwest::Request, reqwest::Error> {
47+
self.request(ErrorBody::from_exit(self, timestamp, exit, lines))
48+
}
49+
50+
fn tags(&self) -> BTreeMap<String, String> {
51+
[
52+
("hostname".to_string(), self.hostname.clone()),
53+
(format!("{}-digest", NAME), self.digest.clone()),
54+
]
55+
.into()
56+
}
3757
}
3858

3959
#[derive(Serialize)]
@@ -46,26 +66,42 @@ pub struct ErrorBody {
4666
}
4767

4868
impl ErrorBody {
49-
pub fn from_config(
69+
pub fn new(
5070
config: &ErrorConfig,
5171
timestamp: &mut impl Timestamp,
52-
exit: &ExitStatus,
53-
lines: impl IntoIterator<Item = String>,
72+
error: ErrorBodyError,
73+
tags: impl IntoIterator<Item = (String, String)>,
5474
) -> Self {
5575
ErrorBody {
5676
timestamp: timestamp.as_secs(),
5777
action: config.action.clone(),
5878
namespace: "process".to_string(),
59-
error: ErrorBodyError::new(exit, lines),
60-
tags: exit_tags(exit)
61-
.into_iter()
62-
.chain([
63-
("hostname".to_string(), config.hostname.clone()),
64-
(format!("{}-digest", NAME), config.digest.clone()),
65-
])
66-
.collect(),
79+
error,
80+
tags: tags.into_iter().chain(config.tags()).collect(),
6781
}
6882
}
83+
84+
pub fn from_spawn(
85+
config: &ErrorConfig,
86+
timestamp: &mut impl Timestamp,
87+
error: &std::io::Error,
88+
) -> Self {
89+
Self::new(config, timestamp, ErrorBodyError::from_spawn(error), vec![])
90+
}
91+
92+
pub fn from_exit(
93+
config: &ErrorConfig,
94+
timestamp: &mut impl Timestamp,
95+
exit: &ExitStatus,
96+
lines: impl IntoIterator<Item = String>,
97+
) -> Self {
98+
Self::new(
99+
config,
100+
timestamp,
101+
ErrorBodyError::from_exit(exit, lines),
102+
exit_tags(exit),
103+
)
104+
}
69105
}
70106

71107
impl From<ErrorBody> for Body {
@@ -81,7 +117,14 @@ pub struct ErrorBodyError {
81117
}
82118

83119
impl ErrorBodyError {
84-
pub fn new(exit: &ExitStatus, lines: impl IntoIterator<Item = String>) -> Self {
120+
pub fn from_spawn(error: &std::io::Error) -> Self {
121+
ErrorBodyError {
122+
name: "StartError".to_string(),
123+
message: format!("[Error starting process: {}]", error),
124+
}
125+
}
126+
127+
pub fn from_exit(exit: &ExitStatus, lines: impl IntoIterator<Item = String>) -> Self {
85128
let (name, exit_context) = if let Some(code) = exit.code() {
86129
("NonZeroExit".to_string(), format!("code {}", code))
87130
} else if let Some(signal) = exit.signal() {
@@ -136,15 +179,59 @@ mod tests {
136179
}
137180

138181
#[test]
139-
fn error_config_request() {
182+
fn error_config_request_from_spawn() {
183+
let config = error_config();
184+
let error = std::io::Error::new(
185+
std::io::ErrorKind::NotFound,
186+
"No such file or directory (os error 2)",
187+
);
188+
189+
let request = config.request_from_spawn(&mut timestamp(), &error).unwrap();
190+
191+
assert_eq!(request.method().as_str(), "POST");
192+
assert_eq!(
193+
request.url().as_str(),
194+
"https://some-endpoint.com/errors?api_key=some_api_key"
195+
);
196+
assert_eq!(
197+
request.headers().get("Content-Type").unwrap(),
198+
"application/json"
199+
);
200+
assert_eq!(
201+
String::from_utf8_lossy(request.body().unwrap().as_bytes().unwrap()),
202+
format!(
203+
concat!(
204+
"{{",
205+
r#""timestamp":{},"#,
206+
r#""action":"some-action","#,
207+
r#""namespace":"process","#,
208+
r#""error":{{"#,
209+
r#""name":"StartError","#,
210+
r#""message":"[Error starting process: No such file or directory (os error 2)]""#,
211+
r#"}},"#,
212+
r#""tags":{{"#,
213+
r#""{}-digest":"some-digest","#,
214+
r#""hostname":"some-hostname""#,
215+
r#"}}"#,
216+
"}}"
217+
),
218+
EXPECTED_SECS, NAME
219+
)
220+
);
221+
}
222+
223+
#[test]
224+
fn error_config_request_from_exit() {
140225
let config = error_config();
141226
// `ExitStatus::from_raw` expects a wait status, not an exit status.
142227
// The wait status for exit code `n` is represented by `n << 8`.
143228
// See `__WEXITSTATUS` in `glibc/bits/waitstatus.h` for reference.
144229
let exit = ExitStatus::from_raw(42 << 8);
145230
let lines = vec!["line 1".to_string(), "line 2".to_string()];
146231

147-
let request = config.request(&mut timestamp(), &exit, lines).unwrap();
232+
let request = config
233+
.request_from_exit(&mut timestamp(), &exit, lines)
234+
.unwrap();
148235

149236
assert_eq!(request.method().as_str(), "POST");
150237
assert_eq!(

src/main.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ fn main() {
5656

5757
match start(cli) {
5858
Ok(code) => exit(code),
59-
Err(err) => error!("{}", err),
59+
Err(err) => {
60+
error!("{}", err);
61+
exit(1);
62+
}
6063
}
6164
}
6265

@@ -68,7 +71,20 @@ async fn start(cli: Cli) -> Result<i32, Box<dyn std::error::Error>> {
6871

6972
let tasks = TaskTracker::new();
7073

71-
let (child, stdout, stderr) = spawn_child(&cli, &tasks)?;
74+
let (child, stdout, stderr) = match spawn_child(&cli, &tasks) {
75+
Ok(spawned_child) => spawned_child,
76+
Err(err) => {
77+
if let Some(config) = error {
78+
tasks.spawn(send_request(
79+
config.request_from_spawn(&mut SystemTimestamp, &err),
80+
));
81+
tasks.close();
82+
tasks.wait().await;
83+
}
84+
85+
return Err(format!("could not spawn child process: {err}").into());
86+
}
87+
};
7288

7389
let (log_stdout, error_stdout) = maybe_spawn_tee(stdout);
7490
let (log_stderr, error_stderr) = maybe_spawn_tee(stderr);
@@ -106,7 +122,7 @@ async fn start(cli: Cli) -> Result<i32, Box<dyn std::error::Error>> {
106122
));
107123
}
108124
} else if let Some(error) = error {
109-
tasks.spawn(send_error_request(
125+
tasks.spawn(send_error_exit_request(
110126
error,
111127
exit_status,
112128
error_message.unwrap(),
@@ -412,7 +428,7 @@ async fn forward_signals_and_wait(mut child: Child) -> io::Result<ExitStatus> {
412428
}
413429
}
414430

415-
async fn send_error_request(
431+
async fn send_error_exit_request(
416432
error: ErrorConfig,
417433
exit_status: ExitStatus,
418434
receiver: oneshot::Receiver<VecDeque<String>>,
@@ -425,7 +441,7 @@ async fn send_error_request(
425441
}
426442
};
427443

428-
send_request(error.request(&mut SystemTimestamp, &exit_status, lines)).await;
444+
send_request(error.request_from_exit(&mut SystemTimestamp, &exit_status, lines)).await;
429445
}
430446

431447
fn command(argv: &[String], should_stdout: bool, should_stderr: bool) -> Command {

0 commit comments

Comments
 (0)