Skip to content

Commit 8ec0db4

Browse files
RoyLinRoyLin
authored andcommitted
release: v0.2.2 — collector throughput stats (operability)
Per-kind event counters (exec/egress/dns/file/llm) logged every 60s via a counting emit() wrapper + a tokio interval, so operators see liveness + throughput. Validated on KVM: exec=774 egress=526 dns=144 file=0 llm=1 over a 60s window. In-kernel ring-drop counting (data-loss visibility) noted as follow-up.
1 parent 73b5209 commit 8ec0db4

7 files changed

Lines changed: 59 additions & 14 deletions

File tree

CHANGELOG.md

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

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

5+
## [0.2.2] — operability
6+
7+
### Added
8+
9+
- **Throughput stats:** the collector logs per-kind event counts (exec / egress / dns /
10+
file / llm) every 60s, so operators can see it is alive and how much it is processing.
11+
Validated on KVM: `exec=774 egress=526 dns=144 file=0 llm=1` over a 60s window. (In-kernel
12+
ring-buffer drop counting — data-loss visibility under extreme load — is a noted follow-up.)
13+
514
## [0.2.1] — robustness + deployability
615

716
### Changed

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.1"
3+
version = "0.2.2"
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.1"
3+
version = "0.2.2"
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: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,30 @@ async fn main() -> anyhow::Result<()> {
106106
);
107107

108108
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?;
109+
let mut stats = Stats::default();
110+
let mut report = tokio::time::interval(Duration::from_secs(60));
111+
report.tick().await; // consume the immediate first tick
109112
loop {
110113
tokio::select! {
111114
_ = sigint.recv() => break,
115+
_ = report.tick() => {
116+
tracing::info!(
117+
exec = stats.exec,
118+
egress = stats.egress,
119+
dns = stats.dns,
120+
file = stats.file,
121+
llm = stats.llm,
122+
"a3s-observer: events processed in the last 60s"
123+
);
124+
stats = Stats::default();
125+
}
112126
// Drain all rings every 20ms. Adequate for production at moderate volume; the
113127
// rings (64-256 KiB) absorb bursts between ticks. For sustained extreme volume,
114128
// switch to AsyncFd (epoll) on the ring fds and/or enlarge the rings.
115129
_ = tokio::time::sleep(Duration::from_millis(20)) => {
116130
while let Some(item) = exec_ring.next() {
117131
if let Some(ev) = read_pod::<ExecEvent>(&item) {
118-
exporter.export(&EnrichedEvent {
132+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
119133
identity: identity_for(&resolver, ev.pid, &ev.comm),
120134
provider: None,
121135
event: AgentEvent::ToolExec {
@@ -135,7 +149,7 @@ async fn main() -> anyhow::Result<()> {
135149
peers.clear(); // ponytail: crude cap; LRU if it ever matters
136150
}
137151
peers.insert(sock_key(ev.pid, ev.fd), peer);
138-
exporter.export(&EnrichedEvent {
152+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
139153
identity: identity_for(&resolver, ev.pid, &ev.comm),
140154
provider: None,
141155
event: AgentEvent::Egress {
@@ -164,7 +178,7 @@ async fn main() -> anyhow::Result<()> {
164178
llm_meta.clear(); // ponytail: crude cap; LRU if it ever matters
165179
}
166180
llm_meta.insert(sock_key(ev.pid, ev.fd), (sni.clone(), provider.clone(), peer));
167-
exporter.export(&EnrichedEvent {
181+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
168182
identity: identity_for(&resolver, ev.pid, &ev.comm),
169183
provider,
170184
event: AgentEvent::Egress {
@@ -180,7 +194,7 @@ async fn main() -> anyhow::Result<()> {
180194
if let Some(ev) = read_pod::<DnsEvent>(&item) {
181195
let len = (ev.len as usize).min(ev.data.len());
182196
if let Some(query) = parse_dns_qname(&ev.data[..len]) {
183-
exporter.export(&EnrichedEvent {
197+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
184198
identity: identity_for(&resolver, ev.pid, &ev.comm),
185199
provider: None,
186200
event: AgentEvent::Dns { pid: ev.pid, query },
@@ -192,7 +206,7 @@ async fn main() -> anyhow::Result<()> {
192206
if let Some(ev) = read_pod::<FileEvent>(&item) {
193207
let path = cstr(&ev.path);
194208
if !path.is_empty() {
195-
exporter.export(&EnrichedEvent {
209+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
196210
identity: identity_for(&resolver, ev.pid, &ev.comm),
197211
provider: None,
198212
event: AgentEvent::FileAccess {
@@ -212,7 +226,7 @@ async fn main() -> anyhow::Result<()> {
212226
llm_meta.remove(&sock_key(ev.pid, ev.fd))
213227
{
214228
if provider.is_some() {
215-
exporter.export(&EnrichedEvent {
229+
emit(exporter.as_ref(), &mut stats, EnrichedEvent {
216230
identity: identity_for(&resolver, ev.pid, &ev.comm),
217231
provider,
218232
event: AgentEvent::LlmCall {
@@ -279,6 +293,28 @@ fn identity_for(r: &impl IdentityResolver, pid: u32, comm: &[u8; 16]) -> Identit
279293
id
280294
}
281295

296+
/// Per-kind event counters for periodic throughput logging (collector operability).
297+
#[derive(Default)]
298+
struct Stats {
299+
exec: u64,
300+
egress: u64,
301+
dns: u64,
302+
file: u64,
303+
llm: u64,
304+
}
305+
306+
/// Export an event and count it by kind for the throughput report.
307+
fn emit(exporter: &dyn Exporter, stats: &mut Stats, ev: EnrichedEvent) {
308+
match &ev.event {
309+
AgentEvent::ToolExec { .. } => stats.exec += 1,
310+
AgentEvent::Egress { .. } => stats.egress += 1,
311+
AgentEvent::Dns { .. } => stats.dns += 1,
312+
AgentEvent::FileAccess { .. } => stats.file += 1,
313+
AgentEvent::LlmCall { .. } => stats.llm += 1,
314+
}
315+
exporter.export(&ev);
316+
}
317+
282318
fn peer_ip(ev: &ConnectEvent) -> IpAddr {
283319
if ev.family == 2 {
284320
IpAddr::V4(Ipv4Addr::new(

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.1"
3+
version = "0.2.2"
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.1"
3+
version = "0.2.2"
44
edition = "2021"
55
license = "MIT"
66
publish = false

0 commit comments

Comments
 (0)