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:
@@ -216,7 +216,10 @@ It prints and reports client diagnostics snapshots with relay reachability,
|
|||||||
LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP,
|
LAN-gateway presence, route-pinning, QUIC datagram budget, TAP status/IP,
|
||||||
broadcast frame flow, frame/datagram counters, and drops. The periodic
|
broadcast frame flow, frame/datagram counters, and drops. The periodic
|
||||||
diagnostics refresh the TAP unicast IP so DHCP results that arrive after
|
diagnostics refresh the TAP unicast IP so DHCP results that arrive after
|
||||||
bridging starts become visible in later status lines. Relay lifecycle events
|
bridging starts become visible in later status lines. Each snapshot also emits
|
||||||
are logged as they arrive, including gateway joins and peer leaves. The client
|
short user-facing lines such as relay/gateway connection status, DHCP address
|
||||||
remembers peer identities from join and catch-up events so later leave logs can
|
presence, and broadcast-flow confirmation when those signals are observed.
|
||||||
identify a disconnected LAN gateway or client MAC when that peer was known.
|
Relay lifecycle events are logged as they arrive, including gateway joins and
|
||||||
|
peer leaves. The client remembers peer identities from join and catch-up events
|
||||||
|
so later leave logs can identify a disconnected LAN gateway or client MAC when
|
||||||
|
that peer was known.
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ use lanparty_client_route::{
|
|||||||
use lanparty_client_tap::TapAdapter;
|
use lanparty_client_tap::TapAdapter;
|
||||||
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
|
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
|
||||||
use lanparty_net::RelayEndpoint;
|
use lanparty_net::RelayEndpoint;
|
||||||
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
|
use lanparty_obs::{
|
||||||
|
ClientDiagnostics, RelayDiagnostics, TapDiagnostics, UserDiagnostic, UserDiagnosticLevel,
|
||||||
|
};
|
||||||
use lanparty_proto::MacAddr;
|
use lanparty_proto::MacAddr;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -438,6 +440,9 @@ fn tap_diagnostics_with_ip(base: &TapDiagnostics, ip: Option<IpAddr>) -> TapDiag
|
|||||||
|
|
||||||
fn print_client_diagnostics(diagnostics: &ClientDiagnostics) {
|
fn print_client_diagnostics(diagnostics: &ClientDiagnostics) {
|
||||||
println!("{}", format_client_diagnostics(diagnostics));
|
println!("{}", format_client_diagnostics(diagnostics));
|
||||||
|
for diagnostic in diagnostics.user_diagnostics() {
|
||||||
|
println!("{}", format_user_diagnostic(&diagnostic));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn print_and_report_client_diagnostics(
|
async fn print_and_report_client_diagnostics(
|
||||||
@@ -482,6 +487,14 @@ fn optional_label<T: std::fmt::Display>(value: Option<T>) -> String {
|
|||||||
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
|
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String {
|
||||||
|
match diagnostic.level() {
|
||||||
|
UserDiagnosticLevel::Info => diagnostic.message().to_owned(),
|
||||||
|
UserDiagnosticLevel::Warning => format!("Warning: {}", diagnostic.message()),
|
||||||
|
UserDiagnosticLevel::Error => format!("Error: {}", diagnostic.message()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_control_event_log(
|
async fn run_control_event_log(
|
||||||
session: &ClientSession,
|
session: &ClientSession,
|
||||||
relay_status: ClientRelayStatus,
|
relay_status: ClientRelayStatus,
|
||||||
@@ -887,6 +900,31 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_user_diagnostic_levels() {
|
||||||
|
assert_eq!(
|
||||||
|
format_user_diagnostic(&UserDiagnostic::new(
|
||||||
|
UserDiagnosticLevel::Info,
|
||||||
|
"Connected to relay"
|
||||||
|
)),
|
||||||
|
"Connected to relay"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_user_diagnostic(&UserDiagnostic::new(
|
||||||
|
UserDiagnosticLevel::Warning,
|
||||||
|
"Waiting for LAN gateway"
|
||||||
|
)),
|
||||||
|
"Warning: Waiting for LAN gateway"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_user_diagnostic(&UserDiagnostic::new(
|
||||||
|
UserDiagnosticLevel::Error,
|
||||||
|
"Relay not reachable"
|
||||||
|
)),
|
||||||
|
"Error: Relay not reachable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refreshes_tap_diagnostics_ip_without_losing_static_fields() {
|
fn refreshes_tap_diagnostics_ip_without_losing_static_fields() {
|
||||||
let base = TapDiagnostics::new(
|
let base = TapDiagnostics::new(
|
||||||
|
|||||||
@@ -367,6 +367,44 @@ impl ClientDiagnostics {
|
|||||||
pub const fn stats(&self) -> &TunnelStats {
|
pub const fn stats(&self) -> &TunnelStats {
|
||||||
&self.stats
|
&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)]
|
#[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_tx(), 0);
|
||||||
assert_eq!(stats.broadcast_frames_rx(), 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