diff --git a/examples/journal-try-send.rs b/examples/journal-try-send.rs new file mode 100644 index 0000000..0280a4c --- /dev/null +++ b/examples/journal-try-send.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 try_send for detailed control + match journal::try_send(&[ + "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 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 try_log for structured logging + match journal::try_log( + 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::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::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; + 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..157f511 100644 --- a/src/journal.rs +++ b/src/journal.rs @@ -17,7 +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 +fn try_collect_and_send(args: T) -> Result<()> where T: Iterator, S: AsRef, @@ -27,22 +27,66 @@ where // 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) } + + 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) }; + 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 `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 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 `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 try_print(lvl: u32, s: &str) -> Result<()> { + try_send(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")]) +} + enum SyslogLevel { // Emerg = 0, // Alert = 1, @@ -67,18 +111,47 @@ 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(&[ + 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 try_log( + level: usize, + file: &str, + line: u32, + module_path: &str, + args: &fmt::Arguments<'_>, +) -> Result<()> { + try_send(&[ &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. +#[deprecated( + since = "0.11.0", + note = "Use `try_log_record` instead for proper error handling" +)] pub fn log_record(record: &Record<'_>) { + let _ = try_log_record(record); +} + +/// Send a `log::Record` to systemd-journald. +/// +/// Returns `Ok(())` on success, or an `Error` on failure. +pub fn try_log_record(record: &Record<'_>) -> Result<()> { let keys = [ format!("PRIORITY={}", SyslogLevel::from(record.level()) as usize), format!("MESSAGE={}", record.args()), @@ -90,9 +163,10 @@ pub fn log_record(record: &Record<'_>) { record.module_path().map(|path| format!("CODE_FUNC={path}")), ]; - collect_and_send(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 4ee5ca1..7f8c6bf 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_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::try_send(&["MESSAGE=test message"]); + assert!(result1.is_ok()); + + let result2 = journal::try_print(6, "test print"); + assert!(result2.is_ok()); + + // 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_try_api_with_special_chars() { + let result = journal::try_send(&[ + "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::try_send(&[]); + // 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;