Skip to content

feat(observed): expose sink interest query - #787

Open
Sander Saares (sandersaares) wants to merge 4 commits into
mainfrom
sasaares-microsoft-observed-sink-interest
Open

Sander Saares (sandersaares) wants to merge 4 commits into
mainfrom
sasaares-microsoft-observed-sink-interest

Conversation

@sandersaares

@sandersaares Sander Saares (sandersaares) commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

[Copilot speaking]

Foreign telemetry adapters need to check downstream interest before constructing events.

Expose Sink::is_interested to report whether any downstream processor is interested in the described event, using the existing processor and composite-sink aggregation. Like EventProcessor::is_interested, the query inspects the event description rather than event field values. Its result reflects current interest, not a lifetime-cacheable decision or a delivery guarantee.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@sandersaares Sander Saares (sandersaares) changed the title feat(observed): expose metadata-only sink interest feat(observed): expose sink interest query Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

⚠️ SemVer check advisory

Potential breaking changes

cargo semver-checks flagged the following on this PR. This is informational -- breaking changes between commits are expected; the major-version bump happens at release time, not on every PR.

fetch

     Cloning baeabbf54c40cc5fd90770c63a8ab2a6237c98eb
    Building fetch v0.17.1 (current)
       Built [  41.185s] (current)
     Parsing fetch v0.17.1 (current)
      Parsed [   0.009s] (current)
    Building fetch v0.17.1 (baseline)
       Built [  42.078s] (baseline)
     Parsing fetch v0.17.1 (baseline)
      Parsed [   0.008s] (baseline)
    Checking fetch v0.17.1 -> v0.17.1 (no change; assume minor)
     Checked [   0.021s] 196 checks: 195 pass, 1 fail, 0 warn, 58 skip

--- failure module_missing: pub module removed or renamed ---

Description:
A publicly-visible module cannot be imported by its prior path. A `pub use` may have been removed, or the module may have been renamed, removed, or made non-public.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/module_missing.ron

Failed in:
  mod fetch::_documentation::telemetry, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/fetch/src/_documentation/telemetry.rs:4
  mod fetch::_documentation, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/fetch/src/_documentation/mod.rs:4
  mod fetch::_documentation::examples, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/fetch/src/_documentation/examples.rs:4

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  86.810s] fetch

http_extensions

     Cloning baeabbf54c40cc5fd90770c63a8ab2a6237c98eb
    Building http_extensions v0.11.1 (current)
       Built [  11.338s] (current)
     Parsing http_extensions v0.11.1 (current)
      Parsed [   0.006s] (current)
    Building http_extensions v0.11.1 (baseline)
       Built [  11.214s] (baseline)
     Parsing http_extensions v0.11.1 (baseline)
      Parsed [   0.006s] (baseline)
    Checking http_extensions v0.11.1 -> v0.11.1 (no change; assume minor)
     Checked [   0.025s] 196 checks: 195 pass, 1 fail, 0 warn, 58 skip

--- failure module_missing: pub module removed or renamed ---

Description:
A publicly-visible module cannot be imported by its prior path. A `pub use` may have been removed, or the module may have been renamed, removed, or made non-public.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/module_missing.ron

Failed in:
  mod http_extensions::_documentation, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/http_extensions/src/_documentation/mod.rs:4
  mod http_extensions::_documentation::recipes, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/http_extensions/src/_documentation/recipes.rs:4

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  23.325s] http_extensions

recoverable

     Cloning baeabbf54c40cc5fd90770c63a8ab2a6237c98eb
    Building recoverable v0.2.0 (current)
       Built [   0.281s] (current)
     Parsing recoverable v0.2.0 (current)
      Parsed [   0.002s] (current)
    Building recoverable v0.2.0 (baseline)
       Built [   0.277s] (baseline)
     Parsing recoverable v0.2.0 (baseline)
      Parsed [   0.002s] (baseline)
    Checking recoverable v0.2.0 -> v0.2.0 (no change; assume minor)
     Checked [   0.015s] 196 checks: 195 pass, 1 fail, 0 warn, 58 skip

--- failure module_missing: pub module removed or renamed ---

Description:
A publicly-visible module cannot be imported by its prior path. A `pub use` may have been removed, or the module may have been renamed, removed, or made non-public.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/module_missing.ron

Failed in:
  mod recoverable::_documentation, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/recoverable/src/_documentation/mod.rs:4
  mod recoverable::_documentation::recipes, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/recoverable/src/_documentation/recipes.rs:4

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [   0.748s] recoverable

