command_ext/trace/
mod.rs

1//! Extension trait to log properties of a command
2//!
3//! # Example
4//!
5//! ```rust
6//! # use std::process::Command;
7//! # use command_ext::{CommandExtTrace, CommandWrap};
8//! # use tracing::Level;
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! let output = Command::new("echo")
11//!     .arg("x")
12//!     .trace_args(Level::DEBUG)
13//!     .trace_status(Level::INFO)
14//!     .trace_stdout(Level::TRACE)
15//!     .trace_stderr(Level::WARN)
16//!     .output()?;
17//! # Ok(())
18//! # }
19//! ```
20
21use std::{ffi::OsStr, process::Command};
22use tracing::{debug, error, info, trace, warn, Level};
23use typed_builder::TypedBuilder;
24
25use crate::{wrap::HasCommand, CommandWrap};
26#[cfg(feature = "check")]
27use crate::{CommandExtCheck, CommandExtError};
28
29#[derive(TypedBuilder, Debug)]
30pub struct CommandTrace<'a> {
31    command: &'a mut Command,
32    #[builder(default, setter(into, strip_option))]
33    /// The log level for args before execution
34    args: Option<Level>,
35    #[builder(default, setter(into, strip_option))]
36    /// Whether to log the environment on execution
37    envs: Option<Level>,
38    #[builder(default, setter(into, strip_option))]
39    /// Whether to log the current directory on execution
40    current_dir: Option<Level>,
41    #[builder(default, setter(into, strip_option))]
42    /// Whether to log the status after execution
43    status: Option<Level>,
44    #[builder(default, setter(into, strip_option))]
45    /// Whether to log stdout after execution
46    stdout: Option<Level>,
47    #[builder(default, setter(into, strip_option))]
48    /// Whether to log stderr after execution
49    stderr: Option<Level>,
50}
51
52macro_rules! log {
53    ($lvl:expr, $fmt:expr, $($arg:tt)*) => {
54        match $lvl {
55            Level::TRACE => {
56                trace!($fmt, $($arg)*);
57            }
58            Level::DEBUG => {
59                debug!($fmt, $($arg)*);
60            }
61            Level::INFO => {
62                info!($fmt, $($arg)*);
63            }
64            Level::WARN => {
65                warn!($fmt, $($arg)*);
66            }
67            Level::ERROR => {
68                error!($fmt, $($arg)*);
69            }
70        }
71    }
72}
73
74impl<'a> CommandTrace<'a> {
75    fn trace_before(&mut self) {
76        if let Some(args) = self.args {
77            log!(
78                args,
79                "args: {} {}",
80                self.command().get_program().to_string_lossy(),
81                self.command()
82                    .get_args()
83                    .collect::<Vec<_>>()
84                    .join(OsStr::new(" "))
85                    .to_string_lossy()
86            );
87        }
88
89        if let Some(envs) = self.envs {
90            self.command().get_envs().for_each(|(k, v)| {
91                log!(
92                    envs,
93                    "envs: {}={}",
94                    k.to_string_lossy(),
95                    v.unwrap_or_default().to_string_lossy()
96                );
97            });
98        }
99
100        if let Some(current_dir) = self.current_dir {
101            log!(
102                current_dir,
103                "current_dir: {}",
104                self.command()
105                    .get_current_dir()
106                    .map(|d| d.to_string_lossy())
107                    .unwrap_or_default()
108            );
109        }
110    }
111}
112
113impl<'a> HasCommand for CommandTrace<'a> {
114    fn command(&self) -> &Command {
115        self.command
116    }
117
118    fn command_mut(&mut self) -> &mut Command {
119        self.command
120    }
121}
122
123impl<'a> CommandWrap for CommandTrace<'a> {
124    fn on_spawn(&mut self) {
125        self.trace_before();
126    }
127
128    fn on_output(&mut self) {
129        self.trace_before();
130    }
131
132    fn on_status(&mut self) {
133        self.trace_before();
134    }
135
136    fn after_output(&mut self, output: &std::io::Result<std::process::Output>) {
137        if let Ok(output) = output {
138            if let Some(status) = self.status {
139                log!(status, "status: {}", output.status);
140            }
141
142            if let Some(stdout) = self.stdout {
143                let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
144                if !out.is_empty() {
145                    log!(stdout, "stdout: {out}",);
146                }
147            }
148            if let Some(stderr) = self.stderr {
149                let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
150                if !err.is_empty() {
151                    log!(stderr, "stderr: {err}",);
152                }
153            }
154        }
155    }
156
157    fn after_status(&mut self, status: &std::io::Result<std::process::ExitStatus>) {
158        if let Ok(status) = status {
159            if let Some(status_filter) = self.status {
160                log!(status_filter, "status: {}", status);
161            }
162        }
163    }
164}
165
166impl<'a> From<&'a mut Command> for CommandTrace<'a> {
167    fn from(value: &'a mut Command) -> Self {
168        Self::builder().command(value).build()
169    }
170}
171
172pub trait CommandExtTrace {
173    fn trace_args<L>(&mut self, filter: L) -> CommandTrace
174    where
175        L: Into<Level>;
176    fn trace_envs<L>(&mut self, filter: L) -> CommandTrace
177    where
178        L: Into<Level>;
179    fn trace_current_dir<L>(&mut self, filter: L) -> CommandTrace
180    where
181        L: Into<Level>;
182    fn trace_status<L>(&mut self, filter: L) -> CommandTrace
183    where
184        L: Into<Level>;
185    fn trace_stdout<L>(&mut self, filter: L) -> CommandTrace
186    where
187        L: Into<Level>;
188    fn trace_stderr<L>(&mut self, filter: L) -> CommandTrace
189    where
190        L: Into<Level>;
191}
192
193impl CommandExtTrace for Command {
194    fn trace_args<L>(&mut self, filter: L) -> CommandTrace
195    where
196        L: Into<Level>,
197    {
198        CommandTrace::builder().command(self).args(filter).build()
199    }
200
201    fn trace_envs<L>(&mut self, filter: L) -> CommandTrace
202    where
203        L: Into<Level>,
204    {
205        CommandTrace::builder().command(self).envs(filter).build()
206    }
207
208    fn trace_current_dir<L>(&mut self, filter: L) -> CommandTrace
209    where
210        L: Into<Level>,
211    {
212        CommandTrace::builder()
213            .command(self)
214            .current_dir(filter)
215            .build()
216    }
217
218    fn trace_status<L>(&mut self, filter: L) -> CommandTrace
219    where
220        L: Into<Level>,
221    {
222        CommandTrace::builder().command(self).status(filter).build()
223    }
224
225    fn trace_stdout<L>(&mut self, filter: L) -> CommandTrace
226    where
227        L: Into<Level>,
228    {
229        CommandTrace::builder().command(self).stdout(filter).build()
230    }
231
232    fn trace_stderr<L>(&mut self, filter: L) -> CommandTrace
233    where
234        L: Into<Level>,
235    {
236        CommandTrace::builder().command(self).stderr(filter).build()
237    }
238}
239
240impl<'a> CommandTrace<'a> {
241    pub fn trace_args<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
242    where
243        L: Into<Level>,
244    {
245        self.args = Some(filter.into());
246        self
247    }
248
249    pub fn trace_envs<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
250    where
251        L: Into<Level>,
252    {
253        self.envs = Some(filter.into());
254        self
255    }
256
257    pub fn trace_current_dir<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
258    where
259        L: Into<Level>,
260    {
261        self.current_dir = Some(filter.into());
262        self
263    }
264
265    pub fn trace_status<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
266    where
267        L: Into<Level>,
268    {
269        self.status = Some(filter.into());
270        self
271    }
272
273    pub fn trace_stdout<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
274    where
275        L: Into<Level>,
276    {
277        self.stdout = Some(filter.into());
278        self
279    }
280
281    pub fn trace_stderr<L>(&'a mut self, filter: L) -> &'a mut CommandTrace
282    where
283        L: Into<Level>,
284    {
285        self.stderr = Some(filter.into());
286        self
287    }
288}
289
290#[cfg(feature = "check")]
291impl<'a> CommandExtCheck for CommandTrace<'a> {
292    type Error = CommandExtError;
293
294    fn check(&mut self) -> Result<std::process::Output, Self::Error> {
295        self.output().map_err(CommandExtError::from).and_then(|r| {
296            r.status
297                .success()
298                .then_some(r.clone())
299                .ok_or_else(|| CommandExtError::Check {
300                    status: r.status,
301                    stdout: String::from_utf8_lossy(&r.stdout).to_string(),
302                    stderr: String::from_utf8_lossy(&r.stderr).to_string(),
303                })
304        })
305    }
306}
307
308#[cfg(test)]
309mod test {
310    use std::process::Command;
311    use test_log::test;
312    use tracing::Level;
313
314    use crate::{CommandExtTrace, CommandWrap};
315
316    #[test]
317    #[cfg_attr(miri, ignore)]
318    fn test_args() -> anyhow::Result<()> {
319        Command::new("echo")
320            .arg("x")
321            .trace_args(Level::ERROR)
322            .output()?;
323        Ok(())
324    }
325
326    #[test]
327    #[cfg_attr(miri, ignore)]
328    fn test_envs() -> anyhow::Result<()> {
329        Command::new("echo")
330            .env("x", "y")
331            .trace_envs(Level::ERROR)
332            .output()?;
333        Ok(())
334    }
335
336    #[test]
337    #[cfg_attr(miri, ignore)]
338    fn test_current_dir() -> anyhow::Result<()> {
339        Command::new("echo")
340            .current_dir(env!("CARGO_MANIFEST_DIR"))
341            .trace_current_dir(Level::ERROR)
342            .output()?;
343        Ok(())
344    }
345
346    #[test]
347    #[cfg_attr(miri, ignore)]
348    fn test_status() -> anyhow::Result<()> {
349        Command::new("echo")
350            .arg("x")
351            .trace_status(Level::ERROR)
352            .output()?;
353
354        Ok(())
355    }
356
357    #[test]
358    #[cfg_attr(miri, ignore)]
359    fn test_stdout() -> anyhow::Result<()> {
360        Command::new("echo")
361            .arg("x")
362            .trace_stdout(Level::ERROR)
363            .output()?;
364
365        Ok(())
366    }
367
368    #[test]
369    #[cfg_attr(miri, ignore)]
370    fn test_stderr() -> anyhow::Result<()> {
371        Command::new("bash")
372            .args(["-c", "echo y 1>&2"])
373            .trace_stderr(Level::ERROR)
374            .output()?;
375
376        Ok(())
377    }
378
379    #[test]
380    #[cfg_attr(miri, ignore)]
381    fn test_multi() -> anyhow::Result<()> {
382        Command::new("bash")
383            .args(["-c", "echo y 1>&2; echo x;"])
384            .env("x", "y")
385            .trace_args(Level::ERROR)
386            .trace_status(Level::ERROR)
387            .trace_stdout(Level::ERROR)
388            .trace_stderr(Level::ERROR)
389            .output()?;
390        Ok(())
391    }
392}