The brief
A web server has written eleven lines to a log. Three of them are not log lines: a comment, a blank line and something that got in by accident. That is what a real log looks like, and a parser that assumes otherwise falls over on its first day.
Your job is to turn the good lines into objects and answer four questions: when was it busiest, how often did it fail, which endpoint is slowest, and which is most used.
The format
A named-group regular expression is the right tool here, because this is a pattern: fixed fields separated by whitespace. Anything that does not match the whole line is not a log line, and the parser should say so by returning None rather than raising.
2026-09-22T08:14:03Z INFO GET /api/doors 200 42ms
└─ timestamp, ISO 8601, UTC
└─ level: INFO, WARN or ERROR
└─ method
└─ path
└─ status
└─ duration
What you will use
Every ranking has to break ties deterministically, or your answer changes between runs and no test can pin it down. Here: slowest first, and alphabetically when two are equally slow.
| Lesson | Here |
|---|---|
| Dataclasses | A frozen Entry — one record per line, hashable and printable |
| The standard library | re to parse, datetime to read the timestamp, Counter to tally |
| Iterators | read() yields entries, so a 2 GB log costs the same as this one |
| Dictionaries | defaultdict(list) to collect the durations per path |
| Errors | A bad line is not an exception — it is a line that is not a log line |
| Functional | max with a key, sorted with a two-part key for the ties |
The numbers to expect
Ten of the eleven lines parse. Three of them are errors. The busiest hour is 09, with five entries. /api/quote is by far the slowest endpoint at 1140 ms on average, and /api/doors is the most used, with four calls.
If your numbers differ, the parser is the first place to look — usually it accepted a line it should have rejected, or rejected one it should have taken.