Trace inventory sources and device storage operations
This commit is contained in:
5 files changed
+179
-4
No files matched your search
@@ -0,0 +1,131 @@
|
|||||||
|
# Wakey Metrics Recipes
|
||||||
|
|
||||||
|
Use Grafana Explore with your Prometheus-compatible data source and the Code
|
||||||
|
query editor. These recipes assume standard OTLP-to-Prometheus name conversion.
|
||||||
|
Discover your actual series first:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
count by (__name__, job) ({__name__=~"wakey[._].*"})
|
||||||
|
```
|
||||||
|
|
||||||
|
With Alloy's Prometheus exporter, service names normally become `job`, and agent
|
||||||
|
instance IDs become `instance`. Inspect the returned labels before adding filters.
|
||||||
|
For example, append `{job="wakey-agent"}` to a metric selector only if that is
|
||||||
|
the job value you actually see. Metrics export every 30 seconds; use a time range
|
||||||
|
that includes recent activity and allow at least two exports for rate queries.
|
||||||
|
|
||||||
|
## The Three Types
|
||||||
|
|
||||||
|
| Type | What it records | How to read it |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Counter | Cumulative events or bytes since process startup | `rate` for per-second throughput; `increase` for a window's estimated total |
|
||||||
|
| UpDownCounter | Adds and subtracts from a current quantity | Read the current value directly; do not use `rate` for active sessions |
|
||||||
|
| Histogram | Distribution of individual measurements | `_sum / _count` for an average; `_bucket` for estimated percentiles |
|
||||||
|
|
||||||
|
Counter resets on restart are handled by `rate` and `increase`. Apply those
|
||||||
|
functions **before** summing across instances so individual resets remain visible.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
Completed attempts per second, including errors and timeouts:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, operation, outcome) (rate(wakey_operations_total[5m]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Estimated attempts over the last hour:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, operation, outcome) (increase(wakey_operations_total[1h]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Average duration over the last five minutes:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, operation) (rate(wakey_operation_duration_seconds_sum[5m]))
|
||||||
|
/
|
||||||
|
sum by (job, operation) (rate(wakey_operation_duration_seconds_count[5m]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Set Grafana's unit to **seconds**. This is a weighted average, not an average of
|
||||||
|
instance averages. Both successful and unsuccessful attempts are included;
|
||||||
|
add `outcome="ok"` to both selectors for successful attempts only.
|
||||||
|
|
||||||
|
Estimated p95 duration:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
histogram_quantile(
|
||||||
|
0.95,
|
||||||
|
sum by (le, job, operation) (
|
||||||
|
rate(wakey_operation_duration_seconds_bucket[5m])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This needs classic histogram bucket series. Keep `le` in the grouping: it is
|
||||||
|
the bucket boundary, in seconds. p95 is an estimate from those boundaries,
|
||||||
|
not an exact sort of recorded durations. Few operations make percentiles noisy.
|
||||||
|
|
||||||
|
CC relay duration includes waiting for the agent; agent execution duration
|
||||||
|
measures its dispatch separately. Do not sum both and call that user latency.
|
||||||
|
|
||||||
|
## Terminals
|
||||||
|
|
||||||
|
Active PTY workers by agent, including detached sessions:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance) (wakey_terminal_active)
|
||||||
|
```
|
||||||
|
|
||||||
|
Read this directly as a count. It is not a count of browser tabs or attachments.
|
||||||
|
|
||||||
|
PTY output bytes per second:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance) (rate(wakey_terminal_output_bytes_total[5m]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Input bytes per second:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance) (rate(wakey_terminal_input_bytes_total[5m]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the unit to **bytes/sec**. Output is measured at the PTY read, even while
|
||||||
|
detached; it is not WebSocket wire traffic or confirmed browser delivery.
|
||||||
|
|
||||||
|
Relay queue-full encounters over the last fifteen minutes:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance) (increase(wakey_terminal_backpressure_total[15m]))
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
|
Device entries in successfully sent snapshots over the last hour:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
sum by (job, instance) (increase(wakey_snapshot_devices_total[1h]))
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeated snapshots count the same devices again. This is collection volume,
|
||||||
|
**not unique devices, current fleet size, or confirmed CC persistence**.
|
||||||
|
|
||||||
|
## Empty or Strange Results
|
||||||
|
|
||||||
|
- No series: generate the relevant activity, wait for export, and check the metrics pipeline.
|
||||||
|
- Different names: use the discovery query; collector translation can change suffixes and labels.
|
||||||
|
- `NaN` average: no operations occurred in the window, so the denominator is zero. Show a gap, not a fake zero latency.
|
||||||
|
- Empty `rate`: it needs multiple samples. Try a longer window and check the time range.
|
||||||
|
- Fractional `increase`: Prometheus extrapolates to the window boundaries; event counts can therefore appear fractional.
|
||||||
|
- Stale active count: the last exported value is not an immediate liveness check after a crash or network loss.
|
||||||
|
- No `instance` label: CC does not currently set a service instance ID; agent telemetry does.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Wakey telemetry setup and limitations](telemetry.md)
|
||||||
|
- [Prometheus rate, increase, and histogram_quantile](https://prometheus.io/docs/prometheus/latest/querying/functions/)
|
||||||
|
- [Alloy OTLP-to-Prometheus conversion](https://grafana.com/docs/alloy/latest/reference/components/otelcol/otelcol.exporter.prometheus/)
|
||||||
@@ -87,9 +87,22 @@ existing debug events too; inspect them before enabling it broadly.
|
|||||||
persistence separately. Snapshot traces are not yet linked across the wire.
|
persistence separately. Snapshot traces are not yet linked across the wire.
|
||||||
- Terminal create/attach/close/resume spans describe lifecycle operations, not
|
- Terminal create/attach/close/resume spans describe lifecycle operations, not
|
||||||
output chunks. Trace-linked OTLP logs use the SDK's tracing-context bridge.
|
output chunks. Trace-linked OTLP logs use the SDK's tracing-context bridge.
|
||||||
|
- Inventory source spans separate neighbor, DHCP lease, and hook-store reads
|
||||||
|
from merging. `db_*` spans cover snapshot replacement, fleet reads, and manual
|
||||||
|
identity changes, including transaction waits and result assembly where applicable.
|
||||||
|
|
||||||
|
Instrument operation boundaries rather than individual rows or terminal chunks.
|
||||||
|
Use `skip_all`, explicitly allow count fields, and instrument async futures rather
|
||||||
|
than holding entered span guards across awaits. Do not record query arguments,
|
||||||
|
SQL parameters, device payloads, or credentials. Span names are fixed across requests.
|
||||||
|
|
||||||
Metrics are exported every 30 seconds and at orderly shutdown:
|
Metrics are exported every 30 seconds and at orderly shutdown:
|
||||||
|
|
||||||
|
The table uses OpenTelemetry instrument names. Prometheus conversion typically
|
||||||
|
replaces dots with underscores and adds type/unit suffixes; histograms produce
|
||||||
|
multiple series. See the [metrics recipe book](telemetry-recipes.md) for discovery
|
||||||
|
queries, rates, averages, percentiles, and Grafana units.
|
||||||
|
|
||||||
| Metric | Meaning |
|
| Metric | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `wakey.operations` | Completed command/snapshot attempts, labeled by operation and outcome |
|
| `wakey.operations` | Completed command/snapshot attempts, labeled by operation and outcome |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tracing::{debug, warn};
|
use tracing::{Instrument, debug, warn};
|
||||||
use wakey_core::{
|
use wakey_core::{
|
||||||
Device, DeviceInventory, DeviceObservationFact, DhcpLease, DhcpLeaseWithState, InventoryQuery,
|
Device, DeviceInventory, DeviceObservationFact, DhcpLease, DhcpLeaseWithState, InventoryQuery,
|
||||||
NeighborEntry, Presence, Query,
|
NeighborEntry, Presence, Query,
|
||||||
@@ -19,10 +19,28 @@ pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
|||||||
/// This is the current center of gravity for the service layer. Higher-level
|
/// This is the current center of gravity for the service layer. Higher-level
|
||||||
/// status and wake flows should prefer deriving from inventory rather than
|
/// status and wake flows should prefer deriving from inventory rather than
|
||||||
/// directly from raw Linux source rows.
|
/// directly from raw Linux source rows.
|
||||||
|
#[tracing::instrument(name = "inventory", skip_all, fields(devices = tracing::field::Empty))]
|
||||||
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
||||||
let neighbors = wakey_linux::devices::query_neighbors(&query).await?;
|
// Instrument futures, not entered guards: each source remains correctly
|
||||||
let leases = get_leases().await?;
|
// parented when Tokio moves the task between workers during an await.
|
||||||
let observations = match wakey_linux::observations::list_local_observations().await {
|
let neighbors = async {
|
||||||
|
let rows = wakey_linux::devices::query_neighbors(&query).await?;
|
||||||
|
debug!(rows = rows.len(), "neighbor source collected");
|
||||||
|
Ok::<_, anyhow::Error>(rows)
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("inventory_neighbors"))
|
||||||
|
.await?;
|
||||||
|
let leases = async {
|
||||||
|
let rows = get_leases().await?;
|
||||||
|
debug!(rows = rows.len(), "DHCP source collected");
|
||||||
|
Ok::<_, anyhow::Error>(rows)
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("inventory_dhcp_leases"))
|
||||||
|
.await?;
|
||||||
|
let observations = match wakey_linux::observations::list_local_observations()
|
||||||
|
.instrument(tracing::info_span!("inventory_hook_store"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(observations) => observations
|
Ok(observations) => observations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(local_observation_to_fact)
|
.filter_map(local_observation_to_fact)
|
||||||
@@ -39,6 +57,7 @@ pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
|||||||
"building device inventory"
|
"building device inventory"
|
||||||
);
|
);
|
||||||
let devices = merge_devices_with_observations(neighbors, leases, observations, &query);
|
let devices = merge_devices_with_observations(neighbors, leases, observations, &query);
|
||||||
|
tracing::Span::current().record("devices", devices.len());
|
||||||
debug!(devices = devices.len(), "merged device inventory");
|
debug!(devices = devices.len(), "merged device inventory");
|
||||||
Ok(DeviceInventory { devices })
|
Ok(DeviceInventory { devices })
|
||||||
}
|
}
|
||||||
@@ -57,6 +76,7 @@ pub fn merge_devices(
|
|||||||
|
|
||||||
/// Merge raw neighbor entries, DHCP leases, and hook observations into device
|
/// Merge raw neighbor entries, DHCP leases, and hook observations into device
|
||||||
/// aggregates.
|
/// aggregates.
|
||||||
|
#[tracing::instrument(name = "inventory_merge", skip_all, fields(neighbors = neighbors.len(), leases = leases.len(), observations = observations.len()))]
|
||||||
pub fn merge_devices_with_observations(
|
pub fn merge_devices_with_observations(
|
||||||
neighbors: Vec<NeighborEntry>,
|
neighbors: Vec<NeighborEntry>,
|
||||||
leases: Vec<DhcpLeaseWithState>,
|
leases: Vec<DhcpLeaseWithState>,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ impl Store {
|
|||||||
///
|
///
|
||||||
/// This is the only write path for agent device state. Every sync source
|
/// This is the only write path for agent device state. Every sync source
|
||||||
/// (WebSocket snapshot, fleet refresh, HTTP upload) must go through here.
|
/// (WebSocket snapshot, fleet refresh, HTTP upload) must go through here.
|
||||||
|
#[tracing::instrument(name = "db_replace_agent_snapshot", skip_all, fields(devices = devices.len(), accepted = tracing::field::Empty))]
|
||||||
pub async fn replace_agent_device_snapshot(
|
pub async fn replace_agent_device_snapshot(
|
||||||
&self,
|
&self,
|
||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
@@ -222,10 +223,12 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed committing device snapshot")?;
|
.context("failed committing device snapshot")?;
|
||||||
|
|
||||||
|
tracing::Span::current().record("accepted", incoming_keys.len());
|
||||||
Ok(incoming_keys.len())
|
Ok(incoming_keys.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all agent device rows with their child MAC/IP/hostname/fact rows.
|
/// List all agent device rows with their child MAC/IP/hostname/fact rows.
|
||||||
|
#[tracing::instrument(name = "db_list_fleet_devices", skip_all)]
|
||||||
pub async fn list_agent_device_rows(&self) -> Result<Vec<AgentDeviceWithChildren>> {
|
pub async fn list_agent_device_rows(&self) -> Result<Vec<AgentDeviceWithChildren>> {
|
||||||
let devices = sqlx::query_as!(
|
let devices = sqlx::query_as!(
|
||||||
AgentDeviceRow,
|
AgentDeviceRow,
|
||||||
@@ -290,6 +293,7 @@ impl Store {
|
|||||||
|
|
||||||
/// List agent device rows for a single agent.
|
/// List agent device rows for a single agent.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[tracing::instrument(name = "db_list_agent_devices", skip_all)]
|
||||||
pub async fn list_agent_device_rows_for_agent(
|
pub async fn list_agent_device_rows_for_agent(
|
||||||
&self,
|
&self,
|
||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
impl Store {
|
impl Store {
|
||||||
|
#[tracing::instrument(name = "db_create_known_device", skip_all, fields(identifiers = input.identifiers.len()))]
|
||||||
pub async fn create_known_device(&self, input: KnownDeviceInput) -> Result<KnownDevice> {
|
pub async fn create_known_device(&self, input: KnownDeviceInput) -> Result<KnownDevice> {
|
||||||
let display_name = normalize_required_text(&input.display_name, "display_name")?;
|
let display_name = normalize_required_text(&input.display_name, "display_name")?;
|
||||||
let notes = normalize_optional_text(input.notes.as_deref());
|
let notes = normalize_optional_text(input.notes.as_deref());
|
||||||
@@ -41,6 +42,7 @@ impl Store {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("created known device disappeared"))
|
.ok_or_else(|| anyhow::anyhow!("created known device disappeared"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_list_known_devices", skip_all)]
|
||||||
pub async fn list_known_devices(&self) -> Result<Vec<KnownDevice>> {
|
pub async fn list_known_devices(&self) -> Result<Vec<KnownDevice>> {
|
||||||
let rows = list_known_device_rows(&self.pool).await?;
|
let rows = list_known_device_rows(&self.pool).await?;
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ impl Store {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_get_known_device", skip_all)]
|
||||||
pub async fn get_known_device(&self, device_id: &str) -> Result<Option<KnownDevice>> {
|
pub async fn get_known_device(&self, device_id: &str) -> Result<Option<KnownDevice>> {
|
||||||
let row = get_known_device_row(&self.pool, device_id).await?;
|
let row = get_known_device_row(&self.pool, device_id).await?;
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ impl Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_forget_known_device", skip_all)]
|
||||||
pub async fn forget_known_device(&self, device_id: &str) -> Result<bool> {
|
pub async fn forget_known_device(&self, device_id: &str) -> Result<bool> {
|
||||||
let result = sqlx::query!("DELETE FROM known_devices WHERE device_id = ?1", device_id)
|
let result = sqlx::query!("DELETE FROM known_devices WHERE device_id = ?1", device_id)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
@@ -69,6 +73,7 @@ impl Store {
|
|||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_merge_known_devices", skip_all)]
|
||||||
pub async fn merge_known_devices(
|
pub async fn merge_known_devices(
|
||||||
&self,
|
&self,
|
||||||
target_device_id: &str,
|
target_device_id: &str,
|
||||||
@@ -134,6 +139,7 @@ impl Store {
|
|||||||
self.get_known_device(target_device_id).await
|
self.get_known_device(target_device_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_attach_device_identifier", skip_all)]
|
||||||
pub async fn attach_device_identifier(
|
pub async fn attach_device_identifier(
|
||||||
&self,
|
&self,
|
||||||
device_id: &str,
|
device_id: &str,
|
||||||
@@ -169,6 +175,7 @@ impl Store {
|
|||||||
self.get_known_device(device_id).await
|
self.get_known_device(device_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "db_detach_device_identifier", skip_all)]
|
||||||
pub async fn detach_device_identifier(
|
pub async fn detach_device_identifier(
|
||||||
&self,
|
&self,
|
||||||
device_id: &str,
|
device_id: &str,
|
||||||
|
|||||||
Reference in new issue
Block a user