templated_uri

     Cloning baeabbf54c40cc5fd90770c63a8ab2a6237c98eb
    Building templated_uri v0.6.0 (current)
       Built [   7.558s] (current)
     Parsing templated_uri v0.6.0 (current)
      Parsed [   0.003s] (current)
    Building templated_uri v0.6.0 (baseline)
       Built [   7.478s] (baseline)
     Parsing templated_uri v0.6.0 (baseline)
      Parsed [   0.003s] (baseline)
    Checking templated_uri v0.6.0 -> v0.6.0 (no change; assume minor)
     Checked [   0.019s] 196 checks: 195 pass, 1 fail, 0 warn, 58 skip

--- failure module_missing: pub module removed or renamed ---

Description:
A publicly-visible module cannot be imported by its prior path. A `pub use` may have been removed, or the module may have been renamed, removed, or made non-public.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/module_missing.ron

Failed in:
  mod templated_uri::_documentation::recipes, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/templated_uri/src/_documentation/recipes.rs:4
  mod templated_uri::_documentation, previously in file /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-baeabbf54c40cc5fd90770c63a8ab2a6237c98eb/5632d05ba0167ce7c9aaea7e6e3daba29c0372a7/crates/templated_uri/src/_documentation/mod.rs:4

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  15.426s] templated_uri

Inconclusive comparisons

cargo semver-checks could not complete the following comparisons. These failures are informational because an unbuildable baseline is not evidence of a breaking API change.

metabench (exit 101)

     Cloning baeabbf54c40cc5fd90770c63a8ab2a6237c98eb
    Building metabench v0.1.1 (current)
error: running cargo-doc on crate 'metabench' failed with output:
-----
   Compiling proc-macro2 v1.0.107
   Compiling quote v1.0.47
   Compiling unicode-ident v1.0.26
   Compiling serde_core v1.0.229
   Compiling zmij v1.0.23
   Compiling serde v1.0.229
   Compiling serde_json v1.0.151
   Compiling libc v0.2.189
    Checking cfg-if v1.0.5
    Checking memchr v2.8.3
   Compiling syn v3.0.6
   Compiling syn v2.0.119
    Checking itoa v1.0.18
   Compiling zerocopy v0.8.59
    Checking bitflags v2.13.2
   Compiling shlex v2.0.1
   Compiling find-msvc-tools v0.1.14
   Compiling hashbrown v0.17.1
   Compiling semver v1.0.28
   Compiling winnow v1.0.4
   Compiling rustversion v1.0.23
   Compiling equivalent v1.0.2
   Compiling indexmap v2.14.2
   Compiling rustc_version v0.4.1
   Compiling toml_parser v1.1.3+spec-1.1.0
   Compiling cc v1.5.1
   Compiling serde_derive v1.0.229
   Compiling zerocopy-derive v0.8.59
   Compiling derive_more-impl v2.1.1
   Compiling rustix v1.1.5
   Compiling toml_datetime v1.1.1+spec-1.1.0
   Compiling autocfg v1.5.1
   Compiling toml_edit v0.25.15+spec-1.1.0
   Compiling num-traits v0.2.19
   Compiling alloca v0.4.0
   Compiling gungraun-macros v0.9.1
    Checking either-or-both v0.3.1
   Compiling proc-macro-error-attr3 v3.1.1
    Checking clap_lex v1.1.1
    Checking ciborium-io v0.2.2
    Checking linux-raw-sys v0.12.1
    Checking regex-syntax v0.8.11
    Checking either v1.18.0
   Compiling thiserror v2.0.21
   Compiling cfg_aliases v0.2.2
   Compiling bincode-next v3.1.1
   Compiling getrandom v0.4.3
    Checking anstyle v1.0.14
    Checking clap_builder v4.6.7
    Checking regex-automata v0.4.18
   Compiling nix v0.31.3
    Checking itertools v0.13.0
    Checking half v2.7.1
    Checking ciborium-ll v0.2.2
   Compiling proc-macro-error3 v3.1.1
    Checking rapidhash v4.5.1
   Compiling proc-macro-crate v3.5.0
   Compiling derive_more v2.1.1
   Compiling thiserror-impl v2.0.21
    Checking cast v0.3.0
    Checking unty-next v0.1.2
   Compiling pastey v0.2.3
    Checking same-file v1.0.6
   Compiling gungraun v0.19.4
    Checking walkdir v2.5.0
    Checking criterion-plot v0.8.2
   Compiling metabench_macros_impl v0.1.1 (/home/runner/work/oxidizer/oxidizer/crates/metabench_macros_impl)
    Checking ciborium v0.2.2
    Checking regex v1.13.1
    Checking clap v4.6.7
    Checking gungraun-runner v0.20.0
    Checking gungraun-runner v0.19.4
    Checking folo_utils v0.1.14
    Checking tinytemplate v1.2.1
    Checking nix v0.27.1
    Checking page_size v0.6.0
    Checking jiff-core v0.1.1
    Checking oorandom v11.1.5
    Checking anes v0.1.6
    Checking fastrand v2.5.0
    Checking once_cell v1.21.4
   Compiling metabench v0.1.1 (/home/runner/work/oxidizer/oxidizer/crates/metabench)
    Checking tempfile v3.27.0
    Checking criterion v0.8.2
    Checking jiff v0.2.37
    Checking command-group v5.0.1
    Checking gungraun-summary v6.0.0
