Addressed every rustc warning surfaced by \`cargo check --workspace
--release --lib --bins\` on opus-DRED-v2. Split across three
categories:
## Real bugs surfaced by the audit (fix, don't silence)
- **crates/wzp-relay/src/federation.rs** — the per-peer RTT monitor
task computed \`rtt_ms\` every 5 s and threw it on the floor. The
\`wzp_federation_peer_rtt_ms\` gauge has been registered in
metrics.rs the whole time but was never receiving samples, leaving
the Grafana panel blank. Wired it up: the task now calls
\`fm_rtt.metrics.federation_peer_rtt_ms.with_label_values(&[&label_rtt]).set(rtt_ms)\`
on every sample. Fixes three warnings (\`rtt_ms\`, \`fm_rtt\`,
\`label_rtt\` were all captured for this task and all dead).
## Dead code removal
- **crates/wzp-relay/src/federation.rs** — removed \`local_delivery_seq:
AtomicU16\` field and its initializer. It was described in comments
as "per-room seq counter for federation media delivered to local
clients" but was declared, initialized to 0, and never read or
written anywhere else. Genuine half-wired feature; deletable with
zero behavior change.
- **crates/wzp-relay/src/room.rs** — removed \`let recv_start =
Instant::now()\` at the top of a recv loop that was never read.
Separate variable \`last_recv_instant\` already measures the actual
gap that's used for the \`max_recv_gap_ms\` stat.
- **crates/wzp-client/src/cli.rs** — removed \`let my_fp = fp.clone()\`
from the signal loop setup. Cloned but never used in any match arm.
## Stub-intent warnings (underscore + explanatory comment)
- **crates/wzp-relay/src/handshake.rs** — \`choose_profile\` hardcodes
\`QualityProfile::GOOD\` and ignores its \`supported\` parameter.
Comment already documented "Cap at GOOD (24k) for now — studio
tiers not yet tested for federation reliability". Renamed to
\`_supported\`, expanded the comment to explicitly note the future
plan (pick highest supported ≤ relay ceiling).
- **crates/wzp-relay/src/federation.rs** — \`forward_to_peers\` takes
\`room_name: &str\` but only uses \`room_hash\`. The caller
(handle_datagram) passes the name for caller-site symmetry with
other helpers; kept the param shape and underscored the binding
with a comment noting it's reserved for future per-name logging.
## Cosmetic fixes
- **crates/wzp-relay/src/event_log.rs** — dropped \`use std::sync::Arc\`
(unused).
- **crates/wzp-relay/src/signal_hub.rs** — trimmed \`use tracing::{info,
warn}\` to \`use tracing::info\`. Also removed unnecessary \`mut\` on
\`hub\` binding in the \`register_unregister\` test.
- **crates/wzp-relay/src/room.rs** — trimmed \`use tracing::{debug,
error, info, trace, warn}\` to \`{error, info, warn}\`. Also removed
unnecessary \`mut\` on \`mgr\` binding in the \`room_join_leave\` test.
- **crates/wzp-relay/src/main.rs** — removed unnecessary \`mut\` on the
\`config\` destructured binding from \`parse_args()\`; and dropped
\`ref caller_alias\` from the \`DirectCallOffer\` match pattern since
the relay just forwards the full \`msg\` (caller_alias is preserved
end-to-end, we don't need to read it on the relay).
- **crates/wzp-crypto/tests/featherchat_compat.rs** — dropped
\`CallSignalType\` from a \`use wzp_client::featherchat::{...}\`
(unused in the test body). Note: this test file has pre-existing
compile errors from SignalMessage schema drift unrelated to this
sweep; that's tracked separately.
## Crate-level annotation
- **crates/wzp-android/src/lib.rs** — added
\`#![allow(dead_code, unused_imports, unused_variables, unused_mut)]\`
with a doc block explaining the crate is dead code since the Tauri
mobile rewrite. The legacy Kotlin+JNI Android app that consumed
this crate was replaced by desktop/src-tauri (live Android recv
path) + crates/wzp-native (Oboe bridge). Rather than piecemeal
cleanup of a crate that shouldn't be maintained, the whole-crate
allow keeps CI clean until someone removes the crate entirely. Kills
all 6 wzp-android warnings (4 unused imports/vars, 1 unused \`mut\`
on a JNI env param, 1 dead \`command_rx\` field) in one line.
## Not touched
- **deps/featherchat/warzone/crates/warzone-protocol/src/x3dh.rs** —
3 unused-variable warnings in \`alice_spk_secret\`, \`alice_bundle\`,
\`bob_bundle_bytes\`. This is a vendored third-party submodule;
upstream's problem, not ours. Would need to be reported to
featherchat upstream if we care.
## Verification
- \`cargo check --workspace --release --lib --bins\` → 0 warnings, 0 errors
- \`cargo check --workspace --release --all-targets\` → only the 3
featherchat submodule warnings remain, plus the pre-existing 3
broken integration tests (SignalMessage schema drift from Phase 2,
tracked separately and explicitly out of scope).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
6.4 KiB
Rust
201 lines
6.4 KiB
Rust
//! JSONL event log for protocol analysis.
|
|
//!
|
|
//! When `--event-log <path>` is set, every media packet emits a structured
|
|
//! event at each decision point (recv, forward, drop, deliver).
|
|
//! Use `wzp-analyzer` to correlate events across multiple relays.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use serde::Serialize;
|
|
use tokio::sync::mpsc;
|
|
use tracing::{error, info};
|
|
|
|
/// A single protocol event for JSONL output.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct Event {
|
|
/// ISO 8601 timestamp with microseconds.
|
|
pub ts: String,
|
|
/// Event type.
|
|
pub event: &'static str,
|
|
/// Room name.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub room: Option<String>,
|
|
/// Source address or peer label.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub src: Option<String>,
|
|
/// Packet sequence number.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub seq: Option<u16>,
|
|
/// Codec identifier.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub codec: Option<String>,
|
|
/// FEC block ID.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub fec_block: Option<u8>,
|
|
/// FEC symbol index.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub fec_sym: Option<u8>,
|
|
/// Is FEC repair packet.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub repair: Option<bool>,
|
|
/// Payload length in bytes.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub len: Option<usize>,
|
|
/// Number of recipients.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub to_count: Option<usize>,
|
|
/// Peer label (for federation events).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub peer: Option<String>,
|
|
/// Drop/error reason.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub reason: Option<String>,
|
|
/// Presence action (active/inactive).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub action: Option<String>,
|
|
/// Participant count (presence events).
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub participants: Option<usize>,
|
|
}
|
|
|
|
impl Event {
|
|
fn now() -> String {
|
|
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string()
|
|
}
|
|
|
|
/// Create a minimal event with just type and timestamp.
|
|
pub fn new(event: &'static str) -> Self {
|
|
Self {
|
|
ts: Self::now(),
|
|
event,
|
|
room: None,
|
|
src: None,
|
|
seq: None,
|
|
codec: None,
|
|
fec_block: None,
|
|
fec_sym: None,
|
|
repair: None,
|
|
len: None,
|
|
to_count: None,
|
|
peer: None,
|
|
reason: None,
|
|
action: None,
|
|
participants: None,
|
|
}
|
|
}
|
|
|
|
/// Set room.
|
|
pub fn room(mut self, room: &str) -> Self { self.room = Some(room.to_string()); self }
|
|
/// Set source.
|
|
pub fn src(mut self, src: &str) -> Self { self.src = Some(src.to_string()); self }
|
|
/// Set packet header fields from a MediaPacket.
|
|
pub fn packet(mut self, pkt: &wzp_proto::MediaPacket) -> Self {
|
|
self.seq = Some(pkt.header.seq);
|
|
self.codec = Some(format!("{:?}", pkt.header.codec_id));
|
|
self.fec_block = Some(pkt.header.fec_block);
|
|
self.fec_sym = Some(pkt.header.fec_symbol);
|
|
self.repair = Some(pkt.header.is_repair);
|
|
self.len = Some(pkt.payload.len());
|
|
self
|
|
}
|
|
/// Set seq only (when full packet not available).
|
|
pub fn seq(mut self, seq: u16) -> Self { self.seq = Some(seq); self }
|
|
/// Set payload length.
|
|
pub fn len(mut self, len: usize) -> Self { self.len = Some(len); self }
|
|
/// Set recipient count.
|
|
pub fn to_count(mut self, n: usize) -> Self { self.to_count = Some(n); self }
|
|
/// Set peer label.
|
|
pub fn peer(mut self, peer: &str) -> Self { self.peer = Some(peer.to_string()); self }
|
|
/// Set drop reason.
|
|
pub fn reason(mut self, reason: &str) -> Self { self.reason = Some(reason.to_string()); self }
|
|
/// Set presence action.
|
|
pub fn action(mut self, action: &str) -> Self { self.action = Some(action.to_string()); self }
|
|
/// Set participant count.
|
|
pub fn participants(mut self, n: usize) -> Self { self.participants = Some(n); self }
|
|
}
|
|
|
|
/// Handle for emitting events. Cheap to clone.
|
|
#[derive(Clone)]
|
|
pub struct EventLog {
|
|
tx: mpsc::UnboundedSender<Event>,
|
|
}
|
|
|
|
impl EventLog {
|
|
/// Emit an event (non-blocking, drops if channel is full).
|
|
pub fn emit(&self, event: Event) {
|
|
let _ = self.tx.send(event);
|
|
}
|
|
}
|
|
|
|
/// No-op event log for when `--event-log` is not set.
|
|
/// All methods are no-ops that compile to nothing.
|
|
#[derive(Clone)]
|
|
pub struct NoopEventLog;
|
|
|
|
/// Unified event log handle — either real or no-op.
|
|
#[derive(Clone)]
|
|
pub enum EventLogger {
|
|
Active(EventLog),
|
|
Noop,
|
|
}
|
|
|
|
impl EventLogger {
|
|
pub fn emit(&self, event: Event) {
|
|
if let EventLogger::Active(log) = self {
|
|
log.emit(event);
|
|
}
|
|
}
|
|
|
|
pub fn is_active(&self) -> bool {
|
|
matches!(self, EventLogger::Active(_))
|
|
}
|
|
}
|
|
|
|
/// Start the event log writer. Returns an `EventLogger` handle.
|
|
pub fn start_event_log(path: Option<PathBuf>) -> EventLogger {
|
|
match path {
|
|
Some(path) => {
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
tokio::spawn(writer_task(path, rx));
|
|
info!("event log enabled");
|
|
EventLogger::Active(EventLog { tx })
|
|
}
|
|
None => EventLogger::Noop,
|
|
}
|
|
}
|
|
|
|
/// Background task that writes events to a JSONL file.
|
|
async fn writer_task(path: PathBuf, mut rx: mpsc::UnboundedReceiver<Event>) {
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
let file = match tokio::fs::File::create(&path).await {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
error!("failed to create event log {}: {e}", path.display());
|
|
return;
|
|
}
|
|
};
|
|
let mut writer = tokio::io::BufWriter::new(file);
|
|
let mut count: u64 = 0;
|
|
|
|
while let Some(event) = rx.recv().await {
|
|
match serde_json::to_string(&event) {
|
|
Ok(json) => {
|
|
if writer.write_all(json.as_bytes()).await.is_err() { break; }
|
|
if writer.write_all(b"\n").await.is_err() { break; }
|
|
count += 1;
|
|
// Flush every 100 events
|
|
if count % 100 == 0 {
|
|
let _ = writer.flush().await;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("event log serialize error: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = writer.flush().await;
|
|
info!(events = count, "event log closed");
|
|
}
|