use once_cell::sync::Lazy;
use regex::Regex;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Severity {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug)]
pub struct ParseSeverityError(pub String);
impl FromStr for Severity {
type Err = ParseSeverityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"TRACE" => Ok(Severity::Trace),
"DEBUG" => Ok(Severity::Debug),
"INFO" => Ok(Severity::Info),
"WARN" | "WARNING" => Ok(Severity::Warn),
"ERROR" | "ERR" => Ok(Severity::Error),
other => Err(ParseSeverityError(other.to_string())),
}
}
}
#[derive(Debug)]
pub struct LogLine {
pub severity: Severity,
pub message: String,
}
static LINE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*\[?(?P<level>[A-Za-z]+)\]?\s+(?P<msg>.+)$").unwrap()
});
pub fn parse_line(raw: &str) -> Option<LogLine> {
let caps = LINE_RE.captures(raw)?;
let severity = caps.name("level")?.as_str().parse().ok()?;
let message = caps.name("msg")?.as_str().trim().to_string();
Some(LogLine { severity, message })
}
use crate::severity::{LogLine, Severity};
use std::collections::HashMap;
use std::fmt;
#[derive(Default)]
pub struct SeverityReport {
counts: HashMap<Severity, usize>,
samples: HashMap<Severity, String>,
}
impl SeverityReport {
pub fn record(&mut self, line: LogLine) {
*self.counts.entry(line.severity).or_insert(0) += 1;
self.samples.entry(line.severity).or_insert(line.message);
}
pub fn total(&self) -> usize {
self.counts.values().sum()
}
pub fn dominant(&self) -> Option<Severity> {
self.counts
.iter()
.max_by_key(|(sev, count)| (**count, **sev))
.map(|(sev, _)| *sev)
}
}
impl fmt::Display for SeverityReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut levels: Vec<_> = self.counts.iter().collect();
levels.sort_by(|a, b| b.0.cmp(a.0));
writeln!(f, "=== Log Summary ({} lines) ===", self.total())?;
for (sev, count) in levels {
let sample = self.samples.get(sev).map(String::as_str).unwrap_or("");
writeln!(f, "{:>6?}: {:>6} e.g. {}", sev, count, sample)?;
}
if let Some(dom) = self.dominant() {
write!(f, "Most frequent level: {:?}", dom)?;
}
Ok(())
}
}
mod report;
mod severity;
use report::SeverityReport;
use severity::parse_line;
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
let stdin = io::stdin();
let mut report = SeverityReport::default();
let lines = stdin
.lock()
.lines()
.filter_map(|res| res.ok())
.filter_map(|raw| parse_line(&raw));
for line in lines {
report.record(line);
}
if report.total() == 0 {
eprintln!("no parseable log lines found on stdin");
std::process::exit(1);
}
println!("{}", report);
Ok(())
}
This snippet builds a small log-analysis pipeline in Rust that reads log lines, classifies each by severity, and prints an aggregated summary. The design separates concerns across three files: a parsing module that turns a raw line into a structured record, an aggregator that accumulates counts and samples, and a main entry point that wires stdin to the report.
In severity.rs, the Severity enum models the fixed set of levels an application emits. Implementing FromStr gives a natural, fallible conversion from a token like WARN into a variant, returning a custom ParseSeverityError for unknown tokens. parse_line uses a lazily-compiled Regex (via once_cell::sync::Lazy) so the pattern is built once and reused across every line, avoiding the classic mistake of recompiling a regex in a hot loop. It returns Option<LogLine> so malformed lines are simply skipped rather than aborting the run.
In report.rs, SeverityReport is the accumulator. Its record method bumps a per-severity counter in a HashMap<Severity, usize> and keeps the first-seen message per level in samples, giving the reader a concrete example without storing every line. The Ord derived on Severity lets the report sort levels from most to least severe when rendering. Implementing Display keeps formatting logic in one place, and total plus dominant expose derived stats. Because record takes ownership of a LogLine, the message String moves into the sample map without a copy.
In main.rs, lines from stdin().lock().lines() are streamed one at a time, so the tool handles arbitrarily large files with constant memory rather than buffering the whole input. Each line is parsed, filtered through filter_map, and folded into the report. The key trade-off is deliberate leniency: unparseable lines are dropped silently, which suits noisy real-world logs but would need a stricter mode for validation use cases. This composition of FromStr, iterators, and a Display accumulator is the idiomatic Rust way to build a focused, testable text-processing tool.
Related snips
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
import type { IncomingMessage, ServerResponse } from "http";
const MIN_BYTES = 1024;
const INCOMPRESSIBLE = /^(image|video|audio)\/|application\/(zip|gzip|x-brotli|pdf|octet-stream)/i;
Response compression (only when it helps)
Share this code
Here's the card — post it anywhere.