From 615c17c0fc927573e89010a859d2a023d1a523d0 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:25:12 +0100 Subject: [PATCH 1/5] fix(observed): correct enrichment, processor and test-harness behavior Rename sink benchmarks to match measured operations Use shared taxonomy and monotonic validation failure counter Skip empty enrichment layers before mutating slots Document manual enrichment entries and default no-op flush Correct expected-event log defaults and OTel source attributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../observed/benches/observed_benchmarks.rs | 25 ++++--- crates/observed/examples/basic.rs | 4 -- crates/observed/examples/enrichments.rs | 4 -- crates/observed/examples/event_routing.rs | 12 ---- .../observed/examples/event_type_matching.rs | 12 ---- crates/observed/examples/layered_app/main.rs | 4 -- .../examples/layered_app/token_issuer.rs | 12 +--- crates/observed/examples/optional_fields.rs | 4 -- crates/observed/examples/sink_pipeline.rs | 4 -- crates/observed/examples/support/otel.rs | 10 --- .../examples/three_enrichment_styles.rs | 4 -- crates/observed/examples/tokio_multithread.rs | 4 -- .../src/enrichment/enrichment_trait.rs | 16 +++++ crates/observed/src/enrichment/mod.rs | 1 - crates/observed/src/enrichment/slot.rs | 68 ++++++++++++++++--- crates/observed/src/processing/processor.rs | 6 +- crates/observed/src/sink/core.rs | 6 +- crates/observed/tests/otel_log_record.rs | 10 +-- crates/observed_testing/src/lib.rs | 4 +- crates/observed_testing/src/mock_processor.rs | 41 +++++++---- 20 files changed, 131 insertions(+), 120 deletions(-) diff --git a/crates/observed/benches/observed_benchmarks.rs b/crates/observed/benches/observed_benchmarks.rs index 1562c4f3d..33d6b137a 100644 --- a/crates/observed/benches/observed_benchmarks.rs +++ b/crates/observed/benches/observed_benchmarks.rs @@ -277,11 +277,11 @@ fn entrypoint(c: &mut Criterion) { group.finish(); } - // --- Context operation benchmarks --- + // --- Sink operation benchmarks --- { - let mut group = c.benchmark_group("emit_context"); - bench_attach_emitter(&mut group, &allocs, &time); - bench_emit_event_direct(&mut group, &allocs, &time); + let mut group = c.benchmark_group("sink_operations"); + bench_construct_processor_free_sink(&mut group, &allocs, &time); + bench_emit_to_noop_sink(&mut group, &allocs, &time); group.finish(); } @@ -468,11 +468,15 @@ fn bench_enrich_push_pop(group: &mut BenchmarkGroup<'_, WallTime>, allocs: &allo } // --------------------------------------------------------------------------- -// Context operation benchmarks +// Sink operation benchmarks // --------------------------------------------------------------------------- -fn bench_attach_emitter(group: &mut BenchmarkGroup<'_, WallTime>, allocs: &alloc_tracker::Session, time: &all_the_time::Session) { - const ID: &str = "attach_emitter"; +fn bench_construct_processor_free_sink( + group: &mut BenchmarkGroup<'_, WallTime>, + allocs: &alloc_tracker::Session, + time: &all_the_time::Session, +) { + const ID: &str = "construct_processor_free_sink"; const EMPTY_PROCESSORS: Vec> = Vec::new(); bench_with_tracking(group, allocs, time, ID, || { @@ -481,11 +485,10 @@ fn bench_attach_emitter(group: &mut BenchmarkGroup<'_, WallTime>, allocs: &alloc }); } -fn bench_emit_event_direct(group: &mut BenchmarkGroup<'_, WallTime>, allocs: &alloc_tracker::Session, time: &all_the_time::Session) { - const ID: &str = "emit_event_no_emitter"; +fn bench_emit_to_noop_sink(group: &mut BenchmarkGroup<'_, WallTime>, allocs: &alloc_tracker::Session, time: &all_the_time::Session) { + const ID: &str = "emit_to_noop_sink"; - // Benchmark the emit path when no sink is registered - measures the - // cost of the context lookup + early return. + // Benchmark the emit path through an explicit no-op sink. let noop = Sink::noop(); bench_with_tracking(group, allocs, time, ID, || { observed::emit!(&noop, SimpleLogEvent { status: 200, retries: 0 }); diff --git a/crates/observed/examples/basic.rs b/crates/observed/examples/basic.rs index 7a6ccf47d..df12274f9 100644 --- a/crates/observed/examples/basic.rs +++ b/crates/observed/examples/basic.rs @@ -60,10 +60,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } fn main() { diff --git a/crates/observed/examples/enrichments.rs b/crates/observed/examples/enrichments.rs index dea3bcc30..1121317af 100644 --- a/crates/observed/examples/enrichments.rs +++ b/crates/observed/examples/enrichments.rs @@ -113,10 +113,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// Simulates a database lookup inside an enrichment scope. diff --git a/crates/observed/examples/event_routing.rs b/crates/observed/examples/event_routing.rs index ae5ccb040..fcd9e5c7b 100644 --- a/crates/observed/examples/event_routing.rs +++ b/crates/observed/examples/event_routing.rs @@ -163,10 +163,6 @@ impl EventProcessor for LogProcessor { .expect("lock is not poisoned") .push(format!("[LOG] {}", event.name())); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// Accepts metric events and records instruments from both metadata paths: @@ -213,10 +209,6 @@ impl EventProcessor for MetricProcessor { )); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// An audit processor that opts in to specific disabled events by name. @@ -237,8 +229,4 @@ impl EventProcessor for AuditProcessor { .expect("lock is not poisoned") .push(format!("[AUDIT] {}", event.name())); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } diff --git a/crates/observed/examples/event_type_matching.rs b/crates/observed/examples/event_type_matching.rs index d4d9d3979..15d0167bd 100644 --- a/crates/observed/examples/event_type_matching.rs +++ b/crates/observed/examples/event_type_matching.rs @@ -247,10 +247,6 @@ impl EventProcessor for TypeRoutingProcessor { let handler = self.handlers.get(&type_id).expect("is_interested guarantees a registered handler"); handler(event, &self.engine); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// A processor that only accepts `HttpRequest` events by checking the event's `TypeId` in `is_interested`. @@ -271,10 +267,6 @@ impl EventProcessor for HttpOnlyProcessor { .expect("lock is not poisoned") .push(format!("HttpRequest {{{}}}", format_fields(&fields))); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// A processor that accepts events by matching their canonical event name @@ -310,10 +302,6 @@ impl EventProcessor for NameRoutingProcessor { .expect("lock is not poisoned") .push(format!("{} {{{}}}", event.name(), format_fields(&fields))); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } /// Collects all log-routed fields from an event into `(key, value)` pairs. diff --git a/crates/observed/examples/layered_app/main.rs b/crates/observed/examples/layered_app/main.rs index 485b60c10..0b710611a 100644 --- a/crates/observed/examples/layered_app/main.rs +++ b/crates/observed/examples/layered_app/main.rs @@ -201,10 +201,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } // --------------------------------------------------------------------------- diff --git a/crates/observed/examples/layered_app/token_issuer.rs b/crates/observed/examples/layered_app/token_issuer.rs index 0cb3cf403..fe006e261 100644 --- a/crates/observed/examples/layered_app/token_issuer.rs +++ b/crates/observed/examples/layered_app/token_issuer.rs @@ -14,7 +14,7 @@ use observed::{Enrichment, Sink, emit, event}; use crate::taxonomy::MicrosoftEnterpriseDataTaxonomy; -const DC: DataClass = DataClass::new("microsoft", "PublicNonPersonalData"); +const DC: DataClass = MicrosoftEnterpriseDataTaxonomy::PublicNonPersonalData.data_class(); // --------------------------------------------------------------------------- // Classified newtypes @@ -69,16 +69,11 @@ struct TokenValidated { /// A failed token validation attempt. /// -/// Records each failure as an up-down counter metric (increments on failure). +/// Records each failure as a monotonic counter metric. #[event("token.validation_failed")] #[warning("Token validation failed")] -#[updown_counter(failure_count, name = "token.validation.failures")] +#[counter(name = "token.validation.failures")] struct TokenValidationFailed { - /// Failure count - recorded as an up-down counter metric. - // TODO: replace #[unredacted] with classified type once metric fields support non-numeric Values - #[unredacted] - failure_count: i64, - /// Error code identifying the failure reason. #[data_class(DC)] error_code: i64, @@ -129,7 +124,6 @@ pub(crate) fn validate_token(sink: &Sink, valid: bool) { emit!( sink, TokenValidationFailed { - failure_count: 1, error_code: 401, // expired } ); diff --git a/crates/observed/examples/optional_fields.rs b/crates/observed/examples/optional_fields.rs index c4a807346..ff7589b0d 100644 --- a/crates/observed/examples/optional_fields.rs +++ b/crates/observed/examples/optional_fields.rs @@ -147,8 +147,4 @@ impl EventProcessor for PrintProcessor { }); println!(); } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } diff --git a/crates/observed/examples/sink_pipeline.rs b/crates/observed/examples/sink_pipeline.rs index ac6fb00f5..e7429183b 100644 --- a/crates/observed/examples/sink_pipeline.rs +++ b/crates/observed/examples/sink_pipeline.rs @@ -93,10 +93,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } fn main() { diff --git a/crates/observed/examples/support/otel.rs b/crates/observed/examples/support/otel.rs index 1fec24209..c3bba6a4a 100644 --- a/crates/observed/examples/support/otel.rs +++ b/crates/observed/examples/support/otel.rs @@ -31,9 +31,6 @@ const CODE_FILE_PATH: &str = "code.file.path"; /// `OTel` attribute key for the source line a call site came from. const CODE_LINE_NUMBER: &str = "code.line.number"; -/// `OTel` attribute key for the crate a call site came from. -const CODE_NAMESPACE: &str = "code.namespace"; - /// Converts a [`Text`] into an `OTel` string, preserving the borrowed-versus- /// shared distinction so neither representation copies. /// @@ -142,12 +139,5 @@ pub(crate) fn populate_log_record(record: &mut impl LogRecord, event: &EventView AnyValue::Int(i64::from(line)), ); } - if let Some(crate_name) = event.source_crate() { - record.add_attribute( - opentelemetry::Key::from_static_str(CODE_NAMESPACE), - AnyValue::String(crate_name.into()), - ); - } - true } diff --git a/crates/observed/examples/three_enrichment_styles.rs b/crates/observed/examples/three_enrichment_styles.rs index 39ae51a72..257f35801 100644 --- a/crates/observed/examples/three_enrichment_styles.rs +++ b/crates/observed/examples/three_enrichment_styles.rs @@ -117,10 +117,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } fn init_telemetry() -> (Sink, SdkLoggerProvider) { diff --git a/crates/observed/examples/tokio_multithread.rs b/crates/observed/examples/tokio_multithread.rs index 149c455f6..1ba4c6104 100644 --- a/crates/observed/examples/tokio_multithread.rs +++ b/crates/observed/examples/tokio_multithread.rs @@ -63,10 +63,6 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } - - fn flush(&self) -> Result<(), observed::FlushError> { - Ok(()) - } } #[tokio::main] diff --git a/crates/observed/src/enrichment/enrichment_trait.rs b/crates/observed/src/enrichment/enrichment_trait.rs index a5180b448..1730a4687 100644 --- a/crates/observed/src/enrichment/enrichment_trait.rs +++ b/crates/observed/src/enrichment/enrichment_trait.rs @@ -16,6 +16,22 @@ use crate::enrichment::EnrichmentEntry; /// /// See the [`Enrichment` derive macro](crate::Enrichment) for field attributes /// and usage examples. +/// +/// # Manual implementation +/// +/// ``` +/// use observed::enrichment::{Enrichment, EnrichmentEntry}; +/// +/// struct RequestContext { +/// request_id: &'static str, +/// } +/// +/// impl Enrichment for RequestContext { +/// fn into_entries(self) -> Vec { +/// vec![EnrichmentEntry::unclassified("request.id", self.request_id)] +/// } +/// } +/// ``` pub trait Enrichment { /// Converts this enrichment struct into its [`EnrichmentEntry`] items. /// diff --git a/crates/observed/src/enrichment/mod.rs b/crates/observed/src/enrichment/mod.rs index da79ec203..e9adf5293 100644 --- a/crates/observed/src/enrichment/mod.rs +++ b/crates/observed/src/enrichment/mod.rs @@ -10,6 +10,5 @@ mod slot; pub use enrich_ext::{EnrichFnExt, EnrichFutureExt, Enriched}; pub use enrichment_trait::Enrichment; -#[doc(hidden)] pub use entry::EnrichmentEntry; pub(crate) use slot::{EnrichmentNode, EnrichmentTransfer, Guard, OptEnrichmentNode, Slot}; diff --git a/crates/observed/src/enrichment/slot.rs b/crates/observed/src/enrichment/slot.rs index 4b5de36c7..e3d082252 100644 --- a/crates/observed/src/enrichment/slot.rs +++ b/crates/observed/src/enrichment/slot.rs @@ -69,6 +69,10 @@ impl Slot { /// Pushes entries onto the enrichment chain and returns a guard. pub(crate) fn push(&self, entries: Arc<[EnrichmentEntry]>) -> Guard { + if entries.is_empty() { + return Guard::empty(); + } + let prev = { let cell = self.0.get_or(|| RefCell::new(None)); let next = Arc::new(EnrichmentNode { @@ -100,6 +104,10 @@ pub(crate) struct Guard { } impl Guard { + pub(crate) fn empty() -> Self { + Self { slots: SmallVec::new() } + } + /// Flattens several guards into one. Each input guard is consumed and its /// `Drop` is disarmed (`mem::take` empties its slots), so the merged guard /// owns the restoration responsibility. @@ -171,25 +179,29 @@ impl EnrichmentTransfer { /// Broadcast within the transfer's known slots; transfers with no /// captured slots are left unchanged. pub(crate) fn push(&mut self, additional_enrichment: impl Enrichment) { - self.push_entries(&Arc::from(additional_enrichment.into_entries())); + let entries = additional_enrichment.into_entries(); + if entries.is_empty() { + return; + } + + self.push_entries(&Arc::from(entries)); } /// Same as [`push`](Self::push), but marks every entry as targeted at /// `target`, so only that sink observes it. Mirrors the `with_target` /// mapping that `EnrichFutureExt::enrich_for` applies. pub(crate) fn push_for(&mut self, target: SinkId, additional_enrichment: impl Enrichment) { - self.push_entries( - &additional_enrichment - .into_entries() - .into_iter() - .map(|entry| entry.with_target(target)) - .collect(), - ); + let entries: Arc<[EnrichmentEntry]> = additional_enrichment + .into_entries() + .into_iter() + .map(|entry| entry.with_target(target)) + .collect(); + self.push_entries(&entries); } /// Layers `entries` onto every captured chain as a single new node. fn push_entries(&mut self, entries: &Arc<[EnrichmentEntry]>) { - if self.slots.is_empty() { + if self.slots.is_empty() || entries.is_empty() { return; } @@ -221,6 +233,14 @@ mod coverage_tests { } } + struct EmptyEnrichment; + + impl Enrichment for EmptyEnrichment { + fn into_entries(self) -> Vec { + Vec::new() + } + } + /// Flattens a chain into its entries, innermost node first. fn chain_entries(head: &OptEnrichmentNode) -> Vec { let mut entries = Vec::new(); @@ -255,6 +275,20 @@ mod coverage_tests { assert!(slot.current().is_none(), "dropping the guard must pop the pushed node"); } + #[test] + fn empty_push_returns_noop_guard_without_touching_the_slot() { + let slot = Slot::new(); + let empty_guard = slot.push(Arc::from(Vec::new())); + assert!(slot.current().is_none()); + + let real_guard = slot.push(Arc::from(vec![EnrichmentEntry::unclassified("k", 1_i64)])); + let merged = Guard::merge([empty_guard, real_guard]); + assert!(slot.current().is_some()); + + drop(merged); + assert!(slot.current().is_none()); + } + #[test] fn transfer_captures_a_slot_and_layers_further_enrichment_onto_it() { let slot = Slot::new(); @@ -281,6 +315,22 @@ mod coverage_tests { assert!(slot.current().is_none()); } + #[test] + fn transfer_ignores_empty_enrichment_layers() { + let slot = Slot::new(); + let mut transfer = EnrichmentTransfer::default(); + transfer.add_slot(&slot); + + transfer.push(EmptyEnrichment); + transfer.push_for(SinkId::new("target"), EmptyEnrichment); + + let guard = transfer.apply(); + assert!(slot.current().is_none()); + + drop(guard); + assert!(slot.current().is_none()); + } + #[test] fn debug_impls_and_relocate() { let mut slot = Slot::new(); diff --git a/crates/observed/src/processing/processor.rs b/crates/observed/src/processing/processor.rs index cb45230fb..f3b8cb724 100644 --- a/crates/observed/src/processing/processor.rs +++ b/crates/observed/src/processing/processor.rs @@ -60,7 +60,7 @@ pub trait EventProcessor: Send + Sync { /// Forces any buffered telemetry produced by this processor out to its /// final destination, surfacing errors. Idempotent and non-terminating - /// the processor remains usable after `flush()` returns. Implementors - /// with nothing to flush should return `Ok(())`. + /// with nothing to flush use the default no-op implementation. /// /// [`Sink::flush`](crate::Sink::flush) iterates all registered /// processors and calls this; it reports every failure, not just the first. @@ -73,7 +73,9 @@ pub trait EventProcessor: Send + Sync { /// /// Returns a [`FlushError`] if flushing buffered telemetry to the final /// destination fails. - fn flush(&self) -> Result<(), FlushError>; + fn flush(&self) -> Result<(), FlushError> { + Ok(()) + } } impl EventProcessor for Arc { diff --git a/crates/observed/src/sink/core.rs b/crates/observed/src/sink/core.rs index 6af44fcd6..255e43c35 100644 --- a/crates/observed/src/sink/core.rs +++ b/crates/observed/src/sink/core.rs @@ -284,8 +284,12 @@ impl Sink { /// This is the entry point used by the `.enrich(&sink, ...)` API in /// [`EnrichFutureExt`](crate::enrichment::EnrichFutureExt) and /// [`EnrichFnExt`](crate::enrichment::EnrichFnExt). Composites with zero children - /// return a no-op guard. + /// and empty enrichment layers return a no-op guard. pub(crate) fn push_enrichment(&self, entries: Arc<[EnrichmentEntry]>) -> Guard { + if entries.is_empty() { + return Guard::empty(); + } + match &*self.inner { SinkInner::Single(state) => state.enrichment.push(entries), SinkInner::Composite { children } => Guard::merge(children.iter().map(|c| c.enrichment.push(Arc::clone(&entries)))), diff --git a/crates/observed/tests/otel_log_record.rs b/crates/observed/tests/otel_log_record.rs index ffac7029c..34f6d2738 100644 --- a/crates/observed/tests/otel_log_record.rs +++ b/crates/observed/tests/otel_log_record.rs @@ -257,10 +257,9 @@ fn metric_only_event_exports_no_log_record() { } /// The exported record name comes from the log signal rather than the event -/// name, and the emitting crate is recorded as `code.namespace`. The two names -/// differ here, so a mapping that fell back to the event name is caught. +/// name, and source attributes omit deprecated crate namespace metadata. #[test] -fn maps_log_name_and_source_crate() { +fn maps_log_name_without_deprecated_source_crate_attribute() { let (sink, provider, exporter) = otel_emitter(); emit!(sink, CacheLookup { hits: 3 }); @@ -272,10 +271,7 @@ fn maps_log_name_and_source_crate() { assert_eq!(record.event_name(), Some("cache.lookup.completed")); let attrs: Vec<_> = record.attributes_iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - assert!(matches!( - find_attr(&attrs, "code.namespace"), - Some(AnyValue::String(s)) if s.as_ref() == env!("CARGO_PKG_NAME") - )); + assert!(find_attr(&attrs, "code.namespace").is_none()); } #[test] diff --git a/crates/observed_testing/src/lib.rs b/crates/observed_testing/src/lib.rs index 8ea9ffd95..837556c9c 100644 --- a/crates/observed_testing/src/lib.rs +++ b/crates/observed_testing/src/lib.rs @@ -25,9 +25,7 @@ //! emit!(sink, MyEvent { count: 42 }); //! //! let event = processor.single_event(); -//! let expected = ExpectedEvent::new("my.event", Severity::Info) -//! .dimension("count", 42i64) -//! .log(); +//! let expected = ExpectedEvent::new("my.event", Severity::Info).dimension("count", 42i64); //! assert_eq!(event, expected); //! ``` diff --git a/crates/observed_testing/src/mock_processor.rs b/crates/observed_testing/src/mock_processor.rs index 6d547f9f0..ac63a993a 100644 --- a/crates/observed_testing/src/mock_processor.rs +++ b/crates/observed_testing/src/mock_processor.rs @@ -209,9 +209,7 @@ impl CapturedEvent { /// # struct TestEvent { #[unredacted] value: i64 } /// emit!(sink, TestEvent { value: 1 }); /// -/// let expected = ExpectedEvent::new("test.event", Severity::Info) -/// .dimension("value", 1i64) -/// .log(); +/// let expected = ExpectedEvent::new("test.event", Severity::Info).dimension("value", 1i64); /// assert_eq!(processor.single_event(), expected); /// ``` #[derive(Clone)] @@ -364,14 +362,14 @@ fn passthrough_redaction_engine() -> data_privacy::RedactionEngine { } // --------------------------------------------------------------------------- -// ExpectedEvent - partial-match builder for test assertions +// ExpectedEvent - exact-match builder for test assertions // --------------------------------------------------------------------------- -/// A partial-match event specification for test assertions. +/// An exact-match event specification for test assertions. /// -/// Construct with [`ExpectedEvent::new`] and chain builder methods for the fields -/// you want to verify. Only specified fields are checked - unspecified fields are -/// ignored during comparison. +/// Construct with [`ExpectedEvent::new`] and chain builder methods for the +/// fields and signals the event must carry. Omitted body and dimensions are +/// expected to be absent, and metric and disabled flags default to `false`. /// /// # Example /// @@ -382,7 +380,6 @@ fn passthrough_redaction_engine() -> data_privacy::RedactionEngine { /// let expected = ExpectedEvent::new("http.request", Severity::Info) /// .body("Request handled") /// .dimension("status", 200i64) -/// .log() /// .metric(); /// ``` #[derive(Debug)] @@ -399,9 +396,10 @@ pub struct ExpectedEvent { impl ExpectedEvent { /// Creates a new expected **log** event with the given name and severity. /// - /// All other fields are unchecked by default. Use builder methods to add - /// constraints. For an event with no log signal - a metric-only or - /// signal-less event - use [`without_severity`](Self::without_severity). + /// Omitted body and dimensions are expected to be absent, the metric and + /// disabled flags default to `false`, and the log signal is expected. For an + /// event with no log signal - a metric-only or signal-less event - use + /// [`without_severity`](Self::without_severity). #[must_use] pub fn new(name: impl Into, severity: Severity) -> Self { Self { @@ -409,7 +407,7 @@ impl ExpectedEvent { severity: Some(severity), body: None, sorted_dimensions: Vec::new(), - expect_log: false, + expect_log: true, expect_metric: false, expect_disabled: false, } @@ -690,7 +688,7 @@ impl PartialEq for ExpectedEventDescription { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod coverage_tests { - use observed::{Event, emit}; + use observed::{Event, emit, event}; use super::*; use crate::events::ProbeEvent; @@ -711,7 +709,7 @@ mod coverage_tests { // `ExpectedEvent == CapturedEvent` (reverse direction) agrees with the // forward direction it delegates to. - let expected_event = ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 42i64).log(); + let expected_event = ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 42i64); assert_eq!(expected_event == captured, captured == expected_event); // `ExpectedEnrichmentEntry == EnrichmentEntry` (reverse direction). @@ -723,4 +721,17 @@ mod coverage_tests { let expected_desc = ExpectedEventDescription::new("test.probe", Severity::Info).log(); assert_eq!(expected_desc, ProbeEvent::DESCRIPTION); } + + #[event("fieldless.log")] + #[info] + struct FieldlessLog; + + #[test] + fn expected_event_new_matches_fieldless_log_without_log_builder() { + let (sink, processor) = test_emitter(observed::SinkId::new("fieldless_log")); + + emit!(sink, FieldlessLog); + + assert_eq!(processor.single_event(), ExpectedEvent::new("fieldless.log", Severity::Info)); + } } From 82ccb53b9ea96fb177726b794d3088a4d20b9e17 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:44:36 +0100 Subject: [PATCH 2/5] fix(observed): apply review feedback Roll back the two changes the crate owner rejected: - EventProcessor::flush goes back to a required method. A default no-op lets a processor that buffers telemetry silently skip flushing it, and losing telemetry is worse than the repeated Ok(()). - EnrichmentEntry returns to #[doc(hidden)] and the manual Enrichment implementation example is removed. The type is public only because Rust has no way to hide it from the derive expansion; the derive macro remains the single supported way to build an enrichment. Also drop the "composites with zero children" note from Sink::push_enrichment, which described an internal dispatch detail, and give EnrichmentTransfer::push_for the same empty-layer early return as push so a targeted layer with no entries allocates nothing either. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/observed/examples/basic.rs | 4 ++++ crates/observed/examples/enrichments.rs | 4 ++++ crates/observed/examples/event_routing.rs | 12 ++++++++++++ crates/observed/examples/event_type_matching.rs | 12 ++++++++++++ crates/observed/examples/layered_app/main.rs | 4 ++++ crates/observed/examples/optional_fields.rs | 4 ++++ crates/observed/examples/sink_pipeline.rs | 4 ++++ .../observed/examples/three_enrichment_styles.rs | 4 ++++ crates/observed/examples/tokio_multithread.rs | 4 ++++ .../observed/src/enrichment/enrichment_trait.rs | 16 ---------------- crates/observed/src/enrichment/mod.rs | 1 + crates/observed/src/enrichment/slot.rs | 12 ++++++------ crates/observed/src/processing/processor.rs | 6 ++---- crates/observed/src/sink/core.rs | 3 +-- 14 files changed, 62 insertions(+), 28 deletions(-) diff --git a/crates/observed/examples/basic.rs b/crates/observed/examples/basic.rs index df12274f9..7a6ccf47d 100644 --- a/crates/observed/examples/basic.rs +++ b/crates/observed/examples/basic.rs @@ -60,6 +60,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } fn main() { diff --git a/crates/observed/examples/enrichments.rs b/crates/observed/examples/enrichments.rs index 1121317af..dea3bcc30 100644 --- a/crates/observed/examples/enrichments.rs +++ b/crates/observed/examples/enrichments.rs @@ -113,6 +113,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// Simulates a database lookup inside an enrichment scope. diff --git a/crates/observed/examples/event_routing.rs b/crates/observed/examples/event_routing.rs index fcd9e5c7b..ae5ccb040 100644 --- a/crates/observed/examples/event_routing.rs +++ b/crates/observed/examples/event_routing.rs @@ -163,6 +163,10 @@ impl EventProcessor for LogProcessor { .expect("lock is not poisoned") .push(format!("[LOG] {}", event.name())); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// Accepts metric events and records instruments from both metadata paths: @@ -209,6 +213,10 @@ impl EventProcessor for MetricProcessor { )); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// An audit processor that opts in to specific disabled events by name. @@ -229,4 +237,8 @@ impl EventProcessor for AuditProcessor { .expect("lock is not poisoned") .push(format!("[AUDIT] {}", event.name())); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } diff --git a/crates/observed/examples/event_type_matching.rs b/crates/observed/examples/event_type_matching.rs index 15d0167bd..d4d9d3979 100644 --- a/crates/observed/examples/event_type_matching.rs +++ b/crates/observed/examples/event_type_matching.rs @@ -247,6 +247,10 @@ impl EventProcessor for TypeRoutingProcessor { let handler = self.handlers.get(&type_id).expect("is_interested guarantees a registered handler"); handler(event, &self.engine); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// A processor that only accepts `HttpRequest` events by checking the event's `TypeId` in `is_interested`. @@ -267,6 +271,10 @@ impl EventProcessor for HttpOnlyProcessor { .expect("lock is not poisoned") .push(format!("HttpRequest {{{}}}", format_fields(&fields))); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// A processor that accepts events by matching their canonical event name @@ -302,6 +310,10 @@ impl EventProcessor for NameRoutingProcessor { .expect("lock is not poisoned") .push(format!("{} {{{}}}", event.name(), format_fields(&fields))); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } /// Collects all log-routed fields from an event into `(key, value)` pairs. diff --git a/crates/observed/examples/layered_app/main.rs b/crates/observed/examples/layered_app/main.rs index 0b710611a..485b60c10 100644 --- a/crates/observed/examples/layered_app/main.rs +++ b/crates/observed/examples/layered_app/main.rs @@ -201,6 +201,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } // --------------------------------------------------------------------------- diff --git a/crates/observed/examples/optional_fields.rs b/crates/observed/examples/optional_fields.rs index ff7589b0d..c4a807346 100644 --- a/crates/observed/examples/optional_fields.rs +++ b/crates/observed/examples/optional_fields.rs @@ -147,4 +147,8 @@ impl EventProcessor for PrintProcessor { }); println!(); } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } diff --git a/crates/observed/examples/sink_pipeline.rs b/crates/observed/examples/sink_pipeline.rs index e7429183b..ac6fb00f5 100644 --- a/crates/observed/examples/sink_pipeline.rs +++ b/crates/observed/examples/sink_pipeline.rs @@ -93,6 +93,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } fn main() { diff --git a/crates/observed/examples/three_enrichment_styles.rs b/crates/observed/examples/three_enrichment_styles.rs index 257f35801..39ae51a72 100644 --- a/crates/observed/examples/three_enrichment_styles.rs +++ b/crates/observed/examples/three_enrichment_styles.rs @@ -117,6 +117,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } fn init_telemetry() -> (Sink, SdkLoggerProvider) { diff --git a/crates/observed/examples/tokio_multithread.rs b/crates/observed/examples/tokio_multithread.rs index 1ba4c6104..149c455f6 100644 --- a/crates/observed/examples/tokio_multithread.rs +++ b/crates/observed/examples/tokio_multithread.rs @@ -63,6 +63,10 @@ impl observed::processing::EventProcessor for SimpleLogProcessor { self.logger.emit(record); } } + + fn flush(&self) -> Result<(), observed::FlushError> { + Ok(()) + } } #[tokio::main] diff --git a/crates/observed/src/enrichment/enrichment_trait.rs b/crates/observed/src/enrichment/enrichment_trait.rs index 1730a4687..a5180b448 100644 --- a/crates/observed/src/enrichment/enrichment_trait.rs +++ b/crates/observed/src/enrichment/enrichment_trait.rs @@ -16,22 +16,6 @@ use crate::enrichment::EnrichmentEntry; /// /// See the [`Enrichment` derive macro](crate::Enrichment) for field attributes /// and usage examples. -/// -/// # Manual implementation -/// -/// ``` -/// use observed::enrichment::{Enrichment, EnrichmentEntry}; -/// -/// struct RequestContext { -/// request_id: &'static str, -/// } -/// -/// impl Enrichment for RequestContext { -/// fn into_entries(self) -> Vec { -/// vec![EnrichmentEntry::unclassified("request.id", self.request_id)] -/// } -/// } -/// ``` pub trait Enrichment { /// Converts this enrichment struct into its [`EnrichmentEntry`] items. /// diff --git a/crates/observed/src/enrichment/mod.rs b/crates/observed/src/enrichment/mod.rs index e9adf5293..da79ec203 100644 --- a/crates/observed/src/enrichment/mod.rs +++ b/crates/observed/src/enrichment/mod.rs @@ -10,5 +10,6 @@ mod slot; pub use enrich_ext::{EnrichFnExt, EnrichFutureExt, Enriched}; pub use enrichment_trait::Enrichment; +#[doc(hidden)] pub use entry::EnrichmentEntry; pub(crate) use slot::{EnrichmentNode, EnrichmentTransfer, Guard, OptEnrichmentNode, Slot}; diff --git a/crates/observed/src/enrichment/slot.rs b/crates/observed/src/enrichment/slot.rs index e3d082252..1436ea5bd 100644 --- a/crates/observed/src/enrichment/slot.rs +++ b/crates/observed/src/enrichment/slot.rs @@ -191,12 +191,12 @@ impl EnrichmentTransfer { /// `target`, so only that sink observes it. Mirrors the `with_target` /// mapping that `EnrichFutureExt::enrich_for` applies. pub(crate) fn push_for(&mut self, target: SinkId, additional_enrichment: impl Enrichment) { - let entries: Arc<[EnrichmentEntry]> = additional_enrichment - .into_entries() - .into_iter() - .map(|entry| entry.with_target(target)) - .collect(); - self.push_entries(&entries); + let entries = additional_enrichment.into_entries(); + if entries.is_empty() { + return; + } + + self.push_entries(&entries.into_iter().map(|entry| entry.with_target(target)).collect()); } /// Layers `entries` onto every captured chain as a single new node. diff --git a/crates/observed/src/processing/processor.rs b/crates/observed/src/processing/processor.rs index f3b8cb724..cb45230fb 100644 --- a/crates/observed/src/processing/processor.rs +++ b/crates/observed/src/processing/processor.rs @@ -60,7 +60,7 @@ pub trait EventProcessor: Send + Sync { /// Forces any buffered telemetry produced by this processor out to its /// final destination, surfacing errors. Idempotent and non-terminating - /// the processor remains usable after `flush()` returns. Implementors - /// with nothing to flush use the default no-op implementation. + /// with nothing to flush should return `Ok(())`. /// /// [`Sink::flush`](crate::Sink::flush) iterates all registered /// processors and calls this; it reports every failure, not just the first. @@ -73,9 +73,7 @@ pub trait EventProcessor: Send + Sync { /// /// Returns a [`FlushError`] if flushing buffered telemetry to the final /// destination fails. - fn flush(&self) -> Result<(), FlushError> { - Ok(()) - } + fn flush(&self) -> Result<(), FlushError>; } impl EventProcessor for Arc { diff --git a/crates/observed/src/sink/core.rs b/crates/observed/src/sink/core.rs index 255e43c35..ed0f64b64 100644 --- a/crates/observed/src/sink/core.rs +++ b/crates/observed/src/sink/core.rs @@ -283,8 +283,7 @@ impl Sink { /// /// This is the entry point used by the `.enrich(&sink, ...)` API in /// [`EnrichFutureExt`](crate::enrichment::EnrichFutureExt) and - /// [`EnrichFnExt`](crate::enrichment::EnrichFnExt). Composites with zero children - /// and empty enrichment layers return a no-op guard. + /// [`EnrichFnExt`](crate::enrichment::EnrichFnExt). pub(crate) fn push_enrichment(&self, entries: Arc<[EnrichmentEntry]>) -> Guard { if entries.is_empty() { return Guard::empty(); From 9105f4f7fe8935842ce901429f87bfa644ec0eeb Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:36:23 +0100 Subject: [PATCH 3/5] fix(observed): drop the dead empty-entries guard in push_entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both callers of `EnrichmentTransfer::push_entries` already return early when the enrichment layer is empty, so the `entries.is_empty()` half of its guard can never be true. It decided nothing, which the mutation gate caught: replacing `||` with `&&` survived on all three platforms. The empty-layer no-op is unchanged — it is enforced where the layer is still owned, in `push` and `push_for`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/observed/src/enrichment/slot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/observed/src/enrichment/slot.rs b/crates/observed/src/enrichment/slot.rs index 1436ea5bd..20d42c99f 100644 --- a/crates/observed/src/enrichment/slot.rs +++ b/crates/observed/src/enrichment/slot.rs @@ -201,7 +201,7 @@ impl EnrichmentTransfer { /// Layers `entries` onto every captured chain as a single new node. fn push_entries(&mut self, entries: &Arc<[EnrichmentEntry]>) { - if self.slots.is_empty() || entries.is_empty() { + if self.slots.is_empty() { return; } From c2e04c7f4900243d0171ff6d3f65f7b2322d6b58 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:35:18 +0100 Subject: [PATCH 4/5] fix(observed_testing): derive the expected log signal from severity `ExpectedEvent` held the log expectation twice: `severity: Option` and a separate `expect_log` flag. A captured event carries a severity exactly when it carries a log signal, so the flag could express states no event can satisfy - `without_severity().log()` was unsatisfiable. Removes `expect_log` and the `log()` builder, drops the now-redundant log comparison, and updates the call sites. Also addresses review feedback: - Scopes the "exact-match" claim to what `ExpectedEvent` models, and says source location and metric instrument metadata are outside the comparison. - Uses "signal" and "flag" consistently with the crate's event model. - Notes at each empty-enrichment guard which cost it avoids, and that the entry slice is allocated by the caller of `Sink::push_enrichment`. - Records beside the OTel mapper why the emitting crate is not exported while file and line are. - Fixes the benchmark overview bullet left behind by the group rename. - Groups the `FieldlessLog` fixture with the imports and imports `SinkId`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/fetch_winhttp_impl/src/telemetry.rs | 5 +- crates/fetch_winhttp_impl/src/transport.rs | 1 - .../observed/benches/observed_benchmarks.rs | 2 +- crates/observed/examples/support/otel.rs | 5 ++ crates/observed/src/enrichment/slot.rs | 9 +++ crates/observed/src/sink/core.rs | 4 ++ crates/observed_testing/src/mock_processor.rs | 48 +++++++------- .../observed_testing/tests/basic_emission.rs | 24 ++----- .../tests/context_propagation.rs | 25 +++---- .../tests/coverage_dyn_sink.rs | 3 +- .../tests/derive_enrichment.rs | 3 +- .../observed_testing/tests/disabled_events.rs | 6 +- crates/observed_testing/tests/enrichment.rs | 66 ++++++------------- crates/observed_testing/tests/generics.rs | 31 +++------ .../observed_testing/tests/metric_routing.rs | 5 +- .../tests/processor_pipeline.rs | 10 +-- crates/observed_testing/tests/redaction.rs | 36 ++++------ 17 files changed, 108 insertions(+), 175 deletions(-) diff --git a/crates/fetch_winhttp_impl/src/telemetry.rs b/crates/fetch_winhttp_impl/src/telemetry.rs index 73a93899a..3ff596209 100644 --- a/crates/fetch_winhttp_impl/src/telemetry.rs +++ b/crates/fetch_winhttp_impl/src/telemetry.rs @@ -185,8 +185,7 @@ mod tests { ExpectedEvent::new("fetch.winhttp.session.initialization.failure", Severity::Error) .body("WinHTTP transport initialization failed") .dimension("winhttp.error_code", 1234_u32) - .dimension("winhttp.operation", "assured_non_blocking_callbacks") - .log(), + .dimension("winhttp.operation", "assured_non_blocking_callbacks"), ); assert_eq!( events[1], @@ -196,7 +195,6 @@ mod tests { events[2], ExpectedEvent::new("fetch.winhttp.request.error", Severity::Error) .body("WinHTTP transport request failed") - .log() .metric(), ); assert!(events[1].dimensions().is_empty()); @@ -216,7 +214,6 @@ mod tests { .body("WinHTTP transport request failed") .dimension("winhttp.connect.duration", 0.25_f64) .dimension("winhttp.connection.fresh", true) - .log() .metric(), ); diff --git a/crates/fetch_winhttp_impl/src/transport.rs b/crates/fetch_winhttp_impl/src/transport.rs index 97d8050ef..5f43d2ee3 100644 --- a/crates/fetch_winhttp_impl/src/transport.rs +++ b/crates/fetch_winhttp_impl/src/transport.rs @@ -483,7 +483,6 @@ mod tests { .body("WinHTTP transport request failed") .dimension("winhttp.connect.duration", 0.25_f64) .dimension("winhttp.connection.fresh", true) - .log() .metric(), ] ); diff --git a/crates/observed/benches/observed_benchmarks.rs b/crates/observed/benches/observed_benchmarks.rs index 33d6b137a..34e47b7ba 100644 --- a/crates/observed/benches/observed_benchmarks.rs +++ b/crates/observed/benches/observed_benchmarks.rs @@ -7,7 +7,7 @@ //! - Full emit pipeline (event -> log record via `OTel` provider) //! - Enrichment resolution (context lookup + Vec building) //! - Metric dimension building -//! - Context operations (enrich) +//! - Sink operations (construction, emit through a no-op sink) //! //! Run with: //! ```sh diff --git a/crates/observed/examples/support/otel.rs b/crates/observed/examples/support/otel.rs index c3bba6a4a..2fc60eeb8 100644 --- a/crates/observed/examples/support/otel.rs +++ b/crates/observed/examples/support/otel.rs @@ -130,6 +130,11 @@ pub(crate) fn populate_log_record(record: &mut impl LogRecord, event: &EventView ControlFlow::Continue(()) }); + // File and line use stable OpenTelemetry code attributes. The emitting + // crate is deliberately not exported: `code.namespace` is deprecated with + // no standalone replacement, and its successor `code.function.name` needs a + // fully qualified function name that the event model does not capture. + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/ if let Some(file) = event.source_file() { record.add_attribute(opentelemetry::Key::from_static_str(CODE_FILE_PATH), AnyValue::String(file.into())); } diff --git a/crates/observed/src/enrichment/slot.rs b/crates/observed/src/enrichment/slot.rs index 20d42c99f..1ce14c0cf 100644 --- a/crates/observed/src/enrichment/slot.rs +++ b/crates/observed/src/enrichment/slot.rs @@ -68,6 +68,10 @@ impl Slot { } /// Pushes entries onto the enrichment chain and returns a guard. + /// + /// An empty layer installs no chain node and records no restoration, so the + /// storage boundary holds for callers that reach it without passing through + /// [`Sink::push_enrichment`](crate::Sink). pub(crate) fn push(&self, entries: Arc<[EnrichmentEntry]>) -> Guard { if entries.is_empty() { return Guard::empty(); @@ -178,6 +182,11 @@ impl EnrichmentTransfer { /// Pushes an additional enrichment node onto every captured chain. /// Broadcast within the transfer's known slots; transfers with no /// captured slots are left unchanged. + /// + /// Emptiness is tested while the entries are still an owned `Vec`, so an + /// empty layer skips the shared-slice allocation as well as the chain + /// nodes. [`push_entries`](Self::push_entries) relies on this and on the + /// same check in [`push_for`](Self::push_for) for its non-empty input. pub(crate) fn push(&mut self, additional_enrichment: impl Enrichment) { let entries = additional_enrichment.into_entries(); if entries.is_empty() { diff --git a/crates/observed/src/sink/core.rs b/crates/observed/src/sink/core.rs index ed0f64b64..71ae56f44 100644 --- a/crates/observed/src/sink/core.rs +++ b/crates/observed/src/sink/core.rs @@ -284,6 +284,10 @@ impl Sink { /// This is the entry point used by the `.enrich(&sink, ...)` API in /// [`EnrichFutureExt`](crate::enrichment::EnrichFutureExt) and /// [`EnrichFnExt`](crate::enrichment::EnrichFnExt). + /// + /// An empty layer returns early, which spares composite fan-out and every + /// downstream slot write. The entry slice is built by the caller, so this + /// does not avoid the slice's own allocation. pub(crate) fn push_enrichment(&self, entries: Arc<[EnrichmentEntry]>) -> Guard { if entries.is_empty() { return Guard::empty(); diff --git a/crates/observed_testing/src/mock_processor.rs b/crates/observed_testing/src/mock_processor.rs index ac63a993a..1ffd06623 100644 --- a/crates/observed_testing/src/mock_processor.rs +++ b/crates/observed_testing/src/mock_processor.rs @@ -369,7 +369,17 @@ fn passthrough_redaction_engine() -> data_privacy::RedactionEngine { /// /// Construct with [`ExpectedEvent::new`] and chain builder methods for the /// fields and signals the event must carry. Omitted body and dimensions are -/// expected to be absent, and metric and disabled flags default to `false`. +/// expected to be absent, the metric signal is expected to be absent, and the +/// disabled flag is expected to be unset. +/// +/// The log signal follows the severity: [`new`](Self::new) expects one and +/// [`without_severity`](Self::without_severity) expects none, because a +/// captured event carries a severity exactly when it carries a log signal. +/// +/// Exactness covers what this type models - name, severity, body, dimensions, +/// the metric signal and the disabled flag. Source location and metric +/// instrument metadata are outside the comparison; assert those through the +/// [`CapturedEvent`] accessors. /// /// # Example /// @@ -388,7 +398,6 @@ pub struct ExpectedEvent { severity: Option, body: Option, sorted_dimensions: Vec<(String, Value)>, - expect_log: bool, expect_metric: bool, expect_disabled: bool, } @@ -396,9 +405,10 @@ pub struct ExpectedEvent { impl ExpectedEvent { /// Creates a new expected **log** event with the given name and severity. /// - /// Omitted body and dimensions are expected to be absent, the metric and - /// disabled flags default to `false`, and the log signal is expected. For an - /// event with no log signal - a metric-only or signal-less event - use + /// Omitted body and dimensions are expected to be absent, the metric signal + /// and the disabled flag are expected to be unset, and the log signal is + /// expected because a severity is. For an event with no log signal - a + /// metric-only or signal-less event - use /// [`without_severity`](Self::without_severity). #[must_use] pub fn new(name: impl Into, severity: Severity) -> Self { @@ -407,7 +417,6 @@ impl ExpectedEvent { severity: Some(severity), body: None, sorted_dimensions: Vec::new(), - expect_log: true, expect_metric: false, expect_disabled: false, } @@ -424,7 +433,6 @@ impl ExpectedEvent { severity: None, body: None, sorted_dimensions: Vec::new(), - expect_log: false, expect_metric: false, expect_disabled: false, } @@ -445,13 +453,6 @@ impl ExpectedEvent { self } - /// Expects the event to have the LOG signal. - #[must_use] - pub fn log(mut self) -> Self { - self.expect_log = true; - self - } - /// Expects the event to have the METRIC signal. #[must_use] pub fn metric(mut self) -> Self { @@ -459,7 +460,7 @@ impl ExpectedEvent { self } - /// Expects the event to be disabled by default. + /// Expects the event to carry the disabled flag. #[must_use] pub fn disabled(mut self) -> Self { self.expect_disabled = true; @@ -473,7 +474,6 @@ impl PartialEq for CapturedEvent { && self.severity == other.severity && self.body.as_deref() == other.body.as_deref() && self.sorted_dimensions == other.sorted_dimensions - && other.expect_log == self.description.is_log() && other.expect_metric == self.description.contains_metrics() && other.expect_disabled == self.description.is_disabled() } @@ -688,12 +688,16 @@ impl PartialEq for ExpectedEventDescription { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod coverage_tests { - use observed::{Event, emit, event}; + use observed::{Event, SinkId, emit, event}; use super::*; use crate::events::ProbeEvent; use crate::test_emitter; + #[event("fieldless.log")] + #[info] + struct FieldlessLog; + #[test] fn mock_processor_default_debug_and_flush() { let processor = MockProcessor::default(); @@ -703,7 +707,7 @@ mod coverage_tests { #[test] fn expected_types_equal_actuals_in_reverse() { - let (sink, processor) = test_emitter(observed::SinkId::new("rev")); + let (sink, processor) = test_emitter(SinkId::new("rev")); emit!(sink, ProbeEvent::new(42)); let captured = processor.single_event(); @@ -722,13 +726,9 @@ mod coverage_tests { assert_eq!(expected_desc, ProbeEvent::DESCRIPTION); } - #[event("fieldless.log")] - #[info] - struct FieldlessLog; - #[test] - fn expected_event_new_matches_fieldless_log_without_log_builder() { - let (sink, processor) = test_emitter(observed::SinkId::new("fieldless_log")); + fn expected_event_new_matches_fieldless_log() { + let (sink, processor) = test_emitter(SinkId::new("fieldless_log")); emit!(sink, FieldlessLog); diff --git a/crates/observed_testing/tests/basic_emission.rs b/crates/observed_testing/tests/basic_emission.rs index 0c6237145..ada630799 100644 --- a/crates/observed_testing/tests/basic_emission.rs +++ b/crates/observed_testing/tests/basic_emission.rs @@ -38,8 +38,7 @@ fn log_event() { ExpectedEvent::new("app.warning", Severity::Warn) .body("Something went wrong") .dimension("code", "42") - .dimension("recoverable", "true") - .log(), + .dimension("recoverable", "true"), ); } @@ -80,7 +79,6 @@ fn log_and_metric_event() { .dimension("method", "GET") .dimension("retries", "3") .dimension("status", "200") - .log() .metric() ); } @@ -106,7 +104,7 @@ fn event_with_custom_field_name() { // The field `system_id` is renamed to `db.system` via #[dimension(log = "db.system")] assert_eq!( processor.single_event(), - ExpectedEvent::new("db.error", Severity::Error).dimension("db.system", "5").log(), + ExpectedEvent::new("db.error", Severity::Error).dimension("db.system", "5"), ); } @@ -119,7 +117,7 @@ fn emit_already_constructed_event() { assert_eq!( processor.single_event(), - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "204").log() + ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "204") ); } @@ -133,10 +131,7 @@ fn emit_event_with_no_fields() { emit!(sink, Heartbeat); - assert_eq!( - processor.single_event(), - ExpectedEvent::new("internal.heartbeat", Severity::Trace).log(), - ); + assert_eq!(processor.single_event(), ExpectedEvent::new("internal.heartbeat", Severity::Trace),); } #[test] @@ -200,17 +195,13 @@ fn multiple_events_accumulate() { let events = processor.events(); assert_eq!(events.len(), 3); - assert_eq!( - events[0], - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "1").log() - ); + assert_eq!(events[0], ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "1")); assert_eq!( events[1], ExpectedEvent::new("app.warning", Severity::Warn) .body("Something went wrong") .dimension("code", "1") .dimension("recoverable", "false") - .log() ); assert_eq!( events[2], @@ -218,7 +209,6 @@ fn multiple_events_accumulate() { .dimension("key_hash", "42") .dimension("lookup_ms", 0.5f64) .dimension("size_bytes", "1024") - .log() .metric() ); } @@ -269,7 +259,6 @@ fn dimensions_types() { .dimension("f64_field", 6.14f64) .dimension("bool_field", true) .dimension("string_field", "test") - .log() ); } @@ -302,8 +291,7 @@ fn borrowed_classified_fields() { ExpectedEvent::new("user.action", Severity::Info) .dimension("count", 3i64) .dimension("label", "click") - .dimension("name", "alice") - .log(), + .dimension("name", "alice"), ); } diff --git a/crates/observed_testing/tests/context_propagation.rs b/crates/observed_testing/tests/context_propagation.rs index 27d5a869e..24811375e 100644 --- a/crates/observed_testing/tests/context_propagation.rs +++ b/crates/observed_testing/tests/context_propagation.rs @@ -45,8 +45,7 @@ fn cross_thread_context_transfer() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("origin", "1") - .dimension("value", "99") - .log(), + .dimension("value", "99"), ); } @@ -64,8 +63,7 @@ fn context_transfer_does_not_affect_source_thread() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("origin", "42") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } @@ -89,8 +87,7 @@ async fn async_enrichment_propagation() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("async_key", "7") - .dimension("value", "42") - .log(), + .dimension("value", "42"), ); } @@ -117,8 +114,7 @@ async fn async_enrichment_with_context_transfer() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("async_key", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); } @@ -147,8 +143,7 @@ async fn async_enrichment_after_attach_stays_visible() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("async_key", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); } @@ -181,14 +176,13 @@ async fn targeted_async_enrichment_after_attach_stays_visible() { // Only the addressed sink emits the targeted entry. assert_eq!( app_proc.single_event(), - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "7").log(), + ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "7"), ); assert_eq!( audit_proc.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("async_key", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); } @@ -237,13 +231,12 @@ async fn targeted_enrichment_carried_by_transfer_survives_generic_attach() { // have widened this entry onto `APP` as well. assert_eq!( app_proc.single_event(), - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "7").log(), + ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "7"), ); assert_eq!( audit_proc.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("async_key", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); } diff --git a/crates/observed_testing/tests/coverage_dyn_sink.rs b/crates/observed_testing/tests/coverage_dyn_sink.rs index 3af205b42..11826d0ac 100644 --- a/crates/observed_testing/tests/coverage_dyn_sink.rs +++ b/crates/observed_testing/tests/coverage_dyn_sink.rs @@ -296,8 +296,7 @@ fn dyn_event_body_bypasses_redaction_while_fields_do_not() { erasing.single_event(), ExpectedEvent::new("dyn.redaction", Severity::Info) .body("body-secret") - .dimension("detail", "") - .log(), + .dimension("detail", ""), ); } diff --git a/crates/observed_testing/tests/derive_enrichment.rs b/crates/observed_testing/tests/derive_enrichment.rs index a138dd0b5..9fdf9fe9a 100644 --- a/crates/observed_testing/tests/derive_enrichment.rs +++ b/crates/observed_testing/tests/derive_enrichment.rs @@ -302,8 +302,7 @@ fn hand_written_enrichment_is_accepted_by_enrich() { ExpectedEvent::new("test.probe", Severity::Info) .dimension("first", 0_i64) .dimension("second", 1_i64) - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } diff --git a/crates/observed_testing/tests/disabled_events.rs b/crates/observed_testing/tests/disabled_events.rs index 752d66bc0..97e07cc3c 100644 --- a/crates/observed_testing/tests/disabled_events.rs +++ b/crates/observed_testing/tests/disabled_events.rs @@ -34,7 +34,6 @@ fn disabled_event_captured_by_default_processor_with_flag() { processor.single_event(), ExpectedEvent::new("internal.trace_detail", Severity::Debug) .dimension("detail", "42") - .log() .disabled(), ); } @@ -49,10 +48,7 @@ fn disabled_event_filtered_by_log_and_metric_proc() { let events = processor.events(); assert_eq!(events.len(), 1); - assert_eq!( - events[0], - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "1").log() - ); + assert_eq!(events[0], ExpectedEvent::new("test.probe", Severity::Info).dimension("value", "1")); } #[test] diff --git a/crates/observed_testing/tests/enrichment.rs b/crates/observed_testing/tests/enrichment.rs index a1f789adf..41076e9a3 100644 --- a/crates/observed_testing/tests/enrichment.rs +++ b/crates/observed_testing/tests/enrichment.rs @@ -50,8 +50,7 @@ fn enrichment_appears_as_dimensions() { processor.single_event(), ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("tenant", "42") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } @@ -83,15 +82,13 @@ fn enrichments_stack_and_unwind() { ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("request_id", "42") .dimension("service", "1") - .dimension("value", "10") - .log(), + .dimension("value", "10"), ); assert_eq!( events[1], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("service", "1") - .dimension("value", "20") - .log(), + .dimension("value", "20"), ); } @@ -117,8 +114,7 @@ fn multiple_enrichment_entries_in_single_call() { .dimension("attempt", "1") .dimension("is_retry", "false") .dimension("request_id", "7") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } @@ -153,16 +149,13 @@ fn targeted_enrichment_visible_to_specific_emitter() { lib_events[0], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("lib.version", "1") - .dimension("value", "2") - .log(), + .dimension("value", "2"), ); // Targeted enrichment should NOT appear on app sink's events assert_eq!( app_events[0], - ExpectedEvent::new("test.probe", observed::Severity::Info) - .dimension("value", "1") - .log(), + ExpectedEvent::new("test.probe", observed::Severity::Info).dimension("value", "1"), ); } @@ -178,9 +171,7 @@ fn isolated_enrichment_excludes_global_context() { assert_eq!( processor.single_event(), - ExpectedEvent::new("test.probe", observed::Severity::Info) - .dimension("value", "1") - .log(), + ExpectedEvent::new("test.probe", observed::Severity::Info).dimension("value", "1"), ); } @@ -209,8 +200,7 @@ fn typed_enrichment_struct_adds_dimensions() { .dimension("request_id", "42") .dimension("attempt", "1") .dimension("is_retry", "false") - .dimension("value", "100") - .log(), + .dimension("value", "100"), ); } @@ -239,8 +229,7 @@ fn typed_enrichment_stacking() { .dimension("is_retry", "true") .dimension("request_id", "7") .dimension("tenant", "55") - .dimension("value", "200") - .log(), + .dimension("value", "200"), ); } @@ -280,8 +269,7 @@ fn enrichment_field_level_attributes() { .dimension("ctx.trace_id", "999") .dimension("logs_only_detail", "2") .dimension("metrics_only_tag", "1") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } @@ -355,15 +343,13 @@ fn enrichment_preserved_when_future_polled() { events[0], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("scope", "1") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); assert_eq!( events[1], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("scope", "2") - .dimension("value", "2") - .log(), + .dimension("value", "2"), ); } @@ -392,15 +378,13 @@ fn composite_enrich_appears_on_every_child_record() { app_proc.single_event(), ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("tenant", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); assert_eq!( audit_proc.single_event(), ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("tenant", "99") - .dimension("value", "7") - .log(), + .dimension("value", "7"), ); } @@ -448,32 +432,28 @@ fn composite_enrich_stacks_with_per_child_scope() { ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("request_id", "42") .dimension("service", "1") - .dimension("value", "100") - .log(), + .dimension("value", "100"), ); // audit[0]: in nested scope, but inner was pushed only on app — sees only outer. assert_eq!( audit_events[0], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("service", "1") - .dimension("value", "200") - .log(), + .dimension("value", "200"), ); // app[1]: outer scope only — only outer. assert_eq!( app_events[1], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("service", "1") - .dimension("value", "300") - .log(), + .dimension("value", "300"), ); // audit[1]: outer scope only — only outer. assert_eq!( audit_events[1], ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("service", "1") - .dimension("value", "300") - .log(), + .dimension("value", "300"), ); } @@ -503,17 +483,14 @@ fn composite_enrich_for_targets_one_child_only() { // AUDIT, so app's `visit_enrichments` filters it out. assert_eq!( app_proc.single_event(), - ExpectedEvent::new("test.probe", observed::Severity::Info) - .dimension("value", "1") - .log(), + ExpectedEvent::new("test.probe", observed::Severity::Info).dimension("value", "1"), ); // Audit's record: target matches its scope, so the entry is emitted. assert_eq!( audit_proc.single_event(), ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("audit_id", "7") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); } @@ -580,8 +557,7 @@ fn transfer_context_captures_every_leaf() { app_proc.single_event(), ExpectedEvent::new("test.probe", observed::Severity::Info) .dimension("tenant", "7") - .dimension("value", "3") - .log(), + .dimension("value", "3"), ); assert!(app.current_enrichments().is_empty()); } diff --git a/crates/observed_testing/tests/generics.rs b/crates/observed_testing/tests/generics.rs index 9f5a26fc9..b789d2e34 100644 --- a/crates/observed_testing/tests/generics.rs +++ b/crates/observed_testing/tests/generics.rs @@ -85,9 +85,7 @@ fn generic_event_with_unredacted_field_emits() { assert_eq!( processor.single_event(), - ExpectedEvent::new("generic.unredacted", Severity::Info) - .dimension("value", 42_i64) - .log(), + ExpectedEvent::new("generic.unredacted", Severity::Info).dimension("value", 42_i64), ); } @@ -104,9 +102,7 @@ fn generic_event_with_redacted_field_emits() { assert_eq!( processor.single_event(), - ExpectedEvent::new("generic.redacted", Severity::Info) - .dimension("value", "hello") - .log(), + ExpectedEvent::new("generic.redacted", Severity::Info).dimension("value", "hello"), ); } @@ -123,9 +119,7 @@ fn generic_event_with_data_class_field_emits() { assert_eq!( processor.single_event(), - ExpectedEvent::new("generic.classified", Severity::Info) - .dimension("value", "secret") - .log(), + ExpectedEvent::new("generic.classified", Severity::Info).dimension("value", "secret"), ); } @@ -144,16 +138,12 @@ fn generic_event_with_option_field_emits_both_arms() { let events = processor.events(); assert_eq!( events[0], - ExpectedEvent::new("generic.optional", Severity::Info) - .dimension("value", "present") - .log(), + ExpectedEvent::new("generic.optional", Severity::Info).dimension("value", "present"), ); // The default `#[if_none("n/a")]` placeholder fills the missing value. assert_eq!( events[1], - ExpectedEvent::new("generic.optional", Severity::Info) - .dimension("value", "n/a") - .log(), + ExpectedEvent::new("generic.optional", Severity::Info).dimension("value", "n/a"), ); } @@ -173,8 +163,7 @@ fn generic_event_with_two_parameters_emits() { processor.single_event(), ExpectedEvent::new("generic.mixed", Severity::Info) .dimension("raw", 7_i64) - .dimension("classified", "mixed") - .log(), + .dimension("classified", "mixed"), ); } @@ -195,8 +184,7 @@ fn generic_enrichments_reach_the_record() { ExpectedEvent::new("generic.unredacted", Severity::Info) .dimension("ctx", 5_i64) .dimension("ctx", "99") - .dimension("value", 1_i64) - .log(), + .dimension("value", 1_i64), ); } @@ -216,8 +204,7 @@ fn generic_data_class_enrichment_reaches_the_record() { processor.single_event(), ExpectedEvent::new("generic.unredacted", Severity::Info) .dimension("ctx", "tenant-7") - .dimension("value", 1_i64) - .log(), + .dimension("value", 1_i64), ); } @@ -276,6 +263,6 @@ fn a_field_routed_to_no_signal_is_absent_from_the_record() { assert_eq!( processor.single_event(), - ExpectedEvent::new("generic.excluded", Severity::Info).dimension("kept", "1").log(), + ExpectedEvent::new("generic.excluded", Severity::Info).dimension("kept", "1"), ); } diff --git a/crates/observed_testing/tests/metric_routing.rs b/crates/observed_testing/tests/metric_routing.rs index 387dfa0a6..ad5d7029d 100644 --- a/crates/observed_testing/tests/metric_routing.rs +++ b/crates/observed_testing/tests/metric_routing.rs @@ -40,10 +40,7 @@ fn event_without_metrics_is_log_only() { emit!(sink, LogEvent { value: 1 }); let event = processor.single_event(); - assert_eq!( - event, - ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 1i64).log(), - ); + assert_eq!(event, ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 1i64),); assert_eq!(event.field_metrics().len(), 0); } diff --git a/crates/observed_testing/tests/processor_pipeline.rs b/crates/observed_testing/tests/processor_pipeline.rs index 9e625d531..159d7f7a0 100644 --- a/crates/observed_testing/tests/processor_pipeline.rs +++ b/crates/observed_testing/tests/processor_pipeline.rs @@ -65,13 +65,11 @@ fn severity_filter_drops_low_severity_events() { assert_eq!(events.len(), 2); assert_eq!( events[0], - ExpectedEvent::new("auth.failed", Severity::Warn).dimension("attempts", "3").log() + ExpectedEvent::new("auth.failed", Severity::Warn).dimension("attempts", "3") ); assert_eq!( events[1], - ExpectedEvent::new("system.crash", Severity::Fatal) - .dimension("exit_code", "1") - .log() + ExpectedEvent::new("system.crash", Severity::Fatal).dimension("exit_code", "1") ); } @@ -130,7 +128,7 @@ fn multiple_processors_receive_events_independently() { assert_eq!(warn_processor.len(), 1); assert_eq!( warn_processor.single_event(), - ExpectedEvent::new("auth.failed", Severity::Warn).dimension("attempts", "5").log() + ExpectedEvent::new("auth.failed", Severity::Warn).dimension("attempts", "5") ); } @@ -175,7 +173,6 @@ fn composite_fans_out_to_each_child() { ExpectedEvent::new("user.login", Severity::Info) .dimension("mfa_used", "false") .dimension("user_id", "1") - .log() ); } @@ -198,7 +195,6 @@ fn emitter_clone_shares_processors() { ExpectedEvent::new("user.login", Severity::Info) .dimension("mfa_used", "true") .dimension("user_id", "99") - .log() ); } diff --git a/crates/observed_testing/tests/redaction.rs b/crates/observed_testing/tests/redaction.rs index d2c728052..128f415d5 100644 --- a/crates/observed_testing/tests/redaction.rs +++ b/crates/observed_testing/tests/redaction.rs @@ -106,8 +106,7 @@ fn redaction_modes_on_classified_fields() { passthrough_proc.single_event(), ExpectedEvent::new("user.action", Severity::Info) .dimension("action_code", 42i64) - .dimension("user", "Alice") - .log(), + .dimension("user", "Alice"), ); // Erase: classified string becomes empty @@ -115,8 +114,7 @@ fn redaction_modes_on_classified_fields() { erase_proc.single_event(), ExpectedEvent::new("user.action", Severity::Info) .dimension("action_code", 42i64) - .dimension("user", "") - .log(), + .dimension("user", ""), ); // Replace('*'): each character replaced @@ -124,8 +122,7 @@ fn redaction_modes_on_classified_fields() { replace_proc.single_event(), ExpectedEvent::new("user.action", Severity::Info) .dimension("action_code", 42i64) - .dimension("user", "*****") - .log(), + .dimension("user", "*****"), ); } @@ -148,8 +145,7 @@ fn per_class_redaction_applies_different_rules() { ExpectedEvent::new("auth.token_used", Severity::Info) .dimension("request_id", 99i64) .dimension("token", "") - .dimension("user", "***") - .log(), + .dimension("user", "***"), ); } @@ -179,8 +175,7 @@ fn public_classified_type_with_passthrough_per_class() { processor.single_event(), ExpectedEvent::new("service.started", Severity::Info) .dimension("port", 8080i64) - .dimension("service", "my-service") - .log(), + .dimension("service", "my-service"), ); } @@ -213,8 +208,7 @@ fn enrichment_string_values_are_redacted() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("tenant", "*******") - .dimension("value", "*") - .log(), + .dimension("value", "*"), ); } @@ -243,8 +237,7 @@ fn enrichment_sensitive_values_are_redacted() { processor.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("retry_count", "") - .dimension("value", "") - .log(), + .dimension("value", ""), ); } @@ -279,8 +272,7 @@ fn enrichment_redaction_uses_per_processor_engine() { pass_proc.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("user_email", "alice@example.com") - .dimension("value", "1") - .log(), + .dimension("value", "1"), ); // Erase processor removes the enrichment value @@ -288,8 +280,7 @@ fn enrichment_redaction_uses_per_processor_engine() { erase_proc.single_event(), ExpectedEvent::new("test.probe", Severity::Info) .dimension("user_email", "") - .dimension("value", "") - .log(), + .dimension("value", ""), ); } @@ -318,8 +309,7 @@ fn insert_mode_replaces_with_custom_string() { processor.single_event(), ExpectedEvent::new("user.action", Severity::Info) .dimension("action_code", 1i64) - .dimension("user", "[REDACTED]") - .log(), + .dimension("user", "[REDACTED]"), ); } @@ -350,8 +340,7 @@ fn passthrough_for_specific_class_erase_rest() { ExpectedEvent::new("auth.token_used", Severity::Info) .dimension("request_id", 1i64) .dimension("token", "") - .dimension("user", "Alice") - .log(), + .dimension("user", "Alice"), ); } @@ -401,7 +390,6 @@ fn data_class_fields_are_redacted_without_being_cloned() { processor.single_event(), ExpectedEvent::new("data_class.borrowed", Severity::Info) .dimension("payload", "****") - .dimension("optional", "**") - .log(), + .dimension("optional", "**"), ); } From befe9d69ed13f2cfbc21bd6789131d6050f5e02e Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:34:58 +0100 Subject: [PATCH 5/5] test(observed_testing): drop trailing commas in two assert_eq! calls A trailing comma after the second argument of `assert_eq!` reads like an empty custom-message argument. Both call sites take the two-argument form, so the comma is removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/observed_testing/tests/basic_emission.rs | 2 +- crates/observed_testing/tests/metric_routing.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/observed_testing/tests/basic_emission.rs b/crates/observed_testing/tests/basic_emission.rs index ada630799..abd198a97 100644 --- a/crates/observed_testing/tests/basic_emission.rs +++ b/crates/observed_testing/tests/basic_emission.rs @@ -131,7 +131,7 @@ fn emit_event_with_no_fields() { emit!(sink, Heartbeat); - assert_eq!(processor.single_event(), ExpectedEvent::new("internal.heartbeat", Severity::Trace),); + assert_eq!(processor.single_event(), ExpectedEvent::new("internal.heartbeat", Severity::Trace)); } #[test] diff --git a/crates/observed_testing/tests/metric_routing.rs b/crates/observed_testing/tests/metric_routing.rs index ad5d7029d..5dc7ddf50 100644 --- a/crates/observed_testing/tests/metric_routing.rs +++ b/crates/observed_testing/tests/metric_routing.rs @@ -40,7 +40,7 @@ fn event_without_metrics_is_log_only() { emit!(sink, LogEvent { value: 1 }); let event = processor.single_event(); - assert_eq!(event, ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 1i64),); + assert_eq!(event, ExpectedEvent::new("test.probe", Severity::Info).dimension("value", 1i64)); assert_eq!(event.field_metrics().len(), 0); }