Skip to main content

arcbox_logging/
lib.rs

1//! Shared logging infrastructure for ArcBox components.
2//!
3//! Provides a unified tracing initialization with:
4//! - Size-based log file rotation (default: 10 MB per file, 5 files max)
5//! - JSON format for files (machine-parseable)
6//! - Human-readable format for stderr (when running in foreground)
7//! - Non-blocking file writes via `tracing-appender`
8
9mod rotating;
10
11use std::path::PathBuf;
12
13use tracing_appender::non_blocking::WorkerGuard;
14use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
15
16pub use rotating::SizeRotatingWriter;
17
18/// Configuration for log initialization.
19pub struct LogConfig {
20    /// Directory to write log files into (e.g. `~/.arcbox/log`).
21    pub log_dir: PathBuf,
22    /// Log file name (e.g. `"daemon.log"`).
23    pub file_name: String,
24    /// Default `EnvFilter` directive when `RUST_LOG` is unset.
25    pub default_filter: String,
26    /// Maximum size in bytes before rotating (default: 10 MB).
27    pub max_file_size: u64,
28    /// Maximum number of rotated files to keep (default: 5).
29    pub max_files: usize,
30    /// When true, also emit human-readable logs to stderr.
31    pub foreground: bool,
32}
33
34impl Default for LogConfig {
35    fn default() -> Self {
36        Self {
37            log_dir: PathBuf::from("."),
38            file_name: "app.log".to_string(),
39            default_filter: "info".to_string(),
40            max_file_size: 10 * 1024 * 1024,
41            max_files: 5,
42            foreground: false,
43        }
44    }
45}
46
47/// Guard that keeps the non-blocking writer alive. Must be held for the
48/// lifetime of the program — dropping it flushes pending writes.
49pub struct LogGuard {
50    _file_guard: WorkerGuard,
51}
52
53impl LogGuard {
54    /// Explicitly drop the guard to flush pending log writes.
55    /// Call this during graceful shutdown before process exit.
56    pub fn flush(self) {
57        // Drop triggers flush in WorkerGuard.
58        drop(self);
59    }
60}
61
62/// Initialize the tracing subscriber with file + optional stderr output.
63///
64/// Returns a [`LogGuard`] that **must** be held until shutdown. Dropping
65/// the guard flushes all pending writes to the log file.
66///
67/// # Panics
68///
69/// Panics if the log directory cannot be created.
70pub fn init(config: LogConfig) -> LogGuard {
71    // Bridge `log` crate → tracing so third-party dependencies emitting via
72    // `log` have their output captured by the tracing subscriber.
73    std::fs::create_dir_all(&config.log_dir).expect("failed to create log directory");
74
75    let rotating_writer = SizeRotatingWriter::new(
76        config.log_dir.join(&config.file_name),
77        config.max_file_size,
78        config.max_files,
79    );
80
81    let (non_blocking, file_guard) = tracing_appender::non_blocking(rotating_writer);
82
83    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
84        .unwrap_or_else(|_| config.default_filter.into());
85
86    // File layer: JSON format for machine parsing.
87    let file_layer = tracing_subscriber::fmt::layer()
88        .json()
89        .with_target(true)
90        .with_writer(non_blocking);
91
92    // Stderr layer: human-readable, only when running in foreground.
93    let stderr_layer = config.foreground.then(|| {
94        tracing_subscriber::fmt::layer()
95            .with_target(false)
96            .with_writer(std::io::stderr)
97    });
98
99    tracing_subscriber::registry()
100        .with(env_filter)
101        .with(file_layer)
102        .with(stderr_layer)
103        .init();
104
105    LogGuard {
106        _file_guard: file_guard,
107    }
108}
109
110/// Initialize tracing with file output + sentry layer.
111///
112/// Same as [`init`] but adds a `sentry::integrations::tracing::layer()`.
113/// Requires sentry to be initialized before calling this.
114#[cfg(feature = "sentry")]
115pub fn init_with_sentry(config: LogConfig) -> LogGuard {
116    std::fs::create_dir_all(&config.log_dir).expect("failed to create log directory");
117
118    let rotating_writer = SizeRotatingWriter::new(
119        config.log_dir.join(&config.file_name),
120        config.max_file_size,
121        config.max_files,
122    );
123
124    let (non_blocking, file_guard) = tracing_appender::non_blocking(rotating_writer);
125
126    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
127        .unwrap_or_else(|_| config.default_filter.into());
128
129    let file_layer = tracing_subscriber::fmt::layer()
130        .json()
131        .with_target(true)
132        .with_writer(non_blocking);
133
134    let stderr_layer = config.foreground.then(|| {
135        tracing_subscriber::fmt::layer()
136            .with_target(false)
137            .with_writer(std::io::stderr)
138    });
139
140    let sentry_layer = sentry::integrations::tracing::layer();
141
142    tracing_subscriber::registry()
143        .with(env_filter)
144        .with(file_layer)
145        .with(stderr_layer)
146        .with(sentry_layer)
147        .init();
148
149    LogGuard {
150        _file_guard: file_guard,
151    }
152}