diff --git a/examples/actix-web.rs b/examples/actix-web.rs index c2b727d..b52a5a3 100644 --- a/examples/actix-web.rs +++ b/examples/actix-web.rs @@ -89,28 +89,22 @@ async fn root() -> HttpResponse { async fn get_user(path: web::Path) -> ActixResult { let user_id = path.into_inner(); async { - logfire::info!("Fetching user with ID: {user_id}", user_id = user_id as i64); + logfire::info!("Fetching user with ID: {user_id}"); // Simulate database lookup tokio::time::sleep(std::time::Duration::from_millis(10)) .instrument(logfire::span!("Database query for user")) .await; - logfire::debug!( - "Database query completed for user {user_id}", - user_id = user_id as i64 - ); + logfire::debug!("Database query completed for user {user_id}"); if user_id == 0 { - logfire::warn!( - "Invalid user ID requested: {user_id}", - user_id = user_id as i64 - ); + logfire::warn!("Invalid user ID requested: {user_id}"); return Ok(HttpResponse::BadRequest().finish()); } if user_id > 1000 { - logfire::error!("User {user_id} not found", user_id = user_id as i64); + logfire::error!("User {user_id} not found"); return Ok(HttpResponse::NotFound().finish()); } @@ -120,10 +114,7 @@ async fn get_user(path: web::Path) -> ActixResult { email: format!("user{user_id}@example.com"), }; - logfire::info!( - "Successfully retrieved user {user_id}", - user_id = user_id as i64 - ); + logfire::info!("Successfully retrieved user {user_id}"); Ok(HttpResponse::Ok().json(user)) } @@ -158,7 +149,7 @@ async fn create_user(payload: web::Json) -> ActixResult &'static str { async fn get_user(Path(user_id): Path) -> Result, StatusCode> { async { - logfire::info!("Fetching user with ID: {user_id}", user_id = user_id as i64); + logfire::info!("Fetching user with ID: {user_id}"); // Simulate database lookup tokio::time::sleep(std::time::Duration::from_millis(10)) .instrument(logfire::span!("Database query for user")) .await; - logfire::debug!( - "Database query completed for user {user_id}", - user_id = user_id as i64 - ); + logfire::debug!("Database query completed for user {user_id}",); if user_id == 0 { - logfire::warn!( - "Invalid user ID requested: {user_id}", - user_id = user_id as i64 - ); + logfire::warn!("Invalid user ID requested: {user_id}",); return Err(StatusCode::BAD_REQUEST); } if user_id > 1000 { - logfire::error!("User {user_id} not found", user_id = user_id as i64); + logfire::error!("User {user_id} not found"); return Err(StatusCode::NOT_FOUND); } @@ -133,14 +127,11 @@ async fn get_user(Path(user_id): Path) -> Result, StatusCode> { email: format!("user{user_id}@example.com"), }; - logfire::info!( - "Successfully retrieved user {user_id}", - user_id = user_id as i64 - ); + logfire::info!("Successfully retrieved user {user_id}",); Ok(Json(user)) } - .instrument(logfire::span!("Fetching user {user_id}", user_id = user_id)) + .instrument(logfire::span!("Fetching user {user_id}")) .await } @@ -173,7 +164,7 @@ async fn create_user( logfire::info!( "Successfully created user {id} with name {name}", - id = user.id as i64, + id = user.id, name = &user.name ); diff --git a/examples/basic.rs b/examples/basic.rs index 6421683..18c7319 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -51,7 +51,7 @@ fn main() -> Result<()> { logfire::info!( "total size of {cwd} is {size} bytes", cwd = cwd.display().to_string(), - size = total_size as i64 + size = total_size ); Ok(()) diff --git a/src/internal/exporters/console.rs b/src/internal/exporters/console.rs index 3d0a931..13275fc 100644 --- a/src/internal/exporters/console.rs +++ b/src/internal/exporters/console.rs @@ -459,7 +459,7 @@ mod tests { let _ = crate::span!(level: Level::DEBUG, "debug span"); let _ = crate::span!(parent: &root, level: Level::DEBUG, "debug span with explicit parent"); - crate::info!("log with values", foo = 42, bar = 33); + crate::info!("log with values", foo = 42i64, bar = 33i64); crate::info!("hello world log"); panic!("oh no!"); })) diff --git a/src/macros/impl_.rs b/src/macros/impl_.rs index b4ee6f4..285597a 100644 --- a/src/macros/impl_.rs +++ b/src/macros/impl_.rs @@ -77,27 +77,71 @@ pub fn converter(_: &T) -> LogfireConverter { LogfireConverter(TryConvertOption(FallbackToConvertValue(PhantomData))) } -/// `usize` might exceed OTLP range of i64. The Python SDK handles this by converting -/// to a string for oversize values, we do the same. -impl LogfireConverter { +/// Convenience to take ownership of borrow on String +impl LogfireConverter<&'_ String> { #[inline] #[must_use] - pub fn convert_value(&self, value: usize) -> Option { - if let Ok(value) = i64::try_from(value) { - Some(value.into()) - } else { - // TODO emit a warning? - Some(value.to_string().into()) + pub fn convert_value(&self, value: &String) -> Option { + Some(String::to_owned(value).into()) + } +} + +macro_rules! impl_into_try_into_i64_value { + ($type:ty) => { + impl LogfireConverter<$type> { + #[inline] + #[must_use] + pub fn convert_value(&self, value: $type) -> Option { + // Attempt to convert the value to an i64. + if let Ok(value) = i64::try_from(value) { + Some(value.into()) + } else { + // If it fails (e.g., overflow), fall back to a string. + // TODO emit a warning? + Some(value.to_string().into()) + } + } + } + }; +} + +macro_rules! impl_into_from_i64_value { + ($type:ty) => { + impl LogfireConverter<$type> { + #[inline] + #[must_use] + pub fn convert_value(&self, value: $type) -> Option { + Some(i64::from(value).into()) + } } + }; +} + +impl_into_from_i64_value!(u8); +impl_into_from_i64_value!(u16); +impl_into_from_i64_value!(u32); +impl_into_try_into_i64_value!(u64); +impl_into_try_into_i64_value!(u128); +impl_into_try_into_i64_value!(usize); +impl_into_from_i64_value!(i8); +impl_into_from_i64_value!(i16); +impl_into_from_i64_value!(i32); +impl_into_try_into_i64_value!(i128); +impl_into_try_into_i64_value!(isize); + +impl LogfireConverter { + #[inline] + #[must_use] + pub fn convert_value(&self, value: f32) -> Option { + Some(f64::from(value).into()) } } -/// Convenience to take ownership of borrow on String -impl LogfireConverter<&'_ String> { +impl LogfireConverter { #[inline] #[must_use] - pub fn convert_value(&self, value: &String) -> Option { - Some(String::to_owned(value).into()) + pub fn convert_value(&self, value: char) -> Option { + Some(value.to_string().into()) } } diff --git a/src/macros/mod.rs b/src/macros/mod.rs index d6e7be0..16050a3 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -172,7 +172,7 @@ macro_rules! trace { /// The macro accepts the following syntax: /// /// ```rust -/// # let value = 42; +/// # let value: i64 = 42; /// logfire::log!( /// parent: tracing::Span::current(), // optional, tracing::Span /// tracing::Level::INFO, // required, see `info!` and variants for convenience @@ -205,34 +205,34 @@ macro_rules! trace { /// let root_span = logfire::span!("Root span"); /// /// // Log with attributes x and y -/// logfire::log!(parent: &root_span, Level::INFO, "Child log", x = 42, y = "hello"); +/// logfire::log!(parent: &root_span, Level::INFO, "Child log", x = 42i64, y = "hello"); /// // or -/// logfire::info!(parent: &root_span, "Child log", x = 42, y = "hello"); +/// logfire::info!(parent: &root_span, "Child log", x = 42i64, y = "hello"); /// /// // Typically a span will be "entered" to set the parent implicitly /// root_span.in_scope(|| { /// // This log will be a child of root_span -/// logfire::log!(Level::INFO, "Nested log", x = 42, y = "hello"); +/// logfire::log!(Level::INFO, "Nested log", x = 42i64, y = "hello"); /// // or -/// logfire::info!("Nested log", x = 42, y = "hello"); +/// logfire::info!("Nested log", x = 42i64, y = "hello"); /// /// // Debug-level child log -/// logfire::log!(Level::DEBUG, "Debugging", x = 42, y = "hello"); +/// logfire::log!(Level::DEBUG, "Debugging", x = 42i64, y = "hello"); /// // or -/// logfire::debug!("Debugging", x = 42, y = "hello"); +/// logfire::debug!("Debugging", x = 42i64, y = "hello"); /// }); /// /// // With x included in the formatted message but not as an attribute -/// let x = 42; +/// let x: i64 = 42; /// logfire::log!(Level::INFO, "Log with x = {x}, y = {y}", y = "hello"); /// // or /// logfire::info!("Log with x = {x}, y = {y}", y = "hello"); /// /// // Attributes can either be a single name or a dotted.name /// // `dotted.name` is not available in the format string. -/// logfire::log!(Level::INFO, "Log with x = {x}, y = {y}", y = "hello", foo.bar = 42); +/// logfire::log!(Level::INFO, "Log with x = {x}, y = {y}", y = "hello", foo.bar = 42i64); /// // or -/// logfire::info!("Log with x = {x}, y = {y}", y = "hello", foo.bar = 42); +/// logfire::info!("Log with x = {x}, y = {y}", y = "hello", foo.bar = 42i64); /// /// // If just an identifier is used without `= value`, it will be captured as an attribute /// let foo = "bar"; @@ -271,16 +271,18 @@ macro_rules! log { #[cfg(test)] mod tests { - use crate::config::{ConsoleOptions, Target}; + use crate::{ + config::{ConsoleOptions, Target}, + logfire::LocalLogfireGuard, + }; use std::sync::{Arc, Mutex}; use tracing::Level; - #[test] - fn test_error_macro() { + fn get_output_guard(level: Level) -> (Arc>>, LocalLogfireGuard) { let output = Arc::new(Mutex::new(Vec::new())); let console_options = ConsoleOptions { target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::ERROR) + ..ConsoleOptions::default().with_min_log_level(level) }; let logfire = crate::configure() @@ -293,8 +295,15 @@ mod tests { let guard = crate::set_local_logfire(logfire); + (output, guard) + } + + #[test] + fn test_error_macro() { + let (output, guard) = get_output_guard(Level::ERROR); + crate::error!("Test error message"); - crate::error!("Test error with value", field = 42); + crate::error!("Test error with value", field = 42i64); guard.shutdown().unwrap(); @@ -308,21 +317,7 @@ mod tests { #[test] fn test_warn_macro() { - let output = Arc::new(Mutex::new(Vec::new())); - let console_options = ConsoleOptions { - target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::WARN) - }; - - let logfire = crate::configure() - .local() - .send_to_logfire(false) - .with_console(Some(console_options)) - .with_default_level_filter(tracing::level_filters::LevelFilter::TRACE) - .finish() - .unwrap(); - - let guard = crate::set_local_logfire(logfire); + let (output, guard) = get_output_guard(Level::WARN); crate::warn!("Test warn message"); crate::warn!("Test warn with value", field = "test"); @@ -339,21 +334,7 @@ mod tests { #[test] fn test_info_macro() { - let output = Arc::new(Mutex::new(Vec::new())); - let console_options = ConsoleOptions { - target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::INFO) - }; - - let logfire = crate::configure() - .local() - .send_to_logfire(false) - .with_console(Some(console_options)) - .with_default_level_filter(tracing::level_filters::LevelFilter::TRACE) - .finish() - .unwrap(); - - let guard = crate::set_local_logfire(logfire); + let (output, guard) = get_output_guard(Level::INFO); crate::info!("Test info message"); crate::info!("Test info with value", field = true); @@ -370,21 +351,7 @@ mod tests { #[test] fn test_debug_macro() { - let output = Arc::new(Mutex::new(Vec::new())); - let console_options = ConsoleOptions { - target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::TRACE) - }; - - let logfire = crate::configure() - .local() - .send_to_logfire(false) - .with_console(Some(console_options)) - .with_default_level_filter(tracing::level_filters::LevelFilter::TRACE) - .finish() - .unwrap(); - - let guard = crate::set_local_logfire(logfire); + let (output, guard) = get_output_guard(Level::DEBUG); crate::debug!("Test debug message"); crate::debug!("Test debug with value", field = 3.14); @@ -401,21 +368,7 @@ mod tests { #[test] fn test_trace_macro() { - let output = Arc::new(Mutex::new(Vec::new())); - let console_options = ConsoleOptions { - target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::TRACE) - }; - - let logfire = crate::configure() - .local() - .send_to_logfire(false) - .with_console(Some(console_options)) - .with_default_level_filter(tracing::level_filters::LevelFilter::TRACE) - .finish() - .unwrap(); - - let guard = crate::set_local_logfire(logfire); + let (output, guard) = get_output_guard(Level::TRACE); crate::trace!("Test trace message"); crate::trace!("Test trace with value", field = "debug_info"); @@ -432,21 +385,7 @@ mod tests { #[test] fn test_log_macro_with_explicit_level() { - let output = Arc::new(Mutex::new(Vec::new())); - let console_options = ConsoleOptions { - target: Target::Pipe(output.clone()), - ..ConsoleOptions::default().with_min_log_level(Level::INFO) - }; - - let logfire = crate::configure() - .local() - .send_to_logfire(false) - .with_console(Some(console_options)) - .with_default_level_filter(tracing::level_filters::LevelFilter::TRACE) - .finish() - .unwrap(); - - let guard = crate::set_local_logfire(logfire); + let (output, guard) = get_output_guard(Level::INFO); crate::log!(Level::INFO, "Test log message"); crate::log!(Level::INFO, "Test log with value", field = "explicit"); @@ -492,4 +431,77 @@ mod tests { assert!(output.contains("Test error with parent")); assert!(output.contains("field") && output.contains("parent_test")); } + + #[test] + fn test_rust_primitive_types_conversion_in_log() { + let (output, guard) = get_output_guard(Level::INFO); + + // i64::MAX = 9_223_372_036_854_775_807 + // so we test overflow with 10_000_000_000_000_000_000 + + crate::info!("an u8: {value}", value = 1u8); + crate::info!("an u16: {value}", value = 2u16); + crate::info!("an u32: {value}", value = 3u32); + crate::info!("an u64: {value}", value = 4u64); + crate::info!( + "an u64 that overflows: {value}", + value = 10_000_000_000_000_000_001u64 + ); + crate::info!("an u128: {value}", value = 5u128); + crate::info!( + "an u128 that overflows: {value}", + value = 10_000_000_000_000_000_002u128 + ); + crate::info!("an usize: {value}", value = 6usize); + crate::info!( + "an usize that overflows: {value}", + value = 10_000_000_000_000_000_003u128 + ); + crate::info!("an usize: {value}", value = 6usize); + crate::info!("an i8: {value}", value = 7i8); + crate::info!("an i16: {value}", value = 8i16); + crate::info!("an i32: {value}", value = 9i32); + crate::info!("an i64: {value}", value = 10i64); + crate::info!("an i128: {value}", value = 10i128); + crate::info!( + "an i128 that overflows: {value}", + value = 10_000_000_000_000_000_004u64 + ); + crate::info!("an isize: {value}", value = 11isize); + crate::info!( + "an isize that overflows: {value}", + value = 10_000_000_000_000_000_005u64 + ); + crate::info!("an f32: {value}", value = 1.2f32); + crate::info!("an f64: {value}", value = 3.4f64); + crate::info!("an bool: {value}", value = true); + crate::info!("an char: {value}", value = 'a'); + + guard.shutdown().unwrap(); + + let output = output.lock().unwrap(); + let output = std::str::from_utf8(&output).unwrap(); + + assert!(output.contains("an u8: 1")); + assert!(output.contains("an u16: 2")); + assert!(output.contains("an u32: 3")); + assert!(output.contains("an u64: 4")); + assert!(output.contains("an u64 that overflows: 10000000000000000001")); + assert!(output.contains("an u128: 5")); + assert!(output.contains("an u128 that overflows: 10000000000000000002")); + assert!(output.contains("an usize: 6")); + assert!(output.contains("an usize that overflows: 10000000000000000003")); + assert!(output.contains("an i8: 7")); + assert!(output.contains("an i16: 8")); + assert!(output.contains("an i32: 9")); + assert!(output.contains("an i64: 10")); + assert!(output.contains("an i128: 10")); + assert!(output.contains("an i128 that overflows: 10000000000000000004")); + assert!(output.contains("an isize: 11")); + assert!(output.contains("an isize that overflows: 10000000000000000005")); + assert!(output.contains("an f32: 1.2")); + assert!(output.contains("an f64: 3.4")); + assert!(output.contains("an bool: true")); + assert!(output.contains("an char: a")); + } } diff --git a/tests/test_log_attributes.rs b/tests/test_log_attributes.rs index cfa45cf..2316f2f 100644 --- a/tests/test_log_attributes.rs +++ b/tests/test_log_attributes.rs @@ -24,16 +24,16 @@ fn test_log_macro_attributes() { let guard = logfire::set_local_logfire(logfire); let _ = log!(Level::INFO, "string_attr_log", foo = "bar"); - let _ = log!(Level::INFO, "int_attr_log", num = 42); + let _ = log!(Level::INFO, "int_attr_log", num = 42i64); let _ = log!(Level::INFO, "bool_attr_log", flag = true); let _ = log!(Level::INFO, "dotted_attr_log", dotted.key = "value"); let _ = log!( Level::INFO, "multi_attr_log", - a = 1, + a = 1i64, b = "two", c = false, - d.e = 3 + d.e = 3i64 ); guard.shutdown().unwrap(); @@ -86,7 +86,7 @@ fn test_log_macro_shorthand_ident() { } let dotted = Dotted { key: "value" }; - let int_val = 42; + let int_val: i64 = 42; let bool_val = true; let multi = Multi { a: 1,