From c6fc3495a121264e1a35564dee5db3638c9875e8 Mon Sep 17 00:00:00 2001 From: mikkurogue Date: Thu, 19 Feb 2026 18:19:53 +0200 Subject: [PATCH] feat: add tracing for output and use --trace to provide stdout --- Cargo.lock | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/lib.rs | 21 +++++++- src/main.rs | 71 ++++++++++++++++++++++---- 4 files changed, 227 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ebf521..bc5fe09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -350,6 +359,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.175" @@ -368,6 +383,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.5" @@ -383,6 +407,15 @@ dependencies = [ "adler2", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -404,6 +437,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "predicates" version = "3.1.3" @@ -470,6 +509,17 @@ name = "regex-automata" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustix" @@ -526,12 +576,27 @@ dependencies = [ "syn", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "strsim" version = "0.11.1" @@ -588,6 +653,76 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" version = "1.0.19" @@ -600,6 +735,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -844,6 +985,8 @@ dependencies = [ "serde", "tempfile", "thiserror", + "tracing", + "tracing-subscriber", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 87f77a5..568b085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ quick-xml = { version = "0.31.0", features = ["serialize"] } csv = "1.3.1" serde = { version = "1", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +tracing = "0.1.44" [dev-dependencies] tempfile = "3" diff --git a/src/lib.rs b/src/lib.rs index 65e910a..fed347d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use quick_xml::reader::Reader; use std::collections::BTreeMap; use std::io::BufRead; use std::path::Path; +use tracing::{debug, instrument}; #[derive(Debug, Clone)] pub struct SheetInfo { @@ -20,6 +21,7 @@ pub struct StyleInfo { pub is_date: bool, } +#[instrument(level = "debug", skip(reader))] pub fn parse_styles(reader: R) -> Result> { let mut xml = Reader::from_reader(reader); let mut buf = Vec::new(); @@ -130,6 +132,7 @@ pub fn parse_styles(reader: R) -> Result> { } buf.clear(); } + debug!(style_count = styles.len(), "Parsed styles"); Ok(styles) } @@ -140,6 +143,7 @@ fn tag_eq_ignore_case(actual: &[u8], expect: &str) -> bool { || actual.ends_with(expect.to_ascii_uppercase().as_bytes()) } +#[instrument(level = "debug", skip(reader))] pub fn parse_workbook_rels(reader: R) -> Result> { // Map r:Id -> full path inside zip (xl/worksheets/sheet1.xml) let mut xml = Reader::from_reader(reader); @@ -172,9 +176,11 @@ pub fn parse_workbook_rels(reader: R) -> Result( reader: R, rels: &BTreeMap, @@ -225,9 +231,15 @@ pub fn parse_workbook( } buf.clear(); } + debug!( + sheet_count = sheets.len(), + is_1904 = is_1904, + "Parsed workbook" + ); Ok((sheets, is_1904)) } +#[instrument(level = "debug", skip(reader))] pub fn read_shared_strings(reader: R) -> Result> { let mut xml = Reader::from_reader(reader); // xml.config_mut().trim_text(true); @@ -264,6 +276,7 @@ pub fn read_shared_strings(reader: R) -> Result> { } buf.clear(); } + debug!(string_count = strings.len(), "Parsed shared strings"); Ok(strings) } @@ -320,7 +333,11 @@ pub fn to_lowercase_filename(name: &str) -> String { }) .collect(); - if s.is_empty() { "sheet".to_string() } else { s } + if s.is_empty() { + "sheet".to_string() + } else { + s + } } // Excel date/time utilities @@ -353,6 +370,7 @@ pub fn excel_serial_to_iso_date(serial: f64, is_1904: bool) -> Option { Some(datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()) } +#[instrument(level = "debug", skip(reader, shared_strings, styles), fields(out_path = %out_path.display()))] pub fn export_sheet_xml_to_csv( reader: R, shared_strings: &[String], @@ -509,6 +527,7 @@ pub fn export_sheet_xml_to_csv( wtr.write_record(row_vals.iter())?; } wtr.flush()?; + debug!(total_rows = current_row_idx, "Exported sheet to CSV"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 23adba3..36757c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ use std::fs::File; -use std::io::{BufReader}; +use std::io::BufReader; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use tracing::{debug, info, instrument, warn}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use zip::read::ZipArchive; // Import functions from lib module use xcsv::{ - StyleInfo, export_sheet_xml_to_csv, parse_styles, parse_workbook, parse_workbook_rels, - read_shared_strings, to_lowercase_filename, + export_sheet_xml_to_csv, parse_styles, parse_workbook, parse_workbook_rels, + read_shared_strings, to_lowercase_filename, StyleInfo, }; #[derive(Parser, Debug)] @@ -19,6 +21,10 @@ struct Cli { #[arg(value_name = "XLSX_PATH")] xlsx_path: PathBuf, + /// Enable debug tracing output + #[arg(long, global = true)] + trace: bool, + #[command(subcommand)] command: Command, } @@ -38,6 +44,7 @@ enum Command { }, } +#[instrument(level = "debug", skip_all)] fn parse_args() -> Cli { Cli::parse() } @@ -53,21 +60,41 @@ fn parse_delimiter(s: &str) -> Result { } } +#[instrument(level = "debug", skip(path), fields(path = %path.display()))] fn open_zip(path: &Path) -> Result>> { + debug!("Opening XLSX file"); let file = File::open(path).with_context(|| format!("Failed to open {:?}", path))?; let reader = BufReader::new(file); let zip = ZipArchive::new(reader).context("Failed to read XLSX (zip) archive")?; + debug!(file_count = zip.len(), "Opened XLSX archive"); Ok(zip) } fn main() -> Result<()> { let cli = parse_args(); + + // Initialize tracing based on --trace flag + // Logs go to stderr to not interfere with stdout output + let filter = if cli.trace { + EnvFilter::new("xcsv=debug") + } else { + EnvFilter::new("xcsv=warn") + }; + + tracing_subscriber::registry() + .with(fmt::layer().with_writer(std::io::stderr)) + .with(filter) + .init(); + + info!(xlsx_path = %cli.xlsx_path.display(), "Starting xcsv"); let mut zip = open_zip(&cli.xlsx_path)?; match cli.command { Command::List => { + info!("Listing sheets"); // Stream-parse workbook rels let rels_map = { + debug!("Parsing workbook relationships"); let f = zip .by_name("xl/_rels/workbook.xml.rels") .context("missing xl/_rels/workbook.xml.rels")?; @@ -76,6 +103,7 @@ fn main() -> Result<()> { }; // Stream-parse workbook let (sheets, _) = { + debug!("Parsing workbook"); let f = zip .by_name("xl/workbook.xml") .context("missing xl/workbook.xml")?; @@ -83,31 +111,42 @@ fn main() -> Result<()> { parse_workbook(reader, &rels_map)? }; + info!(sheet_count = sheets.len(), "Found sheets"); for s in sheets { println!("{}", s.name); } } Command::Export { out_dir, delimiter } => { + info!(out_dir = %out_dir.display(), delimiter = ?char::from(delimiter), "Exporting sheets"); std::fs::create_dir_all(&out_dir).context("create output directory")?; // Stream-parse shared strings if present let shared_strings: Vec = if let Ok(f) = zip.by_name("xl/sharedStrings.xml") { + debug!("Parsing shared strings"); let reader = BufReader::new(f); - read_shared_strings(reader)? + let strings = read_shared_strings(reader)?; + debug!(count = strings.len(), "Loaded shared strings"); + strings } else { + warn!("No shared strings found"); Vec::new() }; // Stream-parse styles if present let styles: Vec = if let Ok(f) = zip.by_name("xl/styles.xml") { + debug!("Parsing styles"); let reader = BufReader::new(f); - parse_styles(reader)? + let s = parse_styles(reader)?; + debug!(count = s.len(), "Loaded styles"); + s } else { + warn!("No styles found"); Vec::new() }; // Workbook rels and sheets let rels_map = { + debug!("Parsing workbook relationships"); let f = zip .by_name("xl/_rels/workbook.xml.rels") .context("missing xl/_rels/workbook.xml.rels")?; @@ -115,6 +154,7 @@ fn main() -> Result<()> { parse_workbook_rels(reader)? }; let (sheets, is_1904) = { + debug!("Parsing workbook"); let f = zip .by_name("xl/workbook.xml") .context("missing xl/workbook.xml")?; @@ -122,16 +162,29 @@ fn main() -> Result<()> { parse_workbook(reader, &rels_map)? }; + info!( + sheet_count = sheets.len(), + is_1904 = is_1904, + "Found sheets" + ); // Export each sheet - for sheet in sheets { + for sheet in &sheets { + debug!(sheet_name = %sheet.name, path = %sheet.path_in_zip, "Exporting sheet"); let filename = format!("{}.csv", to_lowercase_filename(&sheet.name)); - let out_path = out_dir.join(filename); + let out_path = out_dir.join(&filename); let f = zip .by_name(&sheet.path_in_zip) .with_context(|| format!("missing {}", sheet.path_in_zip))?; let reader = BufReader::new(f); - export_sheet_xml_to_csv(reader, &shared_strings, &styles, is_1904, &out_path, delimiter)?; - eprintln!("wrote {:?}", out_path); + export_sheet_xml_to_csv( + reader, + &shared_strings, + &styles, + is_1904, + &out_path, + delimiter, + )?; + info!(output = %out_path.display(), "Wrote CSV"); } } }