Skip to content

Commit adf0c74

Browse files
committed
chore(docs): documentation
1 parent 124dc08 commit adf0c74

6 files changed

Lines changed: 91 additions & 10 deletions

File tree

Cargo.lock

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

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,22 @@ packets = rdpcap("capture.pcap")
330330
conversations = extract_flows_from_packets(packets)
331331
```
332332

333+
Enable verbose mode to see progress feedback on stderr during extraction:
334+
335+
```python
336+
# Quick verbose flag on the function call
337+
conversations = extract_flows("capture.pcap", verbose=True)
338+
# [stackforge] Opening capture file: capture.pcap
339+
# [stackforge] Starting streaming flow extraction...
340+
# [stackforge] Processed 10000 packets (342 flows so far)
341+
# [stackforge] Processed 20000 packets (587 flows so far)
342+
# [stackforge] Flow extraction complete: 612 conversations
343+
344+
# Or via FlowConfig
345+
config = FlowConfig(verbose=True)
346+
conversations = extract_flows("capture.pcap", config=config)
347+
```
348+
333349
Customize timeouts, buffer limits, and memory budget with `FlowConfig`:
334350

335351
```python
8 KB
Binary file not shown.

crates/stackforge-core/src/flow/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct FlowConfig {
3030
pub memory_budget: Option<usize>,
3131
/// Directory for spill files (None = system temp dir).
3232
pub spill_dir: Option<PathBuf>,
33+
/// Print progress feedback to stderr during flow extraction (default: false).
34+
pub verbose: bool,
3335
}
3436

3537
impl Default for FlowConfig {
@@ -46,6 +48,7 @@ impl Default for FlowConfig {
4648
track_max_flow_len: false,
4749
memory_budget: None,
4850
spill_dir: None,
51+
verbose: false,
4952
}
5053
}
5154
}

crates/stackforge-core/src/flow/mod.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,35 @@ pub fn extract_flows_with_config(
7575
packets: &[CapturedPacket],
7676
config: FlowConfig,
7777
) -> Result<Vec<ConversationState>, FlowError> {
78+
let verbose = config.verbose;
79+
let total = packets.len();
7880
let table = ConversationTable::new(config);
7981

82+
if verbose {
83+
eprintln!("[stackforge] Starting flow extraction ({total} packets)...");
84+
}
85+
8086
for (index, captured) in packets.iter().enumerate() {
8187
let timestamp = captured.metadata.timestamp;
8288
table.ingest_packet(&captured.packet, timestamp, index)?;
89+
90+
if verbose && (index + 1) % 10_000 == 0 {
91+
eprintln!(
92+
"[stackforge] Processed {}/{total} packets ({} flows so far)",
93+
index + 1,
94+
table.conversation_count(),
95+
);
96+
}
8397
}
8498

85-
Ok(table.into_conversations())
99+
let conversations = table.into_conversations();
100+
if verbose {
101+
eprintln!(
102+
"[stackforge] Flow extraction complete: {total} packets -> {} conversations",
103+
conversations.len(),
104+
);
105+
}
106+
Ok(conversations)
86107
}
87108

