Skip to content

Commit f1d2f32

Browse files
committed
feat: add performance monitoring and logging capabilities to SipFlow[latest]
1 parent ec1ab2b commit f1d2f32

7 files changed

Lines changed: 199 additions & 12 deletions

File tree

src/bin/sipflow.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ use rustpbx::config::{SipFlowConfig, SipFlowEngine, SipFlowSubdirs};
1111
use rustpbx::sipflow::{
1212
SipFlowBackend, SipFlowItem, SipFlowMsgType,
1313
create_backend,
14+
perf::{PerfCounters, PerfDumper},
1415
protocol::{MsgType, Packet, parse_packet},
1516
storage::extract_callid,
1617
};
1718
use std::collections::HashMap;
1819
use std::net::SocketAddr;
1920
use std::sync::Arc;
21+
use std::sync::atomic::Ordering;
2022
use tokio::net::UdpSocket;
23+
use tracing_appender::non_blocking;
24+
use tracing_subscriber::{fmt, EnvFilter};
2125

2226
#[derive(Parser, Debug)]
2327
#[command(author, version, about = "SipFlow - SIP and RTP flow recording server", long_about = None)]
@@ -38,8 +42,8 @@ struct Args {
3842
#[arg(short, long, default_value = "./config/sipflow")]
3943
root: String,
4044

41-
/// Storage engine: "flowdb" (default) or "sqlite"
42-
#[arg(long, default_value = "flowdb")]
45+
/// Storage engine: "flowdb" or "sqlite" (default)
46+
#[arg(long, default_value = "sqlite")]
4347
engine: String,
4448

4549
/// Channel buffer size
@@ -60,6 +64,16 @@ struct Args {
6064
#[arg(long, default_value_t = 8192)]
6165
id_cache_size: usize,
6266

67+
// ── Logging options ──
68+
69+
/// Log file path
70+
#[arg(long, default_value = "/var/log/sipflow.log")]
71+
log_file: String,
72+
73+
/// Log level (trace, debug, info, warn, error)
74+
#[arg(long, default_value = "info")]
75+
log_level: String,
76+
6377
// ── FlowDB options ──
6478

6579
/// TTL in seconds for FlowDB records (optional, 0 = no ttl)
@@ -111,15 +125,41 @@ fn convert_packet_to_item(packet: Packet) -> (String, SipFlowItem) {
111125

112126
#[tokio::main]
113127
async fn main() -> Result<()> {
114-
tracing_subscriber::fmt::init();
115128
let args = Args::parse();
116129

130+
// Initialize tracing: try log file, fall back to stdout on permission error
131+
if let Some(parent) = std::path::Path::new(&args.log_file).parent() {
132+
let _ = std::fs::create_dir_all(parent);
133+
}
134+
let (_guard, writer) = match std::fs::OpenOptions::new()
135+
.create(true)
136+
.append(true)
137+
.open(&args.log_file)
138+
{
139+
Ok(f) => {
140+
let (w, g) = non_blocking(f);
141+
(g, w)
142+
}
143+
Err(e) => {
144+
eprintln!(
145+
"sipflow: cannot open '{}' ({}), falling back to stdout",
146+
args.log_file, e
147+
);
148+
let (w, g) = non_blocking(std::io::stdout());
149+
(g, w)
150+
}
151+
};
152+
fmt()
153+
.with_env_filter(EnvFilter::new(&args.log_level))
154+
.with_writer(writer)
155+
.init();
156+
117157
// Ensure data directory exists
118158
std::fs::create_dir_all(&args.root)?;
119159

120160
let engine = match args.engine.as_str() {
121-
"sqlite" => SipFlowEngine::Sqlite,
122-
"flowdb" | _ => SipFlowEngine::FlowDb,
161+
"flowdb" => SipFlowEngine::FlowDb,
162+
"sqlite" | _ => SipFlowEngine::Sqlite,
123163
};
124164
let ttl_secs = args.ttl_secs.filter(|&s| s > 0);
125165

@@ -137,6 +177,8 @@ async fn main() -> Result<()> {
137177
};
138178

139179
let backend: Arc<dyn SipFlowBackend> = Arc::from(create_backend(&config)?);
180+
let perf_counters = PerfCounters::new_arc();
181+
140182
let app_state = AppState {
141183
backend: backend.clone(),
142184
};
@@ -148,12 +190,14 @@ async fn main() -> Result<()> {
148190
let (tx, mut rx) = tokio::sync::mpsc::channel::<Packet>(args.buffer_size);
149191

150192
// UDP Receiver Task
193+
let perf_rx = perf_counters.clone();
151194
rustpbx::utils::spawn(async move {
152195
let mut buf = vec![0u8; 65535];
153196
loop {
154197
match socket.recv_from(&mut buf).await {
155198
Ok((size, _)) => {
156199
if let Ok(packet) = parse_packet(&buf[..size]) {
200+
perf_rx.packets_received.fetch_add(1, Ordering::Relaxed);
157201
let _ = tx.try_send(packet);
158202
}
159203
}
@@ -166,6 +210,7 @@ async fn main() -> Result<()> {
166210

167211
// Storage Worker Task
168212
let storage_backend = backend.clone();
213+
let perf_worker = perf_counters.clone();
169214
rustpbx::utils::spawn(async move {
170215
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
171216
loop {
@@ -174,15 +219,32 @@ async fn main() -> Result<()> {
174219
let (call_id, item) = convert_packet_to_item(packet);
175220
if !call_id.is_empty() {
176221
let _ = storage_backend.record(&call_id, item);
222+
perf_worker.items_recorded.fetch_add(1, Ordering::Relaxed);
223+
} else {
224+
perf_worker.items_dropped.fetch_add(1, Ordering::Relaxed);
177225
}
226+
perf_worker.set_pending(rx.len() as i64);
178227
}
179228
_ = interval.tick() => {
180229
let _ = storage_backend.flush().await;
230+
perf_worker.flushes.fetch_add(1, Ordering::Relaxed);
181231
}
182232
}
183233
}
184234
});
185235

236+
// Periodic perf dump (every 10 s, skipped when idle)
237+
let perf_dump = perf_counters.clone();
238+
rustpbx::utils::spawn(async move {
239+
let mut dumper = PerfDumper::new(perf_dump);
240+
loop {
241+
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
242+
if let Some(msg) = dumper.try_dump() {
243+
tracing::info!("{msg}");
244+
}
245+
}
246+
});
247+
186248
// HTTP Server
187249
let app = Router::new()
188250
.route("/health", get(health_handler))

src/config.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,11 +540,11 @@ pub enum SipFlowSubdirs {
540540
/// Storage engine selection for the Local sipflow backend.
541541
#[derive(Debug, Deserialize, Clone, Copy, Serialize, PartialEq, Eq, Default)]
542542
pub enum SipFlowEngine {
543-
/// Use FlowDB LSM-tree engine (default).
544-
#[default]
543+
/// Use FlowDB LSM-tree engine.
545544
#[serde(rename = "flowdb")]
546545
FlowDb,
547-
/// Use the legacy SQLite + raw-file engine.
546+
/// Use SQLite + raw-file engine (default).
547+
#[default]
548548
#[serde(rename = "sqlite")]
549549
Sqlite,
550550
}
@@ -601,7 +601,7 @@ pub enum SipFlowConfig {
601601
flush_interval_secs: u64,
602602
#[serde(default = "default_sipflow_id_cache_size")]
603603
id_cache_size: usize,
604-
/// Storage engine: "flowdb" (default) or "sqlite".
604+
/// Storage engine: "flowdb" or "sqlite" (default).
605605
#[serde(default)]
606606
engine: SipFlowEngine,
607607
/// TTL in seconds for FlowDB records (optional). When set,

src/sipflow/backend/local.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ use tracing::warn;
99

1010
use crate::config::SipFlowSubdirs;
1111
use crate::sipflow::backend::SipFlowBackend;
12+
use crate::sipflow::perf::{PerfCounters, PerfDumper};
1213
use crate::sipflow::protocol::{MsgType, Packet};
1314
use crate::sipflow::storage::{StorageManager, process_packet};
1415
use crate::sipflow::wav_utils::{
1516
generate_wav_to_writer,
1617
};
1718
use crate::sipflow::{SipFlowItem, SipFlowMediaStats, SipFlowMsgType};
19+
use std::sync::atomic::Ordering;
1820

1921
enum Command {
2022
RecordItem {
@@ -50,6 +52,8 @@ impl LocalBackend {
5052
let root_clone = root.clone();
5153
let subdirs_clone = subdirs.clone();
5254
// Spawn background worker task
55+
let perf = PerfCounters::new_arc();
56+
let perf_dumper = perf.clone();
5357
crate::utils::spawn(async move {
5458
let mut storage = StorageManager::new(
5559
&PathBuf::from(&root_clone),
@@ -60,6 +64,7 @@ impl LocalBackend {
6064
);
6165

6266
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
67+
let mut dumper = PerfDumper::new(perf_dumper);
6368

6469
loop {
6570
tokio::select! {
@@ -70,6 +75,7 @@ impl LocalBackend {
7075
Some(cmd) = rx.recv() => {
7176
match cmd {
7277
Command::RecordItem { call_id, item } => {
78+
perf.items_recorded.fetch_add(1, Ordering::Relaxed);
7379
// Convert SipFlowItem to Packet for storage
7480
let default_port = if matches!(&item.msg_type, SipFlowMsgType::Sip)
7581
{
@@ -121,12 +127,18 @@ impl LocalBackend {
121127
}
122128
Command::Flush { done } => {
123129
let _ = storage.force_flush().await;
130+
perf.flushes.fetch_add(1, Ordering::Relaxed);
131+
perf.set_pending(storage.batch_len() as i64);
124132
let _ = done.send(());
125133
}
126134
}
127135
}
128136
_ = interval.tick() => {
129137
let _ = storage.check_flush().await;
138+
perf.set_pending(storage.batch_len() as i64);
139+
if let Some(msg) = dumper.try_dump() {
140+
tracing::info!("{msg}");
141+
}
130142
}
131143
}
132144
}

src/sipflow/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod backend;
22
pub mod flowdb_backend;
33
pub mod flowdb_codec;
4+
pub mod perf;
45
pub mod protocol;
56
pub mod rtp_stats;
67
pub mod sdp_utils;

src/sipflow/perf.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
2+
use std::sync::Arc;
3+
use std::time::Instant;
4+
5+
/// Shared performance counters for sipflow (atomics, no contention on hot path).
6+
pub struct PerfCounters {
7+
pub packets_received: AtomicU64,
8+
pub items_recorded: AtomicU64,
9+
pub items_dropped: AtomicU64,
10+
pub flushes: AtomicU64,
11+
/// Explicitly-set pending/backlog count (e.g. items in channel or batch).
12+
/// Set via [`Self::set_pending`] from wherever the backlog is observable.
13+
pub pending: AtomicI64,
14+
}
15+
16+
impl PerfCounters {
17+
pub fn new_arc() -> Arc<Self> {
18+
Arc::new(Self {
19+
packets_received: AtomicU64::new(0),
20+
items_recorded: AtomicU64::new(0),
21+
items_dropped: AtomicU64::new(0),
22+
flushes: AtomicU64::new(0),
23+
pending: AtomicI64::new(0),
24+
})
25+
}
26+
27+
pub fn set_pending(&self, n: i64) {
28+
self.pending.store(n, Ordering::Relaxed);
29+
}
30+
}
31+
32+
/// Perf-dumper that tracks snapshot state. Call `try_dump()` periodically;
33+
/// it returns a log message at most once every 10 seconds, skipping periods
34+
/// with zero activity, and exports counters to `metrics`.
35+
pub struct PerfDumper {
36+
counters: Arc<PerfCounters>,
37+
last_time: Instant,
38+
last_received: u64,
39+
last_recorded: u64,
40+
last_dropped: u64,
41+
last_flushes: u64,
42+
}
43+
44+
impl PerfDumper {
45+
pub fn new(counters: Arc<PerfCounters>) -> Self {
46+
Self {
47+
counters,
48+
last_time: Instant::now(),
49+
last_received: 0,
50+
last_recorded: 0,
51+
last_dropped: 0,
52+
last_flushes: 0,
53+
}
54+
}
55+
56+
/// Returns `Some(msg)` every ~10 s if any counter changed, `None` otherwise.
57+
pub fn try_dump(&mut self) -> Option<String> {
58+
let now = Instant::now();
59+
let elapsed = now.duration_since(self.last_time).as_secs_f64();
60+
if elapsed < 10.0 {
61+
return None;
62+
}
63+
64+
let received = self.counters.packets_received.load(Ordering::Relaxed);
65+
let recorded = self.counters.items_recorded.load(Ordering::Relaxed);
66+
let dropped = self.counters.items_dropped.load(Ordering::Relaxed);
67+
let flushes = self.counters.flushes.load(Ordering::Relaxed);
68+
let pending = self.counters.pending.load(Ordering::Relaxed);
69+
70+
let dr = received - self.last_received;
71+
let dw = recorded - self.last_recorded;
72+
let dd = dropped - self.last_dropped;
73+
let df = flushes - self.last_flushes;
74+
75+
self.last_received = received;
76+
self.last_recorded = recorded;
77+
self.last_dropped = dropped;
78+
self.last_flushes = flushes;
79+
self.last_time = now;
80+
81+
// ── export to metrics ──
82+
metrics::counter!("sipflow_packets_received_total", "component" => "sipflow")
83+
.increment(dr);
84+
metrics::counter!("sipflow_items_recorded_total", "component" => "sipflow")
85+
.increment(dw);
86+
metrics::counter!("sipflow_items_dropped_total", "component" => "sipflow")
87+
.increment(dd);
88+
metrics::counter!("sipflow_flushes_total", "component" => "sipflow")
89+
.increment(df);
90+
metrics::gauge!("sipflow_pending_items", "component" => "sipflow")
91+
.set(pending as f64);
92+
93+
if dr == 0 && dw == 0 && dd == 0 && df == 0 {
94+
return None;
95+
}
96+
97+
Some(format!(
98+
"sipflow perf: recv={:.1}/s record={:.1}/s drop={:.1}/s flush={:.1}/s pending={} total_recorded={}",
99+
dr as f64 / elapsed,
100+
dw as f64 / elapsed,
101+
dd as f64 / elapsed,
102+
df as f64 / elapsed,
103+
pending,
104+
recorded,
105+
))
106+
}
107+
}

src/sipflow/storage.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ impl StorageManager {
237237
Ok(())
238238
}
239239

240+
/// Number of items waiting to be flushed.
241+
pub fn batch_len(&self) -> usize {
242+
self.batch.len()
243+
}
244+
240245
pub async fn check_flush(&mut self) -> Result<()> {
241246
if !self.batch.is_empty() && self.last_flush.elapsed().as_secs() >= self.flush_interval {
242247
self.flush_batch().await?;

src/sipflow/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ engine = "sqlite"
277277
}
278278
}
279279

280-
/// Default engine (omitted in config) should be FlowDb.
280+
/// Default engine (omitted in config) should be Sqlite.
281281
#[test]
282-
fn test_config_default_engine_is_flowdb() {
282+
fn test_config_default_engine_is_sqlite() {
283283
let toml_str = r#"
284284
type = "local"
285285
root = "/var/sipflow"
@@ -288,7 +288,7 @@ root = "/var/sipflow"
288288
toml::from_str(toml_str).expect("should parse default config");
289289
match cfg {
290290
crate::config::SipFlowConfig::Local { engine, .. } => {
291-
assert_eq!(engine, SipFlowEngine::FlowDb);
291+
assert_eq!(engine, SipFlowEngine::Sqlite);
292292
}
293293
_ => panic!("expected Local config"),
294294
}

0 commit comments

Comments
 (0)