Skip to content

Commit

Permalink
Added support for outputting to stdout when no output is given.
Browse files Browse the repository at this point in the history
Changed prints to print to stderr, so it won't get mixed with stdout.
Parse arguments only once in main and pass them along.
Construct writer once in main and pass it along.
Flush csv (whether outputting to file or stdout) after every input file.
  • Loading branch information
vToMy committed Sep 11, 2024
1 parent 6eade82 commit 6436e3f
Showing 1 changed file with 51 additions and 47 deletions.
98 changes: 51 additions & 47 deletions examples/unifiedlog_parser/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ use macos_unifiedlogs::unified_log::{LogData, UnifiedLogData};
use macos_unifiedlogs::uuidtext::UUIDText;
use simplelog::{Config, SimpleLogger};
use std::error::Error;
use std::fs;
use std::{fs, io};
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;

use clap::Parser;
use csv::Writer;

#[derive(Parser, Debug)]
#[clap(version, about, long_about = None)]
Expand All @@ -35,29 +37,30 @@ struct Args {
input: String,

/// Path to output file. Any directories must already exist
#[clap(short, long)]
#[clap(short, long, default_value = "")]
output: String,
}

fn main() {
println!("Starting Unified Log parser...");
eprintln!("Starting Unified Log parser...");
// Set logging level to warning
SimpleLogger::init(LevelFilter::Warn, Config::default())
.expect("Failed to initialize simple logger");

let args = Args::parse();
let mut writer = construct_writer(&args.output).unwrap();
// Create headers for CSV file
output_header().unwrap();
output_header(&mut writer).unwrap();

if args.input != "" && args.output != "" {
parse_log_archive(&args.input);
if args.input != "" {
parse_log_archive(&args.input, &mut writer);
} else if args.live != "false" {
parse_live_system();
parse_live_system(&mut writer);
}
}

// Parse a provided directory path. Currently expect the path to follow macOS log collect structure
fn parse_log_archive(path: &str) {
// Parse a provided directory path. Currently, expect the path to follow macOS log collect structure
fn parse_log_archive(path: &str, writer: &mut Writer<Box<dyn Write>>) {
let mut archive_path = PathBuf::from(path);

// Parse all UUID files which contain strings and other metadata
Expand All @@ -81,13 +84,14 @@ fn parse_log_archive(path: &str) {
&shared_strings_results,
&timesync_data,
path,
writer,
);

println!("\nFinished parsing Unified Log data. Saved results to: output.csv");
eprintln!("\nFinished parsing Unified Log data.");
}

// Parse a live macOS system
fn parse_live_system() {
fn parse_live_system(writer: &mut Writer<Box<dyn Write>>) {
let strings = collect_strings_system().unwrap();
let shared_strings = collect_shared_strings_system().unwrap();
let timesync_data = collect_timesync_system().unwrap();
Expand All @@ -97,18 +101,20 @@ fn parse_live_system() {
&shared_strings,
&timesync_data,
"/private/var/db/diagnostics",
writer
);

println!("\nFinished parsing Unified Log data. Saved results to: output.csv");
eprintln!("\nFinished parsing Unified Log data.");
}

// Use the provided strings, shared strings, timesync data to parse the Unified Log data at provided path.
// Currently expect the path to follow macOS log collect structure
// Currently, expect the path to follow macOS log collect structure
fn parse_trace_file(
string_results: &[UUIDText],
shared_strings_results: &[SharedCacheStrings],
timesync_data: &[TimesyncBoot],
path: &str,
writer: &mut Writer<Box<dyn Write>>
) {
// We need to persist the Oversize log entries (they contain large strings that don't fit in normal log entries)
// Some log entries have Oversize strings located in different tracev3 files.
Expand All @@ -135,12 +141,12 @@ fn parse_trace_file(
for log_path in paths {
let data = log_path.unwrap();
let full_path = data.path().display().to_string();
println!("Parsing: {}", full_path);
eprintln!("Parsing: {}", full_path);

let log_data = if data.path().exists() {
parse_log(&full_path).unwrap()
} else {
println!("File {} no longer on disk", full_path);
eprintln!("File {} no longer on disk", full_path);
continue;
};

Expand All @@ -160,7 +166,7 @@ fn parse_trace_file(
// Track missing logs
missing_data.push(missing_logs);
log_count += results.len();
output(&results).unwrap();
output(&results, writer).unwrap();
}
}

Expand All @@ -174,12 +180,12 @@ fn parse_trace_file(
for log_path in paths {
let data = log_path.unwrap();
let full_path = data.path().display().to_string();
println!("Parsing: {}", full_path);
eprintln!("Parsing: {}", full_path);

let mut log_data = if data.path().exists() {
parse_log(&full_path).unwrap()
} else {
println!("File {} no longer on disk", full_path);
eprintln!("File {} no longer on disk", full_path);
continue;
};

Expand All @@ -198,7 +204,7 @@ fn parse_trace_file(
missing_data.push(missing_logs);
log_count += results.len();

output(&results).unwrap();
output(&results, writer).unwrap();
}
}

Expand All @@ -212,12 +218,12 @@ fn parse_trace_file(
for log_path in paths {
let data = log_path.unwrap();
let full_path = data.path().display().to_string();
println!("Parsing: {}", full_path);
eprintln!("Parsing: {}", full_path);

let mut log_data = if data.path().exists() {
parse_log(&full_path).unwrap()
} else {
println!("File {} no longer on disk", full_path);
eprintln!("File {} no longer on disk", full_path);
continue;
};

Expand All @@ -237,7 +243,7 @@ fn parse_trace_file(
missing_data.push(missing_logs);
log_count += results.len();

output(&results).unwrap();
output(&results, writer).unwrap();
}
}
archive_path.pop();
Expand All @@ -250,12 +256,12 @@ fn parse_trace_file(
for log_path in paths {
let data = log_path.unwrap();
let full_path = data.path().display().to_string();
println!("Parsing: {}", full_path);
eprintln!("Parsing: {}", full_path);

let mut log_data = if data.path().exists() {
parse_log(&full_path).unwrap()
} else {
println!("File {} no longer on disk", full_path);
eprintln!("File {} no longer on disk", full_path);
continue;
};

Expand All @@ -275,7 +281,7 @@ fn parse_trace_file(
missing_data.push(missing_logs);
log_count += results.len();

output(&results).unwrap();
output(&results, writer).unwrap();
}
}
archive_path.pop();
Expand All @@ -284,7 +290,7 @@ fn parse_trace_file(

// Check if livedata exists. We only have it if 'log collect' was used
if archive_path.exists() {
println!("Parsing: logdata.LiveData.tracev3");
eprintln!("Parsing: logdata.LiveData.tracev3");
let mut log_data = parse_log(&archive_path.display().to_string()).unwrap();
log_data.oversize.append(&mut oversize_strings.oversize);
let (results, missing_logs) = build_log(
Expand All @@ -298,7 +304,7 @@ fn parse_trace_file(
missing_data.push(missing_logs);
log_count += results.len();

output(&results).unwrap();
output(&results, writer).unwrap();
// Track oversize entries
oversize_strings.oversize = log_data.oversize;
archive_path.pop();
Expand All @@ -325,21 +331,25 @@ fn parse_trace_file(
);
log_count += results.len();

output(&results).unwrap();
output(&results, writer).unwrap();
}
println!("Parsed {} log entries", log_count);
eprintln!("Parsed {} log entries", log_count);
}

// Create csv file and create headers
fn output_header() -> Result<(), Box<dyn Error>> {
let args = Args::parse();

let csv_file = OpenOptions::new()
.append(true)
.create(true)
.open(args.output)?;
let mut writer = csv::Writer::from_writer(csv_file);
fn construct_writer(output_path: &str) -> Result<Writer<Box<dyn Write>>, Box<dyn Error>> {
let writer = if output_path != "" {
Box::new(OpenOptions::new()
.append(true)
.create(true)
.open(output_path)?) as Box<dyn Write>
} else {
Box::new(io::stdout()) as Box<dyn Write>
};
Ok(Writer::from_writer(writer))
}

// Create csv file and create headers
fn output_header(writer: &mut Writer<Box<dyn Write>>) -> Result<(), Box<dyn Error>> {
writer.write_record(&[
"Timestamp",
"Event Type",
Expand All @@ -359,19 +369,12 @@ fn output_header() -> Result<(), Box<dyn Error>> {
"Boot UUID",
"System Timezone Name",
])?;
writer.flush()?;
Ok(())
}

// Append or create csv file
fn output(results: &Vec<LogData>) -> Result<(), Box<dyn Error>> {
let args = Args::parse();

let csv_file = OpenOptions::new()
.append(true)
.create(true)
.open(args.output)?;
let mut writer = csv::Writer::from_writer(csv_file);

fn output(results: &Vec<LogData>, writer: &mut Writer<Box<dyn Write>>) -> Result<(), Box<dyn Error>> {
for data in results {
let date_time = Utc.timestamp_nanos(data.time as i64);
writer.write_record(&[
Expand All @@ -394,5 +397,6 @@ fn output(results: &Vec<LogData>) -> Result<(), Box<dyn Error>> {
data.timezone_name.to_owned(),
])?;
}
writer.flush()?;
Ok(())
}

0 comments on commit 6436e3f

Please sign in to comment.