error[E0432]: unresolved import `gungraun_runner::api::ValgrindTool`
  --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gungraun-summary-6.0.0/src/v6.rs:10:59
   |
10 |     CachegrindMetric, DhatMetric, ErrorMetric, EventKind, ValgrindTool,
   |                                                           ^^^^^^^^^^^^ no `ValgrindTool` in `api`

error[E0432]: unresolved imports `gungraun_runner::metrics::model::MetricsDiff`, `gungraun_runner::metrics::model::MetricsSummary`
  --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gungraun-summary-6.0.0/src/v6.rs:12:63
   |
12 | pub use gungraun_runner::metrics::model::{Metric, MetricKind, MetricsDiff, MetricsSummary};
   |                                                               ^^^^^^^^^^^  ^^^^^^^^^^^^^^ no `MetricsSummary` in `metrics::model`
   |                                                               |
   |                                                               no `MetricsDiff` in `metrics::model`

   Compiling metabench_macros v0.1.1 (/home/runner/work/oxidizer/oxidizer/crates/metabench_macros)
error[E0432]: unresolved imports `gungraun_runner::summary::model::Diffs`, `gungraun_runner::summary::model::FlamegraphSummary`, `gungraun_runner::summary::model::ProfileInfo`, `gungraun_runner::summary::model::SummaryFormat`, `gungraun_runner::summary::model::SummaryOutput`, `gungraun_runner::summary::model::ToolMetricSummary`
  --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/gungraun-summary-6.0.0/src/v6.rs:14:38
   |
14 |     BenchmarkKind, BenchmarkSummary, Diffs, FlamegraphSummary, Profile, ProfileData, ProfileInfo,
   |                                      ^^^^^  ^^^^^^^^^^^^^^^^^                        ^^^^^^^^^^^ no `ProfileInfo` in `summary::model`
   |                                      |      |
   |                                      |      no `FlamegraphSummary` in `summary::model`
   |                                      no `Diffs` in `summary::model`
15 |     ProfilePart, ProfileTotal, Profiles, SCHEMA_VERSION, SummaryFormat, SummaryOutput,
   |                                                          ^^^^^^^^^^^^^  ^^^^^^^^^^^^^ no `SummaryOutput` in `summary::model`
   |                                                          |
   |                                                          no `SummaryFormat` in `summary::model`
16 |     ToolMetricSummary, ToolRegression,
   |     ^^^^^^^^^^^^^^^^^ no `ToolMetricSummary` in `summary::model`

For more information about this error, try `rustc --explain E0432`.
error: could not compile `gungraun-summary` (lib) due to 3 previous errors
warning: build failed, waiting for other jobs to finish...

-----

error: failed to build rustdoc for crate metabench v0.1.1
note: this is usually due to a compilation error in the crate,
      and is unlikely to be a bug in cargo-semver-checks
note: the following command can be used to reproduce the error:
      cargo new --lib example &&
          cd example &&
          echo '[workspace]' >> Cargo.toml &&
          cargo add --path /home/runner/work/oxidizer/oxidizer/crates/metabench &&
          cargo check &&
          cargo doc

error: aborting due to failure to build rustdoc for crate metabench v0.1.1

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (baeabbf) to head (dee5ed3).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #787    +/-   ##
========================================
  Coverage   100.0%   100.0%            
========================================
  Files         738      736     -2     
  Lines       98474    98588   +114     
========================================
+ Hits        98474    98588   +114     
Flag Coverage Δ
linux 100.0% <100.0%> (ø)
linux-arm 100.0% <100.0%> (ø)
scheduled ?
windows 100.0% <100.0%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The API reuses existing aggregation logic, has focused contract tests, and has no unresolved findings.

