RustArena

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 hoc split calls 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:

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

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

Your task

Implement the parser. The seed defines LogEntry and ParseLogError. Write:

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.

Next: LOG-102: the daily traffic report