Skip to content

Commit 57d38b0

Browse files
Fix #130: Deduplicate logging setup with centralized init_logging()
- Add LogConfig struct to src/logging.rs encapsulating all logging configuration (verbose level, quiet flag, log_file path, is_server_subprocess mode) - Add init_logging() that consolidates the duplicated subscriber setup: verbosity-to-LevelFilter mapping, file/stderr/no-output branching, optional colorized stdout layer, WorkerGuard return - Add try_init_logging() for standalone server/client paths where a subscriber may already be registered - Add helper functions verbosity_to_level() and build_detailed_layer() to keep the public API clean - Replace ~60-line inline logging blocks in run_async_mode() and run_blocking_mode() in src/main.rs with 5-line LogConfig+init_logging calls - Replace inline logging in run_blocking_mode() server subprocess path with init_logging(is_server_subprocess=true) - Replace inline logging in standalone_server.rs and standalone_client.rs with try_init_logging() calls - Update module documentation in src/logging.rs to accurately describe the new API (removes fictional RUST_LOG/EnvFilter references) - Remove now-unused imports (tracing_subscriber::{LevelFilter, Layer}, ColorizedFormatter) from main.rs, standalone_server.rs, standalone_client.rs - Net change: -5 lines (207 added, 212 removed) with zero duplication - All existing tests pass, clippy clean, cargo fmt applied AI-assisted-by: Claude Opus 4 (claude-sonnet-4-20250514) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d586320 commit 57d38b0

4 files changed

Lines changed: 207 additions & 212 deletions

File tree

src/logging.rs