Review effort: Balanced
Findings: None

What changed in this PR

This PR exposes the existing sink-interest check so foreign telemetry adapters can decide whether to construct an event.

Changes:

  • Makes Sink::is_interested public and documents its current-interest, not delivery-guarantee, contract.
  • Adds public API tests for leaf and composite sinks, initialization changes, and sampling.
  • Updates the design and feature documentation.
File Description
crates/​observed/​tests/​sink_interest.rs Tests the public interest-query contract.
crates/​observed/​src/​sink/​core.rs Exposes and documents the existing query.
crates/​observed/​src/​processing/​processor.rs Clarifies processor-interest documentation.
crates/​observed/​FEATURES.md Lists the sink-interest query.
crates/​observed/​docs/​implementation.md Explains interest aggregation.
crates/​observed/​DESIGN.md Documents the query’s scope and limits.
crates/​observed/​Cargo.toml Adds a test dependency.
Cargo.lock Records the dependency change.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@sandersaares

Copy link
Copy Markdown
Member Author

[Copilot speaking]

Performance goal and measurements

The goal is to avoid constructing and formatting events that no downstream processor wants, while minimizing the extra work for accepted events. Sink::is_interested enables an adapter to reject events early; the dynamic-dispatch optimization in this PR removes a redundant downstream interest traversal.

End-to-end adapter paths

A downstream tracing-adapter microbenchmark gives these instruction counts with the optimized observed dispatch. Percentages are relative to the original adapter path.

Integration Rejected TRACE instructions Accepted INFO instructions
Original adapter, without early filtering 501 848
Subscriber-wide early filtering + optimized dispatch 194 (-61.3%) 940 (+10.8%)
Per-layer early filtering + optimized dispatch 360 (-28.1%) 995 (+17.3%)

These rejection savings require the adapter to use the new query for early admission; exposing the query alone does not change tracing's filtering. Subscriber-wide admission can prevent caller field evaluation. Per-layer admission preserves independent consumers but may still evaluate caller expressions. Accepted events still pay for dynamic admission.

Isolated improvement from this PR's dispatch optimization

Holding the adapter, resolved dependency versions and toolchain fixed, the core optimization saves 55 instructions per accepted event:

Adapter mode Before After Reduction
Subscriber-wide filtering 995 940 5.5%
Per-layer filtering 1,050 995 5.2%

Rejected TRACE stays at 194 and 360 respectively. Inclusive Callgrind attribution places the entire saving in emit_dyn_event (457 to 402 instructions); early adapter admission remains 88 instructions. The optimization retains the first selected recipient rather than repeating the aggregate-interest pass. Sampled paths retain their post-sampling recipient checks.

The isolated core comparison is 9d3ece6 to a6c2de5.

Measurement scope

One warmed emission, a trivial numeric inline field and one synthetic INFO-interested processor, without sampling. Subscriber construction, warm-up, teardown, exporter I/O, message formatting and expensive caller computation are excluded. Every instruction count repeated unchanged. These are instruction-count measurements, not production CPU, latency, throughput or allocation measurements; sampled and composite performance is not represented.

Environment: WSL Linux x86_64, Rust 1.95.0, Valgrind 3.22.0, Gungraun runner 0.19.4, opt-level=3, target-cpu=x86-64-v3.

@sandersaares
Sander Saares (sandersaares) marked this pull request as ready for review September 30, 2026 13:05
Copilot AI balanced review requested due to automatic review settings September 30, 2026 13:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Dynamic routing changes admission and delivery boundaries, and an unresolved stale-interest race needs correction and human review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Comment thread crates/observed/src/sink/core.rs
@martintmk martintmk added the human-review-required Human review required before automated approval. label Sep 30, 2026
@martintmk

Copy link
Copy Markdown
Member

Posted by an AI agent

Human review required at a6c2de5236b3c9dd3ec5ba877306c4e1a7751396: the unsampled dispatch change is not behavior-preserving. Source trace: with processors [A, B], A initially rejects, then B's permitted one-time initialization enables A during admission. Baseline dispatch rechecks A and delivers to both; the new retained iterator skips A and delivers only to B. Please resolve the recipient-selection compatibility issue, also flagged in the open routing finding, and add this regression case before approval. Pending CI alone did not cause this escalation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 03:54
@sandersaares

Copy link
Copy Markdown
Member Author

[Copilot speaking]

