Skip to content

Commit d61eb3b

Browse files
maxholmanclaude
andcommitted
feat(stats): add cumulative total_connections and total_flows counters
active_connections and active_flows are real-time gauges that return to 0 after sessions close. Every UAT agent reports "stats broken" because they check after traffic completes. Add monotonically increasing total_* counters alongside the existing gauges. Also adds packets_dropped to the REST API and OpenAPI spec — it was already present in proto, CLI, and MCP but missing from REST (interface parity violation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1c46390 commit d61eb3b

8 files changed

Lines changed: 102 additions & 2 deletions

File tree

crates/api/src/handlers.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ pub struct StatsResponse {
2727
pub packets_out: u64,
2828
pub active_connections: u64,
2929
pub active_flows: u64,
30+
/// Total packets dropped since daemon start.
31+
pub packets_dropped: u64,
32+
/// Total connections opened since daemon start (monotonically increasing).
33+
pub total_connections: u64,
34+
/// Total flows opened since daemon start (monotonically increasing).
35+
pub total_flows: u64,
3036
}
3137

3238
/// Node info response.
@@ -251,6 +257,9 @@ pub async fn stats(State(state): State<ApiState>) -> Result<Json<StatsResponse>,
251257
packets_out: s.packets_out,
252258
active_connections: s.active_connections,
253259
active_flows: s.active_flows,
260+
packets_dropped: s.packets_dropped,
261+
total_connections: s.total_connections,
262+
total_flows: s.total_flows,
254263
})),
255264
_ => Err(StatusCode::INTERNAL_SERVER_ERROR),
256265
}

