Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"crates/before",
"crates/before-viz",
"crates/rumors-tracing",
"crates/surface-scan",
"crates/suanpan",
]
Expand Down
15 changes: 15 additions & 0 deletions crates/rumors-tracing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "rumors-tracing"
version = "0.1.0"
edition = "2024"
description = "A tracing adapter for rumors gossip sessions: spans per session and stream, one structured event per observed wire item."
license = "MPL-2.0"
readme = "README.md"

[dependencies]
rumors = { path = "../.." }
tracing = "0.1"
ciborium = "0.2"

[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
112 changes: 112 additions & 0 deletions crates/rumors-tracing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# rumors-tracing

<!-- cargo-rdme start -->

A `tracing` adapter for `rumors` gossip sessions: spans per
session and per directed stream, one structured event per observed
wire item.

`rumors` exposes its wire traffic through the bytes-level
observation hook in `rumors::observe`. This crate is that hook's
bridge into the `tracing` ecosystem: attach a `TracingObserver`
to a peer, install whatever `tracing` subscriber your application
already uses, and every session the peer enters becomes a span tree
with each protocol message a structured event inside it. Because
the wire is CBOR end to end, the adapter needs no knowledge of the
protocol's message vocabulary: it unfolds each item generically and
names only the registered rumors atom tags (`rumors::tags`) —
deep inspection of application payloads comes free, since they are
the application's own CBOR.

## Quickstart

```rust
use std::sync::Arc;

use rumors::Peer;
use rumors_tracing::TracingObserver;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), rumors::Error> {
// Install your subscriber first (tracing_subscriber::fmt(),
// a test collector, anything): the adapter emits, it never
// installs.
let alice = Peer::<String>::seed()
.observe(Arc::new(TracingObserver::new()))
.into_rumors();
alice.send("the meeting is at noon".to_string());

let (mut near, mut far) = rumors::link::memory();
let (served, joined) = tokio::join!(alice.gossip(&mut far), async {
Peer::<String>::bootstrap().join(&mut near).await
});
served?;
joined?.expect("alice is established, not herself bootstrapping");
Ok(())
}
```

## What the adapter emits

Everything is emitted under the target `rumors`, so one directive
(`rumors=debug`) scopes it in any filter. Field values that name
protocol vocabulary (`SessionKind`,
`Role`, …) are recorded in their debug form.

- **One `session` span per observed session** (level `INFO`):
fields `kind` (`Gossip`, `Bootstrap`, `Retire`), `protocol`, and
`ordinal` — the adapter's own count of the sessions it has
observed, so concurrent sessions stay distinguishable. (The hook
deliberately carries no session number; numbering is the
observer's concern, and this adapter counts internally.)
- **One `role elected` event** (level `INFO`, inside the session
span) when the session's role election is decided, with the
elected `role`. Sessions whose greetings carry equal versions
hold no election and emit no such event.
- **One `stream` span per directed stream** (level `DEBUG`, child
of the session span): fields `kind` (`control` or `data`) and
`direction`, plus `speaker` and `index` for data streams.
- **One `message` event per wire item** (level `DEBUG`, inside its
stream span): fields `ordinal` (see below), `len` (the item's
exact wire size in bytes), and `item` — the item rendered in RFC
8949 diagnostic-notation style, structure unfolded, embedded-CBOR
tags (24, 63) shown as `<<…>>`, rumors atom tags named
(`version(h'…')`), byte strings as hex. The rendering is bounded:
long byte strings, deep nesting, and long renderings all elide
with explicit marks, so events stay cheap under megabyte supply
runs.

## Ordering across streams

A session's streams pump concurrently and the hook deliberately
imposes no cross-stream ordering, so subscriber-side timestamps
interleave events only as precisely as your subscriber's clock.
The `ordinal` field is the sharper tool: the adapter stamps every
`message` event from one session-scoped atomic counter, so sorting
a session's events by `ordinal` reconstructs the observed
interleaving exactly — the consumer-side pattern the hook's
documentation recommends, built in.

## Cost and back-pressure

Handlers run synchronously inside the session's stream tasks (see
the hook's back-pressure contract in `rumors::observe`): the
adapter therefore does bounded work per item — one parse plus one
capped rendering — and only when the `message` event is enabled by
your subscriber; a disabled target costs the enabled check plus one
relaxed atomic increment (the ordinal must advance even unobserved).
A subscriber that blocks inside `event` stalls the emitting stream,
exactly as any slow `StreamObserver` would: keep slow sinks
behind a channel.

## When not to use it

- **To watch the replica's *contents*** — what messages exist,
in what order — use `rumors`' own pull-based observers
(`rumors::Rumors::unordered_messages` and friends); the wire
view is the wrong altitude for application state.
- **To capture sessions for byte-exact replay or assertion**,
implement `rumors::observe::Observer` directly and keep the
bytes: this adapter renders for human eyes and elides by design.

<!-- cargo-rdme end -->
Loading