88109
/// Extract flows from a streaming packet source (iterator).
@@ -100,16 +121,36 @@ pub fn extract_flows_streaming<I>(
100121
where
101122
I: Iterator<Item = Result<CapturedPacket, PacketError>>,
102123
{
124+
let verbose = config.verbose;
103125
let table = ConversationTable::new(config);
104126

127+
if verbose {
128+
eprintln!("[stackforge] Starting streaming flow extraction...");
129+
}
130+
105131
for (index, result) in packets.enumerate() {
106132
let captured = result.map_err(FlowError::PacketError)?;
107133
let timestamp = captured.metadata.timestamp;
108134
table.ingest_packet(&captured.packet, timestamp, index)?;
109135
// `captured` is dropped here — packet memory freed immediately
136+
137+
if verbose && (index + 1) % 10_000 == 0 {
138+
eprintln!(
139+
"[stackforge] Processed {} packets ({} flows so far)",
140+
index + 1,
141+
table.conversation_count(),
142+
);
143+
}
110144
}
111145

112-
Ok(table.into_conversations())
146+
let conversations = table.into_conversations();
147+
if verbose {
148+
eprintln!(
149+
"[stackforge] Flow extraction complete: {} conversations",
150+
conversations.len(),
151+
);
152+
}
153+
Ok(conversations)
113154
}
114155

115156
/// Extract flows directly from a capture file (PCAP or PcapNG).
@@ -120,6 +161,13 @@ pub fn extract_flows_from_file(
120161
path: impl AsRef<Path>,
121162
config: FlowConfig,
122163
) -> Result<Vec<ConversationState>, FlowError> {
164+
let verbose = config.verbose;
165+
if verbose {
166+
eprintln!(
167+
"[stackforge] Opening capture file: {}",
168+
path.as_ref().display(),
169+
);
170+
}
123171
let iter = CaptureIterator::open(path).map_err(FlowError::PacketError)?;
124172
extract_flows_streaming(iter, config)
125173
}

src/lib.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4076,6 +4076,7 @@ impl PyFlowConfig {
40764076
track_max_flow_len=false,
40774077
memory_budget=None,
40784078
spill_dir=None,
4079+
verbose=false,
40794080
))]
40804081
#[allow(clippy::too_many_arguments)]
40814082
fn new(
@@ -4089,6 +4090,7 @@ impl PyFlowConfig {
40894090
track_max_flow_len: bool,
40904091
memory_budget: Option<usize>,
40914092
spill_dir: Option<String>,
4093+
verbose: bool,
40924094
) -> Self {
40934095
Self {
40944096
inner: stackforge_core::FlowConfig {
@@ -4104,6 +4106,7 @@ impl PyFlowConfig {
41044106
track_max_flow_len,
41054107
memory_budget,
41064108
spill_dir: spill_dir.map(std::path::PathBuf::from),
4109+
verbose,
41074110
..stackforge_core::FlowConfig::default()
41084111
},
41094112
}
@@ -4451,9 +4454,16 @@ impl PyConversation {
44514454
/// Returns:
44524455
/// List of Conversation objects sorted by start time.
44534456
#[pyfunction]
4454-
#[pyo3(signature = (pcap_path, config=None))]
4455-
fn extract_flows(pcap_path: &str, config: Option<PyFlowConfig>) -> PyResult<Vec<PyConversation>> {
4456-
let flow_config = config.map(|c| c.inner).unwrap_or_default();
4457+
#[pyo3(signature = (pcap_path, config=None, verbose=false))]
4458+
fn extract_flows(
4459+
pcap_path: &str,
4460+
config: Option<PyFlowConfig>,
4461+
verbose: bool,
4462+
) -> PyResult<Vec<PyConversation>> {
4463+
let mut flow_config = config.map(|c| c.inner).unwrap_or_default();
4464+
if verbose {
4465+
flow_config.verbose = true;
4466+
}
44574467

44584468
// Use streaming extraction — never loads all packets into memory at once
44594469
let conversations =
@@ -4484,10 +4494,11 @@ fn extract_flows(pcap_path: &str, config: Option<PyFlowConfig>) -> PyResult<Vec<
44844494
/// Returns:
44854495
/// List of Conversation objects sorted by start time.
44864496
#[pyfunction]
4487-
#[pyo3(signature = (packets, config=None))]
4497+
#[pyo3(signature = (packets, config=None, verbose=false))]
44884498
fn extract_flows_from_packets(
44894499
packets: Vec<PyRef<'_, PyPacket>>,
44904500
config: Option<PyFlowConfig>,
4501+
verbose: bool,
44914502
) -> PyResult<Vec<PyConversation>> {
44924503
let captured: Vec<stackforge_core::CapturedPacket> = packets
44934504
.iter()
@@ -4502,7 +4513,10 @@ fn extract_flows_from_packets(
45024513
})
45034514
.collect();
45044515

4505-
let flow_config = config.map(|c| c.inner).unwrap_or_default();
4516+
let mut flow_config = config.map(|c| c.inner).unwrap_or_default();
4517+
if verbose {
4518+
flow_config.verbose = true;
4519+
}
45064520

45074521
let conversations = stackforge_core::extract_flows_with_config(&captured, flow_config)
45084522
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;

0 commit comments

Comments
 (0)