Skip to content

Commit 1bb25eb

Browse files
RoyLinRoyLin
authored andcommitted
release: v0.2.3 — ring-drop counter (data-loss visibility)
Events dropped on a full ring are counted in-kernel (PerCpuArray via a reserve_or_drop helper across all 7 emit sites) and surfaced in the 60s report as `dropped` — data loss is visible, not silent. Validated on KVM: dropped=0 under normal load (1096 events captured, 0 dropped), which also empirically confirms the 20ms poll keeps up without drops.
1 parent 8ec0db4 commit 1bb25eb

8 files changed

Lines changed: 58 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to a3s-observer will be documented in this file.
44

5+
## [0.2.3] — data-loss visibility
6+
7+
### Added
8+
9+
- **Ring-drop counter:** events dropped because a ring buffer was full are counted in-kernel
10+
(`PerCpuArray`, via a `reserve_or_drop` helper across all 7 emit sites) and surfaced in the
11+
60s report as `dropped` — data loss is now visible, not silent. Validated on KVM:
12+
`dropped=0` under normal node load (exec=502, egress=458, dns=136 captured, none dropped),
13+
which also empirically confirms the 20ms poll loop keeps up without drops.
14+
515
## [0.2.2] — operability
616

717
### Added

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2021"
55
license = "MIT"
66
description = "General-purpose, language-agnostic eBPF observability for AI agents (LLM calls, tools, files, network egress)."

a3s-observer-collector/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-collector"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2021"
55
license = "MIT"
66
description = "a3s-observer collector: loads the eBPF probes and exports enriched events."

a3s-observer-collector/src/main.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ use a3s_observer::{
1212
};
1313
use a3s_observer_common::{ConnectEvent, DnsEvent, ExecEvent, FileEvent, LlmEvent, TlsEvent};
1414
use anyhow::Context as _;
15-
use aya::{maps::RingBuf, programs::TracePoint, Ebpf};
15+
use aya::{
16+
maps::{PerCpuArray, RingBuf},
17+
programs::TracePoint,
18+
Ebpf,
19+
};
1620
use std::collections::HashMap;
1721
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1822
use std::time::Duration;
@@ -96,6 +100,9 @@ async fn main() -> anyhow::Result<()> {
96100
ebpf.take_map("LLM_EVENTS")
97101
.context("`LLM_EVENTS` missing")?,
98102
)?;
103+
// Cumulative count of events dropped because a ring was full (data-loss visibility).
104+
let drops: PerCpuArray<_, u64> =
105+
PerCpuArray::try_from(ebpf.take_map("DROPS").context("`DROPS` missing")?)?;
99106

