diff --git a/rust/otap-dataflow/crates/telemetry/src/internal_events.rs b/rust/otap-dataflow/crates/telemetry/src/internal_events.rs index 07a68bd98f..c892e8e1dd 100644 --- a/rust/otap-dataflow/crates/telemetry/src/internal_events.rs +++ b/rust/otap-dataflow/crates/telemetry/src/internal_events.rs @@ -12,7 +12,11 @@ #[doc(hidden)] pub mod _private { - pub use tracing::{debug, error, info, warn}; + pub use tracing::callsite::{Callsite, DefaultCallsite}; + pub use tracing::field::ValueSet; + pub use tracing::metadata::Kind; + pub use tracing::{Event, Level}; + pub use tracing::{callsite2, debug, error, info, valueset, warn}; } /// Macro for logging informational messages. @@ -29,6 +33,8 @@ pub mod _private { /// ``` // TODO: Remove `name` attribute duplication in logging macros below once `tracing::Fmt` supports displaying `name`. // See issue: https://github.com/tokio-rs/tracing/issues/2774 +/// +/// TODO: Update to use valueset! for full `tracing` syntax, see raw_error! #[macro_export] macro_rules! otel_info { ($name:expr $(,)?) => { @@ -80,6 +86,8 @@ macro_rules! otel_warn { /// use otap_df_telemetry::otel_debug; /// otel_debug!("processing.batch", batch_size = 100); /// ``` +/// +/// TODO: Update to use valueset! for full `tracing` syntax, see raw_error! #[macro_export] macro_rules! otel_debug { ($name:expr $(,)?) => { @@ -102,6 +110,8 @@ macro_rules! otel_debug { /// use otap_df_telemetry::otel_error; /// otel_error!("export.failure", error_code = 500); /// ``` +/// +/// TODO: Update to use valueset! for full `tracing` syntax, see raw_error! #[macro_export] macro_rules! otel_error { ($name:expr $(,)?) => { @@ -118,3 +128,46 @@ macro_rules! otel_error { ) }; } + +/// Log an error message directly to stderr, bypassing the tracing dispatcher. +/// +/// Note! the way this is written, it supports the full `tracing` syntax for +/// debug and display formatting of field values, following tracing::valueset! +/// where ? signifies debug and % signifies display. +/// +/// ```ignore +/// use otap_df_telemetry::raw_error; +/// raw_error!("logging.write.failed", error = ?err, thing = %display); +/// ``` +#[macro_export] +macro_rules! raw_error { + ($name:expr $(, $($fields:tt)*)?) => {{ + use $crate::self_tracing::{ConsoleWriter, RawLoggingLayer}; + use $crate::_private::Callsite; + + static __CALLSITE: $crate::_private::DefaultCallsite = $crate::_private::callsite2! { + name: $name, + kind: $crate::_private::Kind::EVENT, + target: module_path!(), + level: $crate::_private::Level::ERROR, + fields: $($($fields)*)? + }; + + let meta = __CALLSITE.metadata(); + let layer = RawLoggingLayer::new(ConsoleWriter::no_color()); + + // Use closure to extend valueset lifetime (same pattern as tracing::event!) + (|valueset: $crate::_private::ValueSet<'_>| { + let event = $crate::_private::Event::new(meta, &valueset); + layer.dispatch_event(&event); + })($crate::_private::valueset!(meta.fields(), $($($fields)*)?)); + }}; +} + +mod tests { + #[test] + fn test_raw_error() { + let err = crate::error::Error::ConfigurationError("bad config".into()); + raw_error!("raw error message", error = ?err); + } +} diff --git a/rust/otap-dataflow/crates/telemetry/src/self_tracing/formatter.rs b/rust/otap-dataflow/crates/telemetry/src/self_tracing/formatter.rs index 2d6b4c2b79..4821bc5209 100644 --- a/rust/otap-dataflow/crates/telemetry/src/self_tracing/formatter.rs +++ b/rust/otap-dataflow/crates/telemetry/src/self_tracing/formatter.rs @@ -91,6 +91,16 @@ impl RawLoggingLayer { pub fn new(writer: ConsoleWriter) -> Self { Self { writer } } + + /// Process a tracing Event directly, bypassing the dispatcher. + pub fn dispatch_event(&self, event: &Event<'_>) { + // TODO: there are allocations implied in LogRecord::new that we + // would prefer to avoid; it will be an extensive change in the + // ProtoBuffer impl to stack-allocate this as a temporary. + let record = LogRecord::new(event); + let callsite = SavedCallsite::new(event.metadata()); + self.writer.print_log_record(&record, &callsite); + } } /// Type alias for a cursor over a byte buffer. @@ -119,13 +129,20 @@ impl ConsoleWriter { /// Output format: `2026-01-06T10:30:45.123Z INFO target::name (file.rs:42): body [attr=value, ...]` pub fn format_log_record(&self, record: &LogRecord, callsite: &SavedCallsite) -> String { let mut buf = [0u8; LOG_BUFFER_SIZE]; - let len = self.write_log_record(&mut buf, record, callsite); + let len = self.encode_log_record(&mut buf, record, callsite); // The buffer contains valid UTF-8 since we only write ASCII and valid UTF-8 strings String::from_utf8_lossy(&buf[..len]).into_owned() } - /// Write a LogRecord to a byte buffer. Returns the number of bytes written. - pub fn write_log_record( + /// Print a LogRecord directly to stdout or stderr (based on level). + pub fn print_log_record(&self, record: &LogRecord, callsite: &SavedCallsite) { + let mut buf = [0u8; LOG_BUFFER_SIZE]; + let len = self.encode_log_record(&mut buf, record, callsite); + self.write_line(callsite.level(), &buf[..len]); + } + + /// Encode a LogRecord to a byte buffer. Returns the number of bytes written. + fn encode_log_record( &self, buf: &mut [u8], record: &LogRecord, @@ -323,15 +340,7 @@ where // Allocates a buffer on the stack, formats the event to a LogRecord // with partial OTLP bytes. fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { - // TODO: there are allocations implied here that we would prefer - // to avoid, it will be an extensive change in the ProtoBuffer to - // stack-allocate this temporary. - let record = LogRecord::new(event); - let callsite = SavedCallsite::new(event.metadata()); - - let mut buf = [0u8; LOG_BUFFER_SIZE]; - let len = self.writer.write_log_record(&mut buf, &record, &callsite); - self.writer.write_line(callsite.level(), &buf[..len]); + self.dispatch_event(event); } // Note! This tracing layer does not implement Span-related features @@ -595,7 +604,7 @@ mod tests { let mut buf = [0u8; LOG_BUFFER_SIZE]; let writer = ConsoleWriter::no_color(); - let len = writer.write_log_record(&mut buf, &record, &test_callsite()); + let len = writer.encode_log_record(&mut buf, &record, &test_callsite()); // Fills exactly to capacity due to overflow. // Note! we could append a ... or some other indicator.