rust 120 lines · 3 tabs

Streaming Log File Aggregation With Buffered Line Iteration in Rust

Shared by codesnips Jul 2026
3 tabs
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
pub enum LogLevel {
    Info,
    Warn,
    Error,
}

impl LogLevel {
    fn from_str(s: &str) -> Option<LogLevel> {
        match s {
            "INFO" => Some(LogLevel::Info),
            "WARN" => Some(LogLevel::Warn),
            "ERROR" => Some(LogLevel::Error),
            _ => None,
        }
    }
}

pub struct ParsedLine {
    pub level: LogLevel,
    pub endpoint: String,
    pub latency_ms: u64,
}

impl ParsedLine {
    // Format: "<ts> <LEVEL> <endpoint> <latency_ms>"
    pub fn parse(line: &str) -> Option<ParsedLine> {
        let mut parts = line.splitn(4, ' ');
        let _ts = parts.next()?;
        let level = LogLevel::from_str(parts.next()?)?;
        let endpoint = parts.next()?;
        let latency = parts.next()?.trim().parse::<u64>().ok()?;
        Some(ParsedLine {
            level,
            endpoint: endpoint.to_string(),
            latency_ms: latency,
        })
    }
}

#[derive(Default)]
pub struct Stats {
    pub total_lines: u64,
    pub parse_failures: u64,
    pub error_count: u64,
    pub latency_by_endpoint: HashMap<String, (u64, u64)>,
}
3 files · rust Explain with highlit

This snippet shows how to process arbitrarily large log files in constant memory by streaming them line by line rather than reading the whole file into a String. The core insight, visible across all three tabs, is that BufReader::lines() yields one owned String at a time, so the peak memory footprint is bounded by the longest line plus a small buffer — even a multi-gigabyte file never fully lands in RAM.

In the log_line parser tab, LogLevel and ParsedLine model just the fields the aggregator cares about. ParsedLine::parse does a cheap splitn-based split instead of a regex, returning Option so malformed lines are skipped rather than aborting the run. Keeping the parser allocation-light matters because it runs once per line; borrowing with &str and only materializing an owned endpoint String avoids copying the entire input.

The stream aggregator tab holds the actual streaming loop. aggregate_file opens the file, wraps it in a BufReader, and drives reader.lines() as an iterator. Each item is a Result<String, io::Error> because I/O can fail mid-stream (a truncated network mount, a bad sector), so the ? on line_result propagates real read errors while ParsedLine::parse(...) quietly drops unparseable rows. The running Stats struct accumulates counts, error tallies, and a per-endpoint HashMap of latencies — state that grows with the number of distinct endpoints, not the number of lines, which is the property that keeps memory flat. A key pitfall this avoids is fs::read_to_string, which would defeat the entire purpose by buffering the whole file.

The main entrypoint tab wires it into a CLI: it reads the path from env::args, calls aggregate_file, and prints a summary, converting any error into a non-zero exit via process::exit. The trade-off of the line-by-line approach is that it assumes newline-delimited records; multi-line stack traces would need a stateful reader. For the common case of one event per line, this pattern gives predictable memory, early failure on I/O errors, and clean separation between parsing and aggregation.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming Log File Aggregation With Buffered Line Iteration in Rust — share card
Link copied