Addressed the recipient-selection feedback in dee5ed3 by clarifying the supported contract and adding a deterministic regression. Single-pass decisions are retained intentionally: interest checks are advisory observations, not an atomic snapshot, and earlier decisions need not be reconsidered within the same emission.

The leaf/composite regression covers the described initialization sequence: the earlier rejected recipient stays skipped for that dynamic emission and is selected on the next. Decisions are not cached across emissions. Typed lazy construction and sampled recipient rechecks remain unchanged. Runtime code is unchanged, preserving the measured accepted-event saving. The corresponding inline review thread is resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The description must disclose or separate the significant dynamic-delivery semantics change.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Comment on lines +376 to +377
/// Routes an already-built event without a lazy-construction interest pass.
pub(crate) fn emit_dynamic(&self, event: &dyn DynEvent) {
@Vaiz

Copy link
Copy Markdown
Contributor

At head dee5ed39, PR #787 makes one public API addition, updates an existing trait’s documented contract, and changes dynamic-emission behavior. All changes are in observed.

1. New public method

impl observed::Sink {
    #[must_use]
    #[inline]
    pub fn is_interested(
        &self,
        description: &observed::metadata::EventDescription,
    ) -> bool;
}
  • Returns true if any processor is interested; composites aggregate interest across their children.
  • Returns false for no-op sinks and sinks without processors.
  • Uses event metadata, not field values.
  • Does not construct or dispatch events, flush processors, invoke sampling, or read timestamps/enrichments.
  • Reports current interest, not guaranteed delivery or a lifetime-cacheable filtering decision. Callers should query each candidate event; emission checks interest independently.
  • Available without a feature gate.

This replaces the previously private Sink::is_interested_in helper; it is not a rename of an existing public method.

2. Updated public trait contract

The signature is unchanged:

observed::processing::EventProcessor::is_interested(
    &self,
    description: &EventDescription,
) -> bool;

Its documentation now explicitly states:

  • It can be called by Sink::is_interested independently of emission.
  • Pre-construction checks apply to lazy typed events; already-built dynamic events use routing checks.
  • Interest is advisory for the current check. One-time initialization can change selection for subsequent emissions.
  • Checks do not form an atomic snapshot across processors, and earlier decisions need not be reconsidered during the same emission.
  • The previous unconditional wording that “a processor that returns false never receives the event” is replaced by admission-versus-routing semantics.

The existing restriction remains: interest depends on metadata and state that changes at most once; per-call sampling/rate limiting belongs in process.

3. Changed behavior behind an unchanged public function

observed::interop::emit_dyn_event(
    sink: &Sink,
    event: &dyn DynEvent,
);

Dynamic events now bypass the lazy typed-event machinery:

  • One routing-description snapshot: DynEvent::description() is captured once for routing, rather than being read again for each interest check through the old wrapper.
  • Empty sinks do not inspect the event: no-op sinks and sinks whose leaves all have no processors return without calling description().
  • Unsampled leaves use a single selection pass: the first interested processor is retained for delivery rather than rechecked; subsequent processors are checked after earlier processors run.
  • Sampled leaves retain post-sampling rechecks: admission selects an interested processor, but after sampling every processor is checked again, including those previously rejected.
  • Uninterested leaves skip clock/enrichment access and sampling.
  • Reentrancy timing changes: initial admission checks occur outside the guard; after the first recipient is selected, dispatch, subsequent checks, samplers, and subsequent composite leaves are guarded.

Leaf-local timestamps, enrichment, sampling, and processor order remain preserved.

No public items are removed, no existing public signatures change, and no public types, fields, variants, re-exports, or feature flags are added or changed. The removed event-state types and new dispatch helpers are crate-private/private.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this document can be cut down. For example "Public API tests use frozen clocks" is just a common good practice and mentioning this in a document doesn't add any value

Comment on lines +577 to +584
let timestamp = self.clock.system_time();
let view = EventView::new(event, self.enrichment.current(), self.isolated_enrichment, self.id, timestamp);
first.process(&view);
// Evaluate each remaining recipient after earlier processors have run,
// so their initialization effects are visible without caching interest.
for processor in interested {
processor.process(&view);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplification in event_state.rs is worth keeping. We can reduce duplication in sink/core.rs without reintroducing the dynamic-event wrapper: extract only view construction and recipient delivery.

Concretely, add use std::iter; and use std::time::SystemTime;, then use this structure inside impl SingleSinkState:

#[inline]
fn deliver<'p>(
    &self,
    event: &dyn DynEvent,
    timestamp: SystemTime,
    recipients: impl Iterator<Item = &'p Arc<dyn EventProcessor>>,
) {
    let view = EventView::new(
        event,
        self.enrichment.current(),
        self.isolated_enrichment,
        self.id,
        timestamp,
    );
    for processor in recipients {
        processor.process(&view);
    }
}

fn dispatch(&self, event: &dyn DynEvent, description: &EventDescription) {
    let timestamp = self.clock.system_time();

    if let Some(sampler) = &self.sampler
        && sampler.sample(&EventSamplingContext::new(
            description,
            self.id,
            timestamp,
        )) == EventSamplingDecision::Drop
    {
        return;
    }

    self.deliver(
        event,
        timestamp,
        self.processors
            .iter()
            .filter(|processor| processor.is_interested(description)),
    );
}

fn prepare_dynamic_dispatch<'a>(
    &'a self,
    event: &'a dyn DynEvent,
    description: &'a EventDescription,
) -> Option<impl FnOnce() + 'a> {
    let mut interested = self.processors
        .iter()
        .filter(move |processor| processor.is_interested(description));
    let first = interested.next()?;

    Some(move || {
        if self.sampler.is_some() {
            // Sampling can change interest; recheck every processor afterward.
            self.dispatch(event, description);
            return;
        }

        self.deliver(
            event,
            self.clock.system_time(),
            iter::once(first).chain(interested),
        );
    })
}

