Trace terminal recovery and isolate neighbor resolver latency
This commit is contained in:
7 files changed
+84
-3
No files matched your search
Generated
+1
@@ -3692,6 +3692,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
"wakey-core",
|
"wakey-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,23 @@ sum by (job, instance) (increase(wakey_terminal_backpressure_total[15m]))
|
|||||||
This counts encounters, not their duration and not dropped chunks. A burst of
|
This counts encounters, not their duration and not dropped chunks. A burst of
|
||||||
image output can raise it while output still arrives intact through backpressure.
|
image output can raise it while output still arrives intact through backpressure.
|
||||||
|
|
||||||
|
Average duration of completed backpressure episodes, in seconds:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance, outcome) (
|
||||||
|
rate(wakey_operation_duration_seconds_sum{operation="terminal_backpressure_wait"}[5m])
|
||||||
|
)
|
||||||
|
/
|
||||||
|
sum by (job, instance, outcome) (
|
||||||
|
rate(wakey_operation_duration_seconds_count{operation="terminal_backpressure_wait"}[5m])
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
An episode starts when the output channel is full and ends when the pending
|
||||||
|
backlog drains or the relay disconnects. It is not per-frame network latency;
|
||||||
|
unfinished episodes do not appear until they end. Keep the outcome grouping so
|
||||||
|
disconnected episodes are not confused with successfully drained ones.
|
||||||
|
|
||||||
## Snapshots
|
## Snapshots
|
||||||
|
|
||||||
Device entries in successfully sent snapshots over the last hour:
|
Device entries in successfully sent snapshots over the last hour:
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ existing debug events too; inspect them before enabling it broadly.
|
|||||||
with backoff and CC disconnect duration. Sending credentials is not an auth ACK;
|
with backoff and CC disconnect duration. Sending credentials is not an auth ACK;
|
||||||
CC's authentication event is the authoritative result. DNS diagnostics are a
|
CC's authentication event is the authoritative result. DNS diagnostics are a
|
||||||
separate lookup, not a measurement of the resolver inside WebSocket connect.
|
separate lookup, not a measurement of the resolver inside WebSocket connect.
|
||||||
|
- Terminal recovery uses `terminal_snapshot_generate` (encoded byte count),
|
||||||
|
transport-attachment spans, and `terminal_snapshot_request`. These measure
|
||||||
|
local work, not browser restoration or end-to-end image replay. Resume operation
|
||||||
|
success means credentials were queued for the worker. `terminal_backpressure_wait`
|
||||||
|
operation durations cover one full-queue backlog episode until it drains (`ok`)
|
||||||
|
or disconnects (`disconnected`); cancellation can retain the default `error` outcome.
|
||||||
|
|
||||||
Instrument operation boundaries rather than individual rows or terminal chunks.
|
Instrument operation boundaries rather than individual rows or terminal chunks.
|
||||||
Use `skip_all`, explicitly allow count fields, and instrument async futures rather
|
Use `skip_all`, explicitly allow count fields, and instrument async futures rather
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct RelayOutputBuffer {
|
|||||||
sender: Option<mpsc::Sender<Message>>,
|
sender: Option<mpsc::Sender<Message>>,
|
||||||
pending: VecDeque<Message>,
|
pending: VecDeque<Message>,
|
||||||
snapshot_pending: bool,
|
snapshot_pending: bool,
|
||||||
|
backpressure_wait: Option<wakey_core::telemetry::Operation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayOutputBuffer {
|
impl RelayOutputBuffer {
|
||||||
@@ -44,6 +45,9 @@ impl RelayOutputBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn disconnect(&mut self) {
|
fn disconnect(&mut self) {
|
||||||
|
if let Some(mut wait) = self.backpressure_wait.take() {
|
||||||
|
wait.outcome("disconnected");
|
||||||
|
}
|
||||||
self.sender = None;
|
self.sender = None;
|
||||||
self.pending.clear();
|
self.pending.clear();
|
||||||
self.snapshot_pending = false;
|
self.snapshot_pending = false;
|
||||||
@@ -80,6 +84,10 @@ impl RelayOutputBuffer {
|
|||||||
match sender.try_send(frame) {
|
match sender.try_send(frame) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(mpsc::error::TrySendError::Full(frame)) => {
|
Err(mpsc::error::TrySendError::Full(frame)) => {
|
||||||
|
// One measurement per backlog episode, not per select poll.
|
||||||
|
self.backpressure_wait = Some(wakey_core::telemetry::Operation::new(
|
||||||
|
"terminal_backpressure_wait",
|
||||||
|
));
|
||||||
wakey_core::telemetry::count(
|
wakey_core::telemetry::count(
|
||||||
wakey_core::telemetry::Counter::TerminalBackpressure,
|
wakey_core::telemetry::Counter::TerminalBackpressure,
|
||||||
1,
|
1,
|
||||||
@@ -111,6 +119,9 @@ impl RelayOutputBuffer {
|
|||||||
permit.send(frame);
|
permit.send(frame);
|
||||||
if self.pending.is_empty() {
|
if self.pending.is_empty() {
|
||||||
self.snapshot_pending = false;
|
self.snapshot_pending = false;
|
||||||
|
if let Some(mut wait) = self.backpressure_wait.take() {
|
||||||
|
wait.outcome("ok");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,10 +145,14 @@ impl TerminalState {
|
|||||||
self.parser.screen_mut().set_size(rows, cols);
|
self.parser.screen_mut().set_size(rows, cols);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "terminal_snapshot_generate", skip_all, fields(bytes = tracing::field::Empty))]
|
||||||
fn snapshot(&self) -> Vec<u8> {
|
fn snapshot(&self) -> Vec<u8> {
|
||||||
self.parser
|
let snapshot = self
|
||||||
|
.parser
|
||||||
.screen()
|
.screen()
|
||||||
.snapshot_formatted(TERMINAL_SCROLLBACK_ROWS)
|
.snapshot_formatted(TERMINAL_SCROLLBACK_ROWS);
|
||||||
|
tracing::Span::current().record("bytes", snapshot.len());
|
||||||
|
snapshot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +277,7 @@ impl TerminalManager {
|
|||||||
|
|
||||||
#[tracing::instrument(name = "terminal_resume", skip_all, fields(terminal_id = %terminal_id))]
|
#[tracing::instrument(name = "terminal_resume", skip_all, fields(terminal_id = %terminal_id))]
|
||||||
pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> {
|
pub fn resume(&self, terminal_id: &TerminalId, relay_token: String) -> Result<()> {
|
||||||
|
let mut operation = wakey_core::telemetry::Operation::new("terminal_resume");
|
||||||
let active = self.active.lock().expect("terminal manager poisoned");
|
let active = self.active.lock().expect("terminal manager poisoned");
|
||||||
let session = active
|
let session = active
|
||||||
.get(terminal_id.as_str())
|
.get(terminal_id.as_str())
|
||||||
@@ -269,6 +285,8 @@ impl TerminalManager {
|
|||||||
if session.relay_credentials.send(relay_token).is_err() {
|
if session.relay_credentials.send(relay_token).is_err() {
|
||||||
anyhow::bail!("terminal session {terminal_id} has stopped");
|
anyhow::bail!("terminal session {terminal_id} has stopped");
|
||||||
}
|
}
|
||||||
|
operation.outcome("ok");
|
||||||
|
info!("terminal resume credentials queued for worker");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,6 +1208,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(relay.is_connected());
|
assert!(relay.is_connected());
|
||||||
assert!(relay.has_pending());
|
assert!(relay.has_pending());
|
||||||
|
assert!(relay.backpressure_wait.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rx.recv().await.expect("original queued frame").into_data(),
|
rx.recv().await.expect("original queued frame").into_data(),
|
||||||
vec![1]
|
vec![1]
|
||||||
@@ -1203,6 +1222,20 @@ mod tests {
|
|||||||
vec![2]
|
vec![2]
|
||||||
);
|
);
|
||||||
assert!(!relay.has_pending());
|
assert!(!relay.has_pending());
|
||||||
|
assert!(relay.backpressure_wait.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disconnect_finishes_backpressure_episode() {
|
||||||
|
let (tx, _rx) = mpsc::channel(1);
|
||||||
|
let mut relay = RelayOutputBuffer::default();
|
||||||
|
relay.connect(tx);
|
||||||
|
relay.queue_live(Message::Binary(vec![1].into()));
|
||||||
|
relay.queue_live(Message::Binary(vec![2].into()));
|
||||||
|
assert!(relay.backpressure_wait.is_some());
|
||||||
|
relay.disconnect();
|
||||||
|
assert!(relay.backpressure_wait.is_none());
|
||||||
|
assert!(!relay.has_pending());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use futures_util::{SinkExt, StreamExt};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tracing::{info, warn};
|
use tracing::{Instrument, info, warn};
|
||||||
use wakey_agent::protocol::{
|
use wakey_agent::protocol::{
|
||||||
AgentCapability, DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS,
|
AgentCapability, DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS,
|
||||||
ServerMessage, TerminalAgentHandshake, TerminalControl, TerminalId, TerminalOperatorHandshake,
|
ServerMessage, TerminalAgentHandshake, TerminalControl, TerminalId, TerminalOperatorHandshake,
|
||||||
@@ -307,10 +307,17 @@ async fn handle_agent_terminal_socket(state: AppState, terminal_id: String, mut
|
|||||||
let (mut outbound, pending) = match state
|
let (mut outbound, pending) = match state
|
||||||
.terminals
|
.terminals
|
||||||
.attach_agent(&terminal_id, &auth.0, &auth.1)
|
.attach_agent(&terminal_id, &auth.0, &auth.1)
|
||||||
|
.instrument(
|
||||||
|
tracing::info_span!("terminal_agent_transport_attach", terminal_id = %terminal_id),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(outbound) => outbound,
|
Ok(outbound) => outbound,
|
||||||
Err(code) => {
|
Err(code) => {
|
||||||
|
warn!(
|
||||||
|
terminal_id,
|
||||||
|
code, "agent terminal transport attachment rejected"
|
||||||
|
);
|
||||||
close_socket(&mut socket, code).await;
|
close_socket(&mut socket, code).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -393,10 +400,17 @@ async fn handle_operator_terminal_socket(
|
|||||||
let mut outbound = match state
|
let mut outbound = match state
|
||||||
.terminals
|
.terminals
|
||||||
.attach_operator(&terminal_id, &auth.0, &auth.1)
|
.attach_operator(&terminal_id, &auth.0, &auth.1)
|
||||||
|
.instrument(
|
||||||
|
tracing::info_span!("terminal_operator_transport_attach", terminal_id = %terminal_id),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(attached) => attached,
|
Ok(attached) => attached,
|
||||||
Err(code) => {
|
Err(code) => {
|
||||||
|
warn!(
|
||||||
|
terminal_id,
|
||||||
|
code, "operator terminal transport attachment rejected"
|
||||||
|
);
|
||||||
close_socket(&mut socket, code).await;
|
close_socket(&mut socket, code).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -407,6 +421,7 @@ async fn handle_operator_terminal_socket(
|
|||||||
if let Err(code) = state
|
if let Err(code) = state
|
||||||
.terminals
|
.terminals
|
||||||
.relay_to_agent(&terminal_id, TerminalRelayFrame::Text(snapshot))
|
.relay_to_agent(&terminal_id, TerminalRelayFrame::Text(snapshot))
|
||||||
|
.instrument(tracing::info_span!("terminal_snapshot_request", terminal_id = %terminal_id))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
warn!(terminal_id, code, "failed to request terminal snapshot");
|
warn!(terminal_id, code, "failed to request terminal snapshot");
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ macaddr = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["net", "sync"] }
|
tokio = { workspace = true, features = ["net", "sync"] }
|
||||||
|
tracing = "0.1"
|
||||||
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"}
|
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ use futures::future::try_join_all;
|
|||||||
use lda_ipjs::subcommands::neighbor;
|
use lda_ipjs::subcommands::neighbor;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
use tracing::Instrument;
|
||||||
use wakey_core::{InventoryQuery, NeighborEntry, NeighborState, Query};
|
use wakey_core::{InventoryQuery, NeighborEntry, NeighborState, Query};
|
||||||
|
|
||||||
/// Resolve a hostname through the local resolver and return all reported IPs.
|
/// Resolve a hostname through the local resolver and return all reported IPs.
|
||||||
|
#[tracing::instrument(name = "neighbor_hostname_resolve", skip_all)]
|
||||||
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||||
Ok(tokio::net::lookup_host((machine_name, 0))
|
Ok(tokio::net::lookup_host((machine_name, 0))
|
||||||
.await
|
.await
|
||||||
@@ -54,6 +56,10 @@ pub async fn get_neighbors(
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
|
let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
|
||||||
|
.instrument(tracing::info_span!(
|
||||||
|
"neighbor_table_read",
|
||||||
|
backend = "netlink"
|
||||||
|
))
|
||||||
.await
|
.await
|
||||||
.context("rtnetlink failed")?
|
.context("rtnetlink failed")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -68,6 +74,7 @@ pub async fn get_neighbors(
|
|||||||
if ip_filter.is_empty() {
|
if ip_filter.is_empty() {
|
||||||
let dev = dev_strs.first().copied();
|
let dev = dev_strs.first().copied();
|
||||||
results = neighbor::get_with_backend(neighbor::Backend::Json, None, dev, &nud_filter)
|
results = neighbor::get_with_backend(neighbor::Backend::Json, None, dev, &nud_filter)
|
||||||
|
.instrument(tracing::info_span!("neighbor_table_read", backend = "json"))
|
||||||
.await
|
.await
|
||||||
.context("ip -j neigh failed")?
|
.context("ip -j neigh failed")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -82,6 +89,7 @@ pub async fn get_neighbors(
|
|||||||
dev,
|
dev,
|
||||||
&nud_filter,
|
&nud_filter,
|
||||||
)
|
)
|
||||||
|
.instrument(tracing::info_span!("neighbor_table_read", backend = "json"))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("ip -j neigh failed for {ip}"))?
|
.with_context(|| format!("ip -j neigh failed for {ip}"))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Reference in new issue
Block a user