Trace fleet refresh, grouping, and wake routing

This commit is contained in:
lda committed 2026-10-10 01:48:26 +07:00 Verified
1 parent 2ecf283d91
commit 621cb34a3f
3 files changed
+43 -2

No files matched your search

+4
View File
@@ -90,6 +90,10 @@ existing debug events too; inspect them before enabling it broadly.
- Inventory source spans separate neighbor, DHCP lease, and hook-store reads - Inventory source spans separate neighbor, DHCP lease, and hook-store reads
from merging. `db_*` spans cover snapshot replacement, fleet reads, and manual from merging. `db_*` spans cover snapshot replacement, fleet reads, and manual
identity changes, including transaction waits and result assembly where applicable. identity changes, including transaction waits and result assembly where applicable.
- Fleet spans cover list/load/grouping, refresh fan-out with per-agent outcomes,
and wake dispatch. Counts distinguish grouped, filtered, and returned rows;
wake events identify the chosen agent and whether the route was explicit,
without recording device identifiers or addresses.
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
@@ -61,6 +61,7 @@ impl Default for FleetAccumulator {
} }
} }
#[tracing::instrument(name = "fleet_group_devices", skip_all, fields(known = known_devices.len(), observed = agent_devices.len()))]
pub(crate) fn build_fleet_devices( pub(crate) fn build_fleet_devices(
known_devices: Vec<KnownDevice>, known_devices: Vec<KnownDevice>,
agent_devices: Vec<AgentDeviceWithChildren>, agent_devices: Vec<AgentDeviceWithChildren>,
@@ -5,7 +5,7 @@ use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use futures_util::StreamExt; use futures_util::StreamExt;
use tracing::warn; use tracing::{Instrument, info, warn};
use wakey_agent::protocol::{AgentCommand, InventoryRequest, WakeRequest}; use wakey_agent::protocol::{AgentCommand, InventoryRequest, WakeRequest};
use crate::api::ApiError; use crate::api::ApiError;
@@ -29,6 +29,7 @@ use types::{
const REFRESH_CONCURRENCY: usize = 8; const REFRESH_CONCURRENCY: usize = 8;
#[tracing::instrument(name = "fleet_list", skip_all)]
pub async fn list_fleet_devices( pub async fn list_fleet_devices(
State(state): State<AppState>, State(state): State<AppState>,
Query(query): Query<ListFleetDevicesQuery>, Query(query): Query<ListFleetDevicesQuery>,
@@ -46,6 +47,7 @@ pub async fn list_fleet_devices(
} }
} }
#[tracing::instrument(name = "fleet_refresh", skip_all, fields(agents = tracing::field::Empty, succeeded = tracing::field::Empty, failed = tracing::field::Empty, accepted = tracing::field::Empty))]
pub async fn refresh_fleet_devices( pub async fn refresh_fleet_devices(
State(state): State<AppState>, State(state): State<AppState>,
Json(req): Json<RefreshFleetDevicesRequest>, Json(req): Json<RefreshFleetDevicesRequest>,
@@ -57,9 +59,12 @@ pub async fn refresh_fleet_devices(
req.agent_ids.clone() req.agent_ids.clone()
}; };
let agent_ids = normalize_refresh_agent_ids(agent_ids); let agent_ids = normalize_refresh_agent_ids(agent_ids);
tracing::Span::current().record("agents", agent_ids.len());
let results = futures_util::stream::iter(agent_ids.into_iter().map(|agent_id| { let results = futures_util::stream::iter(agent_ids.into_iter().map(|agent_id| {
let state = state.clone(); let state = state.clone();
// Each concurrent future carries its own span through relay and storage.
let span = tracing::info_span!("fleet_refresh_agent", agent_id = %agent_id, outcome = tracing::field::Empty, accepted = tracing::field::Empty);
async move { async move {
let response = relay_agent_command( let response = relay_agent_command(
&state, &state,
@@ -75,8 +80,16 @@ pub async fn refresh_fleet_devices(
req.timeout_ms, req.timeout_ms,
) )
.await; .await;
refresh_fleet_agent_result(&state, &agent_id, response).await let result = refresh_fleet_agent_result(&state, &agent_id, response).await;
tracing::Span::current().record("outcome", result.status.as_str());
tracing::Span::current().record("accepted", result.accepted);
if result.status != "ok" {
// The response retains diagnostic detail; do not export payloads.
warn!("fleet agent refresh failed");
}
result
} }
.instrument(span)
})) }))
.buffer_unordered(REFRESH_CONCURRENCY) .buffer_unordered(REFRESH_CONCURRENCY)
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -85,6 +98,15 @@ pub async fn refresh_fleet_devices(
let mut results = results; let mut results = results;
results.sort_by(|a, b| a.agent_id.cmp(&b.agent_id)); results.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
let total_accepted = results.iter().map(|result| result.accepted).sum::<usize>(); let total_accepted = results.iter().map(|result| result.accepted).sum::<usize>();
let succeeded = results
.iter()
.filter(|result| result.status == "ok")
.count();
let span = tracing::Span::current();
span.record("succeeded", succeeded);
span.record("failed", results.len() - succeeded);
span.record("accepted", total_accepted);
info!("fleet refresh completed");
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
@@ -165,10 +187,13 @@ async fn refresh_fleet_agent_result(
} }
} }
#[tracing::instrument(name = "fleet_wake", skip_all, fields(explicit_route = req.route_id.is_some(), outcome = tracing::field::Empty))]
pub async fn wake_fleet_device( pub async fn wake_fleet_device(
State(state): State<AppState>, State(state): State<AppState>,
Json(req): Json<WakeFleetDeviceRequest>, Json(req): Json<WakeFleetDeviceRequest>,
) -> Result<impl IntoResponse, ApiError> { ) -> Result<impl IntoResponse, ApiError> {
// Early database/relay errors retain an outcome even without a command result.
tracing::Span::current().record("outcome", "error");
let query = ListFleetDevicesQuery { let query = ListFleetDevicesQuery {
query: None, query: None,
presence: None, presence: None,
@@ -190,6 +215,7 @@ pub async fn wake_fleet_device(
.into_iter() .into_iter()
.find(|device| device.device_key == req.device_key) .find(|device| device.device_key == req.device_key)
.ok_or_else(|| { .ok_or_else(|| {
tracing::Span::current().record("outcome", "device_not_found");
ApiError::new( ApiError::new(
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
"fleet_device_not_found", "fleet_device_not_found",
@@ -205,6 +231,7 @@ pub async fn wake_fleet_device(
None => device.recommended_route, None => device.recommended_route,
} }
.ok_or_else(|| { .ok_or_else(|| {
tracing::Span::current().record("outcome", "route_unavailable");
ApiError::new( ApiError::new(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"wake_route_unavailable", "wake_route_unavailable",
@@ -213,6 +240,7 @@ pub async fn wake_fleet_device(
})?; })?;
let Some(mac) = route.mac else { let Some(mac) = route.mac else {
tracing::Span::current().record("outcome", "route_without_mac");
return Err(ApiError::new( return Err(ApiError::new(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"wake_route_unavailable", "wake_route_unavailable",
@@ -220,6 +248,7 @@ pub async fn wake_fleet_device(
)); ));
}; };
if !route.connected { if !route.connected {
tracing::Span::current().record("outcome", "route_disconnected");
return Err(ApiError::new( return Err(ApiError::new(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"wake_route_unavailable", "wake_route_unavailable",
@@ -227,6 +256,7 @@ pub async fn wake_fleet_device(
)); ));
} }
if !route.wakeable { if !route.wakeable {
tracing::Span::current().record("outcome", "route_not_wakeable");
return Err(ApiError::new( return Err(ApiError::new(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"wake_route_unavailable", "wake_route_unavailable",
@@ -234,6 +264,7 @@ pub async fn wake_fleet_device(
)); ));
} }
info!(agent_id = %route.agent_id, has_ip = route.ip.is_some(), explicit_route = req.route_id.is_some(), "fleet wake route selected");
let command = relay_agent_command( let command = relay_agent_command(
&state, &state,
&route.agent_id, &route.agent_id,
@@ -245,6 +276,7 @@ pub async fn wake_fleet_device(
req.timeout_ms, req.timeout_ms,
) )
.await?; .await?;
tracing::Span::current().record("outcome", command.status.as_str());
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
@@ -252,6 +284,7 @@ pub async fn wake_fleet_device(
)) ))
} }
#[tracing::instrument(name = "fleet_load", skip_all, fields(grouped = tracing::field::Empty, filtered = tracing::field::Empty, returned = tracing::field::Empty))]
async fn load_fleet_devices( async fn load_fleet_devices(
state: &AppState, state: &AppState,
query: &ListFleetDevicesQuery, query: &ListFleetDevicesQuery,
@@ -290,9 +323,12 @@ async fn load_fleet_devices(
identifier_map, identifier_map,
}; };
let mut devices = build_fleet_devices(known_devices, agent_devices, &context); let mut devices = build_fleet_devices(known_devices, agent_devices, &context);
tracing::Span::current().record("grouped", devices.len());
filter_fleet_devices(&mut devices, query); filter_fleet_devices(&mut devices, query);
tracing::Span::current().record("filtered", devices.len());
let limit = query.limit.unwrap_or(500).clamp(1, 1000); let limit = query.limit.unwrap_or(500).clamp(1, 1000);
// this is going to be 10 million times harder if i decide to do this lazily // this is going to be 10 million times harder if i decide to do this lazily
devices.truncate(limit); devices.truncate(limit);
tracing::Span::current().record("returned", devices.len());
Ok(devices) Ok(devices)
} }