Skip to main content

robit_ai/
logging.rs

1//! Shared logging initialization for Robit binaries.
2//!
3//! Provides a unified way to initialize logging with support for:
4//! - Config file `app.log_level` setting
5//! - Config file `app.log_file` setting (log to file, daily rotation)
6//! - Environment variable `RUST_LOG` (takes precedence)
7//! - Sensible defaults for third-party crates
8
9use crate::config::AppConfig;
10use std::fs::OpenOptions;
11use std::path::PathBuf;
12use tracing_subscriber::prelude::*;
13use tracing_subscriber::{filter::Directive, EnvFilter};
14
15/// Get the log file path: {working_dir}/.robit/logs/robit-YYYY-MM-DD.log
16///
17/// Creates the directory if it doesn't exist.
18fn get_log_file_path(working_dir: &PathBuf) -> Result<PathBuf, Box<dyn std::error::Error>> {
19    let logs_dir = working_dir.join(".robit").join("logs");
20
21    // Create logs directory if it doesn't exist
22    std::fs::create_dir_all(&logs_dir)?;
23
24    // Format date as YYYY-MM-DD
25    let date = chrono::Local::now().format("%Y-%m-%d");
26    let log_file = logs_dir.join(format!("robit-{}.log", date));
27
28    Ok(log_file)
29}
30
31/// Build the EnvFilter from config and defaults.
32fn build_filter(
33    app_config: Option<&AppConfig>,
34    target_crate: &str,
35    additional_directives: &[&str],
36) -> EnvFilter {
37    let mut filter = EnvFilter::from_default_env();
38
39    // If no RUST_LOG is set, build from config and defaults
40    if std::env::var("RUST_LOG").is_err() {
41        // Use log_level from config if present, otherwise default to info
42        let global_level = app_config
43            .and_then(|c| c.log_level.as_deref())
44            .unwrap_or("info");
45
46        // Add target crate directive
47        if let Ok(dir) = format!("{}={}", target_crate, global_level).parse() {
48            filter = filter.add_directive(dir);
49        }
50
51        // Also set robit crates to the same level
52        for robit_crate in &["robit_agent", "robit_chatbot", "robit_ai"] {
53            if robit_crate != &target_crate {
54                if let Ok(dir) = format!("{}={}", robit_crate, global_level).parse() {
55                    filter = filter.add_directive(dir);
56                }
57            }
58        }
59
60        // Add additional directives
61        for dir_str in additional_directives {
62            if let Ok(dir) = dir_str.parse::<Directive>() {
63                filter = filter.add_directive(dir);
64            }
65        }
66
67        // Default third-party crates to warn
68        for dep_crate in &[
69            "reqwest",
70            "hyper",
71            "hyper_util",
72            "tungstenite",
73            "tokio_tungstenite",
74            "tokio",
75            "tauri",
76        ] {
77            if let Ok(dir) = format!("{}=warn", dep_crate).parse() {
78                filter = filter.add_directive(dir);
79            }
80        }
81    }
82
83    filter
84}
85
86/// Initialize logging with optional app config and a target crate name.
87///
88/// Priority order:
89/// 1. `RUST_LOG` environment variable (full control)
90/// 2. `app.log_level` from config.toml (sets global level)
91/// 3. Defaults to `info` for the target crate and `warn` for third-party crates
92///
93/// If `app.log_file = true`, logs are also written to:
94///   {working_dir}/.robit/logs/robit-YYYY-MM-DD.log (daily rotation)
95///
96/// # Arguments
97/// - `app_config`: Optional `AppConfig` from config.toml
98/// - `target_crate`: Name of the target crate (e.g. "robit_tui", "robit_qq")
99/// - `working_dir`: Working directory for the agent (where .robit/logs is created)
100/// - `additional_directives`: Optional additional `Directive`s for specific crates
101pub fn init_logging(
102    app_config: Option<&AppConfig>,
103    target_crate: &str,
104    working_dir: &PathBuf,
105    additional_directives: &[&str],
106) {
107    let filter = build_filter(app_config, target_crate, additional_directives);
108
109    // Check if file logging is enabled
110    let log_file_enabled = app_config.and_then(|c| c.log_file).unwrap_or(false);
111
112    if log_file_enabled {
113        // Log to both console and file
114        match get_log_file_path(working_dir) {
115            Ok(log_path) => {
116                match OpenOptions::new()
117                    .create(true)
118                    .append(true)
119                    .open(&log_path)
120                {
121                    Ok(file) => {
122                        let file_writer = tracing_subscriber::fmt::writer::MakeWriterExt::with_max_level(file, tracing::Level::TRACE);
123
124                        // Create layers
125                        let console_layer = tracing_subscriber::fmt::layer()
126                            .with_writer(std::io::stdout)
127                            .with_filter(filter.clone());
128
129                        let file_layer = tracing_subscriber::fmt::layer()
130                            .with_writer(file_writer)
131                            .with_ansi(false)
132                            .with_filter(filter);
133
134                        // Combine layers
135                        let registry = tracing_subscriber::registry()
136                            .with(console_layer)
137                            .with(file_layer);
138
139                        registry.init();
140
141                        tracing::info!("Logging to file: {}", log_path.display());
142                    }
143                    Err(e) => {
144                        // Fallback to console-only logging
145                        eprintln!("Failed to open log file: {}. Falling back to console-only logging.", e);
146                        tracing_subscriber::fmt().with_env_filter(filter).init();
147                    }
148                }
149            }
150            Err(e) => {
151                // Fallback to console-only logging
152                eprintln!("Failed to prepare log path: {}. Falling back to console-only logging.", e);
153                tracing_subscriber::fmt().with_env_filter(filter).init();
154            }
155        }
156    } else {
157        // Console-only logging (default)
158        tracing_subscriber::fmt().with_env_filter(filter).init();
159    }
160}
161
162/// Initialize logging but discard output (for TUI mode).
163///
164/// Same as `init_logging` but logs go to `/dev/null` instead of stdout.
165pub fn init_logging_silent(
166    app_config: Option<&AppConfig>,
167    target_crate: &str,
168    _working_dir: &PathBuf,
169    additional_directives: &[&str],
170) {
171    let filter = build_filter(app_config, target_crate, additional_directives);
172
173    tracing_subscriber::fmt()
174        .with_env_filter(filter)
175        .with_writer(std::io::sink)
176        .init();
177}