This preserves the PR's ordering: unsampled dynamic delivery retains the first selected recipient; later interest checks remain lazy and observe preceding processor callbacks; sampled delivery rechecks every processor after sampling; timestamps are read before sampling and enrichment is captured afterward. Admission and reentrancy-guard placement remain unchanged.

The helper uses a monomorphized iterator, with no iterator boxing, recipient collection, added allocation, or new runtime mode branch. #[inline] makes eliminating the helper call straightforward, although an exact zero-overhead claim would require generated-code or benchmark evidence.

I would share this delivery stage, not force construction, admission, sampling, and guard acquisition into one universal pipeline.

@martintmk
martintmk self-requested a review October 1, 2026 07:24
@martintmk

Copy link
Copy Markdown
Member

AI review preparation has started for #787 at head dee5ed39baaf8e89f9db6afe929626cd0b3c9728.

The submitted GitHub review will contain the result.

@martintmk martintmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent

Warning: Incomplete review

I reviewed the sink-interest API and public contracts from source, dynamic-dispatch correctness, tests, performance-related code, naming, telemetry, and documentation at dee5ed39baaf8e89f9db6afe929626cd0b3c9728. No recovery mechanisms changed. I could not check:

  • Exported API comparison: matching base/head API captures were unavailable, and the credentialed review environment did not permit generating them.

No new findings were identified in the reviewed areas. The existing request to disclose the dynamic-delivery semantics change remains applicable; it is not duplicated here.

I did not run local builds, tests, or benchmarks. Supplied Linux ARM CI evidence shows all 19 added integration tests passing twice on verified synthetic merge 5d88263263236159f4827b2dda15d92bf70df184, containing this head and later main 27a8b19174fb99633555d9c2194e7efd1782b087. This is not a locally run base/head reproduction.

No overall verdict is given.

@martintmk martintmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent

Warning: Incomplete review

I reviewed public contracts, correctness, tests, performance, naming, telemetry, and documentation. Resilience had no relevant behavior. I could not check:

  • Exported public API: exact base/head outputs were unavailable, and building PR code on the shared credentialed host was not permitted.

No local builds, tests, or benchmarks were run. The earlier request to disclose changed dynamic-delivery semantics remains applicable and is not duplicated here.

No overall verdict is given.

pub(crate) struct IntermediateEvent<'a, F> {
inner: Inner<'a, F>,
/// Holds a typed event builder until processor interest permits construction.
pub(crate) struct IntermediateEvent<F> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent · Non-blocking

IntermediateEvent retains a redundant construction layer

Problem
The dynamic variant was removed, leaving IntermediateEvent to store only the typed builder and source location that Sink::emit already has; emit_impl then evaluates this wrapper immediately before dispatch. It no longer encodes a choice or enforces an invariant.

Why this matters
The extra internal type and forwarding method obscure where construction happens and keep a state-machine layer with only one state.

Suggested fix
Keep EvaluatedEvent as the adapter, but perform the interest check and build in Sink::emit before acquiring the recursion guard, then remove IntermediateEvent and update the implementation guide's construction-state wording.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-review-required Human review required before automated approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants