Intermediate Projects

Log File Analyzer

A log file analyzer streams structured server logs to extract severity levels (INFO, WARN, ERROR), HTTP status codes, endpoints, and request latencies, computing error rates, status-code distributions, and tail-latency percentiles (p50, p95, p99) per route.

Streaming Log Analysis Pipeline:

  Log Line: `"2026-10-27T12:00:01Z ERROR GET /v1/checkout status=503 latency_ms=420"`
        |
        v  Zero-Copy Line Parser (`LogEntryView<'a>`)
  [ Aggregator (`LogMetrics`) ]
    ├── Level Counts (`info`, `warn`, `error`)
    ├── Status Class Counts (`2xx`, `4xx`, `5xx`)
    └── Per-Endpoint Latencies (`HashMap<String, Vec<u32>>`)
            |
            v
      Compute `p50`, `p95`, `p99` & Error Rate per Endpoint!

Zero-Copy Parsing Combined with Percentile Aggregation

By parsing each log line into a borrowed LogEntryView<'a> (&'a str fields), the analyzer avoids allocating strings for lines or fields that are only inspected transiently, allocating an owned String key only when inserting a new route into the endpoint table.

Complete Runnable Implementation

use std::collections::HashMap;

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

#[derive(Debug, PartialEq, Eq)]
pub struct LogEntryView<'a> {
    pub timestamp: &'a str,
    pub level: LogLevel,
    pub method: &'a str,
    pub path: &'a str,
    pub status: u16,
    pub latency_ms: u32,
}

impl<'a> LogEntryView<'a> {
    /// Parses `"<ts> <LEVEL> <METHOD> <PATH> status=<CODE> latency_ms=<MS>"` with 0 heap allocations!
    pub fn parse(line: &'a str) -> Option<Self> {
        let mut tokens = line.split_ascii_whitespace();
        let timestamp = tokens.next()?;
        let level = match tokens.next()? {
            "INFO" => LogLevel::Info,
            "WARN" => LogLevel::Warn,
            "ERROR" => LogLevel::Error,
            _ => return None,
        };
        let method = tokens.next()?;
        let path = tokens.next()?;
        let status = tokens.next()?.strip_prefix("status=")?.parse().ok()?;
        let latency_ms = tokens.next()?.strip_prefix("latency_ms=")?.parse().ok()?;

        Some(Self {
            timestamp,
            level,
            method,
            path,
            status,
            latency_ms,
        })
    }
}

#[derive(Debug, PartialEq, Eq)]
pub struct RouteSummary {
    pub total_requests: usize,
    pub error_5xx_count: usize,
    pub p50_ms: u32,
    pub p95_ms: u32,
    pub max_ms: u32,
}

#[derive(Default)]
pub struct LogAnalyzer {
    pub level_counts: HashMap<LogLevel, usize>,
    route_latencies: HashMap<String, Vec<u32>>,
    route_5xx: HashMap<String, usize>,
    pub malformed_lines: usize,
}

impl LogAnalyzer {
    pub fn ingest_log_stream(&mut self, log_data: &str) {
        for line in log_data.lines() {
            if line.trim().is_empty() {
                continue;
            }
            let Some(entry) = LogEntryView::parse(line) else {
                self.malformed_lines += 1;
                continue;
            };

            *self.level_counts.entry(entry.level).or_insert(0) += 1;

            if let Some(latencies) = self.route_latencies.get_mut(entry.path) {
                latencies.push(entry.latency_ms);
            } else {
                self.route_latencies
                    .insert(entry.path.to_string(), vec![entry.latency_ms]);
            }

            if (500..600).contains(&entry.status) {
                *self.route_5xx.entry(entry.path.to_string()).or_insert(0) += 1;
            }
        }
    }

    pub fn route_summary(&self, path: &str) -> Option<RouteSummary> {
        let mut samples = self.route_latencies.get(path)?.clone();
        if samples.is_empty() {
            return None;
        }
        samples.sort_unstable();
        let n = samples.len();
        let p50_ms = samples[((n - 1) * 50) / 100];
        let p95_ms = samples[((n - 1) * 95) / 100];
        let max_ms = samples[n - 1];
        let error_5xx_count = self.route_5xx.get(path).copied().unwrap_or(0);

        Some(RouteSummary {
            total_requests: n,
            error_5xx_count,
            p50_ms,
            p95_ms,
            max_ms,
        })
    }
}

fn main() {
    let logs = "\
2026-10-27T10:00:01Z INFO  GET  /v1/items status=200 latency_ms=12
2026-10-27T10:00:02Z INFO  GET  /v1/items status=200 latency_ms=15
2026-10-27T10:00:03Z WARN  GET  /v1/items status=200 latency_ms=85
2026-10-27T10:00:04Z ERROR GET  /v1/items status=502 latency_ms=240
2026-10-27T10:00:05Z INFO  POST /v1/auth  status=200 latency_ms=8
corrupted_line_without_fields
";

    let mut analyzer = LogAnalyzer::default();
    analyzer.ingest_log_stream(logs);

    assert_eq!(analyzer.malformed_lines, 1);
    assert_eq!(analyzer.level_counts.get(&LogLevel::Error), Some(&1));

    let items_summary = analyzer.route_summary("/v1/items").unwrap();
    assert_eq!(items_summary.total_requests, 4);
    assert_eq!(items_summary.error_5xx_count, 1);
    assert_eq!(items_summary.max_ms, 240);

    println!("/v1/items summary: {items_summary:?}");
    println!("Malformed lines skipped: {}", analyzer.malformed_lines);
}
/v1/items summary: RouteSummary { total_requests: 4, error_5xx_count: 1, p50_ms: 15, p95_ms: 85, max_ms: 240 }
Malformed lines skipped: 1