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 — the CARGO_PKG_VERSION of 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 more Snapshot entries, 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 from system::detect_cpu_info() and system::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. Use Utc::now() at the start of the cycle, not at send time.
  • interfaces — always present, even if empty. InterfaceMetric is the only required collector payload because rates are the SDK's reason to exist.
  • Everything else is Option. Agents collect heavyweight data (full connections, processes) less frequently than every snapshot; intermediate snapshots leave them None.

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/net or netstat -ibn. They wrap on 32-bit kernels — the SDK promotes to u64 and trusts the kernel-reported width.
  • *_delta and *_rate are populated by traffic::InterfaceRateTracker::sample(). First-call rates are 0.0 and history is None because deltas aren't computable without a prior sample.
  • rx_history / tx_history are 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_ms is the average RTT from a 3-packet ping (ping -c 3 -W 1). *_loss_pct is the loss as reported by ping.
  • History entries are Option<f64> because individual probes can fail — the slot is preserved as None so 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_pct is rounded to one decimal. Measuring it sleeps for ~200 ms inside system::measure_cpu_usage() — call it on a dedicated tick, not in tight loops.
  • memory_available_bytes matches the kernel's MemAvailable on Linux and free + inactive + speculative from vm_stat on macOS.
  • cpu_per_core is populated on Linux only; None on 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]. Both AlertSeverity and AlertCategory are not currently #[non_exhaustive]; add the attribute before extending if you want a non-breaking growth path.
  • New public struct → safe; just add it.