Count status codes in an access log
Web servers write one line per request. Before anything reaches a dashboard, something has to turn those lines into numbers, and real logs are never perfectly clean.
Write count_by_status(lines):
-
linesis a list of strings in the common log format:203.0.113.9 - - [04/Oct/2026:10:00:01 +0000] "GET /api/orders HTTP/1.1" 200 512 -
The status code is the first field after the quoted request.
-
Return a dict mapping each status code (an
int) to how many lines had it. -
Skip blank lines and malformed lines: no quoted request, or a status that isn’t exactly three digits.
-
The size field can be
-(no body). That line still counts.
>>> count_by_status([
... '1.2.3.4 - - [04/Oct/2026:10:00:01 +0000] "GET / HTTP/1.1" 200 512',
... '1.2.3.4 - - [04/Oct/2026:10:00:02 +0000] "GET /x HTTP/1.1" 404 -',
... 'garbage',
... ])
{200: 1, 404: 1}
Hints get more specific as you go. Reveal one, try again, then come back.
Find the request by its quotes. Everything after the closing quote is status size.
line.rfind('"') gives you the closing quote. Split what follows on whitespace and take the first
token.
Check the token before converting: len(tok) == 3 and tok.isdigit(). Also check there were two
quotes at all, or a line with none will slip through.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Anchor on the quotes. The request is the only quoted part, so the text after the last " is
status size. Validate the status before counting it.
def count_by_status(lines):
counts = {}
for line in lines:
first = line.find('"')
last = line.rfind('"')
if first == -1 or last == first:
continue
rest = line[last + 1:].split()
if not rest:
continue
tok = rest[0]
if len(tok) != 3 or not tok.isdigit():
continue
status = int(tok)
counts[status] = counts.get(status, 0) + 1
return counts
Splitting the whole line on spaces looks easier, but the timestamp holds a space and paths can too.
Anchoring on structure is sturdier than counting fields. A regex like
r'"[^"]*"\s+(\d{3})\s' works as well.
Complexity
O(total characters). One scan per line, one dict update per good line.
Why this matters
Every log pipeline starts here. Parsing decides what counts as data and what counts as junk, and it should count the junk too. A sudden jump in skipped lines usually means the log format changed upstream, which is worth an alert before the dashboards quietly drift.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.