Wire format
April 19, 2026 · View on GitHub
Every type defined in src/types.rs is serde-derived JSON. Optional fields use #[serde(skip_serializing_if = "Option::is_none")] so payloads stay small and the schema can grow without breaking existing peers.
IngestRequest
The top-level payload an agent POSTs to the cloud.
struct IngestRequest {
agent_version: String,
host: HostInfo,
snapshots: Vec<Snapshot>,
}
agent_version— theCARGO_PKG_VERSIONof the agent binary (not the SDK). Servers may use it for compatibility decisions.host— the static metadata for this machine, sent on every request even though it rarely changes (the cloud uses it to upsert the host record).snapshots— one or moreSnapshotentries, in chronological order. Agents typically buffer 1–10 snapshots before sending, trading freshness for fewer round-trips.
HostInfo
Identifies and describes the machine.
struct HostInfo {
host_id: Uuid,
hostname: String,
os: Option<String>,
kernel: Option<String>,
uptime_secs: Option<u64>,
cpu_model: Option<String>,
cpu_cores: Option<u32>,
memory_total_bytes: Option<u64>,
}
host_id— assigned once, persisted by the agent (e.g., to/var/lib/netwatch-agent/host_id). Stable across restarts. The cloud uses this as the primary key.hostname— what the agent reports today; OK if it changes between requests.os/kernel— informational. Format isn't enforced (e.g.,"Ubuntu 24.04","6.8.0-31-generic").cpu_model/cpu_cores/memory_total_bytes— populated fromsystem::detect_cpu_info()andsystem::detect_memory_total()at agent startup.
Snapshot
A point-in-time slice of host state. The big one.
struct Snapshot {
timestamp: DateTime<Utc>,
interfaces: Vec<InterfaceMetric>,
health: Option<HealthMetric>,
connection_count: Option<u32>,
system: Option<SystemMetric>,
disk_usage: Option<Vec<DiskUsage>>,
disk_io: Option<DiskIo>,
tcp_time_wait: Option<u32>,
tcp_close_wait: Option<u32>,
processes: Option<Vec<ProcessBandwidth>>,
connections: Option<Vec<ConnectionDetail>>,
alerts: Option<Vec<Alert>>,
dns_analytics: Option<DnsAnalytics>,
}
timestamp— UTC. UseUtc::now()at the start of the cycle, not at send time.interfaces— always present, even if empty.InterfaceMetricis the only required collector payload because rates are the SDK's reason to exist.- Everything else is
Option. Agents collect heavyweight data (fullconnections,processes) less frequently than every snapshot; intermediate snapshots leave themNone.
InterfaceMetric
Per-interface counters and derived rates.
struct InterfaceMetric {
name: String,
is_up: bool,
rx_bytes: u64,
tx_bytes: u64,
rx_bytes_delta: u64,
tx_bytes_delta: u64,
rx_packets: u64,
tx_packets: u64,
rx_errors: u64,
tx_errors: u64,
rx_drops: u64,
tx_drops: u64,
rx_rate: Option<f64>, // bytes/sec
tx_rate: Option<f64>, // bytes/sec
rx_history: Option<Vec<u64>>, // up to 60 samples
tx_history: Option<Vec<u64>>, // up to 60 samples
}
- Cumulative counters (
rx_bytes,tx_packets, …) come straight from/sys/class/netornetstat -ibn. They wrap on 32-bit kernels — the SDK promotes tou64and trusts the kernel-reported width. *_deltaand*_rateare populated bytraffic::InterfaceRateTracker::sample(). First-call rates are0.0and history isNonebecause deltas aren't computable without a prior sample.rx_history/tx_historyare the last 60 rate samples (~1 minute at 1 Hz). Older samples drop off the front.
HealthMetric
Gateway and DNS reachability.
struct HealthMetric {
gateway_ip: Option<String>,
gateway_rtt_ms: Option<f64>,
gateway_loss_pct: Option<f64>,
dns_ip: Option<String>,
dns_rtt_ms: Option<f64>,
dns_loss_pct: Option<f64>,
gateway_rtt_history: Option<Vec<Option<f64>>>,
dns_rtt_history: Option<Vec<Option<f64>>>,
}
*_rtt_msis the average RTT from a 3-packet ping (ping -c 3 -W 1).*_loss_pctis the loss as reported byping.- History entries are
Option<f64>because individual probes can fail — the slot is preserved asNoneso consumers can render gaps in the time series correctly.
SystemMetric
struct SystemMetric {
cpu_usage_pct: Option<f64>,
memory_total_bytes: Option<u64>,
memory_used_bytes: Option<u64>,
memory_available_bytes: Option<u64>,
load_avg_1m: Option<f64>,
load_avg_5m: Option<f64>,
load_avg_15m: Option<f64>,
swap_total_bytes: Option<u64>,
swap_used_bytes: Option<u64>,
cpu_per_core: Option<Vec<f64>>,
}
cpu_usage_pctis rounded to one decimal. Measuring it sleeps for ~200 ms insidesystem::measure_cpu_usage()— call it on a dedicated tick, not in tight loops.memory_available_bytesmatches the kernel'sMemAvailableon Linux andfree + inactive + speculativefromvm_staton macOS.cpu_per_coreis populated on Linux only;Noneon macOS.
IngestResponse
What the cloud sends back.
struct IngestResponse {
accepted: u32,
rejected: u32,
host_id: Uuid,
results: Vec<SnapshotResult>,
}
struct SnapshotResult {
index: usize, // index into the request's snapshots[]
status: u16, // HTTP-like (200, 400, 422, …)
message: String,
}
Agents that batch should retry only the rejected snapshots, identified by index.
Forward-compatibility checklist
When you add a new field:
- New optional field on an existing struct → safe; mark
#[serde(default, skip_serializing_if = "Option::is_none")]. - New required field on an existing struct → breaking. Major-version bump.
- New enum variant on a public enum → breaking unless the enum is
#[non_exhaustive]. BothAlertSeverityandAlertCategoryare not currently#[non_exhaustive]; add the attribute before extending if you want a non-breaking growth path. - New public struct → safe; just add it.