@@ -41,6 +41,15 @@ struct Args {
41
41
/// Path to output file. Any directories must already exist
42
42
#[ clap( short, long, default_value = "" ) ]
43
43
output : String ,
44
+
45
+ /// Output format. Options: csv, jsonl. Default is autodetect.
46
+ #[ clap( short, long, default_value = "auto" ) ]
47
+ format : String ,
48
+
49
+ /// Append to output file
50
+ /// If false, will overwrite output file
51
+ #[ clap( short, long, default_value = "false" ) ]
52
+ append : bool ,
44
53
}
45
54
46
55
fn main ( ) {
@@ -50,19 +59,27 @@ fn main() {
50
59
. expect ( "Failed to initialize simple logger" ) ;
51
60
52
61
let args = Args :: parse ( ) ;
53
- let mut writer = construct_writer ( & args. output ) . unwrap ( ) ;
54
- // Create headers for CSV file
55
- output_header ( & mut writer) . unwrap ( ) ;
62
+ let output_format = if args. format . is_empty ( ) || args. format == "auto" {
63
+ std:: path:: Path :: new ( & args. output )
64
+ . extension ( )
65
+ . and_then ( std:: ffi:: OsStr :: to_str)
66
+ . unwrap_or ( "csv" )
67
+ . to_string ( )
68
+ } else {
69
+ args. format . clone ( )
70
+ } ;
56
71
57
- if args. input != "" {
72
+ let mut writer = OutputWriter :: new ( & args. output , & output_format, args. append ) . unwrap ( ) ;
73
+
74
+ if !args. input . is_empty ( ) {
58
75
parse_log_archive ( & args. input , & mut writer) ;
59
76
} else if args. live != "false" {
60
77
parse_live_system ( & mut writer) ;
61
78
}
62
79
}
63
80
64
81
// Parse a provided directory path. Currently, expect the path to follow macOS log collect structure
65
- fn parse_log_archive ( path : & str , writer : & mut Writer < Box < dyn Write > > ) {
82
+ fn parse_log_archive ( path : & str , writer : & mut OutputWriter ) {
66
83
let mut archive_path = PathBuf :: from ( path) ;
67
84
68
85
// Parse all UUID files which contain strings and other metadata
@@ -93,7 +110,7 @@ fn parse_log_archive(path: &str, writer: &mut Writer<Box<dyn Write>>) {
93
110
}
94
111
95
112
// Parse a live macOS system
96
- fn parse_live_system ( writer : & mut Writer < Box < dyn Write > > ) {
113
+ fn parse_live_system ( writer : & mut OutputWriter ) {
97
114
let strings = collect_strings_system ( ) . unwrap ( ) ;
98
115
let shared_strings = collect_shared_strings_system ( ) . unwrap ( ) ;
99
116
let timesync_data = collect_timesync_system ( ) . unwrap ( ) ;
@@ -116,7 +133,7 @@ fn parse_trace_file(
116
133
shared_strings_results : & [ SharedCacheStrings ] ,
117
134
timesync_data : & HashMap < String , TimesyncBoot > ,
118
135
path : & str ,
119
- writer : & mut Writer < Box < dyn Write > > ,
136
+ writer : & mut OutputWriter ,
120
137
) {
121
138
// We need to persist the Oversize log entries (they contain large strings that don't fit in normal log entries)
122
139
// Some log entries have Oversize strings located in different tracev3 files.
@@ -302,7 +319,7 @@ fn iterate_chunks(
302
319
strings_data : & [ UUIDText ] ,
303
320
shared_strings : & [ SharedCacheStrings ] ,
304
321
timesync_data : & HashMap < String , TimesyncBoot > ,
305
- writer : & mut Writer < Box < dyn Write > > ,
322
+ writer : & mut OutputWriter ,
306
323
oversize_strings : & mut UnifiedLogData ,
307
324
) -> usize {
308
325
let log_bytes = fs:: read ( path) . unwrap ( ) ;
@@ -341,71 +358,116 @@ fn iterate_chunks(
341
358
count
342
359
}
343
360
344
- fn construct_writer ( output_path : & str ) -> Result < Writer < Box < dyn Write > > , Box < dyn Error > > {
345
- let writer = if output_path != "" {
346
- Box :: new (
347
- OpenOptions :: new ( )
348
- . append ( true )
349
- . create ( true )
350
- . open ( output_path) ?,
351
- ) as Box < dyn Write >
352
- } else {
353
- Box :: new ( io:: stdout ( ) ) as Box < dyn Write >
354
- } ;
355
- Ok ( Writer :: from_writer ( writer) )
361
+ pub struct OutputWriter {
362
+ writer : OutputWriterEnum ,
356
363
}
357
364
358
- // Create csv file and create headers
359
- fn output_header ( writer : & mut Writer < Box < dyn Write > > ) -> Result < ( ) , Box < dyn Error > > {
360
- writer. write_record ( & [
361
- "Timestamp" ,
362
- "Event Type" ,
363
- "Log Type" ,
364
- "Subsystem" ,
365
- "Thread ID" ,
366
- "PID" ,
367
- "EUID" ,
368
- "Library" ,
369
- "Library UUID" ,
370
- "Activity ID" ,
371
- "Category" ,
372
- "Process" ,
373
- "Process UUID" ,
374
- "Message" ,
375
- "Raw Message" ,
376
- "Boot UUID" ,
377
- "System Timezone Name" ,
378
- ] ) ?;
379
- writer. flush ( ) ?;
380
- Ok ( ( ) )
365
+ enum OutputWriterEnum {
366
+ Csv ( Box < Writer < Box < dyn Write > > > ) ,
367
+ Json ( Box < dyn Write > ) ,
368
+ }
369
+
370
+ impl OutputWriter {
371
+ pub fn new (
372
+ output_path : & str ,
373
+ output_format : & str ,
374
+ append : bool ,
375
+ ) -> Result < Self , Box < dyn Error > > {
376
+ let writer: Box < dyn Write > = if !output_path. is_empty ( ) {
377
+ Box :: new (
378
+ OpenOptions :: new ( )
379
+ . write ( true )
380
+ . create ( true )
381
+ . truncate ( !append)
382
+ . append ( append)
383
+ . open ( output_path) ?,
384
+ )
385
+ } else {
386
+ Box :: new ( io:: stdout ( ) )
387
+ } ;
388
+
389
+ let writer_enum = match output_format {
390
+ "csv" => {
391
+ let mut csv_writer = Writer :: from_writer ( writer) ;
392
+ // Write CSV headers
393
+ csv_writer. write_record ( [
394
+ "Timestamp" ,
395
+ "Event Type" ,
396
+ "Log Type" ,
397
+ "Subsystem" ,
398
+ "Thread ID" ,
399
+ "PID" ,
400
+ "EUID" ,
401
+ "Library" ,
402
+ "Library UUID" ,
403
+ "Activity ID" ,
404
+ "Category" ,
405
+ "Process" ,
406
+ "Process UUID" ,
407
+ "Message" ,
408
+ "Raw Message" ,
409
+ "Boot UUID" ,
410
+ "System Timezone Name" ,
411
+ ] ) ?;
412
+ csv_writer. flush ( ) ?;
413
+ OutputWriterEnum :: Csv ( Box :: new ( csv_writer) )
414
+ }
415
+ "jsonl" => OutputWriterEnum :: Json ( writer) ,
416
+ _ => {
417
+ eprintln ! ( "Unsupported output format: {}" , output_format) ;
418
+ std:: process:: exit ( 1 ) ;
419
+ }
420
+ } ;
421
+
422
+ Ok ( OutputWriter {
423
+ writer : writer_enum,
424
+ } )
425
+ }
426
+
427
+ pub fn write_record ( & mut self , record : & LogData ) -> Result < ( ) , Box < dyn Error > > {
428
+ match & mut self . writer {
429
+ OutputWriterEnum :: Csv ( csv_writer) => {
430
+ let date_time = Utc . timestamp_nanos ( record. time as i64 ) ;
431
+ csv_writer. write_record ( & [
432
+ date_time. to_rfc3339_opts ( SecondsFormat :: Millis , true ) ,
433
+ record. event_type . to_owned ( ) ,
434
+ record. log_type . to_owned ( ) ,
435
+ record. subsystem . to_owned ( ) ,
436
+ record. thread_id . to_string ( ) ,
437
+ record. pid . to_string ( ) ,
438
+ record. euid . to_string ( ) ,
439
+ record. library . to_owned ( ) ,
440
+ record. library_uuid . to_owned ( ) ,
441
+ record. activity_id . to_string ( ) ,
442
+ record. category . to_owned ( ) ,
443
+ record. process . to_owned ( ) ,
444
+ record. process_uuid . to_owned ( ) ,
445
+ record. message . to_owned ( ) ,
446
+ record. raw_message . to_owned ( ) ,
447
+ record. boot_uuid . to_owned ( ) ,
448
+ record. timezone_name . to_owned ( ) ,
449
+ ] ) ?;
450
+ }
451
+ OutputWriterEnum :: Json ( json_writer) => {
452
+ writeln ! ( json_writer, "{}" , serde_json:: to_string( record) . unwrap( ) ) ?;
453
+ }
454
+ }
455
+ Ok ( ( ) )
456
+ }
457
+
458
+ pub fn flush ( & mut self ) -> Result < ( ) , Box < dyn Error > > {
459
+ match & mut self . writer {
460
+ OutputWriterEnum :: Csv ( csv_writer) => csv_writer. flush ( ) ?,
461
+ OutputWriterEnum :: Json ( json_writer) => json_writer. flush ( ) ?,
462
+ }
463
+ Ok ( ( ) )
464
+ }
381
465
}
382
466
383
467
// Append or create csv file
384
- fn output (
385
- results : & Vec < LogData > ,
386
- writer : & mut Writer < Box < dyn Write > > ,
387
- ) -> Result < ( ) , Box < dyn Error > > {
468
+ fn output ( results : & Vec < LogData > , writer : & mut OutputWriter ) -> Result < ( ) , Box < dyn Error > > {
388
469
for data in results {
389
- let date_time = Utc . timestamp_nanos ( data. time as i64 ) ;
390
- writer. write_record ( & [
391
- date_time. to_rfc3339_opts ( SecondsFormat :: Millis , true ) ,
392
- data. event_type . to_owned ( ) ,
393
- data. log_type . to_owned ( ) ,
394
- data. subsystem . to_owned ( ) ,
395
- data. thread_id . to_string ( ) ,
396
- data. pid . to_string ( ) ,
397
- data. euid . to_string ( ) ,
398
- data. library . to_owned ( ) ,
399
- data. library_uuid . to_owned ( ) ,
400
- data. activity_id . to_string ( ) ,
401
- data. category . to_owned ( ) ,
402
- data. process . to_owned ( ) ,
403
- data. process_uuid . to_owned ( ) ,
404
- data. message . to_owned ( ) ,
405
- data. raw_message . to_owned ( ) ,
406
- data. boot_uuid . to_owned ( ) ,
407
- data. timezone_name . to_owned ( ) ,
408
- ] ) ?;
470
+ writer. write_record ( data) ?;
409
471
}
410
472
writer. flush ( ) ?;
411
473
Ok ( ( ) )
0 commit comments