feat(client): print user-facing diagnostics
The PLAN calls out client diagnostics that a user can read directly, not only machine-shaped counters. The Windows client already built ClientDiagnostics snapshots, but it printed a dense status line and left the UserDiagnostic model unused. Derive user-facing diagnostics from ClientDiagnostics in lanparty-obs so a future GUI and the current CLI can share the same status vocabulary. The messages report the states the runtime actually observes: relay reachability, LAN gateway presence, TAP IP presence, and observed broadcast traffic. TAP IPs are only described as DHCP when they are non-link-local IPv4 addresses, because link-local IPv4 and IPv6 addresses do not prove DHCP success. The client now prints those user-facing lines after the existing detailed counter line. Gateway latency is intentionally not reported here; the current protocol does not measure gateway RTT. Test Plan: - cargo fmt --check - cargo test -p lanparty-obs -p lanparty-client-win - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check - git diff --cached --check Refs: PLAN.md Logging / diagnostics
This commit is contained in:
@@ -367,6 +367,44 @@ impl ClientDiagnostics {
|
||||
pub const fn stats(&self) -> &TunnelStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_diagnostics(&self) -> Vec<UserDiagnostic> {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
diagnostics.push(if self.relay.reachable() {
|
||||
UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to relay")
|
||||
} else {
|
||||
UserDiagnostic::new(UserDiagnosticLevel::Error, "Relay not reachable")
|
||||
});
|
||||
|
||||
diagnostics.push(if self.relay.gateway_connected() {
|
||||
UserDiagnostic::new(UserDiagnosticLevel::Info, "Connected to LAN gateway")
|
||||
} else {
|
||||
UserDiagnostic::new(UserDiagnosticLevel::Warning, "Waiting for LAN gateway")
|
||||
});
|
||||
|
||||
if let Some(message) = tap_ip_user_message(self.tap.ip()) {
|
||||
diagnostics.push(UserDiagnostic::new(UserDiagnosticLevel::Info, message));
|
||||
}
|
||||
|
||||
if self.stats.broadcast_frames_tx() > 0 || self.stats.broadcast_frames_rx() > 0 {
|
||||
diagnostics.push(UserDiagnostic::new(
|
||||
UserDiagnosticLevel::Info,
|
||||
"Broadcast traffic flowing",
|
||||
));
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
fn tap_ip_user_message(ip: Option<IpAddr>) -> Option<String> {
|
||||
match ip {
|
||||
Some(IpAddr::V4(ip)) if !ip.is_link_local() => Some(format!("DHCP received: {ip}")),
|
||||
Some(ip) => Some(format!("TAP IP detected: {ip}")),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||
@@ -505,4 +543,74 @@ mod tests {
|
||||
assert_eq!(stats.broadcast_frames_tx(), 0);
|
||||
assert_eq!(stats.broadcast_frames_rx(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_user_diagnostics_from_client_snapshot() {
|
||||
let diagnostics = ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(true, true, true),
|
||||
QuicDiagnostics::new(true, Some(1400)),
|
||||
TapDiagnostics::new(
|
||||
true,
|
||||
Some(MacAddr::new([0x02, 1, 2, 3, 4, 5])),
|
||||
Some(1200),
|
||||
Some("10.73.42.51".parse().unwrap()),
|
||||
),
|
||||
TunnelStats::new(1, 2, 3, 4, 5, 6).with_broadcast_frames(7, 8),
|
||||
);
|
||||
|
||||
let user_diagnostics = diagnostics.user_diagnostics();
|
||||
let messages: Vec<_> = user_diagnostics
|
||||
.iter()
|
||||
.map(UserDiagnostic::message)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
messages,
|
||||
[
|
||||
"Connected to relay",
|
||||
"Connected to LAN gateway",
|
||||
"DHCP received: 10.73.42.51",
|
||||
"Broadcast traffic flowing",
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
user_diagnostics
|
||||
.iter()
|
||||
.all(|diagnostic| diagnostic.level() == UserDiagnosticLevel::Info)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_user_diagnostic_warnings_for_missing_connections() {
|
||||
let diagnostics = ClientDiagnostics::new(
|
||||
RelayDiagnostics::new(false, false, false),
|
||||
QuicDiagnostics::new(false, None),
|
||||
TapDiagnostics::new(false, None, None, None),
|
||||
TunnelStats::default(),
|
||||
);
|
||||
|
||||
let user_diagnostics = diagnostics.user_diagnostics();
|
||||
|
||||
assert_eq!(
|
||||
user_diagnostics
|
||||
.iter()
|
||||
.map(UserDiagnostic::message)
|
||||
.collect::<Vec<_>>(),
|
||||
["Relay not reachable", "Waiting for LAN gateway"]
|
||||
);
|
||||
assert_eq!(user_diagnostics[0].level(), UserDiagnosticLevel::Error);
|
||||
assert_eq!(user_diagnostics[1].level(), UserDiagnosticLevel::Warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avoids_calling_link_local_tap_ip_dhcp() {
|
||||
assert_eq!(
|
||||
tap_ip_user_message(Some("169.254.10.20".parse().unwrap())),
|
||||
Some("TAP IP detected: 169.254.10.20".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
tap_ip_user_message(Some("fe80::1".parse().unwrap())),
|
||||
Some("TAP IP detected: fe80::1".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user