Lines changed: 165 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,190 @@
11
//! # Logging Configuration Module
22
//!
3-
//! This module provides centralized logging setup and management for the
4-
//! benchmark suite. It configures the `tracing` framework to provide
5-
//! flexible, structured, and performant logging to both the console and
6-
//! optional log files.
3+
//! Centralised logging setup for the IPC benchmark suite. All
4+
//! subscriber initialization flows through [`init_logging`] (or
5+
//! [`try_init_logging`] when a subscriber may already be set).
76
//!
8-
//! ## Key Features
7+
//! ## Features
98
//!
10-
//! - **Dual Output**: Supports simultaneous logging to both the console
11-
//! (stdout) and a dedicated log file.
12-
//! - **Level Control**: Allows independent configuration of log levels for
13-
//! console and file outputs.
14-
//! - **Dynamic Filtering**: Uses `tracing_subscriber` to allow log levels
15-
//! to be set via environment variables (e.g., `RUST_LOG`).
16-
//! - **Human-Readable Format**: Configures a clean, readable format for
17-
//! console output to improve developer experience.
9+
//! - **Dual output**: simultaneous logging to a rolling daily file
10+
//! (or stderr) *and* colorised stdout for user-facing output.
11+
//! - **Verbosity control**: maps `-v` / `-vv` flags to INFO / DEBUG /
12+
//! TRACE via [`LogConfig::verbose`].
13+
//! - **Quiet mode**: suppresses the stdout layer entirely.
14+
//! - **Server subprocess mode**: minimal stderr-only logging at DEBUG
15+
//! level to avoid interfering with stdout pipe signalling.
1816
//!
1917
//! ## Usage
2018
//!
21-
//! The primary function, `init_logging`, should be called once at the
22-
//! beginning of the application's `main` function to set up the global
23-
//! logger.
24-
//!
2519
//! ```rust,ignore
26-
//! // In main.rs
27-
//! use ipc_benchmark::logging;
20+
//! use ipc_benchmark::logging::{init_logging, LogConfig};
2821
//!
2922
//! fn main() -> anyhow::Result<()> {
30-
//! let log_file = Some("benchmark.log".to_string());
31-
//! logging::init_logging(log_level, &log_file)?;
23+
//! let config = LogConfig {
24+
//! verbose: 1,
25+
//! quiet: false,
26+
//! log_file: None,
27+
//! is_server_subprocess: false,
28+
//! };
29+
//! let _guard = init_logging(&config)?;
3230
//! // ... rest of the application
3331
//! Ok(())
3432
//! }
3533
//! ```
3634
35+
use anyhow::Result;
3736
use colored::*;
3837
use std::cell::RefCell;
3938
use std::fmt;
4039
use tracing::{Event, Level, Subscriber};
40+
use tracing_appender::non_blocking::WorkerGuard;
41+
use tracing_subscriber::filter::LevelFilter;
4142
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
4243
use tracing_subscriber::fmt::FmtContext;
44+
use tracing_subscriber::prelude::*;
4345
use tracing_subscriber::registry::LookupSpan;
46+
use tracing_subscriber::Layer;
47+
48+
/// Configuration for the logging subsystem.
49+
///
50+
/// Construct this from CLI arguments and pass to [`init_logging`] or
51+
/// [`try_init_logging`].
52+
///
53+
/// ## Fields
54+
///
55+
/// * `verbose` — verbosity level: 0 = INFO, 1 = DEBUG, 2+ = TRACE
56+
/// * `quiet` — if true, suppress the colorised stdout layer
57+
/// * `log_file` — destination for the detailed log layer:
58+
/// - `None` — daily-rotating file in current directory
59+
/// - `Some("stderr")` — write to stderr
60+
/// - `Some(path)` — daily-rotating file at the given path
61+
/// * `is_server_subprocess` — if true, use a minimal stderr-only
62+
/// subscriber at DEBUG level (ignores other fields)
63+
pub struct LogConfig {
64+
pub verbose: u8,
65+
pub quiet: bool,
66+
pub log_file: Option<String>,
67+
pub is_server_subprocess: bool,
68+
}
69+
70+
/// Initialize the global tracing subscriber.
71+
///
72+
/// This must be called exactly once per process. Returns an optional
73+
/// [`WorkerGuard`] that the caller must keep alive for the duration
74+
/// of the program when file logging is active (dropping it flushes
75+
/// and closes the log file).
76+
///
77+
/// # Errors
78+
///
79+
/// Returns an error if the subscriber cannot be initialised (e.g.
80+
/// one is already set).
81+
pub fn init_logging(config: &LogConfig) -> Result<Option<WorkerGuard>> {
82+
// Server subprocess: minimal stderr, no file, no stdout layer.
83+
if config.is_server_subprocess {
84+
tracing_subscriber::fmt()
85+
.with_writer(std::io::stderr)
86+
.with_max_level(tracing::Level::DEBUG)
87+
.init();
88+
return Ok(None);
89+
}
90+
91+
let log_level = verbosity_to_level(config.verbose);
92+
93+
let (detailed_log_layer, guard) = build_detailed_layer(config.log_file.as_deref(), log_level)?;
94+
95+
let stdout_log = if !config.quiet {
96+
Some(
97+
tracing_subscriber::fmt::layer()
98+
.with_writer(std::io::stdout)
99+
.event_format(ColorizedFormatter)
100+
.with_filter(log_level),
101+
)
102+
} else {
103+
None
104+
};
105+
106+
tracing_subscriber::registry()
107+
.with(detailed_log_layer)
108+
.with(stdout_log)
109+
.init();
110+
111+
Ok(guard)
112+
}
113+
114+
/// Attempt to initialize logging, silently succeeding if a
115+
/// subscriber is already registered.
116+
///
117+
/// Used by standalone server/client paths which may be invoked
118+
/// after the parent process has already set up logging.
119+
///
120+
/// # Returns
121+
///
122+
/// `Ok(())` regardless of whether a new subscriber was installed.
123+
pub fn try_init_logging(config: &LogConfig) -> Result<()> {
124+
if config.quiet {
125+
return Ok(());
126+
}
127+
128+
let log_level = verbosity_to_level(config.verbose);
129+
130+
// Standalone paths use stderr + colorised output only.
131+
let _ = tracing_subscriber::fmt()
132+
.with_writer(std::io::stderr)
133+
.with_max_level(log_level)
134+
.event_format(ColorizedFormatter)
135+
.try_init();
136+
137+
Ok(())
138+
}
139+
140+
/// Map the `-v` count to a [`LevelFilter`].
141+
fn verbosity_to_level(verbose: u8) -> LevelFilter {
142+
match verbose {
143+
0 => LevelFilter::INFO,
144+
1 => LevelFilter::DEBUG,
145+
_ => LevelFilter::TRACE,
146+
}
147+
}
148+
149+
/// Build the detailed (file or stderr) log layer and optional guard.
150+
fn build_detailed_layer(
151+
log_file: Option<&str>,
152+
level: LevelFilter,
153+
) -> Result<(
154+
Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync>,
155+
Option<WorkerGuard>,
156+
)> {
157+
if let Some("stderr") = log_file {
158+
let layer = tracing_subscriber::fmt::layer()
159+
.with_writer(std::io::stderr)
160+
.with_filter(level)
161+
.boxed();
162+
return Ok((layer, None));
163+
}
164+
165+
let file_appender = match log_file {
166+
Some(path_str) => {
167+
let log_path = std::path::Path::new(path_str);
168+
let log_dir = log_path
169+
.parent()
170+
.unwrap_or_else(|| std::path::Path::new("."));
171+
let log_filename = log_path
172+
.file_name()
173+
.unwrap_or_else(|| std::ffi::OsStr::new("ipc_benchmark.log"));
174+
tracing_appender::rolling::daily(log_dir, log_filename)
175+
}
176+
None => tracing_appender::rolling::daily(".", "ipc_benchmark.log"),
177+
};
178+
179+
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
180+
let layer = tracing_subscriber::fmt::layer()
181+
.with_writer(non_blocking)
182+
.with_ansi(false)
183+
.with_filter(level)
184+
.boxed();
185+
186+
Ok((layer, Some(guard)))
187+
}
44188

45189
// A thread-local buffer for formatting log messages to avoid allocations on every event.
46190
// A generous capacity is chosen to prevent reallocations for most log messages.

0 commit comments

Comments
 (0)