Skip to main content

agent_trace/
observability.rs

1use anyhow::{Context, Result};
2use std::io::{self, Write};
3use std::path::Path;
4use tracing_subscriber::EnvFilter;
5
6/// Stable user-facing CLI output.
7///
8/// This is intentionally separate from diagnostic `tracing` events: messages
9/// emitted here are part of the command-line contract and must stay script-safe.
10pub trait CliOutput: Send + Sync {
11    fn line(&self, message: &str) -> Result<()>;
12    fn warn(&self, message: &str) -> Result<()>;
13    fn error(&self, message: &str) -> Result<()>;
14    fn raw_stdout(&self, content: &str) -> Result<()>;
15}
16
17#[derive(Debug, Clone, Copy)]
18pub struct TerminalOutput {
19    quiet: bool,
20}
21
22impl TerminalOutput {
23    pub fn new(quiet: bool) -> Self {
24        Self { quiet }
25    }
26}
27
28impl CliOutput for TerminalOutput {
29    fn line(&self, message: &str) -> Result<()> {
30        if self.quiet {
31            return Ok(());
32        }
33        let mut stdout = io::stdout().lock();
34        writeln!(stdout, "{message}").context("writing stdout")
35    }
36
37    fn warn(&self, message: &str) -> Result<()> {
38        let mut stderr = io::stderr().lock();
39        writeln!(stderr, "{message}").context("writing stderr")
40    }
41
42    fn error(&self, message: &str) -> Result<()> {
43        let mut stderr = io::stderr().lock();
44        writeln!(stderr, "{message}").context("writing stderr")
45    }
46
47    fn raw_stdout(&self, content: &str) -> Result<()> {
48        let mut stdout = io::stdout().lock();
49        stdout
50            .write_all(content.as_bytes())
51            .context("writing raw stdout")?;
52        stdout.flush().context("flushing stdout")
53    }
54}
55
56#[derive(Debug, Clone, Copy)]
57pub struct NoopOutput;
58
59impl CliOutput for NoopOutput {
60    fn line(&self, _message: &str) -> Result<()> {
61        Ok(())
62    }
63
64    fn warn(&self, _message: &str) -> Result<()> {
65        Ok(())
66    }
67
68    fn error(&self, _message: &str) -> Result<()> {
69        Ok(())
70    }
71
72    fn raw_stdout(&self, _content: &str) -> Result<()> {
73        Ok(())
74    }
75}
76
77pub fn init_tracing(verbosity: u8) -> Result<()> {
78    let default_level = match verbosity {
79        0 => "warn",
80        1 => "info",
81        _ => "debug",
82    };
83    let filter =
84        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
85
86    tracing_subscriber::fmt()
87        .with_writer(io::stderr)
88        .with_env_filter(filter)
89        .try_init()
90        .map_err(|e| anyhow::anyhow!("initializing tracing subscriber: {e}"))
91}
92
93pub fn format_permission_denied(path: &Path, reason: &str) -> String {
94    format!("Permission denied: {} — {}", path.display(), reason)
95}