From 7fcde07f12ffc67992a067c8fe7bcaf8f1d13aa0 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Fri, 27 Mar 2026 16:54:07 +0100 Subject: [PATCH 1/8] feat(unifiedlog_iterator): added file logging on top of terminal logging. --- examples/unifiedlog_iterator/Cargo.toml | 6 +- examples/unifiedlog_iterator/src/logger.rs | 206 +++++++++++++++++++++ examples/unifiedlog_iterator/src/main.rs | 39 +++- 3 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 examples/unifiedlog_iterator/src/logger.rs diff --git a/examples/unifiedlog_iterator/Cargo.toml b/examples/unifiedlog_iterator/Cargo.toml index 666a1cfb..588c3c76 100644 --- a/examples/unifiedlog_iterator/Cargo.toml +++ b/examples/unifiedlog_iterator/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -simplelog = "0.12.2" csv = "1.4.0" chrono = "0.4.43" log = "0.4.29" @@ -15,3 +14,8 @@ macos-unifiedlogs = { path = "../../" } clap = { version = "4.5.58", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } libc = "0.2" +# JSON logging (term + optional file output) +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["fmt", "json"] } +tracing-log = "0.1" +tracing-appender = "0.2" diff --git a/examples/unifiedlog_iterator/src/logger.rs b/examples/unifiedlog_iterator/src/logger.rs new file mode 100644 index 00000000..d95cc539 --- /dev/null +++ b/examples/unifiedlog_iterator/src/logger.rs @@ -0,0 +1,206 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and limitations under the License. + +use chrono::Utc; +use clap::ValueEnum; +use serde_json::{Map, Value}; +use std::fs::OpenOptions; +use std::path::Path; +use std::fmt; +use tracing::{Event, Subscriber}; +use tracing::field::{Field, Visit}; +use tracing_log::LogTracer; +use tracing_subscriber::fmt as tracing_fmt; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::registry::LookupSpan; + +// Field visitor to convert event fields into a JSON map, while filtering out unwanted log crate fields. +struct JsonFieldVisitor { + map: Map, +} + +impl Visit for JsonFieldVisitor { + fn record_i64(&mut self, field: &Field, value: i64) { + self.insert(field, Value::from(value)); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.insert(field, Value::from(value)); + } + + fn record_bool(&mut self, field: &Field, value: bool) { + self.insert(field, Value::from(value)); + } + + fn record_str(&mut self, field: &Field, value: &str) { + self.insert(field, Value::from(value)); + } + + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.insert(field, Value::String(format!("{:?}", value))); + } +} + +impl JsonFieldVisitor { + fn insert(&mut self, field: &Field, value: Value) { + let name = field.name(); + + // Filter unwanted log crate fields + if matches!(name, "log.target" | "log.module_path" | "log.file" | "log.line") { + return; + } + + self.map.insert(name.to_string(), value); + } +} + +// Custom event formatter that output structured JSON with event fields and span context. +// Filters out unwanted log crate fields as per the JsonFieldVisitor and includes timestamp and log level in the output. +struct FilteredJson; + +impl tracing_fmt::FormatEvent for FilteredJson +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> tracing_fmt::FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &tracing_fmt::FmtContext<'_, S, N>, + mut writer: tracing_fmt::format::Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let mut out = Map::new(); + // ---- Base fields ---- + out.insert( + "datetime".into(), + Value::String(Utc::now().to_rfc3339()), + ); + out.insert( + "level".into(), + Value::String(event.metadata().level().to_string()), + ); + // ---- Event fields ---- + let mut visitor = JsonFieldVisitor { + map: Map::new(), + }; + event.record(&mut visitor); + + for (k, v) in visitor.map { + out.insert(k, v); + } + + // ---- Span context ---- + if let Some(scope) = ctx.event_scope() { + let mut spans = Vec::new(); + + for span in scope.from_root() { + let mut span_obj = Map::new(); + + span_obj.insert( + "name".into(), + Value::String(span.name().to_string()), + ); + + // Capture span fields (if any) + let mut fields = Map::new(); + span.extensions().get::>() + .map(|f| { + fields.insert("fields".into(), Value::String(f.to_string())); + }); + + if !fields.is_empty() { + span_obj.insert("fields".into(), Value::Object(fields)); + } + + spans.push(Value::Object(span_obj)); + } + + out.insert("spans".into(), Value::Array(spans)); + } + + writeln!(writer, "{}", serde_json::to_string(&out).unwrap()) + } +} + +/// Log level configuration for CLI. +/// +/// This is separate from `log::LevelFilter` to get nice clap `ValueEnum` support. +#[derive(ValueEnum, Clone, Debug)] +pub enum LogLevel { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for tracing_subscriber::filter::LevelFilter { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Off => tracing_subscriber::filter::LevelFilter::OFF, + LogLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR, + LogLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN, + LogLevel::Info => tracing_subscriber::filter::LevelFilter::INFO, + LogLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, + LogLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, + } + } +} + +impl std::fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + LogLevel::Off => "off", + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + }; + write!(f, "{}", s) + } +} + +/// Initialize tracing-based logging. +/// +/// Always logs to stderr/terminal, and optionally also writes structured JSON to a file. +pub fn init_logging( + log_file: Option<&Path>, + level: LogLevel, +) -> Result, Box> { + // Forward `log` records (log crate macros) into tracing + let _ = LogTracer::init(); + + let level_filter: tracing_subscriber::filter::LevelFilter = level.into(); + + let stdout_layer = tracing_fmt::layer() + .with_writer(std::io::stderr); + + let (json_layer, guard) = if let Some(path) = log_file { + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + let (non_blocking, guard) = tracing_appender::non_blocking(file); + + let json_layer = tracing_fmt::layer() + .with_writer(non_blocking) + .event_format(FilteredJson); + + (Some(json_layer), Some(guard)) + } else { + (None, None) + }; + + let _ = tracing_subscriber::registry() + .with(level_filter) + .with(stdout_layer) + .with(json_layer) + .try_init(); + Ok(guard) +} diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index bb99d370..bd802bb2 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -6,14 +6,13 @@ // See the License for the specific language governing permissions and limitations under the License. use chrono::{SecondsFormat, TimeZone, Utc}; -use log::{debug, error, info, warn, LevelFilter}; +use tracing::{debug, error, info, warn}; use macos_unifiedlogs::filesystem::{LiveSystemProvider, LogarchiveProvider}; use macos_unifiedlogs::iterator::UnifiedLogIterator; use macos_unifiedlogs::parser::{build_log, collect_timesync, parse_log}; use macos_unifiedlogs::timesync::TimesyncBoot; use macos_unifiedlogs::traits::FileProvider; use macos_unifiedlogs::unified_log::{LogData, UnifiedLogData}; -use simplelog::{ColorChoice, Config, TermLogger, TerminalMode}; use std::collections::HashMap; use std::error::Error; use std::fmt::Display; @@ -35,8 +34,10 @@ extern "C" fn handle_sigint(_sig: libc::c_int) { } use crate::bookmark::Bookmark; +use crate::logger::{init_logging, LogLevel}; mod bookmark; +mod logger; struct IterationContext { missing_data: Vec, @@ -152,6 +153,18 @@ struct Args { #[clap(short, long, default_value = "false")] append: bool, + /// Log verbosity level (off, error, warn, info, debug, trace) + #[clap(short, long, default_value_t = LogLevel::Warn)] + log_level: LogLevel, + + /// Disable logging to JSONL file (logs still go to terminal) + #[clap(long, default_value = "false")] + no_log_file: bool, + + /// Path to JSONL file to write internal logs to (when file logging is enabled) + #[clap(long, default_value = "unifiedlog_iterator.log.jsonl")] + log_file: PathBuf, + /// Resume from last position using bookmark #[clap(long, default_value = "false")] resume: bool, @@ -193,16 +206,22 @@ impl From for &str { } fn main() { - TermLogger::init( - LevelFilter::Warn, - Config::default(), - TerminalMode::Stderr, - ColorChoice::Auto, - ) - .expect("Failed to initialize simple logger"); + let args = Args::parse(); + + let log_file_opt = if args.no_log_file { + None + } else { + Some(&args.log_file) + }; + + let _log_guard = init_logging(log_file_opt.map(PathBuf::as_path), args.log_level) + .unwrap_or_else(|e| { + eprintln!("Failed to initialize logger: {e}"); + std::process::exit(1); + }); + info!("Starting Unified Log parser..."); - let args = Args::parse(); let output_format = args.format; // Determine source ID for bookmark From ad5817361a9b4a8ffc751938d2d18cfb94069e61 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Fri, 27 Mar 2026 17:01:44 +0100 Subject: [PATCH 2/8] chore: cargo fmt --- examples/unifiedlog_iterator/src/logger.rs | 31 +++++++++------------- examples/unifiedlog_iterator/src/main.rs | 31 +++++++++++++--------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/examples/unifiedlog_iterator/src/logger.rs b/examples/unifiedlog_iterator/src/logger.rs index d95cc539..8f754020 100644 --- a/examples/unifiedlog_iterator/src/logger.rs +++ b/examples/unifiedlog_iterator/src/logger.rs @@ -7,15 +7,15 @@ use chrono::Utc; use clap::ValueEnum; use serde_json::{Map, Value}; +use std::fmt; use std::fs::OpenOptions; use std::path::Path; -use std::fmt; -use tracing::{Event, Subscriber}; use tracing::field::{Field, Visit}; +use tracing::{Event, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::fmt as tracing_fmt; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; // Field visitor to convert event fields into a JSON map, while filtering out unwanted log crate fields. struct JsonFieldVisitor { @@ -49,7 +49,10 @@ impl JsonFieldVisitor { let name = field.name(); // Filter unwanted log crate fields - if matches!(name, "log.target" | "log.module_path" | "log.file" | "log.line") { + if matches!( + name, + "log.target" | "log.module_path" | "log.file" | "log.line" + ) { return; } @@ -74,18 +77,13 @@ where ) -> fmt::Result { let mut out = Map::new(); // ---- Base fields ---- - out.insert( - "datetime".into(), - Value::String(Utc::now().to_rfc3339()), - ); + out.insert("datetime".into(), Value::String(Utc::now().to_rfc3339())); out.insert( "level".into(), Value::String(event.metadata().level().to_string()), ); // ---- Event fields ---- - let mut visitor = JsonFieldVisitor { - map: Map::new(), - }; + let mut visitor = JsonFieldVisitor { map: Map::new() }; event.record(&mut visitor); for (k, v) in visitor.map { @@ -99,14 +97,12 @@ where for span in scope.from_root() { let mut span_obj = Map::new(); - span_obj.insert( - "name".into(), - Value::String(span.name().to_string()), - ); + span_obj.insert("name".into(), Value::String(span.name().to_string())); // Capture span fields (if any) let mut fields = Map::new(); - span.extensions().get::>() + span.extensions() + .get::>() .map(|f| { fields.insert("fields".into(), Value::String(f.to_string())); }); @@ -177,8 +173,7 @@ pub fn init_logging( let level_filter: tracing_subscriber::filter::LevelFilter = level.into(); - let stdout_layer = tracing_fmt::layer() - .with_writer(std::io::stderr); + let stdout_layer = tracing_fmt::layer().with_writer(std::io::stderr); let (json_layer, guard) = if let Some(path) = log_file { let file = OpenOptions::new() diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index bd802bb2..e287cdca 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -6,7 +6,6 @@ // See the License for the specific language governing permissions and limitations under the License. use chrono::{SecondsFormat, TimeZone, Utc}; -use tracing::{debug, error, info, warn}; use macos_unifiedlogs::filesystem::{LiveSystemProvider, LogarchiveProvider}; use macos_unifiedlogs::iterator::UnifiedLogIterator; use macos_unifiedlogs::parser::{build_log, collect_timesync, parse_log}; @@ -21,8 +20,9 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use tracing::{debug, error, info, warn}; -use clap::{builder, Parser, ValueEnum}; +use clap::{Parser, ValueEnum, builder}; use csv::Writer; /// Global atomic flag to track SIGINT signal @@ -34,7 +34,7 @@ extern "C" fn handle_sigint(_sig: libc::c_int) { } use crate::bookmark::Bookmark; -use crate::logger::{init_logging, LogLevel}; +use crate::logger::{LogLevel, init_logging}; mod bookmark; mod logger; @@ -90,7 +90,9 @@ fn filter_and_update_bookmark( .collect(); // Update bookmark with max timestamp seen (not just filtered) to avoid re-scanning - if let Some(max_time) = max_seen_timestamp && let Ok(mut book) = bookmark.lock() { + if let Some(max_time) = max_seen_timestamp + && let Ok(mut book) = bookmark.lock() + { book.update_timestamp(max_time); } @@ -315,7 +317,9 @@ fn main() { } } Err(_) => { - eprintln!("Warning: Could not acquire bookmark lock (mutex poisoned). Bookmark not saved."); + eprintln!( + "Warning: Could not acquire bookmark lock (mutex poisoned). Bookmark not saved." + ); } } std::process::exit(0); @@ -355,9 +359,11 @@ fn parse_single_file( message: e.to_string(), }) .and_then(|mut reader| { - parse_log(&mut reader, path.to_str().unwrap_or_default()).map_err(|err| RuntimeError::FileParse { - path: path.to_string_lossy().to_string(), - message: format!("{err}"), + parse_log(&mut reader, path.to_str().unwrap_or_default()).map_err(|err| { + RuntimeError::FileParse { + path: path.to_string_lossy().to_string(), + message: format!("{err}"), + } }) }) .map(|ref log| { @@ -398,7 +404,9 @@ fn parse_single_file( } // Update bookmark with max timestamp seen (not just written) to avoid re-scanning - if max_seen_timestamp > 0.0 && let Ok(mut bookmark) = bookmark.lock() { + if max_seen_timestamp > 0.0 + && let Ok(mut bookmark) = bookmark.lock() + { bookmark.update_timestamp(max_seen_timestamp); } } else { @@ -485,10 +493,7 @@ fn parse_trace_file( }, }; let resume_timestamp = if resume { - bookmark - .lock() - .map(|b| b.last_timestamp) - .unwrap_or(0.0) + bookmark.lock().map(|b| b.last_timestamp).unwrap_or(0.0) } else { 0.0 }; From 5f83d9f5f3ea9b17b9a857f5d6763214dca9d1c3 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Mon, 30 Mar 2026 11:57:39 +0200 Subject: [PATCH 3/8] feat: default logging to file is now false. As per previous behaviour. --- examples/unifiedlog_iterator/src/main.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index e287cdca..c1e2ffa1 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -159,13 +159,9 @@ struct Args { #[clap(short, long, default_value_t = LogLevel::Warn)] log_level: LogLevel, - /// Disable logging to JSONL file (logs still go to terminal) - #[clap(long, default_value = "false")] - no_log_file: bool, - - /// Path to JSONL file to write internal logs to (when file logging is enabled) - #[clap(long, default_value = "unifiedlog_iterator.log.jsonl")] - log_file: PathBuf, + /// Path to JSONL file to write internal logs to + #[clap(long)] + log_file: Option, /// Resume from last position using bookmark #[clap(long, default_value = "false")] @@ -210,13 +206,9 @@ impl From for &str { fn main() { let args = Args::parse(); - let log_file_opt = if args.no_log_file { - None - } else { - Some(&args.log_file) - }; + let log_file_opt = args.log_file.as_deref(); - let _log_guard = init_logging(log_file_opt.map(PathBuf::as_path), args.log_level) + let _log_guard = init_logging(log_file_opt, args.log_level) .unwrap_or_else(|e| { eprintln!("Failed to initialize logger: {e}"); std::process::exit(1); From 0e84b26b540ebea4826d9b91fc1a51a127aec023 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Mon, 30 Mar 2026 11:58:52 +0200 Subject: [PATCH 4/8] chore: cargo fmt --- examples/unifiedlog_iterator/src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index c1e2ffa1..aac29280 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -208,11 +208,10 @@ fn main() { let log_file_opt = args.log_file.as_deref(); - let _log_guard = init_logging(log_file_opt, args.log_level) - .unwrap_or_else(|e| { - eprintln!("Failed to initialize logger: {e}"); - std::process::exit(1); - }); + let _log_guard = init_logging(log_file_opt, args.log_level).unwrap_or_else(|e| { + eprintln!("Failed to initialize logger: {e}"); + std::process::exit(1); + }); info!("Starting Unified Log parser..."); From 69d1526fb346084c925447976a175140792db831 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Mon, 30 Mar 2026 12:03:28 +0200 Subject: [PATCH 5/8] chore: cargo fmt and cargo clippy --- examples/unifiedlog_iterator/src/logger.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/unifiedlog_iterator/src/logger.rs b/examples/unifiedlog_iterator/src/logger.rs index 8f754020..6d8c5c47 100644 --- a/examples/unifiedlog_iterator/src/logger.rs +++ b/examples/unifiedlog_iterator/src/logger.rs @@ -101,11 +101,12 @@ where // Capture span fields (if any) let mut fields = Map::new(); - span.extensions() + if let Some(f) = span + .extensions() .get::>() - .map(|f| { - fields.insert("fields".into(), Value::String(f.to_string())); - }); + { + fields.insert("fields".into(), Value::String(f.to_string())); + } if !fields.is_empty() { span_obj.insert("fields".into(), Value::Object(fields)); From ef490197265f489554617bd06788259022ffc959 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Mon, 30 Mar 2026 12:27:45 +0200 Subject: [PATCH 6/8] feat: rename logging types to verbosity to avoid conflicting/confusing names with LogData.log_type --- examples/unifiedlog_iterator/src/logger.rs | 34 +++++++++++----------- examples/unifiedlog_iterator/src/main.rs | 14 ++++----- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/unifiedlog_iterator/src/logger.rs b/examples/unifiedlog_iterator/src/logger.rs index 6d8c5c47..21fa2ebf 100644 --- a/examples/unifiedlog_iterator/src/logger.rs +++ b/examples/unifiedlog_iterator/src/logger.rs @@ -126,7 +126,7 @@ where /// /// This is separate from `log::LevelFilter` to get nice clap `ValueEnum` support. #[derive(ValueEnum, Clone, Debug)] -pub enum LogLevel { +pub enum VerbosityLevel { Off, Error, Warn, @@ -135,28 +135,28 @@ pub enum LogLevel { Trace, } -impl From for tracing_subscriber::filter::LevelFilter { - fn from(level: LogLevel) -> Self { +impl From for tracing_subscriber::filter::LevelFilter { + fn from(level: VerbosityLevel) -> Self { match level { - LogLevel::Off => tracing_subscriber::filter::LevelFilter::OFF, - LogLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR, - LogLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN, - LogLevel::Info => tracing_subscriber::filter::LevelFilter::INFO, - LogLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, - LogLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, + VerbosityLevel::Off => tracing_subscriber::filter::LevelFilter::OFF, + VerbosityLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR, + VerbosityLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN, + VerbosityLevel::Info => tracing_subscriber::filter::LevelFilter::INFO, + VerbosityLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, + VerbosityLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, } } } -impl std::fmt::Display for LogLevel { +impl std::fmt::Display for VerbosityLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - LogLevel::Off => "off", - LogLevel::Error => "error", - LogLevel::Warn => "warn", - LogLevel::Info => "info", - LogLevel::Debug => "debug", - LogLevel::Trace => "trace", + VerbosityLevel::Off => "off", + VerbosityLevel::Error => "error", + VerbosityLevel::Warn => "warn", + VerbosityLevel::Info => "info", + VerbosityLevel::Debug => "debug", + VerbosityLevel::Trace => "trace", }; write!(f, "{}", s) } @@ -167,7 +167,7 @@ impl std::fmt::Display for LogLevel { /// Always logs to stderr/terminal, and optionally also writes structured JSON to a file. pub fn init_logging( log_file: Option<&Path>, - level: LogLevel, + level: VerbosityLevel, ) -> Result, Box> { // Forward `log` records (log crate macros) into tracing let _ = LogTracer::init(); diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index aac29280..452235c6 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -34,7 +34,7 @@ extern "C" fn handle_sigint(_sig: libc::c_int) { } use crate::bookmark::Bookmark; -use crate::logger::{LogLevel, init_logging}; +use crate::logger::{VerbosityLevel, init_logging}; mod bookmark; mod logger; @@ -156,12 +156,12 @@ struct Args { append: bool, /// Log verbosity level (off, error, warn, info, debug, trace) - #[clap(short, long, default_value_t = LogLevel::Warn)] - log_level: LogLevel, + #[clap(short, long, default_value_t = VerbosityLevel::Warn)] + verbosity_level: VerbosityLevel, /// Path to JSONL file to write internal logs to - #[clap(long)] - log_file: Option, + #[clap(long = "log-file", value_name = "FILE")] + log_file_path: Option, /// Resume from last position using bookmark #[clap(long, default_value = "false")] @@ -206,9 +206,9 @@ impl From for &str { fn main() { let args = Args::parse(); - let log_file_opt = args.log_file.as_deref(); + let log_file_opt = args.log_file_path.as_deref(); - let _log_guard = init_logging(log_file_opt, args.log_level).unwrap_or_else(|e| { + let _log_guard = init_logging(log_file_opt, args.verbosity_level).unwrap_or_else(|e| { eprintln!("Failed to initialize logger: {e}"); std::process::exit(1); }); From bbc9e4313747c5303bfef0eef91c525542e4dd2f Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Fri, 5 Jun 2026 16:01:06 +0200 Subject: [PATCH 7/8] fix(unifiedlog_iterator): clean up tracing logging implementation - Remove direct `log` and `tracing-log` dependencies; tracing-subscriber handles the log->tracing bridge automatically via its default `tracing-log` feature. - Remove manual `LogTracer::init()` call which conflicted with tracing-subscriber's built-in log compatibility layer, causing "logger already initialized" errors at runtime. - Replace 3 leftover `log::error!` macro calls with `tracing::error!` using structured field syntax for consistency. - Propagate `try_init()` error instead of silently discarding it. - Fix stale doc comment referencing `log::LevelFilter`. --- examples/unifiedlog_iterator/Cargo.toml | 3 +-- examples/unifiedlog_iterator/src/logger.rs | 12 +++++------- examples/unifiedlog_iterator/src/main.rs | 6 +++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/unifiedlog_iterator/Cargo.toml b/examples/unifiedlog_iterator/Cargo.toml index 2f2f9a78..f801c6fe 100644 --- a/examples/unifiedlog_iterator/Cargo.toml +++ b/examples/unifiedlog_iterator/Cargo.toml @@ -8,7 +8,6 @@ edition = "2024" [dependencies] csv = "1.4.0" chrono = "0.4.44" -log = "0.4.29" serde_json = "1.0.149" macos-unifiedlogs = { path = "../../" } clap = { version = "4.6.1", features = ["derive"] } @@ -17,5 +16,5 @@ libc = "0.2.186" # JSON logging (term + optional file output) tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "json"] } -tracing-log = "0.1" + tracing-appender = "0.2" diff --git a/examples/unifiedlog_iterator/src/logger.rs b/examples/unifiedlog_iterator/src/logger.rs index 21fa2ebf..a10f2c13 100644 --- a/examples/unifiedlog_iterator/src/logger.rs +++ b/examples/unifiedlog_iterator/src/logger.rs @@ -12,7 +12,6 @@ use std::fs::OpenOptions; use std::path::Path; use tracing::field::{Field, Visit}; use tracing::{Event, Subscriber}; -use tracing_log::LogTracer; use tracing_subscriber::fmt as tracing_fmt; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -124,7 +123,7 @@ where /// Log level configuration for CLI. /// -/// This is separate from `log::LevelFilter` to get nice clap `ValueEnum` support. +/// This is separate from `tracing_subscriber::filter::LevelFilter` to get nice clap `ValueEnum` support. #[derive(ValueEnum, Clone, Debug)] pub enum VerbosityLevel { Off, @@ -169,9 +168,6 @@ pub fn init_logging( log_file: Option<&Path>, level: VerbosityLevel, ) -> Result, Box> { - // Forward `log` records (log crate macros) into tracing - let _ = LogTracer::init(); - let level_filter: tracing_subscriber::filter::LevelFilter = level.into(); let stdout_layer = tracing_fmt::layer().with_writer(std::io::stderr); @@ -193,10 +189,12 @@ pub fn init_logging( (None, None) }; - let _ = tracing_subscriber::registry() + tracing_subscriber::registry() .with(level_filter) .with(stdout_layer) .with(json_layer) - .try_init(); + .try_init() + .map_err(|e| Box::new(e) as Box)?; + Ok(guard) } diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index f501a669..67602763 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -577,7 +577,7 @@ fn parse_trace_file( { return Err(BrokenPipeError); } - log::error!("Failed to output remaining log data: {err:?}"); + error!("Failed to output remaining log data: {error}", error = err); } } info!( @@ -599,7 +599,7 @@ fn iterate_chunks( let mut buf = Vec::new(); if let Err(err) = reader.read_to_end(&mut buf) { - log::error!("Failed to read tracev3 file: {err:?}"); + error!("Failed to read tracev3 file: {error}", error = err); return Ok((0, 0)); } @@ -646,7 +646,7 @@ fn iterate_chunks( debug!("Broken pipe detected, saving bookmark before exit..."); return Err(BrokenPipeError); } - log::error!("Failed to output log data: {err:?}"); + error!("Failed to output log data: {error}", error = err); } if missing_logs.catalog_data.is_empty() From 1bfd09a457ce88cd3bb7e3babdf60583fbe2b237 Mon Sep 17 00:00:00 2001 From: Dario Borreguero Rincon Date: Fri, 5 Jun 2026 16:20:36 +0200 Subject: [PATCH 8/8] fix(unifiedlog_iterator): _log_guard now always drops naturally at the end of main's scope, flushing any buffered log events regardless of which exit path was taken. --- examples/unifiedlog_iterator/src/main.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/unifiedlog_iterator/src/main.rs b/examples/unifiedlog_iterator/src/main.rs index 67602763..d0ece4b5 100644 --- a/examples/unifiedlog_iterator/src/main.rs +++ b/examples/unifiedlog_iterator/src/main.rs @@ -18,6 +18,7 @@ use std::fmt::Display; use std::fs; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::process::ExitCode; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use tracing::{debug, error, info, warn}; @@ -203,15 +204,21 @@ impl From for &str { } } -fn main() { +// We return ExitCode instead of calling std::process::exit() so that all +// destructors run on scope exit. This guarantees the tracing WorkerGuard +// flushes any buffered log events before the process terminates (SIGINT path). +fn main() -> ExitCode { let args = Args::parse(); let log_file_opt = args.log_file_path.as_deref(); - let _log_guard = init_logging(log_file_opt, args.verbosity_level).unwrap_or_else(|e| { - eprintln!("Failed to initialize logger: {e}"); - std::process::exit(1); - }); + let _log_guard = match init_logging(log_file_opt, args.verbosity_level) { + Ok(guard) => guard, + Err(e) => { + eprintln!("Failed to initialize logger: {e}"); + return ExitCode::FAILURE; + } + }; info!("Starting Unified Log parser..."); @@ -313,7 +320,7 @@ fn main() { ); } } - std::process::exit(0); + return ExitCode::SUCCESS; } // Save bookmark on normal exit @@ -335,6 +342,8 @@ fn main() { if let Err(e) = result { error!("Error during parsing: {error}", error = e); } + + ExitCode::SUCCESS } fn parse_single_file(