crates/cli/src/output.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub fn print_response(resp: &ManagementResponse) -> Result<(), CtlError> {
128128
let _ = writeln!(tw, "connections:\t{}", s.active_connections);
129129
let _ = writeln!(tw, "flows:\t{}", s.active_flows);
130130
let _ = writeln!(tw, "dropped:\t{}", s.packets_dropped);
131+
let _ = writeln!(tw, "total connections:\t{}", s.total_connections);
132+
let _ = writeln!(tw, "total flows:\t{}", s.total_flows);
131133
let _ = tw.flush();
132134
}
133135
Some(management_response::Response::Peers(p)) => {

crates/core/src/control/metrics.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ pub struct Metrics {
1414
active_connections: AtomicU64,
1515
active_flows: AtomicU64,
1616
packets_dropped: AtomicU64,
17+
/// Monotonically increasing count of all connections ever opened.
18+
total_connections: AtomicU64,
19+
/// Monotonically increasing count of all flows ever opened.
20+
total_flows: AtomicU64,
1721
}
1822

1923
impl Metrics {
@@ -31,6 +35,8 @@ impl Metrics {
3135
active_connections: self.active_connections.load(Ordering::Relaxed),
3236
active_flows: self.active_flows.load(Ordering::Relaxed),
3337
packets_dropped: self.packets_dropped.load(Ordering::Relaxed),
38+
total_connections: self.total_connections.load(Ordering::Relaxed),
39+
total_flows: self.total_flows.load(Ordering::Relaxed),
3440
}
3541
}
3642

@@ -52,6 +58,7 @@ impl Metrics {
5258

5359
pub fn inc_active_connections(&self) {
5460
self.active_connections.fetch_add(1, Ordering::Relaxed);
61+
self.total_connections.fetch_add(1, Ordering::Relaxed);
5562
}
5663

5764
pub fn dec_active_connections(&self) {
@@ -64,6 +71,7 @@ impl Metrics {
6471

6572
pub fn inc_active_flows(&self) {
6673
self.active_flows.fetch_add(1, Ordering::Relaxed);
74+
self.total_flows.fetch_add(1, Ordering::Relaxed);
6775
}
6876

6977
pub fn dec_active_flows(&self) {
@@ -72,3 +80,59 @@ impl Metrics {
7280
}
7381

7482
pub type SharedMetrics = Arc<Metrics>;
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::Metrics;
87+
88+
#[test]
89+
fn total_connections_increments_and_does_not_decrement() {
90+
let metrics = Metrics::new();
91+
92+
metrics.inc_active_connections();
93+
assert_eq!(metrics.snapshot().total_connections, 1);
94+
assert_eq!(metrics.snapshot().active_connections, 1);
95+
96+
metrics.dec_active_connections();
97+
// active_connections decrements, total_connections must not
98+
assert_eq!(metrics.snapshot().total_connections, 1);
99+
assert_eq!(metrics.snapshot().active_connections, 0);
100+
}
101+
102+
#[test]
103+
fn total_flows_increments_and_does_not_decrement() {
104+
let metrics = Metrics::new();
105+
106+
metrics.inc_active_flows();
107+
assert_eq!(metrics.snapshot().total_flows, 1);
108+
assert_eq!(metrics.snapshot().active_flows, 1);
109+
110+
metrics.dec_active_flows();
111+
// active_flows decrements, total_flows must not
112+
assert_eq!(metrics.snapshot().total_flows, 1);
113+
assert_eq!(metrics.snapshot().active_flows, 0);
114+
}
115+
116+
#[test]
117+
fn cumulative_counters_survive_connection_churn() {
118+
let metrics = Metrics::new();
119+
120+
// Simulate 5 connection open/close cycles
121+
for _ in 0..5 {
122+
metrics.inc_active_connections();
123+
metrics.dec_active_connections();
124+
}
125+
126+
assert_eq!(metrics.snapshot().total_connections, 5);
127+
assert_eq!(metrics.snapshot().active_connections, 0);
128+
129+
// Simulate 3 flow open/close cycles
130+
for _ in 0..3 {
131+
metrics.inc_active_flows();
132+
metrics.dec_active_flows();
133+
}
134+
135+
assert_eq!(metrics.snapshot().total_flows, 3);
136+
assert_eq!(metrics.snapshot().active_flows, 0);
137+
}
138+
}

crates/core/src/ipc.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ fn dispatch_request(request: &ManagementRequest, api: &dyn NodeApi) -> Managemen
313313
active_connections: m.active_connections,
314314
active_flows: m.active_flows,
315315
packets_dropped: m.packets_dropped,
316+
total_connections: m.total_connections,
317+
total_flows: m.total_flows,
316318
})
317319
}
318320

crates/core/src/node_api.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ pub struct Metrics {
7878
pub active_connections: u64,
7979
pub active_flows: u64,
8080
pub packets_dropped: u64,
81+
/// Total connections opened since daemon start (monotonically increasing).
82+
pub total_connections: u64,
83+
/// Total flows opened since daemon start (monotonically increasing).
84+
pub total_flows: u64,
8185
}
8286

8387
/// Overall node info.

crates/mcp/src/convert.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ pub fn format_response(resp: &ManagementResponse) -> Result<String, String> {
2525
}
2626
Some(management_response::Response::Stats(s)) => Ok(format!(
2727
"bytes in: {}\nbytes out: {}\npackets in: {}\npackets out: {}\n\
28-
connections: {}\nflows: {}\ndropped: {}",
28+
connections: {}\nflows: {}\ndropped: {}\ntotal connections: {}\ntotal flows: {}",
2929
s.bytes_in,
3030
s.bytes_out,
3131
s.packets_in,
3232
s.packets_out,
3333
s.active_connections,
3434
s.active_flows,
3535
s.packets_dropped,
36+
s.total_connections,
37+
s.total_flows,
3638
)),
3739
Some(management_response::Response::Peers(p)) => {
3840
if p.peers.is_empty() {

crates/wire/proto/management.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ message StatsResponse {
161161
uint64 active_connections = 5;
162162
uint64 active_flows = 6;
163163
uint64 packets_dropped = 7;
164+
uint64 total_connections = 8; // monotonically increasing; survives session close
165+
uint64 total_flows = 9; // monotonically increasing; survives session close
164166
}
165167

166168
message PeerInfo {

website/src/data/openapi.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@
7777
"packets_in",
7878
"packets_out",
7979
"active_connections",
80-
"active_flows"
80+
"active_flows",
81+
"packets_dropped",
82+
"total_connections",
83+
"total_flows"
8184
],
8285
"properties": {
8386
"bytes_in": {
@@ -103,6 +106,18 @@
103106
"active_flows": {
104107
"type": "integer",
105108
"description": "Number of active L4 network flows being tracked."
109+
},
110+
"packets_dropped": {
111+
"type": "integer",
112+
"description": "Total packets dropped since daemon start."
113+
},
114+
"total_connections": {
115+
"type": "integer",
116+
"description": "Total peer connections opened since daemon start (monotonically increasing)."
117+
},
118+
"total_flows": {
119+
"type": "integer",
120+
"description": "Total L4 flows opened since daemon start (monotonically increasing)."
106121
}
107122
}
108123
},

0 commit comments

Comments
 (0)