From 6be5c6f6371d30f031e03e35ddfdfea851f4a4de Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Tue, 1 Sep 2026 05:31:57 -0400 Subject: [PATCH 1/2] bpf: collect configurable network stats in maps --- CHANGELOG.md | 8 +- README.md | 24 +- bpf/src/bpf/nettrack.bpf.c | 420 +++++++--- bpf/src/lib.rs | 98 ++- bpf/src/nettrack.rs | 1434 +++++++++++++++++++++++--------- bpf/testdata/nettrack_e2e.toml | 11 + reference.toml | 27 +- 7 files changed, 1500 insertions(+), 522 deletions(-) create mode 100644 bpf/testdata/nettrack_e2e.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index d097589..5b7662f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `nettrack` BPF module for network I/O monitoring Config options: `frequency`: sampling frequency in Hz - `scaled`: report rates in bits/s instead of cumulative bytes + `scaled`: report rates in bits/s instead of interval bytes +- Configurable `nettrack` protocols, directions, metrics, and host/process/peer scopes +- Bounded per-process and per-peer network aggregation with independent peer sampling frequency ### Fixed +- Count successful TCP and UDP payload bytes instead of requested send sizes +- Replace `nettrack` perf-event and ring-buffer boundaries with cumulative map snapshots - Spawn a separate thread for timer interrupt to fix starvation issues ([189c785](https://github.com/category-labs/manytrace/commit/189c785)) ## [0.1.1] - 2025-07-11 ### Added -- Initial release with core functionality \ No newline at end of file +- Initial release with core functionality diff --git a/README.md b/README.md index ebe25a7..d7267df 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,28 @@ counters = ["cpu-cycles", "instructions", "cache-misses", "ipc"] # page-faults, context-switches, cpu-migrations, ipc ``` +#### network tracking + +collects successful TCP and UDP application payload bytes. counters are aggregated in BPF maps +and sampled from userspace, so network tracking does not use periodic per-CPU interrupts or a +ring buffer. host byte throughput is the low-overhead default; process and peer aggregation are +independent opt-in scopes. + +```toml +[bpf.nettrack] +frequency = 5 +peer_frequency = 1 +scaled = true +protocols = ["tcp", "udp"] +directions = ["send", "receive"] +scopes = ["host"] # add "process" or "peer" for more detail +metrics = ["bytes"] # optional: "operations", "errors" +``` + +process-name filters require `bpf.thread_tracker`. peer tracks contain remote IP addresses and +ports and may make traces sensitive; their kernel-map and Perfetto cardinality are bounded by +`max_peer_entries` and `max_peer_tracks`. + #### user tracing ![spans](_assets/manytrace_spans.png) @@ -103,4 +125,4 @@ random_process_id = true ## License -Licensed under the GNU General Public License ([LICENSE](LICENSE) or https://www.gnu.org/licenses/gpl-3.0.html). \ No newline at end of file +Licensed under the GNU General Public License ([LICENSE](LICENSE) or https://www.gnu.org/licenses/gpl-3.0.html). diff --git a/bpf/src/bpf/nettrack.bpf.c b/bpf/src/bpf/nettrack.bpf.c index 2fdbec1..af8e2c5 100644 --- a/bpf/src/bpf/nettrack.bpf.c +++ b/bpf/src/bpf/nettrack.bpf.c @@ -14,149 +14,369 @@ // along with this program. If not, see . #include "vmlinux.h" +#include +#include #include #include -#include char LICENSE[] SEC("license") = "GPL"; +#define AF_INET 2 +#define AF_INET6 10 +#define EAGAIN 11 +#define MSG_TRUNC 0x20 + +enum nettrack_protocol { + NET_PROTOCOL_TCP = 0, + NET_PROTOCOL_UDP = 1, +}; + struct net_stats { - u64 tcp_send_bytes; - u64 tcp_recv_bytes; - u64 udp_send_bytes; - u64 udp_recv_bytes; - u64 last_update_ns; + u64 send_bytes; + u64 recv_bytes; + u64 send_operations; + u64 recv_operations; + u64 send_errors; + u64 recv_errors; + u64 send_would_block; + u64 recv_would_block; + u64 tcp_recv_eof; + u64 udp_recv_truncated; + u64 first_seen_ns; + u64 last_seen_ns; }; -struct net_event { - u64 timestamp; - u64 tcp_send_bytes; - u64 tcp_recv_bytes; - u64 udp_send_bytes; - u64 udp_recv_bytes; - u64 total_send_bytes; - u64 total_recv_bytes; - u32 cpu; +struct process_key { + u32 tgid; + u8 protocol; + u8 padding[3]; +}; + +struct peer_key { + u32 tgid; + u8 protocol; + u8 family; + u16 local_port; + u16 remote_port; + u16 padding; + u8 remote_addr[16]; }; struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); - __uint(max_entries, 1); + __uint(max_entries, 2); __type(key, u32); __type(value, struct net_stats); -} net_stats SEC(".maps"); +} global_stats SEC(".maps"); struct { - __uint(type, BPF_MAP_TYPE_RINGBUF); - __uint(max_entries, 1024 * 1024); -} events SEC(".maps"); + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 1024); + __type(key, struct process_key); + __type(value, struct net_stats); +} process_stats SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 4096); + __type(key, struct peer_key); + __type(value, struct net_stats); +} peer_stats SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1024); + __type(key, u32); + __type(value, u32); +} tracked_tgids SEC(".maps"); + +const volatile struct { + bool tcp_enabled; + bool udp_enabled; + bool send_enabled; + bool receive_enabled; + bool host_enabled; + bool process_enabled; + bool peer_enabled; + bool bytes_enabled; + bool operations_enabled; + bool errors_enabled; + bool filter_enabled; +} cfg = { + .tcp_enabled = true, + .udp_enabled = true, + .send_enabled = true, + .receive_enabled = true, + .host_enabled = true, + .process_enabled = false, + .peer_enabled = false, + .bytes_enabled = true, + .operations_enabled = false, + .errors_enabled = false, + .filter_enabled = false, +}; + +static __always_inline bool should_track_tgid(u32 tgid) +{ + if (tgid == 0) + return false; + if (!cfg.filter_enabled) + return true; + return bpf_map_lookup_elem(&tracked_tgids, &tgid) != NULL; +} -static __always_inline u64 get_time() { - return bpf_ktime_get_ns(); +static __always_inline void initialize_stats(struct net_stats *stats, u64 now) +{ + __builtin_memset(stats, 0, sizeof(*stats)); + stats->first_seen_ns = now; + stats->last_seen_ns = now; } -static __always_inline void submit_net_event(struct net_stats *stats, u64 now) { - struct net_event *e; - - e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); - if (!e) { +static __always_inline void update_stats(struct net_stats *stats, bool send, int ret, + int protocol, u32 msg_flags, u64 now, + bool shared) +{ + if (shared) + __sync_lock_test_and_set(&stats->last_seen_ns, now); + else + stats->last_seen_ns = now; + + if (ret < 0) { + if (!cfg.errors_enabled) + return; + if (send) { + if (shared) + __sync_fetch_and_add(&stats->send_errors, 1); + else + stats->send_errors++; + if (ret == -EAGAIN) { + if (shared) + __sync_fetch_and_add(&stats->send_would_block, 1); + else + stats->send_would_block++; + } + } else { + if (shared) + __sync_fetch_and_add(&stats->recv_errors, 1); + else + stats->recv_errors++; + if (ret == -EAGAIN) { + if (shared) + __sync_fetch_and_add(&stats->recv_would_block, 1); + else + stats->recv_would_block++; + } + } return; } - - e->timestamp = now; - e->tcp_send_bytes = stats->tcp_send_bytes; - e->tcp_recv_bytes = stats->tcp_recv_bytes; - e->udp_send_bytes = stats->udp_send_bytes; - e->udp_recv_bytes = stats->udp_recv_bytes; - e->total_send_bytes = stats->tcp_send_bytes + stats->udp_send_bytes; - e->total_recv_bytes = stats->tcp_recv_bytes + stats->udp_recv_bytes; - e->cpu = bpf_get_smp_processor_id(); - - bpf_ringbuf_submit(e, 0); - - stats->tcp_send_bytes = 0; - stats->tcp_recv_bytes = 0; - stats->udp_send_bytes = 0; - stats->udp_recv_bytes = 0; - stats->last_update_ns = now; + + if (send) { + if (cfg.bytes_enabled && ret > 0) { + if (shared) + __sync_fetch_and_add(&stats->send_bytes, (u64)ret); + else + stats->send_bytes += (u64)ret; + } + if (cfg.operations_enabled) { + if (shared) + __sync_fetch_and_add(&stats->send_operations, 1); + else + stats->send_operations++; + } + } else { + if (cfg.bytes_enabled && ret > 0) { + if (shared) + __sync_fetch_and_add(&stats->recv_bytes, (u64)ret); + else + stats->recv_bytes += (u64)ret; + } + if (cfg.operations_enabled) { + if (shared) + __sync_fetch_and_add(&stats->recv_operations, 1); + else + stats->recv_operations++; + + if (protocol == NET_PROTOCOL_TCP && ret == 0) { + if (shared) + __sync_fetch_and_add(&stats->tcp_recv_eof, 1); + else + stats->tcp_recv_eof++; + } + if (protocol == NET_PROTOCOL_UDP && (msg_flags & MSG_TRUNC)) { + if (shared) + __sync_fetch_and_add(&stats->udp_recv_truncated, 1); + else + stats->udp_recv_truncated++; + } + } + } } -SEC("kprobe/udp_sendmsg") -int BPF_KPROBE(trace_udp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size) +static __always_inline void update_process_stats(u32 tgid, int protocol, bool send, + int ret, u32 msg_flags, u64 now) { - u32 zero = 0; - struct net_stats *stats = bpf_map_lookup_elem(&net_stats, &zero); - if (!stats) { - return 0; + struct process_key key = { + .tgid = tgid, + .protocol = protocol, + }; + struct net_stats *stats = bpf_map_lookup_elem(&process_stats, &key); + if (stats) { + update_stats(stats, send, ret, protocol, msg_flags, now, true); + return; } - - stats->udp_send_bytes += size; - - return 0; + + struct net_stats initial; + initialize_stats(&initial, now); + update_stats(&initial, send, ret, protocol, msg_flags, now, false); + if (bpf_map_update_elem(&process_stats, &key, &initial, BPF_NOEXIST) == 0) + return; + + stats = bpf_map_lookup_elem(&process_stats, &key); + if (stats) + update_stats(stats, send, ret, protocol, msg_flags, now, true); } -SEC("kretprobe/udp_recvmsg") -int BPF_KRETPROBE(trace_udp_recvmsg_ret, int ret) +static __always_inline bool peer_from_sock(struct sock *sk, struct peer_key *key) { - u32 zero = 0; - struct net_stats *stats = bpf_map_lookup_elem(&net_stats, &zero); - if (!stats) { - return 0; + u16 family = BPF_CORE_READ(sk, __sk_common.skc_family); + key->family = family; + key->local_port = BPF_CORE_READ(sk, __sk_common.skc_num); + key->remote_port = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport)); + + if (family == AF_INET) { + u32 address = BPF_CORE_READ(sk, __sk_common.skc_daddr); + __builtin_memcpy(key->remote_addr, &address, sizeof(address)); + return address != 0 && key->remote_port != 0; } - - if (ret > 0) { - stats->udp_recv_bytes += (u64)ret; + if (family == AF_INET6) { + BPF_CORE_READ_INTO(key->remote_addr, sk, __sk_common.skc_v6_daddr); + return key->remote_port != 0; } - - return 0; + return false; } -SEC("kprobe/tcp_sendmsg") -int BPF_KPROBE(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size) +static __always_inline bool peer_from_message(struct msghdr *msg, struct peer_key *key) { - u32 zero = 0; - struct net_stats *stats = bpf_map_lookup_elem(&net_stats, &zero); - if (!stats) { - return 0; + void *name; + u16 family; + + if (!msg) + return false; + name = BPF_CORE_READ(msg, msg_name); + if (!name) + return false; + if (bpf_probe_read_kernel(&family, sizeof(family), name) != 0) + return false; + + if (family == AF_INET) { + struct sockaddr_in address; + if (bpf_probe_read_kernel(&address, sizeof(address), name) != 0) + return false; + key->family = AF_INET; + key->remote_port = bpf_ntohs(address.sin_port); + __builtin_memcpy(key->remote_addr, &address.sin_addr, sizeof(address.sin_addr)); + return key->remote_port != 0; } - - stats->tcp_send_bytes += size; - - return 0; + if (family == AF_INET6) { + struct sockaddr_in6 address; + if (bpf_probe_read_kernel(&address, sizeof(address), name) != 0) + return false; + key->family = AF_INET6; + key->remote_port = bpf_ntohs(address.sin6_port); + __builtin_memcpy(key->remote_addr, &address.sin6_addr, sizeof(address.sin6_addr)); + return key->remote_port != 0; + } + return false; } -SEC("kretprobe/tcp_recvmsg") -int BPF_KRETPROBE(trace_tcp_recvmsg_ret, int ret) +static __always_inline void update_peer_stats(struct sock *sk, struct msghdr *msg, + u32 tgid, int protocol, bool send, + int ret, u32 msg_flags, u64 now) { - u32 zero = 0; - struct net_stats *stats = bpf_map_lookup_elem(&net_stats, &zero); - if (!stats) { - return 0; - } - - if (ret > 0) { - stats->tcp_recv_bytes += (u64)ret; + struct peer_key key = { + .tgid = tgid, + .protocol = protocol, + }; + + key.local_port = BPF_CORE_READ(sk, __sk_common.skc_num); + bool found = false; + if (protocol == NET_PROTOCOL_UDP) + found = peer_from_message(msg, &key); + if (!found) + found = peer_from_sock(sk, &key); + if (!found) + return; + + struct net_stats *stats = bpf_map_lookup_elem(&peer_stats, &key); + if (stats) { + update_stats(stats, send, ret, protocol, msg_flags, now, true); + return; } - - return 0; + + struct net_stats initial; + initialize_stats(&initial, now); + update_stats(&initial, send, ret, protocol, msg_flags, now, false); + if (bpf_map_update_elem(&peer_stats, &key, &initial, BPF_NOEXIST) == 0) + return; + + stats = bpf_map_lookup_elem(&peer_stats, &key); + if (stats) + update_stats(stats, send, ret, protocol, msg_flags, now, true); } -SEC("perf_event") -int handle_boundary_event(void *ctx) +static __always_inline int account_socket_io(struct sock *sk, struct msghdr *msg, + int protocol, bool send, int ret) { - u32 zero = 0; - struct net_stats *stats = bpf_map_lookup_elem(&net_stats, &zero); - if (!stats) { + if ((send && !cfg.send_enabled) || (!send && !cfg.receive_enabled)) return 0; - } - - u64 now = get_time(); - if (stats->last_update_ns == 0) { - stats->last_update_ns = now; + + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tgid = pid_tgid >> 32; + if (!should_track_tgid(tgid)) return 0; + + if (!sk || (protocol == NET_PROTOCOL_TCP && !cfg.tcp_enabled) + || (protocol == NET_PROTOCOL_UDP && !cfg.udp_enabled)) + return 0; + + u64 now = bpf_ktime_get_ns(); + u32 msg_flags = msg ? BPF_CORE_READ(msg, msg_flags) : 0; + + if (cfg.host_enabled) { + u32 key = protocol; + struct net_stats *stats = bpf_map_lookup_elem(&global_stats, &key); + if (stats) + update_stats(stats, send, ret, protocol, msg_flags, now, false); } - - submit_net_event(stats, now); - + if (cfg.process_enabled) + update_process_stats(tgid, protocol, send, ret, msg_flags, now); + if (cfg.peer_enabled) + update_peer_stats(sk, msg, tgid, protocol, send, ret, msg_flags, now); return 0; } + +SEC("fexit/tcp_sendmsg") +int BPF_PROG(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size, int ret) +{ + return account_socket_io(sk, msg, NET_PROTOCOL_TCP, true, ret); +} + +SEC("fexit/tcp_recvmsg") +int BPF_PROG(trace_tcp_recvmsg, struct sock *sk, struct msghdr *msg, size_t len, + int flags, int *addr_len, int ret) +{ + return account_socket_io(sk, msg, NET_PROTOCOL_TCP, false, ret); +} + +SEC("fexit/udp_sendmsg") +int BPF_PROG(trace_udp_sendmsg, struct sock *sk, struct msghdr *msg, size_t len, int ret) +{ + return account_socket_io(sk, msg, NET_PROTOCOL_UDP, true, ret); +} + +SEC("fexit/udp_recvmsg") +int BPF_PROG(trace_udp_recvmsg, struct sock *sk, struct msghdr *msg, size_t len, + int flags, int *addr_len, int ret) +{ + return account_socket_io(sk, msg, NET_PROTOCOL_UDP, false, ret); +} diff --git a/bpf/src/lib.rs b/bpf/src/lib.rs index 8f8fa13..83e39f3 100644 --- a/bpf/src/lib.rs +++ b/bpf/src/lib.rs @@ -46,7 +46,7 @@ fn get_monotonic_timestamp() -> u64 { } pub use cpuutil::CpuUtilConfig; -pub use nettrack::NetTrackConfig; +pub use nettrack::{NetDirection, NetMetric, NetProtocol, NetScope, NetTrackConfig}; pub use perfcounter::PerfCounterConfig; pub use profiler::ProfilerConfig; pub use schedtrace::SchedTraceConfig; @@ -124,7 +124,12 @@ impl BpfConfig { .as_ref() .map(|cfg| !cfg.filter_process.is_empty() || global_filter.is_some()) .unwrap_or(false); - cpu_needs || profiler_needs || schedtrace_needs + let nettrack_needs = self + .nettrack + .as_ref() + .map(|cfg| !cfg.filter_process.is_empty() || global_filter.is_some()) + .unwrap_or(false); + cpu_needs || profiler_needs || schedtrace_needs || nettrack_needs }; if needs_process_filtering && self.thread_tracker.is_none() { @@ -195,15 +200,26 @@ impl BpfConfig { (None, HashSet::new()) }; - let nettrack = if let Some(cfg) = self.nettrack { + let (nettrack, nettrack_filters) = if let Some(mut cfg) = self.nettrack { + if let Some(ref global) = global_filter { + cfg.filter_process = global.clone(); + } debug!( module = "nettrack", frequency = cfg.frequency, + peer_frequency = cfg.peer_frequency, + protocols = ?cfg.protocols, + directions = ?cfg.directions, + scopes = ?cfg.scopes, + metrics = ?cfg.metrics, + pid_filters = ?cfg.pid_filters, + filter_process = ?cfg.filter_process, "initializing network tracking" ); - Some(nettrack::Object::new(cfg)) + let filters: HashSet = cfg.filter_process.iter().cloned().collect(); + (Some(nettrack::Object::new(cfg)), filters) } else { - None + (None, HashSet::new()) }; let perfcounter = if let Some(cfg) = self.perfcounter { @@ -229,6 +245,7 @@ impl BpfConfig { cpuutil_filters, profiler_filters, schedtrace_filters, + nettrack_filters, watched_tgids: HashSet::new(), }) } @@ -245,6 +262,7 @@ pub struct BpfObject { cpuutil_filters: HashSet, profiler_filters: HashSet, schedtrace_filters: HashSet, + nettrack_filters: HashSet, watched_tgids: HashSet, } @@ -308,15 +326,18 @@ impl BpfObject { let has_cpuutil = cpuutil_rc.is_some(); let has_profiler = profiler_rc.is_some(); let has_schedtrace = schedtrace_rc.is_some(); + let has_nettrack = nettrack_rc.is_some(); - if has_cpuutil || has_profiler || has_schedtrace { + if has_cpuutil || has_profiler || has_schedtrace || has_nettrack { let mut user_callback = callback.clone(); let cpuutil_filters = self.cpuutil_filters.clone(); let profiler_filters = self.profiler_filters.clone(); let schedtrace_filters = self.schedtrace_filters.clone(); + let nettrack_filters = self.nettrack_filters.clone(); let cpuutil_ref = cpuutil_rc.clone(); let profiler_ref = profiler_rc.clone(); let schedtrace_ref = schedtrace_rc.clone(); + let nettrack_ref = nettrack_rc.clone(); let mut watched_tgids = std::mem::take(&mut self.watched_tgids); let wrapper_callback = move |message: Message<'_>| -> i32 { @@ -380,6 +401,23 @@ impl BpfObject { } } } + if let Some(ref net) = nettrack_ref { + let base_name = name.split('/').next_back().unwrap_or(name); + if nettrack_filters.contains(name) + || nettrack_filters.contains(base_name) + { + if let Err(e) = net.borrow_mut().filter(pid) { + tracing::warn!( + "Failed to add process {} (pid {}) to nettrack filter: {}", + name, + pid, + e + ); + } else { + watched_tgids.insert(pid); + } + } + } } protocol::TrackType::Thread { tid, pid } => { if watched_tgids.contains(&pid) { @@ -702,6 +740,54 @@ nettrack = {} let nettrack_config = config.nettrack.as_ref().unwrap(); assert_eq!(nettrack_config.frequency, 9); // default_frequency assert_eq!(nettrack_config.ringbuf, 1024 * 1024); // default_ringbuf_size + assert_eq!(nettrack_config.scopes, vec![NetScope::Host]); + assert_eq!(nettrack_config.metrics, vec![NetMetric::Bytes]); + assert_eq!(nettrack_config.peer_frequency, 1); + } + + #[test] + fn test_detailed_nettrack_config() { + let config = BpfConfig::from_toml_str( + r#" +[nettrack] +frequency = 5 +peer_frequency = 2 +protocols = ["tcp"] +directions = ["send"] +scopes = ["host", "process", "peer"] +metrics = ["bytes", "operations", "errors"] +pid_filters = [1234] +max_process_entries = 64 +max_peer_entries = 128 +max_peer_tracks = 32 +"#, + ) + .unwrap(); + let config = config.nettrack.unwrap(); + assert_eq!(config.protocols, vec![NetProtocol::Tcp]); + assert_eq!(config.directions, vec![NetDirection::Send]); + assert_eq!( + config.scopes, + vec![NetScope::Host, NetScope::Process, NetScope::Peer] + ); + assert_eq!( + config.metrics, + vec![NetMetric::Bytes, NetMetric::Operations, NetMetric::Errors] + ); + assert_eq!(config.max_peer_entries, 128); + assert_eq!(config.max_peer_tracks, 32); + } + + #[test] + fn test_nettrack_process_filter_requires_threadtrack() { + let config = BpfConfig::from_toml_str( + r#" +[nettrack] +filter_process = ["monad-node"] +"#, + ) + .unwrap(); + assert!(config.build().is_err()); } #[test] diff --git a/bpf/src/nettrack.rs b/bpf/src/nettrack.rs index ee988db..e62d4e7 100644 --- a/bpf/src/nettrack.rs +++ b/bpf/src/nettrack.rs @@ -19,37 +19,223 @@ mod nettrack_skel { use nettrack_skel::*; -use crate::{perf_event, BpfError, Filterable}; +use crate::{get_monotonic_timestamp, BpfError, Filterable}; use libbpf_rs::skel::{OpenSkel, Skel, SkelBuilder}; -use libbpf_rs::{OpenObject, RingBufferBuilder}; -use libbpf_sys::{PERF_COUNT_SW_CPU_CLOCK, PERF_TYPE_SOFTWARE}; +use libbpf_rs::{MapCore, MapFlags, OpenObject}; use protocol::{Counter, Event, Labels, Message, Track, TrackId, TrackType}; use rand::Rng; use serde::{Deserialize, Serialize}; use std::borrow::Cow; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::marker::PhantomData; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::hash::Hash; use std::mem::MaybeUninit; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::Duration; use tracing::debug; +const AF_INET: u8 = 2; +const AF_INET6: u8 = 10; +const TCP_INDEX: usize = 0; +const UDP_INDEX: usize = 1; +const MAP_BATCH_SIZE: u32 = 256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NetProtocol { + Tcp, + Udp, +} + +impl NetProtocol { + fn index(self) -> usize { + match self { + Self::Tcp => TCP_INDEX, + Self::Udp => UDP_INDEX, + } + } + + fn from_index(index: u8) -> Option { + match index as usize { + TCP_INDEX => Some(Self::Tcp), + UDP_INDEX => Some(Self::Udp), + _ => None, + } + } + + fn name(self) -> &'static str { + match self { + Self::Tcp => "tcp", + Self::Udp => "udp", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NetDirection { + Send, + Receive, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NetScope { + Host, + Process, + Peer, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NetMetric { + Bytes, + Operations, + Errors, +} + +fn default_frequency() -> u64 { + 9 +} + +fn default_peer_frequency() -> u64 { + 1 +} + +fn default_ringbuf_size() -> usize { + 1024 * 1024 +} + +fn default_protocols() -> Vec { + vec![NetProtocol::Tcp, NetProtocol::Udp] +} + +fn default_directions() -> Vec { + vec![NetDirection::Send, NetDirection::Receive] +} + +fn default_scopes() -> Vec { + vec![NetScope::Host] +} + +fn default_metrics() -> Vec { + vec![NetMetric::Bytes] +} + +fn default_process_entries() -> u32 { + 1024 +} + +fn default_peer_entries() -> u32 { + 4096 +} + +fn default_peer_tracks() -> usize { + 512 +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetTrackConfig { #[serde(default = "default_frequency")] pub frequency: u64, + #[serde(default = "default_peer_frequency")] + pub peer_frequency: u64, + /// Kept for configuration compatibility. Map-based sampling has no ring buffer. #[serde(default = "default_ringbuf_size")] pub ringbuf: usize, #[serde(default)] pub scaled: bool, + #[serde(default = "default_protocols")] + pub protocols: Vec, + #[serde(default = "default_directions")] + pub directions: Vec, + #[serde(default = "default_scopes")] + pub scopes: Vec, + #[serde(default = "default_metrics")] + pub metrics: Vec, + #[serde(default)] + pub pid_filters: Vec, + #[serde(default)] + pub filter_process: Vec, + #[serde(default = "default_process_entries")] + pub max_process_entries: u32, + #[serde(default = "default_peer_entries")] + pub max_peer_entries: u32, + #[serde(default = "default_peer_tracks")] + pub max_peer_tracks: usize, } -fn default_frequency() -> u64 { - 9 +impl Default for NetTrackConfig { + fn default() -> Self { + Self { + frequency: default_frequency(), + peer_frequency: default_peer_frequency(), + ringbuf: default_ringbuf_size(), + scaled: false, + protocols: default_protocols(), + directions: default_directions(), + scopes: default_scopes(), + metrics: default_metrics(), + pid_filters: Vec::new(), + filter_process: Vec::new(), + max_process_entries: default_process_entries(), + max_peer_entries: default_peer_entries(), + max_peer_tracks: default_peer_tracks(), + } + } } -fn default_ringbuf_size() -> usize { - 1024 * 1024 +impl NetTrackConfig { + fn has_protocol(&self, protocol: NetProtocol) -> bool { + self.protocols.contains(&protocol) + } + + fn has_direction(&self, direction: NetDirection) -> bool { + self.directions.contains(&direction) + } + + fn has_scope(&self, scope: NetScope) -> bool { + self.scopes.contains(&scope) + } + + fn has_metric(&self, metric: NetMetric) -> bool { + self.metrics.contains(&metric) + } + + fn validate(&self) -> Result<(), BpfError> { + if self.frequency == 0 { + return Err(BpfError::LoadError( + "nettrack frequency must be greater than zero".to_string(), + )); + } + if self.has_scope(NetScope::Peer) && self.peer_frequency == 0 { + return Err(BpfError::LoadError( + "nettrack peer_frequency must be greater than zero".to_string(), + )); + } + if self.protocols.is_empty() + || self.directions.is_empty() + || self.scopes.is_empty() + || self.metrics.is_empty() + { + return Err(BpfError::LoadError( + "nettrack protocols, directions, scopes, and metrics must not be empty".to_string(), + )); + } + if self.has_scope(NetScope::Process) && self.max_process_entries == 0 { + return Err(BpfError::LoadError( + "nettrack max_process_entries must be greater than zero".to_string(), + )); + } + if self.has_scope(NetScope::Peer) + && (self.max_peer_entries == 0 || self.max_peer_tracks == 0) + { + return Err(BpfError::LoadError( + "nettrack peer entry and track limits must be greater than zero".to_string(), + )); + } + Ok(()) + } } pub struct Object { @@ -69,351 +255,715 @@ impl Object { where F: for<'a> FnMut(Message<'a>) -> i32 + 'bd, { - let nettrack = NetTrack::new( - &mut self.object, - self.config.clone(), - callback, - self.config.frequency, - )?; - Ok(nettrack) + self.config.validate()?; + NetTrack::new(&mut self.object, self.config.clone(), callback) } } #[repr(C)] -#[derive(Debug)] -pub struct NetEvent { - pub timestamp: u64, - pub tcp_send_bytes: u64, - pub tcp_recv_bytes: u64, - pub udp_send_bytes: u64, - pub udp_recv_bytes: u64, - pub total_send_bytes: u64, - pub total_recv_bytes: u64, - pub cpu: u32, -} - -unsafe impl plain::Plain for NetEvent {} - -#[derive(Debug)] -struct CounterIds { - tcp_send: u64, - tcp_recv: u64, - udp_send: u64, - udp_recv: u64, - total_send: u64, - total_recv: u64, -} - -impl CounterIds { - fn new() -> Self { - let mut rng = rand::thread_rng(); +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct StatsValue { + send_bytes: u64, + recv_bytes: u64, + send_operations: u64, + recv_operations: u64, + send_errors: u64, + recv_errors: u64, + send_would_block: u64, + recv_would_block: u64, + tcp_recv_eof: u64, + udp_recv_truncated: u64, + first_seen_ns: u64, + last_seen_ns: u64, +} + +unsafe impl plain::Plain for StatsValue {} + +impl StatsValue { + fn accumulate(&mut self, other: Self) { + self.send_bytes = self.send_bytes.saturating_add(other.send_bytes); + self.recv_bytes = self.recv_bytes.saturating_add(other.recv_bytes); + self.send_operations = self.send_operations.saturating_add(other.send_operations); + self.recv_operations = self.recv_operations.saturating_add(other.recv_operations); + self.send_errors = self.send_errors.saturating_add(other.send_errors); + self.recv_errors = self.recv_errors.saturating_add(other.recv_errors); + self.send_would_block = self.send_would_block.saturating_add(other.send_would_block); + self.recv_would_block = self.recv_would_block.saturating_add(other.recv_would_block); + self.tcp_recv_eof = self.tcp_recv_eof.saturating_add(other.tcp_recv_eof); + self.udp_recv_truncated = self + .udp_recv_truncated + .saturating_add(other.udp_recv_truncated); + if self.first_seen_ns == 0 + || (other.first_seen_ns != 0 && other.first_seen_ns < self.first_seen_ns) + { + self.first_seen_ns = other.first_seen_ns; + } + self.last_seen_ns = self.last_seen_ns.max(other.last_seen_ns); + } + + fn delta(self, previous: Option) -> Self { + let Some(previous) = previous.filter(|p| p.first_seen_ns == self.first_seen_ns) else { + return self; + }; Self { - tcp_send: rng.gen(), - tcp_recv: rng.gen(), - udp_send: rng.gen(), - udp_recv: rng.gen(), - total_send: rng.gen(), - total_recv: rng.gen(), + send_bytes: self.send_bytes.saturating_sub(previous.send_bytes), + recv_bytes: self.recv_bytes.saturating_sub(previous.recv_bytes), + send_operations: self + .send_operations + .saturating_sub(previous.send_operations), + recv_operations: self + .recv_operations + .saturating_sub(previous.recv_operations), + send_errors: self.send_errors.saturating_sub(previous.send_errors), + recv_errors: self.recv_errors.saturating_sub(previous.recv_errors), + send_would_block: self + .send_would_block + .saturating_sub(previous.send_would_block), + recv_would_block: self + .recv_would_block + .saturating_sub(previous.recv_would_block), + tcp_recv_eof: self.tcp_recv_eof.saturating_sub(previous.tcp_recv_eof), + udp_recv_truncated: self + .udp_recv_truncated + .saturating_sub(previous.udp_recv_truncated), + first_seen_ns: self.first_seen_ns, + last_seen_ns: self.last_seen_ns, } } } -#[derive(Default, Clone)] -struct NetStats { - tcp_send_total: u64, - tcp_recv_total: u64, - udp_send_total: u64, - udp_recv_total: u64, - last_timestamp: u64, +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +struct ProcessKey { + tgid: u32, + protocol: u8, + padding: [u8; 3], } -impl NetStats { - fn accumulate(&mut self, event: &NetEvent) { - self.tcp_send_total += event.tcp_send_bytes; - self.tcp_recv_total += event.tcp_recv_bytes; - self.udp_send_total += event.udp_send_bytes; - self.udp_recv_total += event.udp_recv_bytes; - self.last_timestamp = event.timestamp; - } +unsafe impl plain::Plain for ProcessKey {} - fn reset(&mut self) { - self.tcp_send_total = 0; - self.tcp_recv_total = 0; - self.udp_send_total = 0; - self.udp_recv_total = 0; +#[repr(C)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +struct PeerKey { + tgid: u32, + protocol: u8, + family: u8, + local_port: u16, + remote_port: u16, + padding: u16, + remote_addr: [u8; 16], +} + +unsafe impl plain::Plain for PeerKey {} + +impl PeerKey { + fn protocol(self) -> Option { + NetProtocol::from_index(self.protocol) } - fn total_send(&self) -> u64 { - self.tcp_send_total + self.udp_send_total + fn remote_ip(self) -> Option { + match self.family { + AF_INET => Some(IpAddr::V4(Ipv4Addr::new( + self.remote_addr[0], + self.remote_addr[1], + self.remote_addr[2], + self.remote_addr[3], + ))), + AF_INET6 => Some(IpAddr::V6(Ipv6Addr::from(self.remote_addr))), + _ => None, + } } - fn total_recv(&self) -> u64 { - self.tcp_recv_total + self.udp_recv_total + fn track_name(self) -> String { + let protocol = self.protocol().map(NetProtocol::name).unwrap_or("network"); + match self.remote_ip() { + Some(IpAddr::V4(ip)) => format!("{protocol} {ip}:{}", self.remote_port), + Some(IpAddr::V6(ip)) => format!("{protocol} [{ip}]:{}", self.remote_port), + None => format!("{protocol} unknown-peer"), + } } } -impl<'a> TryFrom<&'a [u8]> for &'a NetEvent { - type Error = BpfError; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ScopeKey { + Host, + Process(u32), + Peer(PeerKey), +} - fn try_from(data: &'a [u8]) -> Result { - plain::from_bytes(data) - .map_err(|e| BpfError::MapError(format!("failed to parse net event: {:?}", e))) - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum CounterKind { + SendBytes, + RecvBytes, + SendOperations, + RecvOperations, + SendErrors, + RecvErrors, + SendWouldBlock, + RecvWouldBlock, + TcpRecvEof, + UdpRecvTruncated, + TotalSendBytes, + TotalRecvBytes, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct SeriesKey { + scope: ScopeKey, + protocol: NetProtocol, + kind: CounterKind, +} + +struct CounterSample { + kind: CounterKind, + name: String, + value: f64, + unit: &'static str, } pub struct NetTrack<'this, F> { - #[allow(dead_code)] skel: NettrackSkel<'this>, - ringbuf: libbpf_rs::RingBuffer<'this>, - _perf_links: Vec, - _phantom: PhantomData, + callback: F, + config: NetTrackConfig, + last_sample_ns: u64, + last_peer_sample_ns: u64, + global_previous: [StatsValue; 2], + process_previous: HashMap, + peer_previous: HashMap, + series_ids: HashMap, + submitted_tracks: HashSet, + peer_track_ids: HashMap, } impl<'this, F> NetTrack<'this, F> where F: for<'a> FnMut(Message<'a>) -> i32 + 'this, { - fn load_and_attach_skel( + fn new( open_object: &'this mut MaybeUninit, - config: &NetTrackConfig, - ) -> Result, BpfError> { + config: NetTrackConfig, + callback: F, + ) -> Result { let skel_builder = NettrackSkelBuilder::default(); - let mut open_skel = skel_builder .open(open_object) - .map_err(|e| BpfError::LoadError(format!("failed to open bpf skeleton: {}", e)))?; + .map_err(|e| BpfError::LoadError(format!("failed to open bpf skeleton: {e}")))?; + let process_entries = if config.has_scope(NetScope::Process) { + config.max_process_entries + } else { + 1 + }; + let peer_entries = if config.has_scope(NetScope::Peer) { + config.max_peer_entries + } else { + 1 + }; + open_skel + .maps + .process_stats + .set_max_entries(process_entries) + .map_err(|e| BpfError::LoadError(format!("failed to size process map: {e}")))?; open_skel .maps - .events - .set_max_entries(config.ringbuf as u32) - .map_err(|e| BpfError::LoadError(format!("failed to set ring buffer size: {}", e)))?; + .peer_stats + .set_max_entries(peer_entries) + .map_err(|e| BpfError::LoadError(format!("failed to size peer map: {e}")))?; + + let filter_enabled = !config.pid_filters.is_empty() || !config.filter_process.is_empty(); + let rodata = open_skel.maps.rodata_data.as_mut().unwrap(); + rodata + .cfg + .tcp_enabled + .write(config.has_protocol(NetProtocol::Tcp)); + rodata + .cfg + .udp_enabled + .write(config.has_protocol(NetProtocol::Udp)); + rodata + .cfg + .send_enabled + .write(config.has_direction(NetDirection::Send)); + rodata + .cfg + .receive_enabled + .write(config.has_direction(NetDirection::Receive)); + rodata + .cfg + .host_enabled + .write(config.has_scope(NetScope::Host)); + rodata + .cfg + .process_enabled + .write(config.has_scope(NetScope::Process)); + rodata + .cfg + .peer_enabled + .write(config.has_scope(NetScope::Peer)); + rodata + .cfg + .bytes_enabled + .write(config.has_metric(NetMetric::Bytes)); + rodata + .cfg + .operations_enabled + .write(config.has_metric(NetMetric::Operations)); + rodata + .cfg + .errors_enabled + .write(config.has_metric(NetMetric::Errors)); + rodata.cfg.filter_enabled.write(filter_enabled); + + let tcp_enabled = config.has_protocol(NetProtocol::Tcp); + let udp_enabled = config.has_protocol(NetProtocol::Udp); + let send_enabled = config.has_direction(NetDirection::Send); + let receive_enabled = config.has_direction(NetDirection::Receive); + open_skel + .progs + .trace_tcp_sendmsg + .set_autoload(tcp_enabled && send_enabled); + open_skel + .progs + .trace_tcp_recvmsg + .set_autoload(tcp_enabled && receive_enabled); + open_skel + .progs + .trace_udp_sendmsg + .set_autoload(udp_enabled && send_enabled); + open_skel + .progs + .trace_udp_recvmsg + .set_autoload(udp_enabled && receive_enabled); let mut skel = open_skel .load() - .map_err(|e| BpfError::LoadError(format!("failed to load bpf program: {}", e)))?; + .map_err(|e| BpfError::LoadError(format!("failed to load bpf program: {e}")))?; + + for &pid in &config.pid_filters { + let pid = u32::try_from(pid).map_err(|_| { + BpfError::LoadError(format!("nettrack PID filter must be positive: {pid}")) + })?; + skel.maps + .tracked_tgids + .update(&pid.to_ne_bytes(), &1u32.to_ne_bytes(), MapFlags::ANY) + .map_err(|e| BpfError::MapError(format!("failed to add PID filter: {e}")))?; + } skel.attach() - .map_err(|e| BpfError::AttachError(format!("failed to attach bpf programs: {}", e)))?; + .map_err(|e| BpfError::AttachError(format!("failed to attach bpf programs: {e}")))?; + + debug!( + protocols = ?config.protocols, + directions = ?config.directions, + scopes = ?config.scopes, + metrics = ?config.metrics, + frequency = config.frequency, + peer_frequency = config.peer_frequency, + "initialized map-based network tracking" + ); - Ok(skel) + let now = get_monotonic_timestamp(); + Ok(Self { + skel, + callback, + config, + last_sample_ns: now, + last_peer_sample_ns: now, + global_previous: [StatsValue::default(); 2], + process_previous: HashMap::new(), + peer_previous: HashMap::new(), + series_ids: HashMap::new(), + submitted_tracks: HashSet::new(), + peer_track_ids: HashMap::new(), + }) } - fn setup_perf_events( - skel: &mut NettrackSkel<'this>, - freq: u64, - ) -> Result, BpfError> { - let perf_fds = - perf_event::perf_event_per_cpu(PERF_TYPE_SOFTWARE, PERF_COUNT_SW_CPU_CLOCK, freq) - .map_err(|e| BpfError::LoadError(format!("failed to open perf events: {}", e)))?; + fn parse_value(data: &[u8], kind: &str) -> Result { + plain::from_bytes::(data) + .copied() + .map_err(|e| BpfError::MapError(format!("failed to parse {kind}: {e:?}"))) + } - let prog = &mut skel.progs.handle_boundary_event; - let links = perf_event::attach_perf_event(&perf_fds, prog) - .map_err(|e| BpfError::AttachError(format!("failed to attach perf events: {}", e)))?; + fn parse_key(data: &[u8], kind: &str) -> Result { + plain::from_bytes::(data) + .copied() + .map_err(|e| BpfError::MapError(format!("failed to parse {kind}: {e:?}"))) + } - Ok(links) + fn read_global(&self, protocol: NetProtocol) -> Result { + let key = (protocol.index() as u32).to_ne_bytes(); + let values = self + .skel + .maps + .global_stats + .lookup_percpu(&key, MapFlags::ANY) + .map_err(|e| BpfError::MapError(format!("failed to read host network map: {e}")))? + .unwrap_or_default(); + let mut total = StatsValue::default(); + for value in values { + total.accumulate(Self::parse_value(&value, "host network statistics")?); + } + Ok(total) } - fn new( - open_object: &'this mut MaybeUninit, - config: NetTrackConfig, - callback: F, - freq: u64, - ) -> Result { - let mut skel = Self::load_and_attach_skel(open_object, &config)?; - let counter_ids = CounterIds::new(); - let ringbuf = Self::create_ring_buffer(&skel, callback, counter_ids, config.scaled)?; - let perf_links = Self::setup_perf_events(&mut skel, freq)?; + fn read_processes(&self) -> Result, BpfError> { + self.skel + .maps + .process_stats + .lookup_batch(MAP_BATCH_SIZE, MapFlags::ANY, MapFlags::ANY) + .map_err(|e| BpfError::MapError(format!("failed to read process network map: {e}")))? + .map(|(key, value)| { + Ok(( + Self::parse_key(&key, "process network key")?, + Self::parse_value(&value, "process network statistics")?, + )) + }) + .collect() + } - Ok(NetTrack { - skel, - ringbuf, - _perf_links: perf_links, - _phantom: PhantomData, - }) + fn read_peers(&self) -> Result, BpfError> { + self.skel + .maps + .peer_stats + .lookup_batch(MAP_BATCH_SIZE, MapFlags::ANY, MapFlags::ANY) + .map_err(|e| BpfError::MapError(format!("failed to read peer network map: {e}")))? + .map(|(key, value)| { + Ok(( + Self::parse_key(&key, "peer network key")?, + Self::parse_value(&value, "peer network statistics")?, + )) + }) + .collect() } - fn create_counters<'a>( - stats: &NetStats, - ids: &CounterIds, - scaled: bool, - time_diff_s: Option, - ) -> Vec<(&'a str, f64, u64)> { - if scaled { - let time_diff = time_diff_s.unwrap_or(1.0); - vec![ - ( - "tcp_send_rate", - (stats.tcp_send_total as f64 * 8.0) / time_diff, - ids.tcp_send, - ), + fn samples_for( + &self, + protocol: NetProtocol, + stats: StatsValue, + elapsed_seconds: f64, + ) -> Vec { + let prefix = protocol.name(); + let mut samples = Vec::new(); + if self.config.has_metric(NetMetric::Bytes) { + let (send_name, recv_name, send_value, recv_value, unit) = if self.config.scaled { ( - "tcp_recv_rate", - (stats.tcp_recv_total as f64 * 8.0) / time_diff, - ids.tcp_recv, - ), - ( - "udp_send_rate", - (stats.udp_send_total as f64 * 8.0) / time_diff, - ids.udp_send, - ), + format!("{prefix}_send_rate"), + format!("{prefix}_recv_rate"), + stats.send_bytes as f64 * 8.0 / elapsed_seconds, + stats.recv_bytes as f64 * 8.0 / elapsed_seconds, + "bits/s", + ) + } else { ( - "udp_recv_rate", - (stats.udp_recv_total as f64 * 8.0) / time_diff, - ids.udp_recv, - ), + format!("{prefix}_send"), + format!("{prefix}_recv"), + stats.send_bytes as f64, + stats.recv_bytes as f64, + "bytes", + ) + }; + samples.push(CounterSample { + kind: CounterKind::SendBytes, + name: send_name, + value: send_value, + unit, + }); + samples.push(CounterSample { + kind: CounterKind::RecvBytes, + name: recv_name, + value: recv_value, + unit, + }); + } + if self.config.has_metric(NetMetric::Operations) { + let operation = if protocol == NetProtocol::Udp { + "datagrams" + } else { + "calls" + }; + samples.push(CounterSample { + kind: CounterKind::SendOperations, + name: format!("{prefix}_send_{operation}"), + value: stats.send_operations as f64, + unit: "count", + }); + samples.push(CounterSample { + kind: CounterKind::RecvOperations, + name: format!("{prefix}_recv_{operation}"), + value: stats.recv_operations as f64, + unit: "count", + }); + if protocol == NetProtocol::Tcp { + samples.push(CounterSample { + kind: CounterKind::TcpRecvEof, + name: "tcp_recv_eof".to_string(), + value: stats.tcp_recv_eof as f64, + unit: "count", + }); + } else { + samples.push(CounterSample { + kind: CounterKind::UdpRecvTruncated, + name: "udp_recv_truncated".to_string(), + value: stats.udp_recv_truncated as f64, + unit: "count", + }); + } + } + if self.config.has_metric(NetMetric::Errors) { + for (kind, suffix, value) in [ + (CounterKind::SendErrors, "send_errors", stats.send_errors), + (CounterKind::RecvErrors, "recv_errors", stats.recv_errors), ( - "total_send_rate", - (stats.total_send() as f64 * 8.0) / time_diff, - ids.total_send, + CounterKind::SendWouldBlock, + "send_would_block", + stats.send_would_block, ), ( - "total_recv_rate", - (stats.total_recv() as f64 * 8.0) / time_diff, - ids.total_recv, + CounterKind::RecvWouldBlock, + "recv_would_block", + stats.recv_would_block, ), - ] - } else { - vec![ - ("tcp_send", stats.tcp_send_total as f64, ids.tcp_send), - ("tcp_recv", stats.tcp_recv_total as f64, ids.tcp_recv), - ("udp_send", stats.udp_send_total as f64, ids.udp_send), - ("udp_recv", stats.udp_recv_total as f64, ids.udp_recv), - ("total_send", stats.total_send() as f64, ids.total_send), - ("total_recv", stats.total_recv() as f64, ids.total_recv), - ] + ] { + samples.push(CounterSample { + kind, + name: format!("{prefix}_{suffix}"), + value: value as f64, + unit: "count", + }); + } + } + samples + } + + fn ensure_peer_track(&mut self, key: PeerKey) -> Option { + if let Some(id) = self.peer_track_ids.get(&key) { + return Some(*id); } + if self.peer_track_ids.len() >= self.config.max_peer_tracks { + return None; + } + let id = rand::thread_rng().gen::(); + let name = key.track_name(); + let result = (self.callback)(Message::Event(Event::Track(Track { + name: &name, + track_type: TrackType::Custom { id }, + parent: Some(TrackType::Process { + pid: key.tgid as i32, + }), + }))); + if result != 0 { + return None; + } + self.peer_track_ids.insert(key, id); + Some(id) } - fn submit_counter( - name: &str, - value: f64, - id: u64, + fn emit_sample( + &mut self, + scope: ScopeKey, + protocol: NetProtocol, + sample: CounterSample, timestamp: u64, - submitted_tracks: &mut HashMap, - callback: &mut G, - unit: &str, - ) -> i32 - where - G: FnMut(Message) -> i32, - { + ) -> i32 { + let parent = match scope { + ScopeKey::Host => None, + ScopeKey::Process(tgid) => Some(TrackType::Process { pid: tgid as i32 }), + ScopeKey::Peer(key) => { + let Some(id) = self.ensure_peer_track(key) else { + return 0; + }; + Some(TrackType::Custom { id }) + } + }; + let series_key = SeriesKey { + scope, + protocol, + kind: sample.kind, + }; + let id = *self + .series_ids + .entry(series_key) + .or_insert_with(|| rand::thread_rng().gen::()); let track_id = TrackId::Counter { id }; - - if submitted_tracks.insert(track_id, ()).is_none() { - let track = Message::Event(Event::Track(Track { - name, + if self.submitted_tracks.insert(track_id) { + let result = (self.callback)(Message::Event(Event::Track(Track { + name: &sample.name, track_type: TrackType::Counter { id, - unit: Some(unit), + unit: Some(sample.unit), }, - parent: None, - })); - let result = callback(track); + parent, + }))); if result != 0 { return result; } } debug!( - name = name, - value = value, - timestamp = timestamp, + name = sample.name, + value = sample.value, + timestamp, "emitting network counter" ); - - callback(Message::Event(Event::Counter(Counter { - name, - value, + (self.callback)(Message::Event(Event::Counter(Counter { + name: &sample.name, + value: sample.value, timestamp, track_id, labels: Cow::Owned(Labels::new()), - unit: Some(unit), + unit: Some(sample.unit), }))) } - fn create_ring_buffer( - skel: &NettrackSkel<'this>, - mut callback: F, - counter_ids: CounterIds, - scaled: bool, - ) -> Result, BpfError> { - let mut submitted_tracks: HashMap = HashMap::new(); - - let mut builder = RingBufferBuilder::new(); - let nprocs = libbpf_rs::num_possible_cpus().unwrap(); - let mut boundaries_reported = 0; - let mut stats = NetStats::default(); - let mut prev_timestamp = 0u64; - - builder - .add(&skel.maps.events, move |data| { - let net_event: &NetEvent = data.try_into().unwrap(); - stats.accumulate(net_event); - boundaries_reported += 1; - - if boundaries_reported == nprocs { - boundaries_reported = 0; - - if scaled && prev_timestamp == 0 { - prev_timestamp = stats.last_timestamp; - stats.reset(); - return 0; - } + fn emit_stats( + &mut self, + scope: ScopeKey, + protocol: NetProtocol, + stats: StatsValue, + elapsed_seconds: f64, + timestamp: u64, + ) -> i32 { + for sample in self.samples_for(protocol, stats, elapsed_seconds) { + let result = self.emit_sample(scope, protocol, sample, timestamp); + if result != 0 { + return result; + } + } + 0 + } - let time_diff_s = if scaled && prev_timestamp > 0 { - let time_diff_ns = stats.last_timestamp.saturating_sub(prev_timestamp); - if time_diff_ns > 0 { - Some(time_diff_ns as f64 / 1_000_000_000.0) - } else { - None - } - } else { - None - }; - - let counters = Self::create_counters(&stats, &counter_ids, scaled, time_diff_s); - let unit = if scaled { "bits/s" } else { "bytes" }; - - for (name, value, id) in counters { - let result = Self::submit_counter( - name, - value, - id, - stats.last_timestamp, - &mut submitted_tracks, - &mut callback, - unit, - ); - if result != 0 { - return result; - } + fn sample_host_and_processes(&mut self, now: u64) -> Result<(), BpfError> { + let elapsed_ns = now.saturating_sub(self.last_sample_ns); + let elapsed_seconds = (elapsed_ns.max(1) as f64) / 1_000_000_000.0; + + if self.config.has_scope(NetScope::Host) { + let mut host_delta = StatsValue::default(); + for protocol in [NetProtocol::Tcp, NetProtocol::Udp] { + if !self.config.has_protocol(protocol) { + continue; + } + let current = self.read_global(protocol)?; + let previous = self.global_previous[protocol.index()]; + let delta = current.delta(Some(previous)); + host_delta.accumulate(delta); + self.global_previous[protocol.index()] = current; + if self.emit_stats(ScopeKey::Host, protocol, delta, elapsed_seconds, now) != 0 { + break; + } + } + if self.config.has_metric(NetMetric::Bytes) { + let (send_name, recv_name, send_value, recv_value, unit) = if self.config.scaled { + ( + "total_send_rate", + "total_recv_rate", + host_delta.send_bytes as f64 * 8.0 / elapsed_seconds, + host_delta.recv_bytes as f64 * 8.0 / elapsed_seconds, + "bits/s", + ) + } else { + ( + "total_send", + "total_recv", + host_delta.send_bytes as f64, + host_delta.recv_bytes as f64, + "bytes", + ) + }; + for sample in [ + CounterSample { + kind: CounterKind::TotalSendBytes, + name: send_name.to_string(), + value: send_value, + unit, + }, + CounterSample { + kind: CounterKind::TotalRecvBytes, + name: recv_name.to_string(), + value: recv_value, + unit, + }, + ] { + if self.emit_sample(ScopeKey::Host, NetProtocol::Tcp, sample, now) != 0 { + break; } + } + } + } - prev_timestamp = stats.last_timestamp; - stats.reset(); + if self.config.has_scope(NetScope::Process) { + for (key, current) in self.read_processes()? { + let Some(protocol) = NetProtocol::from_index(key.protocol) else { + continue; + }; + let previous = self.process_previous.insert(key, current); + let delta = current.delta(previous); + if self.emit_stats( + ScopeKey::Process(key.tgid), + protocol, + delta, + elapsed_seconds, + now, + ) != 0 + { + break; } + } + } - 0 - }) - .map_err(|e| BpfError::MapError(format!("failed to add ring buffer: {}", e)))?; + self.last_sample_ns = now; + Ok(()) + } - builder - .build() - .map_err(|e| BpfError::MapError(format!("failed to build ring buffer: {}", e))) + fn sample_peers(&mut self, now: u64) -> Result<(), BpfError> { + let elapsed_ns = now.saturating_sub(self.last_peer_sample_ns); + let elapsed_seconds = (elapsed_ns.max(1) as f64) / 1_000_000_000.0; + for (key, current) in self.read_peers()? { + let Some(protocol) = key.protocol() else { + continue; + }; + let previous = self.peer_previous.insert(key, current); + let delta = current.delta(previous); + if self.emit_stats(ScopeKey::Peer(key), protocol, delta, elapsed_seconds, now) != 0 { + break; + } + } + self.last_peer_sample_ns = now; + Ok(()) } - pub fn poll(&mut self, timeout: Duration) -> Result<(), BpfError> { - self.ringbuf - .poll(timeout) - .map_err(|e| BpfError::MapError(format!("failed to poll ring buffer: {}", e)))?; + fn sample_if_due(&mut self, force: bool) -> Result<(), BpfError> { + let now = get_monotonic_timestamp(); + let sample_interval = 1_000_000_000u64 / self.config.frequency; + if force || now.saturating_sub(self.last_sample_ns) >= sample_interval { + self.sample_host_and_processes(now)?; + } + if self.config.has_scope(NetScope::Peer) { + let peer_interval = 1_000_000_000u64 / self.config.peer_frequency; + if force || now.saturating_sub(self.last_peer_sample_ns) >= peer_interval { + self.sample_peers(now)?; + } + } Ok(()) } + pub fn poll(&mut self, _timeout: Duration) -> Result<(), BpfError> { + self.sample_if_due(false) + } + pub fn consume(&mut self) -> Result<(), BpfError> { - self.ringbuf - .consume() - .map_err(|e| BpfError::MapError(format!("failed to consume ring buffer: {}", e)))?; - Ok(()) + self.sample_if_due(false) + } + + pub fn flush(&mut self) -> Result<(), BpfError> { + self.sample_if_due(true) + } + + pub fn add_pid_filter(&mut self, pid: u32) -> Result<(), BpfError> { + self.skel + .maps + .tracked_tgids + .update(&pid.to_ne_bytes(), &1u32.to_ne_bytes(), MapFlags::ANY) + .map_err(|e| BpfError::MapError(format!("failed to add PID filter: {e}"))) } } @@ -421,179 +971,247 @@ impl<'this, F> Filterable for NetTrack<'this, F> where F: for<'a> FnMut(Message<'a>) -> i32 + 'this, { - fn filter(&mut self, _pid: i32) -> Result<(), BpfError> { - Ok(()) + fn filter(&mut self, pid: i32) -> Result<(), BpfError> { + let pid = u32::try_from(pid) + .map_err(|_| BpfError::MapError(format!("invalid PID filter: {pid}")))?; + self.add_pid_filter(pid) + } +} + +impl fmt::Display for PeerKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.track_name()) } } #[cfg(test)] -mod root_tests { +mod tests { use super::*; - use rstest::*; + use std::cell::RefCell; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream, UdpSocket}; - use std::{ - cell::RefCell, - rc::Rc, - thread, - time::{Duration, Instant}, - }; - - fn is_root() -> bool { - unsafe { libc::geteuid() == 0 } - } - - #[derive(Debug, Clone)] - struct TestCounter { - name: String, - value: f64, - #[allow(dead_code)] - timestamp: u64, + use std::rc::Rc; + use std::thread; + + fn generate_test_traffic() -> (u16, u16) { + let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind TCP listener"); + let tcp_address = listener.local_addr().unwrap(); + let tcp_server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = vec![0u8; 4096]; + stream.read_exact(&mut request).unwrap(); + stream.write_all(&vec![1u8; 2048]).unwrap(); + }); + let mut tcp_client = TcpStream::connect(tcp_address).unwrap(); + tcp_client.write_all(&vec![2u8; 4096]).unwrap(); + let mut response = vec![0u8; 2048]; + tcp_client.read_exact(&mut response).unwrap(); + drop(tcp_client); + tcp_server.join().unwrap(); + + let udp_server_socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let udp_address = udp_server_socket.local_addr().unwrap(); + let udp_server = thread::spawn(move || { + let mut request = vec![0u8; 1000]; + let (size, peer) = udp_server_socket.recv_from(&mut request).unwrap(); + assert_eq!(size, request.len()); + assert_eq!( + udp_server_socket.send_to(&vec![3u8; 500], peer).unwrap(), + 500 + ); + }); + let udp_client = UdpSocket::bind("127.0.0.1:0").unwrap(); + assert_eq!( + udp_client.send_to(&vec![4u8; 1000], udp_address).unwrap(), + 1000 + ); + let mut udp_response = vec![0u8; 500]; + assert_eq!(udp_client.recv_from(&mut udp_response).unwrap().0, 500); + udp_server.join().unwrap(); + (tcp_address.port(), udp_address.port()) } - struct NetTrackFixture { - events: Rc>>, + #[test] + fn defaults_are_low_overhead() { + let config = NetTrackConfig::default(); + assert_eq!(config.scopes, vec![NetScope::Host]); + assert_eq!(config.metrics, vec![NetMetric::Bytes]); + assert_eq!(config.peer_frequency, 1); } - #[fixture] - fn nettrack_setup() -> NetTrackFixture { - let events = Rc::new(RefCell::new(Vec::new())); - - NetTrackFixture { events } + #[test] + fn cumulative_values_produce_interval_deltas() { + let previous = StatsValue { + send_bytes: 100, + recv_bytes: 40, + first_seen_ns: 10, + ..StatsValue::default() + }; + let current = StatsValue { + send_bytes: 160, + recv_bytes: 75, + first_seen_ns: 10, + ..StatsValue::default() + }; + let delta = current.delta(Some(previous)); + assert_eq!(delta.send_bytes, 60); + assert_eq!(delta.recv_bytes, 35); } - fn create_test_callback(events: Rc>>) -> impl FnMut(Message) -> i32 { - move |message| { - if let Message::Event(Event::Counter(c)) = message { - let test_counter = TestCounter { - name: c.name.to_string(), - value: c.value, - timestamp: c.timestamp, - }; - events.borrow_mut().push(test_counter); - } - 0 - } + #[test] + fn changed_generation_uses_current_value() { + let previous = StatsValue { + send_bytes: 100, + first_seen_ns: 10, + ..StatsValue::default() + }; + let current = StatsValue { + send_bytes: 20, + first_seen_ns: 20, + ..StatsValue::default() + }; + assert_eq!(current.delta(Some(previous)).send_bytes, 20); } - fn start_tcp_server(listener: TcpListener) -> thread::JoinHandle<()> { - thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("failed to accept connection"); - let mut buf = [0; 1024]; - while stream.read(&mut buf).unwrap_or(0) > 0 { - stream - .write_all(b"response") - .expect("failed to write response"); - } - }) + #[test] + fn peer_names_format_ipv4_and_ipv6() { + let ipv4 = PeerKey { + protocol: TCP_INDEX as u8, + family: AF_INET, + remote_port: 443, + remote_addr: [127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ..PeerKey::default() + }; + assert_eq!(ipv4.track_name(), "tcp 127.0.0.1:443"); + + let ipv6 = PeerKey { + protocol: UDP_INDEX as u8, + family: AF_INET6, + remote_port: 53, + remote_addr: Ipv6Addr::LOCALHOST.octets(), + ..PeerKey::default() + }; + assert_eq!(ipv6.track_name(), "udp [::1]:53"); } - fn start_udp_server(socket: UdpSocket) -> thread::JoinHandle<()> { - thread::spawn(move || { - let mut buf = [0; 1024]; - loop { - if let Ok((size, src)) = socket.recv_from(&mut buf) { - if size == 0 { - break; - } - socket - .send_to(&buf[..size], src) - .expect("failed to send UDP response"); - } - } - }) + #[test] + fn invalid_configuration_is_rejected() { + let config = NetTrackConfig { + frequency: 0, + ..NetTrackConfig::default() + }; + assert!(config.validate().is_err()); } - fn generate_network_traffic<'b, F>( - tcp_addr: std::net::SocketAddr, - udp_addr: std::net::SocketAddr, - duration: Duration, - nettrack: &mut NetTrack<'b, F>, - ) where - F: for<'a> FnMut(Message<'a>) -> i32 + 'b, - { - let start = Instant::now(); - let data = vec![b'x'; 1024]; - - while start.elapsed() < duration { - if let Ok(mut tcp_stream) = TcpStream::connect(tcp_addr) { - let _ = tcp_stream.write_all(&data); - let mut response = [0; 1024]; - let _ = tcp_stream.read(&mut response); + #[test] + #[ignore = "requires root"] + fn root_tests_exact_tcp_and_udp_payload_bytes() { + assert_eq!(unsafe { libc::geteuid() }, 0); + + let counters = Rc::new(RefCell::new(HashMap::::new())); + let callback_counters = counters.clone(); + let callback = move |message: Message<'_>| { + if let Message::Event(Event::Counter(counter)) = message { + callback_counters + .borrow_mut() + .insert(counter.name.to_string(), counter.value); } - - let udp_client = UdpSocket::bind("127.0.0.1:0").expect("failed to bind UDP client"); - udp_client - .send_to(&data, udp_addr) - .expect("failed to send UDP data"); - let mut response = [0; 1024]; - let _ = udp_client.recv(&mut response); - - let _ = nettrack.consume(); - } + 0 + }; + let config = NetTrackConfig { + scopes: vec![NetScope::Host], + metrics: vec![NetMetric::Bytes], + pid_filters: vec![unsafe { libc::getpid() }], + ..NetTrackConfig::default() + }; + let mut object = Object::new(config); + let mut tracker = object.build(callback).expect("failed to load nettrack"); + + generate_test_traffic(); + + tracker.flush().expect("failed to sample network maps"); + let counters = counters.borrow(); + assert_eq!(counters.get("tcp_send"), Some(&6144.0)); + assert_eq!(counters.get("tcp_recv"), Some(&6144.0)); + assert_eq!(counters.get("udp_send"), Some(&1500.0)); + assert_eq!(counters.get("udp_recv"), Some(&1500.0)); + assert_eq!(counters.get("total_send"), Some(&7644.0)); + assert_eq!(counters.get("total_recv"), Some(&7644.0)); } - fn verify_events(events: &[TestCounter]) { - let event_types = [ - "tcp_send", - "tcp_recv", - "udp_send", - "udp_recv", - "total_send", - "total_recv", - ]; - - for event_type in &event_types { - let type_events: Vec<_> = events.iter().filter(|c| c.name == *event_type).collect(); - assert!(!type_events.is_empty(), "no {} events", event_type); - } - - for event in events { - assert!(event.value >= 0.0, "bytes should not be negative"); - } + #[derive(Default)] + struct ScopedCapture { + process_counter_ids: HashSet, + process_counters: HashMap, + peer_tracks: Vec, } - #[rstest] + #[test] #[ignore = "requires root"] - fn test_nettrack_tcp_udp_rates(nettrack_setup: NetTrackFixture) { - assert!(is_root()); - + fn root_tests_process_and_peer_scopes() { + assert_eq!(unsafe { libc::geteuid() }, 0); + + let capture = Rc::new(RefCell::new(ScopedCapture::default())); + let callback_capture = capture.clone(); + let callback = move |message: Message<'_>| { + let mut capture = callback_capture.borrow_mut(); + if let Message::Event(event) = message { + match event { + Event::Track(track) => match track.track_type { + TrackType::Custom { .. } + if matches!(track.parent, Some(TrackType::Process { .. })) => + { + capture.peer_tracks.push(track.name.to_string()); + } + TrackType::Counter { id, .. } + if matches!(track.parent, Some(TrackType::Process { .. })) => + { + capture.process_counter_ids.insert(TrackId::Counter { id }); + } + _ => {} + }, + Event::Counter(counter) + if capture.process_counter_ids.contains(&counter.track_id) => + { + capture + .process_counters + .insert(counter.name.to_string(), counter.value); + } + _ => {} + } + } + 0 + }; let config = NetTrackConfig { - frequency: 100, - ringbuf: default_ringbuf_size(), - scaled: false, + scopes: vec![NetScope::Process, NetScope::Peer], + metrics: vec![NetMetric::Bytes], + pid_filters: vec![unsafe { libc::getpid() }], + max_peer_tracks: 16, + ..NetTrackConfig::default() }; - let mut object = Object::new(config); - let callback = create_test_callback(nettrack_setup.events.clone()); - let mut nettrack = object.build(callback).expect("failed to build nettrack"); - - let tcp_listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind TCP listener"); - let tcp_addr = tcp_listener - .local_addr() - .expect("failed to get TCP address"); - - let udp_socket = UdpSocket::bind("127.0.0.1:0").expect("failed to bind UDP socket"); - let udp_addr = udp_socket.local_addr().expect("failed to get UDP address"); - - let tcp_thread = start_tcp_server(tcp_listener); - let udp_thread = start_udp_server(udp_socket); - - thread::sleep(Duration::from_millis(100)); - generate_network_traffic( - tcp_addr, - udp_addr, - Duration::from_millis(500), - &mut nettrack, + let mut tracker = object.build(callback).expect("failed to load nettrack"); + let (tcp_port, udp_port) = generate_test_traffic(); + tracker.flush().expect("failed to sample network maps"); + + let capture = capture.borrow(); + assert_eq!(capture.process_counters.get("tcp_send"), Some(&6144.0)); + assert_eq!(capture.process_counters.get("tcp_recv"), Some(&6144.0)); + assert_eq!(capture.process_counters.get("udp_send"), Some(&1500.0)); + assert_eq!(capture.process_counters.get("udp_recv"), Some(&1500.0)); + assert!( + capture + .peer_tracks + .contains(&format!("tcp 127.0.0.1:{tcp_port}")), + "missing TCP peer track: {:?}", + capture.peer_tracks + ); + assert!( + capture + .peer_tracks + .contains(&format!("udp 127.0.0.1:{udp_port}")), + "missing UDP peer track: {:?}", + capture.peer_tracks ); - thread::sleep(Duration::from_millis(200)); - let _ = nettrack.consume(); - - drop(tcp_thread); - drop(udp_thread); - - let collected_events = nettrack_setup.events.borrow(); - assert!(!collected_events.is_empty(), "no events were captured"); - verify_events(&collected_events); } } diff --git a/bpf/testdata/nettrack_e2e.toml b/bpf/testdata/nettrack_e2e.toml new file mode 100644 index 0000000..7b077d3 --- /dev/null +++ b/bpf/testdata/nettrack_e2e.toml @@ -0,0 +1,11 @@ +[bpf.nettrack] +frequency = 10 +peer_frequency = 5 +scaled = false +protocols = ["tcp", "udp"] +directions = ["send", "receive"] +scopes = ["host", "process", "peer"] +metrics = ["bytes", "operations", "errors"] +max_process_entries = 64 +max_peer_entries = 128 +max_peer_tracks = 32 diff --git a/reference.toml b/reference.toml index 5a8dba1..6b1abab 100644 --- a/reference.toml +++ b/reference.toml @@ -124,13 +124,13 @@ ringbuf = 262144 # Tracks network I/O rates for TCP and UDP traffic [bpf.nettrack] -# Sampling frequency in Hz (how often to collect network statistics) +# Host and process map sampling frequency in Hz # Default: 9 frequency = 9 -# Size of the ring buffer for network tracking events (in bytes) -# Default: 1048576 (1 MB) -ringbuf = 1048576 +# Peer maps are normally read less often to limit userspace and trace overhead +# Default: 1 +peer_frequency = 1 # Enable scaled mode to convert byte counts to bits/s rates # When true: reports network rates in bits per second (bits/s) @@ -138,6 +138,23 @@ ringbuf = 1048576 # Default: false scaled = false +# Protocols, directions, aggregation scopes, and metrics to collect. +# Low-overhead defaults are ["tcp", "udp"], ["send", "receive"], +# ["host"], and ["bytes"]. Process and peer aggregation are opt-in. +protocols = ["tcp", "udp"] +directions = ["send", "receive"] +scopes = ["host"] +metrics = ["bytes"] + +# Optional PID and process-name filters. Process names require thread_tracker. +pid_filters = [] +filter_process = [] + +# Bounded map and Perfetto cardinality controls +max_process_entries = 1024 +max_peer_entries = 4096 +max_peer_tracks = 512 + # --------------------------- # Performance Counter Module # --------------------------- @@ -195,4 +212,4 @@ random_process_id = true [[user]] socket = "/var/run/manytrace/debug.sock" -log_filter = "TRACE" \ No newline at end of file +log_filter = "TRACE" From 7785809fc5b86e577e345300b57bb528b1fdd971 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Tue, 1 Sep 2026 05:32:02 -0400 Subject: [PATCH 2/2] tracing-manytrace: add network profiling demo --- tracing-manytrace/examples/net_demo.rs | 253 +++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 tracing-manytrace/examples/net_demo.rs diff --git a/tracing-manytrace/examples/net_demo.rs b/tracing-manytrace/examples/net_demo.rs new file mode 100644 index 0000000..1b64a96 --- /dev/null +++ b/tracing-manytrace/examples/net_demo.rs @@ -0,0 +1,253 @@ +// Copyright (C) 2025 Category Labs, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use agent::AgentBuilder; +use clap::Parser; +use std::hint::black_box; +use std::io::{self, Read, Write}; +use std::net::{Shutdown, TcpListener, TcpStream, UdpSocket}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use tracing::{info, info_span, instrument, warn}; +use tracing_manytrace::{ManytraceLayer, TracingExtension}; +use tracing_subscriber::prelude::*; + +const TCP_PAYLOAD_BYTES: usize = 32 * 1024; +const UDP_PAYLOAD_BYTES: usize = 1200; +const TICK: Duration = Duration::from_millis(100); + +#[derive(Parser)] +#[command(about = "Instrumented TCP/UDP workload for a manytrace demo")] +struct Args { + #[arg(help = "Path to the manytrace agent socket")] + socket_path: String, + + #[arg(default_value_t = 12, help = "Workload duration in seconds")] + seconds: u64, +} + +#[inline(never)] +fn cpu_burn(seed: u64, iterations: usize) -> u64 { + let mut value = seed | 1; + for i in 0..iterations { + value = value + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(i as u64 ^ value.rotate_left(17)); + } + black_box(value) +} + +fn phase(elapsed: Duration) -> (&'static str, usize, usize) { + match elapsed.as_secs() % 4 { + 0 => ("warmup", 1, 2), + 1 => ("medium", 4, 8), + 2 => ("peak", 12, 24), + _ => ("cooldown", 2, 5), + } +} + +#[instrument(skip(listener))] +fn tcp_server(listener: TcpListener) -> io::Result<()> { + let (mut stream, peer) = listener.accept()?; + info!(%peer, "TCP peer accepted"); + let mut buffer = vec![0u8; TCP_PAYLOAD_BYTES]; + + loop { + match stream.read_exact(&mut buffer) { + Ok(()) => stream.write_all(&buffer)?, + Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break, + Err(error) => return Err(error), + } + } + + info!("TCP server observed client EOF"); + Ok(()) +} + +#[instrument(skip(stream))] +fn tcp_client(mut stream: TcpStream, duration: Duration) -> io::Result<()> { + let mut probe = [0u8; 1]; + stream.set_nonblocking(true)?; + if let Err(error) = stream.read(&mut probe) { + info!(kind = ?error.kind(), "expected nonblocking TCP receive error"); + } + stream.set_nonblocking(false)?; + + let payload = vec![0x5a; TCP_PAYLOAD_BYTES]; + let mut echo = vec![0u8; TCP_PAYLOAD_BYTES]; + let started = Instant::now(); + let deadline = started + duration; + + while Instant::now() < deadline { + let tick_started = Instant::now(); + let elapsed = tick_started.duration_since(started); + let (phase_name, tcp_bursts, _) = phase(elapsed); + let span = info_span!( + "tcp_burst", + phase = phase_name, + bursts = tcp_bursts, + payload_bytes = TCP_PAYLOAD_BYTES + ); + let _entered = span.enter(); + for _ in 0..tcp_bursts { + stream.write_all(&payload)?; + stream.read_exact(&mut echo)?; + } + black_box(cpu_burn(elapsed.as_nanos() as u64, 80_000)); + if let Some(remaining) = TICK.checked_sub(tick_started.elapsed()) { + thread::sleep(remaining); + } + } + + stream.shutdown(Shutdown::Write)?; + let eof_bytes = stream.read(&mut probe)?; + info!(eof_bytes, "TCP client observed server EOF"); + Ok(()) +} + +#[instrument(skip(socket, running))] +fn udp_server(socket: UdpSocket, running: Arc) -> io::Result<()> { + socket.set_read_timeout(Some(TICK))?; + let mut buffer = [0u8; UDP_PAYLOAD_BYTES]; + while running.load(Ordering::Relaxed) { + match socket.recv_from(&mut buffer) { + Ok((size, peer)) => { + socket.send_to(&buffer[..size], peer)?; + } + Err(error) + if matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ) => {} + Err(error) => return Err(error), + } + } + Ok(()) +} + +#[instrument(skip(socket))] +fn udp_client(socket: UdpSocket, peer: std::net::SocketAddr, duration: Duration) -> io::Result<()> { + socket.connect(peer)?; + let mut probe = [0u8; UDP_PAYLOAD_BYTES]; + socket.set_nonblocking(true)?; + if let Err(error) = socket.recv(&mut probe) { + info!(kind = ?error.kind(), "expected nonblocking UDP receive error"); + } + socket.set_nonblocking(false)?; + socket.set_read_timeout(Some(Duration::from_secs(1)))?; + + if let Err(error) = socket.send(&vec![0u8; 70_000]) { + info!(kind = ?error.kind(), "expected oversized UDP send error"); + } + + let payload = [0xa5; UDP_PAYLOAD_BYTES]; + let started = Instant::now(); + let deadline = started + duration; + while Instant::now() < deadline { + let tick_started = Instant::now(); + let elapsed = tick_started.duration_since(started); + let (phase_name, _, udp_datagrams) = phase(elapsed); + let span = info_span!( + "udp_burst", + phase = phase_name, + datagrams = udp_datagrams, + payload_bytes = UDP_PAYLOAD_BYTES + ); + let _entered = span.enter(); + for _ in 0..udp_datagrams { + socket.send(&payload)?; + let received = socket.recv(&mut probe)?; + if received != payload.len() { + warn!(received, expected = payload.len(), "short UDP echo"); + } + } + if let Some(remaining) = TICK.checked_sub(tick_started.elapsed()) { + thread::sleep(remaining); + } + } + Ok(()) +} + +#[instrument] +fn cpu_worker(duration: Duration) { + let started = Instant::now(); + let deadline = started + duration; + let mut checksum = 1u64; + while Instant::now() < deadline { + let span = info_span!("compute_batch", iteration = checksum & 0xff); + let _entered = span.enter(); + checksum ^= cpu_burn(checksum, 1_500_000); + thread::yield_now(); + } + info!(checksum, "CPU worker complete"); +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let extension = Arc::new(TracingExtension::new()); + let _agent = AgentBuilder::new(args.socket_path) + .register_tracing(Box::new((*extension).clone())) + .build()?; + + tracing_subscriber::registry() + .with(ManytraceLayer::new(extension.clone())) + .with(tracing_subscriber::fmt::layer()) + .init(); + + info!("waiting for manytrace to connect"); + while !extension.is_active() { + thread::sleep(Duration::from_millis(50)); + } + + let duration = Duration::from_secs(args.seconds); + info!(seconds = args.seconds, "starting network workload"); + + let tcp_listener = TcpListener::bind("127.0.0.1:0")?; + let tcp_address = tcp_listener.local_addr()?; + let udp_server_socket = UdpSocket::bind("127.0.0.1:0")?; + let udp_address = udp_server_socket.local_addr()?; + let udp_client_socket = UdpSocket::bind("127.0.0.1:0")?; + let udp_running = Arc::new(AtomicBool::new(true)); + + let tcp_server_handle = thread::Builder::new() + .name("tcp-server".to_string()) + .spawn(move || tcp_server(tcp_listener))?; + let tcp_client_handle = thread::Builder::new() + .name("tcp-client".to_string()) + .spawn(move || tcp_client(TcpStream::connect(tcp_address)?, duration))?; + let udp_running_server = udp_running.clone(); + let udp_server_handle = thread::Builder::new() + .name("udp-server".to_string()) + .spawn(move || udp_server(udp_server_socket, udp_running_server))?; + let udp_client_handle = thread::Builder::new() + .name("udp-client".to_string()) + .spawn(move || udp_client(udp_client_socket, udp_address, duration))?; + let cpu_handle = thread::Builder::new() + .name("cpu-worker".to_string()) + .spawn(move || cpu_worker(duration))?; + + tcp_client_handle.join().expect("TCP client panicked")?; + udp_client_handle.join().expect("UDP client panicked")?; + udp_running.store(false, Ordering::Relaxed); + tcp_server_handle.join().expect("TCP server panicked")?; + udp_server_handle.join().expect("UDP server panicked")?; + cpu_handle.join().expect("CPU worker panicked"); + + info!("network workload complete"); + thread::sleep(Duration::from_millis(250)); + Ok(()) +}