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