Step 1 of 6
LOG-101: parse the access log
Welcome to your first week on the platform team. Every exercise in this module is a ticket, the way work actually arrives: a goal, some context, and acceptance criteria. There's less hand-holding than before; everything you need was covered in earlier modules, and the hints are there if you get stuck.
LOG-101: Parse access log lines into structured entries
Our services write one line per HTTP request:
2026-10-01T09:00:03Z GET /api/orders 200 35(timestamp, method, path, status code, latency in milliseconds). Today every script re-parses these with ad hocsplitcalls and crashes on bad lines. We want one parser the whole team can use:line.parse::<LogEntry>(), returning a precise error for each kind of bad input.
The shape of a good parser
You've seen every piece of this before; here they come together:
FromStr(module 23) lets callers writeline.parse::<LogEntry>(), like they would for a number.- An error enum (module 20) with one variant per failure, carrying the offending text. Callers can
matchon it, and tests can check exactly which error came back. Displayturns each variant into a message for humans.std::error::Errormakes it a "real" error, so it works with?in functions returningBox<dyn Error>and withanyhow.
Here's the same pattern on a smaller format, a sensor reading like kitchen=21.5:
use std::fmt;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct Reading {
sensor: String,
celsius: f64,
}
#[derive(Debug, PartialEq)]
enum ParseReadingError {
MissingEquals,
BadNumber(String),
}
impl fmt::Display for ParseReadingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseReadingError::MissingEquals => write!(f, "expected SENSOR=VALUE"),
ParseReadingError::BadNumber(s) => write!(f, "not a temperature: {s}"),
}
}
}
impl std::error::Error for ParseReadingError {}
impl FromStr for Reading {
type Err = ParseReadingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (sensor, value) = s.split_once('=').ok_or(ParseReadingError::MissingEquals)?;
let value = value.trim();
let celsius = value.parse().map_err(|_| ParseReadingError::BadNumber(value.to_string()))?;
Ok(Reading { sensor: sensor.trim().to_string(), celsius })
}
}
fn main() {
for line in ["kitchen=21.5", "garage = -3", "attic", "cellar=cold"] {
match line.parse::<Reading>() {
Ok(r) => println!("{} is {:.1} C", r.sensor, r.celsius),
Err(e) => println!("skipping {line:?}: {e}"),
}
}
}
kitchen is 21.5 C
garage is -3.0 C
skipping "attic": expected SENSOR=VALUE
skipping "cellar=cold": not a temperature: cold
ok_or(...)?turns anOptioninto aResultand returns early onNone.map_err(|_| ...)?replaces the library's error (ParseFloatError) with ours, keeping the bad text for the message.- Each error variant carries just enough to explain itself. "Something went wrong" helps nobody at 3 a.m.
Splitting a line into an exact number of fields is neat with a slice pattern and let ... else:
let fields: Vec<&str> = line.split_whitespace().collect();
let [timestamp, method, path, status, latency] = fields[..] else {
return Err(/* wrong number of fields: fields.len() */);
};
Common mistakes
unwrap()inside a parser. One bad line in a 10 GB log and the whole job dies. Parsers return errors; the caller decides what to do.- A
Stringas the error type. It works, but callers can't match on it and tests end up comparing message text. - Losing the bad input.
BadStatusalone doesn't say what was wrong;BadStatus("2x0")does. - Accepting "valid-looking" garbage.
"999".parse::<u16>()succeeds, but 999 isn't an HTTP status. Validate the range.
Your task
Implement the parser. The seed defines LogEntry and ParseLogError. Write:
impl fmt::Display for ParseLogErrorwith exactly these messages:expected 5 fields, found N,unknown HTTP method: M,bad status code: S,bad latency: Limpl FromStr for LogEntry: split on whitespace; there must be exactly 5 fields (WrongFieldCountwith the count otherwise); the method must be one ofGET,POST,PUT,PATCH,DELETE(case-sensitive,BadMethodotherwise); the status must be a number from 100 to 599 (BadStatuswith the text otherwise); the latency must parse as au32(BadLatencywith the text otherwise). Check in that order.
Challenge 2
LOG-104: Make the HTTP method a type
Reviewers keep catching
if entry.method == "GTE"typos in analytics code. Strings can't be checked by the compiler; an enum can.
Replace method: String with a Method enum (Get, Post, Put, Patch, Delete, deriving Debug, Clone, Copy, PartialEq). Implement FromStr for Method (with type Err = ParseLogError, returning BadMethod for anything else; still case-sensitive) and Display for Method (prints GET, POST, ...). Then make LogEntry::from_str use method.parse()?. Everything else behaves as before.
Challenge 3
LOG-107: Parse a whole log file and report every bad line
The nightly job should keep going past bad lines and list them all at the end, with line numbers, so whoever owns the noisy service can fix it.
Using the parser from LOG-101 (in the seed), write fn parse_log(text: &str) -> (Vec<LogEntry>, Vec<String>). Go through text line by line, numbering lines from 1. Skip blank lines and comment lines starting with # (after trimming). Every other line either becomes a LogEntry or produces a message line N: ERROR using the error's Display, for example line 3: bad status code: 2x0. Return the entries and the messages, both in file order.