flowscope 0.14.0

Passive flow & session tracking for packet capture (runtime-free, cross-platform)
Documentation
//! One log line per HTTP request/response pair.
//!
//! Uses [`flowscope::http::HttpExchangeParser`] (plan 107, 0.10)
//! — the aggregator that stitches each request to its response
//! and emits a single [`flowscope::http::HttpExchange`] event
//! with elapsed RTT and an [`flowscope::http::HttpOutcome`]
//! discriminant.
//!
//! Compare to `http_log.rs` which dumps the per-message
//! `HttpRequest` / `HttpResponse` stream — this version is what
//! you reach for when you want one access-log-shaped row per
//! exchange.
//!
//! ```bash
//! cargo run --features http,pcap --example http_exchanges
//! ```

use flowscope::driver::SlotMessage;
use flowscope::driver::{Driver, Event};
use flowscope::extract::{FiveTuple, FiveTupleKey};
use flowscope::http::{HttpExchange, HttpExchangeParser, HttpOutcome};
use flowscope::pcap::PcapFlowSource;

fn main() -> flowscope::Result<()> {
    let path = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "tests/data/http_session.pcap".to_string());

    let mut builder = Driver::builder(FiveTuple::bidirectional());
    let mut ex_slot = builder.session_on_ports(HttpExchangeParser::new(), [80, 8080]);
    let mut driver = builder.build();

    println!(
        "{:<8} {:<6} {:<24} {:<32} {:<8} {:>5} {:>9}",
        "outcome", "status", "src → dst", "host + method + path", "ua", "ms", "bytes"
    );

    let mut events: Vec<Event<FiveTupleKey>> = Vec::new();
    let mut msgs: Vec<SlotMessage<HttpExchange, FiveTupleKey>> = Vec::new();

    for owned in PcapFlowSource::open(&path)?.views() {
        let owned = owned?;
        events.clear();
        driver.track_into(&owned, &mut events);
        msgs.clear();
        ex_slot.drain(&mut msgs);

        for m in &msgs {
            let key = &m.key;
            let ex = &m.message;
            let outcome = match ex.outcome {
                HttpOutcome::Completed if ex.is_success() => "2xx",
                HttpOutcome::Completed if ex.status_class() == Some(3) => "3xx",
                HttpOutcome::Completed if ex.is_error() => "err",
                HttpOutcome::Completed => "1xx",
                HttpOutcome::NoResponse => "no-resp",
                HttpOutcome::Reset => "rst",
                _ => "?",
            };
            let status = ex
                .response
                .as_ref()
                .map(|r| r.status.to_string())
                .unwrap_or_else(|| "-".to_string());
            let host = ex.request.host().unwrap_or("?");
            let ua = ex
                .request
                .user_agent()
                .map(|s| {
                    if s.len() > 8 {
                        s[..8].to_string()
                    } else {
                        s.to_string()
                    }
                })
                .unwrap_or_else(|| "-".to_string());
            let elapsed_ms = ex.elapsed.map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0);
            let bytes = ex.response.as_ref().map(|r| r.body.len()).unwrap_or(0);
            println!(
                "{outcome:<8} {status:<6} {:<24} {:<32} {ua:<8} {elapsed_ms:>5.1} {bytes:>9}",
                format!("{}{}", key.a.ip(), key.b.ip()),
                format!(
                    "{host} {} {}",
                    ex.request.method_str().unwrap_or("?"),
                    ex.request.path_str().unwrap_or("?")
                ),
            );
        }
    }

    // Final flush.
    events.clear();
    driver.finish_into(&mut events);
    msgs.clear();
    ex_slot.drain(&mut msgs);
    let _ = events; // events drained — exchanges may also flush.
    for m in &msgs {
        let key = &m.key;
        let ex = &m.message;
        let status = ex
            .response
            .as_ref()
            .map(|r| r.status.to_string())
            .unwrap_or_else(|| "-".to_string());
        let host = ex.request.host().unwrap_or("?");
        let bytes = ex.response.as_ref().map(|r| r.body.len()).unwrap_or(0);
        println!(
            "{:<8} {:<6} {:<24} {:<32} {:<8} {:>5} {:>9}",
            "flush",
            status,
            format!("{}{}", key.a.ip(), key.b.ip()),
            format!(
                "{host} {} {}",
                ex.request.method_str().unwrap_or("?"),
                ex.request.path_str().unwrap_or("?")
            ),
            "-",
            "-",
            bytes,
        );
    }
    Ok(())
}