From e1e1584379ddae76ae1180718f6bc7a1f64afc6c Mon Sep 17 00:00:00 2001 From: Tonguechaude Date: Wed, 30 Jul 2025 13:45:21 +0200 Subject: [PATCH 1/3] journal: Add Result-based API for better error handling --- examples/journal-send-result.rs | 88 +++++++++++++++++++++++++++++++++ src/journal.rs | 84 +++++++++++++++++++++++++++++++ tests/journal.rs | 38 ++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 examples/journal-send-result.rs diff --git a/examples/journal-send-result.rs b/examples/journal-send-result.rs new file mode 100644 index 0000000..64e2efc --- /dev/null +++ b/examples/journal-send-result.rs @@ -0,0 +1,88 @@ +#![warn(rust_2018_idioms)] + +#[cfg(feature = "journal")] +mod x { + //! Demonstrate the new Result-based journal API with proper error handling. + + use systemd::journal; + + pub fn main() { + println!("Demonstrating new Result-based journal API..."); + + // Using send_result for detailed control + match journal::send_result(&[ + "MESSAGE=Hello from Rust with Result API!", + "PRIORITY=6", + "CODE_FILE=journal-send-result.rs", + "CODE_LINE=15", + "CUSTOM_FIELD=test_value", + ]) { + Ok(()) => println!("Detailed message sent successfully"), + Err(e) => println!("Failed to send detailed message: {}", e), + } + + // Using print_result for simple messages + if let Err(e) = journal::print_result(6, "Simple message with Result API") { + println!("Failed to send simple message: {}", e); + } else { + println!("Simple message sent successfully"); + } + + // Using log_result for structured logging + match journal::log_result( + 4, // Warning level + file!(), + line!(), + module_path!(), + &format_args!("Structured log entry with count: {}", 42), + ) { + Ok(()) => println!("Structured log sent successfully"), + Err(e) => println!("Failed to send structured log: {}", e), + } + + // Demonstrating error handling with invalid data + // This should still succeed as systemd is quite permissive + match journal::send_result(&["INVALID_KEY_WITHOUT_VALUE"]) { + Ok(()) => println!("Even invalid-looking data was accepted"), + Err(e) => println!("Invalid data was rejected: {}", e), + } + + // Chaining operations with proper error handling + let operations = [ + || journal::print_result(3, "Error level message"), + || journal::print_result(4, "Warning level message"), + || journal::print_result(6, "Info level message"), + || journal::print_result(7, "Debug level message"), + ]; + + let mut success_count = 0; + for (i, op) in operations.iter().enumerate() { + match op() { + Ok(()) => { + success_count += 1; + println!("Operation {} succeeded", i + 1); + } + Err(e) => println!("Operation {} failed: {}", i + 1, e), + } + } + + println!( + "Summary: {}/{} operations succeeded", + success_count, + operations.len() + ); + println!("Example completed. Check your journal with: journalctl -t journal-send-result"); + } +} + +#[cfg(not(feature = "journal"))] +mod x { + pub fn main() { + println!("This example requires the 'journal' feature."); + println!("Run with: cargo run --example journal-send-result --features journal"); + } +} + +fn main() { + x::main() +} diff --git a/src/journal.rs b/src/journal.rs index 6aa2f87..bd30641 100644 --- a/src/journal.rs +++ b/src/journal.rs @@ -30,19 +30,66 @@ where unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) } } +fn collect_and_send_result(args: T) -> Result<()> +where + T: Iterator, + S: AsRef, +{ + let iovecs: Vec = args + // SAFETY: we manually guarantee that the lifetime of const_iovec does not exceed that of + // the data it's referencing in order to avoid additional allocations. + .map(|x| unsafe { const_iovec::from_str(x) }) + .collect(); + + if iovecs.len() > c_int::MAX as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Too many iovecs for systemd journal" + )); + } + + let result = unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) }; + ffi_result(result).map(|_| ()) +} + /// Send preformatted fields to systemd. /// /// This is a relatively low-level operation and probably not suitable unless /// you need precise control over which fields are sent to systemd. +#[deprecated( + since = "0.11.0", + note = "Use `send_result` instead for proper error handling" +)] pub fn send(args: &[&str]) -> c_int { collect_and_send(args.iter()) } +/// Send preformatted fields to systemd. +/// +/// This is a relatively low-level operation and probably not suitable unless +/// you need precise control over which fields are sent to systemd. +/// +/// Returns `Ok(())` on success, or an `Error` on failure. +pub fn send_result(args: &[&str]) -> Result<()> { + collect_and_send_result(args.iter()) +} + /// Send a simple message to systemd-journald. +#[deprecated( + since = "0.11.0", + note = "Use `print_result` instead for proper error handling" +)] pub fn print(lvl: u32, s: &str) -> c_int { send(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) } +/// Send a simple message to systemd-journald. +/// +/// Returns `Ok(())` on success, or an `Error` on failure. +pub fn print_result(lvl: u32, s: &str) -> Result<()> { + send_result(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) +} + enum SyslogLevel { // Emerg = 0, // Alert = 1, @@ -77,6 +124,25 @@ pub fn log(level: usize, file: &str, line: u32, module_path: &str, args: &fmt::A ]); } +/// Record a log entry, with custom priority and location. +/// +/// Returns `Ok(())` on success, or an `Error` on failure. +pub fn log_result( + level: usize, + file: &str, + line: u32, + module_path: &str, + args: &fmt::Arguments<'_>, +) -> Result<()> { + send_result(&[ + &format!("PRIORITY={level}"), + &format!("MESSAGE={args}"), + &format!("CODE_LINE={line}"), + &format!("CODE_FILE={file}"), + &format!("CODE_MODULE={module_path}"), + ]) +} + /// Send a `log::Record` to systemd-journald. pub fn log_record(record: &Record<'_>) { let keys = [ @@ -93,6 +159,24 @@ pub fn log_record(record: &Record<'_>) { collect_and_send(keys.iter().chain(opt_keys.iter().flatten())); } +/// Send a `log::Record` to systemd-journald. +/// +/// Returns `Ok(())` on success, or an `Error` on failure. +pub fn log_record_result(record: &Record<'_>) -> Result<()> { + let keys = [ + format!("PRIORITY={}", SyslogLevel::from(record.level()) as usize), + format!("MESSAGE={}", record.args()), + format!("TARGET={}", record.target()), + ]; + let opt_keys = [ + record.line().map(|line| format!("CODE_LINE={line}")), + record.file().map(|file| format!("CODE_FILE={file}")), + record.module_path().map(|path| format!("CODE_FUNC={path}")), + ]; + + collect_and_send_result(keys.iter().chain(opt_keys.iter().flatten())) +} + /// Logger implementation over systemd-journald. pub struct JournalLog; impl Log for JournalLog { diff --git a/tests/journal.rs b/tests/journal.rs index 4ee5ca1..69121e0 100644 --- a/tests/journal.rs +++ b/tests/journal.rs @@ -22,6 +22,7 @@ fn have_journal() -> bool { } #[test] +#[allow(deprecated)] fn test() { journal::send(&["CODE_FILE=HI", "CODE_LINE=1213", "CODE_FUNCTION=LIES"]); journal::print(1, &format!("Rust can talk to the journal: {}", 4)); @@ -35,6 +36,41 @@ fn test() { sd_journal_log!(4, "HI {:?}", 2); } +#[test] +fn test_result_api() { + // Test the new Result-based API + journal::send_result(&["CODE_FILE=HI", "CODE_LINE=1213", "CODE_FUNCTION=LIES"]).unwrap(); + journal::print_result(1, &format!("Rust can talk to the journal: {}", 4)).unwrap(); + + // Test that the functions return Ok(()) on success + let result1 = journal::send_result(&["MESSAGE=test message"]); + assert!(result1.is_ok()); + + let result2 = journal::print_result(6, "test print"); + assert!(result2.is_ok()); + + // Test log_result function + let result3 = journal::log_result(6, "test.rs", 42, "test_module", &format_args!("test log entry")); + assert!(result3.is_ok()); +} + +#[test] +fn test_result_api_with_special_chars() { + let result = journal::send_result(&[ + "MESSAGE=Test with special chars: éàü©", + "CUSTOM=Line\nbreak", + ]); + assert!(result.is_ok()); +} + +#[test] +fn test_empty_args() { + // Test with an empty array + let result = journal::send_result(&[]); + // Systemd should accept this + assert!(result.is_ok()); +} + #[test] fn cursor() { if !have_journal() { @@ -117,6 +153,7 @@ fn test_seek() { } #[test] +#[allow(deprecated)] fn test_simple_match() { if !have_journal() { return; @@ -172,6 +209,7 @@ fn test_simple_match() { } #[test] +#[allow(deprecated)] fn get_data() { if !have_journal() { return; From dad3b4d6e259932bbe1e09d701bcaca736a6f04b Mon Sep 17 00:00:00 2001 From: Tonguechaude Date: Tue, 7 Oct 2025 20:56:31 +0200 Subject: [PATCH 2/3] fix : Use iovecs.len().try_into() instead of attempting to manually determine if a cast would overflow. --- src/journal.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/journal.rs b/src/journal.rs index bd30641..3bd091c 100644 --- a/src/journal.rs +++ b/src/journal.rs @@ -41,14 +41,14 @@ where .map(|x| unsafe { const_iovec::from_str(x) }) .collect(); - if iovecs.len() > c_int::MAX as usize { - return Err(io::Error::new( + let iovecs_len: c_int = iovecs.len().try_into().map_err(|_| { + io::Error::new( io::ErrorKind::InvalidInput, "Too many iovecs for systemd journal" - )); - } + ) + })?; - let result = unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) }; + let result = unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs_len) }; ffi_result(result).map(|_| ()) } From 8ee86cd1293bc2dce6eeb772754f0fab8bb31e6d Mon Sep 17 00:00:00 2001 From: Tonguechaude Date: Tue, 7 Oct 2025 21:06:59 +0200 Subject: [PATCH 3/3] fix : follow try_ prefix convention for result api in journal.rs --- ...nal-send-result.rs => journal-try-send.rs} | 22 +++--- src/journal.rs | 76 ++++++++----------- tests/journal.rs | 22 +++--- 3 files changed, 55 insertions(+), 65 deletions(-) rename examples/{journal-send-result.rs => journal-try-send.rs} (78%) diff --git a/examples/journal-send-result.rs b/examples/journal-try-send.rs similarity index 78% rename from examples/journal-send-result.rs rename to examples/journal-try-send.rs index 64e2efc..0280a4c 100644 --- a/examples/journal-send-result.rs +++ b/examples/journal-try-send.rs @@ -9,8 +9,8 @@ mod x { pub fn main() { println!("Demonstrating new Result-based journal API..."); - // Using send_result for detailed control - match journal::send_result(&[ + // Using try_send for detailed control + match journal::try_send(&[ "MESSAGE=Hello from Rust with Result API!", "PRIORITY=6", "CODE_FILE=journal-send-result.rs", @@ -21,15 +21,15 @@ mod x { Err(e) => println!("Failed to send detailed message: {}", e), } - // Using print_result for simple messages - if let Err(e) = journal::print_result(6, "Simple message with Result API") { + // Using try_print for simple messages + if let Err(e) = journal::try_print(6, "Simple message with Result API") { println!("Failed to send simple message: {}", e); } else { println!("Simple message sent successfully"); } - // Using log_result for structured logging - match journal::log_result( + // Using try_log for structured logging + match journal::try_log( 4, // Warning level file!(), line!(), @@ -42,17 +42,17 @@ mod x { // Demonstrating error handling with invalid data // This should still succeed as systemd is quite permissive - match journal::send_result(&["INVALID_KEY_WITHOUT_VALUE"]) { + match journal::try_send(&["INVALID_KEY_WITHOUT_VALUE"]) { Ok(()) => println!("Even invalid-looking data was accepted"), Err(e) => println!("Invalid data was rejected: {}", e), } // Chaining operations with proper error handling let operations = [ - || journal::print_result(3, "Error level message"), - || journal::print_result(4, "Warning level message"), - || journal::print_result(6, "Info level message"), - || journal::print_result(7, "Debug level message"), + || journal::try_print(3, "Error level message"), + || journal::try_print(4, "Warning level message"), + || journal::try_print(6, "Info level message"), + || journal::try_print(7, "Debug level message"), ]; let mut success_count = 0; diff --git a/src/journal.rs b/src/journal.rs index 3bd091c..157f511 100644 --- a/src/journal.rs +++ b/src/journal.rs @@ -17,20 +17,7 @@ use std::os::raw::c_void; use std::os::unix::io::AsRawFd; use std::{fmt, io, ptr, result, slice, time}; -fn collect_and_send(args: T) -> c_int -where - T: Iterator, - S: AsRef, -{ - let iovecs: Vec = args - // SAFETY: we manually guarantee that the lifetime of const_iovec does not exceed that of - // the data it's referencing in order to avoid additional allocations. - .map(|x| unsafe { const_iovec::from_str(x) }) - .collect(); - unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) } -} - -fn collect_and_send_result(args: T) -> Result<()> +fn try_collect_and_send(args: T) -> Result<()> where T: Iterator, S: AsRef, @@ -52,42 +39,52 @@ where ffi_result(result).map(|_| ()) } +fn collect_and_send(args: T) -> c_int +where + T: Iterator, + S: AsRef, +{ + try_collect_and_send(args).map_or_else(|_| -1, |_| 0) +} + /// Send preformatted fields to systemd. /// /// This is a relatively low-level operation and probably not suitable unless /// you need precise control over which fields are sent to systemd. #[deprecated( since = "0.11.0", - note = "Use `send_result` instead for proper error handling" + note = "Use `try_send` instead for proper error handling" )] pub fn send(args: &[&str]) -> c_int { collect_and_send(args.iter()) } + /// Send preformatted fields to systemd. /// /// This is a relatively low-level operation and probably not suitable unless /// you need precise control over which fields are sent to systemd. /// /// Returns `Ok(())` on success, or an `Error` on failure. -pub fn send_result(args: &[&str]) -> Result<()> { - collect_and_send_result(args.iter()) +pub fn try_send(args: &[&str]) -> Result<()> { + try_collect_and_send(args.iter()) } /// Send a simple message to systemd-journald. #[deprecated( since = "0.11.0", - note = "Use `print_result` instead for proper error handling" + note = "Use `try_print` instead for proper error handling" )] pub fn print(lvl: u32, s: &str) -> c_int { send(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) } + /// Send a simple message to systemd-journald. /// /// Returns `Ok(())` on success, or an `Error` on failure. -pub fn print_result(lvl: u32, s: &str) -> Result<()> { - send_result(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) +pub fn try_print(lvl: u32, s: &str) -> Result<()> { + try_send(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) } enum SyslogLevel { @@ -114,27 +111,25 @@ impl From for SyslogLevel { } /// Record a log entry, with custom priority and location. +#[deprecated( + since = "0.11.0", + note = "Use `try_log` instead for proper error handling" +)] pub fn log(level: usize, file: &str, line: u32, module_path: &str, args: &fmt::Arguments<'_>) { - send(&[ - &format!("PRIORITY={level}"), - &format!("MESSAGE={args}"), - &format!("CODE_LINE={line}"), - &format!("CODE_FILE={file}"), - &format!("CODE_MODULE={module_path}"), - ]); + let _ = try_log(level, file, line, module_path, args); } /// Record a log entry, with custom priority and location. /// /// Returns `Ok(())` on success, or an `Error` on failure. -pub fn log_result( +pub fn try_log( level: usize, file: &str, line: u32, module_path: &str, args: &fmt::Arguments<'_>, ) -> Result<()> { - send_result(&[ + try_send(&[ &format!("PRIORITY={level}"), &format!("MESSAGE={args}"), &format!("CODE_LINE={line}"), @@ -143,26 +138,20 @@ pub fn log_result( ]) } + /// Send a `log::Record` to systemd-journald. +#[deprecated( + since = "0.11.0", + note = "Use `try_log_record` instead for proper error handling" +)] pub fn log_record(record: &Record<'_>) { - let keys = [ - format!("PRIORITY={}", SyslogLevel::from(record.level()) as usize), - format!("MESSAGE={}", record.args()), - format!("TARGET={}", record.target()), - ]; - let opt_keys = [ - record.line().map(|line| format!("CODE_LINE={line}")), - record.file().map(|file| format!("CODE_FILE={file}")), - record.module_path().map(|path| format!("CODE_FUNC={path}")), - ]; - - collect_and_send(keys.iter().chain(opt_keys.iter().flatten())); + let _ = try_log_record(record); } /// Send a `log::Record` to systemd-journald. /// /// Returns `Ok(())` on success, or an `Error` on failure. -pub fn log_record_result(record: &Record<'_>) -> Result<()> { +pub fn try_log_record(record: &Record<'_>) -> Result<()> { let keys = [ format!("PRIORITY={}", SyslogLevel::from(record.level()) as usize), format!("MESSAGE={}", record.args()), @@ -174,9 +163,10 @@ pub fn log_record_result(record: &Record<'_>) -> Result<()> { record.module_path().map(|path| format!("CODE_FUNC={path}")), ]; - collect_and_send_result(keys.iter().chain(opt_keys.iter().flatten())) + try_collect_and_send(keys.iter().chain(opt_keys.iter().flatten())) } + /// Logger implementation over systemd-journald. pub struct JournalLog; impl Log for JournalLog { diff --git a/tests/journal.rs b/tests/journal.rs index 69121e0..7f8c6bf 100644 --- a/tests/journal.rs +++ b/tests/journal.rs @@ -37,26 +37,26 @@ fn test() { } #[test] -fn test_result_api() { - // Test the new Result-based API - journal::send_result(&["CODE_FILE=HI", "CODE_LINE=1213", "CODE_FUNCTION=LIES"]).unwrap(); - journal::print_result(1, &format!("Rust can talk to the journal: {}", 4)).unwrap(); +fn test_try_api() { + // Test the new try-based API + journal::try_send(&["CODE_FILE=HI", "CODE_LINE=1213", "CODE_FUNCTION=LIES"]).unwrap(); + journal::try_print(1, &format!("Rust can talk to the journal: {}", 4)).unwrap(); // Test that the functions return Ok(()) on success - let result1 = journal::send_result(&["MESSAGE=test message"]); + let result1 = journal::try_send(&["MESSAGE=test message"]); assert!(result1.is_ok()); - let result2 = journal::print_result(6, "test print"); + let result2 = journal::try_print(6, "test print"); assert!(result2.is_ok()); - // Test log_result function - let result3 = journal::log_result(6, "test.rs", 42, "test_module", &format_args!("test log entry")); + // Test try_log function + let result3 = journal::try_log(6, "test.rs", 42, "test_module", &format_args!("test log entry")); assert!(result3.is_ok()); } #[test] -fn test_result_api_with_special_chars() { - let result = journal::send_result(&[ +fn test_try_api_with_special_chars() { + let result = journal::try_send(&[ "MESSAGE=Test with special chars: éàü©", "CUSTOM=Line\nbreak", ]); @@ -66,7 +66,7 @@ fn test_result_api_with_special_chars() { #[test] fn test_empty_args() { // Test with an empty array - let result = journal::send_result(&[]); + let result = journal::try_send(&[]); // Systemd should accept this assert!(result.is_ok()); }