100107
tracing::info!(
101108
attached,
@@ -113,13 +120,18 @@ async fn main() -> anyhow::Result<()> {
113120
tokio::select! {
114121
_ = sigint.recv() => break,
115122
_ = report.tick() => {
123+
let dropped: u64 = drops
124+
.get(&0, 0)
125+
.map(|v| v.iter().copied().sum())
126+
.unwrap_or(0);
116127
tracing::info!(
117128
exec = stats.exec,
118129
egress = stats.egress,
119130
dns = stats.dns,
120131
file = stats.file,
121132
llm = stats.llm,
122-
"a3s-observer: events processed in the last 60s"
133+
dropped,
134+
"a3s-observer: events in the last 60s (dropped = cumulative ring-full)"
123135
);
124136
stats = Stats::default();
125137
}

a3s-observer-common/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-common"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2021"
55
license = "MIT"
66
description = "Shared no_std types crossing the eBPF <-> userspace boundary for a3s-observer."

a3s-observer-ebpf/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-ebpf"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2021"
55
license = "MIT"
66
publish = false

a3s-observer-ebpf/src/main.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use aya_ebpf::{
1212
bpf_probe_read_user_buf, bpf_probe_read_user_str_bytes,
1313
},
1414
macros::{map, tracepoint},
15-
maps::{HashMap, RingBuf},
15+
maps::{ring_buf::RingBufEntry, HashMap, PerCpuArray, RingBuf},
1616
programs::TracePointContext,
1717
};
1818

@@ -34,6 +34,10 @@ static FILE_EVENTS: RingBuf = RingBuf::with_byte_size(256 * 1024, 0);
3434
#[map]
3535
static LLM_EVENTS: RingBuf = RingBuf::with_byte_size(64 * 1024, 0);
3636

37+
// Count of events dropped because a ring was full — data-loss visibility under extreme load.
38+
#[map]
39+
static DROPS: PerCpuArray<u64> = PerCpuArray::with_max_entries(1, 0);
40+
3741
// Per-LLM-socket accumulator: (pid<<32|fd) -> running byte/time stats, started at the
3842
// ClientHello and flushed on close. Only TLS-to-provider sockets are tracked → stays small.
3943
#[map]
@@ -57,6 +61,20 @@ fn sock_key(pid: u32, fd: u64) -> u64 {
5761
((pid as u64) << 32) | (fd & 0xffff_ffff)
5862
}
5963

64+
/// Reserve a ring-buffer slot, counting a drop if the ring is full (so userspace can report
65+
/// data loss instead of losing events silently).
66+
fn reserve_or_drop<T>(ring: &RingBuf) -> Option<RingBufEntry<T>> {
67+
let entry = ring.reserve::<T>(0);
68+
if entry.is_none() {
69+
unsafe {
70+
if let Some(c) = DROPS.get_ptr_mut(0) {
71+
*c = (*c).wrapping_add(1);
72+
}
73+
}
74+
}
75+
entry
76+
}
77+
6078
/// Read a `u64` (e.g. a pointer or length) from a user-space address.
6179
fn read_user_u64(addr: *const u8) -> Option<u64> {
6280
let mut b = [0u8; 8];
@@ -75,7 +93,7 @@ pub fn exec(ctx: TracePointContext) -> u32 {
7593
}
7694

7795
fn try_exec(ctx: &TracePointContext) -> Result<u32, i64> {
78-
let Some(mut entry) = EVENTS.reserve::<ExecEvent>(0) else {
96+
let Some(mut entry) = reserve_or_drop::<ExecEvent>(&EVENTS) else {
7997
return Ok(0);
8098
};
8199
let ev = entry.as_mut_ptr();
@@ -146,7 +164,7 @@ fn try_tls(ctx: &TracePointContext) -> Result<u32, i64> {
146164
},
147165
0,
148166
);
149-
let Some(mut entry) = TLS_EVENTS.reserve::<TlsEvent>(0) else {
167+
let Some(mut entry) = reserve_or_drop::<TlsEvent>(&TLS_EVENTS) else {
150168
return Ok(0);
151169
};
152170
let ev = entry.as_mut_ptr();
@@ -196,7 +214,7 @@ fn try_connect(ctx: &TracePointContext) -> Result<u32, i64> {
196214
if family != 2 && family != 10 {
197215
return Ok(0); // only AF_INET / AF_INET6
198216
}
199-
let Some(mut entry) = CONNECT_EVENTS.reserve::<ConnectEvent>(0) else {
217+
let Some(mut entry) = reserve_or_drop::<ConnectEvent>(&CONNECT_EVENTS) else {
200218
return Ok(0);
201219
};
202220
let ev = entry.as_mut_ptr();
@@ -249,7 +267,7 @@ fn try_dns(ctx: &TracePointContext) -> Result<u32, i64> {
249267
if count < 13 {
250268
return Ok(0); // DNS header(12) + >=1 question byte
251269
}
252-
let Some(mut entry) = DNS_EVENTS.reserve::<DnsEvent>(0) else {
270+
let Some(mut entry) = reserve_or_drop::<DnsEvent>(&DNS_EVENTS) else {
253271
return Ok(0);
254272
};
255273
let ev = entry.as_mut_ptr();
@@ -325,7 +343,7 @@ fn try_dns_msghdr(ctx: &TracePointContext) -> Result<u32, i64> {
325343
if iov_base == 0 || iov_len < 13 {
326344
return Ok(0);
327345
}
328-
let Some(mut entry) = DNS_EVENTS.reserve::<DnsEvent>(0) else {
346+
let Some(mut entry) = reserve_or_drop::<DnsEvent>(&DNS_EVENTS) else {
329347
return Ok(0);
330348
};
331349
let ev = entry.as_mut_ptr();
@@ -366,7 +384,7 @@ fn try_open(ctx: &TracePointContext) -> Result<u32, i64> {
366384
return Ok(0); // O_RDONLY — skip; keep only O_WRONLY / O_RDWR
367385
}
368386
let filename: *const u8 = unsafe { ctx.read_at(24)? };
369-
let Some(mut entry) = FILE_EVENTS.reserve::<FileEvent>(0) else {
387+
let Some(mut entry) = reserve_or_drop::<FileEvent>(&FILE_EVENTS) else {
370388
return Ok(0);
371389
};
372390
let ev = entry.as_mut_ptr();
@@ -455,7 +473,7 @@ pub fn sock_close(ctx: TracePointContext) -> u32 {
455473
return 0; // not an LLM socket
456474
};
457475
let _ = LLM_SOCKS.remove(&key);
458-
if let Some(mut entry) = LLM_EVENTS.reserve::<LlmEvent>(0) {
476+
if let Some(mut entry) = reserve_or_drop::<LlmEvent>(&LLM_EVENTS) {
459477
let now = unsafe { bpf_ktime_get_ns() };
460478
let ev = entry.as_mut_ptr();
461479
unsafe {

0 commit comments

Comments
 (0)