The Status Board
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L4
Problem
In this batch of raw Nginx access logs, a line counts only when its quoted request names a method and a path and a three-digit status follows the closing quote; a request target with a raw space Nginx logged verbatim still counts, whatever trails the status (size, referer, user agent) doesn't matter, and every other line is junk to skip. The on-call engineer triaging the batch wants how many times each status code appears and every unique resource path that was hit, in the order the paths first show up. A path's query string is not part of its identity, so /api/users?page=1 and /api/users?page=2 both count as /api/users.
A pile of raw access logs, and two numbers that matter hiding inside the quotes.
Example
Input:
log_lines = ["10.0.0.1 - - [01/Jan/2024:00:00:00 +0000] \"GET /api/users?page=1 HTTP/1.1\" 200 512","10.0.0.2 - - [01/Jan/2024:00:00:01 +0000] \"GET /api/users?page=2 HTTP/1.1\" 200 512","10.0.0.1 - - [01/Jan/2024:00:00:02 +0000] \"POST /api/orders HTTP/1.1\" 201 128","10.0.0.3 - - [01/Jan/2024:00:00:03 +0000] \"GET /search?q=logs page HTTP/1.1\" 404 64","10.0.0.1 - - [01/Jan/2024:00:00:04 +0000] \"GET /health HTTP/1.1\" 200 2"]
Output:
[{"200":3,"201":1,"404":1},["/api/users","/api/orders","/search","/health"]]Worked solution and explanation
What this problem is really testing
Beneath the log-parsing costume, this is field extraction anchored on quote delimiters plus order-preserving deduplication. The move everyone reaches for is fixed columns: line.split()[8] for the status, [6] for the path. It looks bulletproof against tidy sample data and it is, right up until a request field carries a raw space. A bot sends GET /search?q=logs page HTTP/1.1, Nginx logs the request line verbatim, and now every whitespace column after it is shoved one slot to the right. [8] is the protocol, not a status, so the line either crashes or gets silently dropped. The tell of seniority is seeing that the quotes are the only stable landmark in the line, and that a plain set would quietly scramble the first-seen path order the output demands.
Working through it
Step 1: Isolate the request field by its quotes, not by a token index
The request method, path, and protocol sit between double quotes, and the status code is the first token after the closing quote. Split the line on the quote character: index 1 is the request field, index 2 begins with the status. This holds even when the request contains extra spaces or when a referer and user-agent (also quoted) trail the line, because you are counting quotes, not spaces.
Step 2: Strip the query string from the path
Take the second token of the request field as the path, then split it on ? and keep the part before it. This is the trap the whole scenario is built around: /api/users?page=1 and /api/users?page=2 are the same resource, and skipping this step quietly multiplies your unique-path count.
Step 3: Count statuses and collect paths in first-seen order
Read the status as the first token after the closing quote and validate it is three digits, which also filters out truncated and junk lines. Tally statuses in a dict. Keep unique paths in a list, appending only on first sight, with a companion set for the membership test so the dedup stays O(1) per line while the list preserves order.
Step 4: Return the counts and the path list
Return a 2-element list: the status-counts dict followed by the list of unique paths. With no well-formed lines that is [{}, []].
- Fixed-index split (fragile)
line.split()[8]for the status andline.split()[6]for the path assume the request is always exactly three tokens. The instant a request logs a raw space, the status moves to index 9 and this reads the protocol string as the status.- Quote-anchored split (robust)
parts = line.split(chr(34))puts the request atparts[1]and the status at the head ofparts[2]. The status is found relative to the closing quote, so internal request spaces and trailing quoted fields cannot move it.
The solution
def analyze_logs(log_lines: list) -> list:
status_counts = {}
unique_paths = []
seen = set()
for line in log_lines:
parts = line.split('"')
if len(parts) < 3:
continue
request = parts[1].split()
after = parts[2].split()
if len(request) < 2 or not after:
continue
status = after[0]
if len(status) != 3 or not status.isdigit():
continue
path = request[1].split("?")[0]
status_counts[status] = status_counts.get(status, 0) + 1
if path not in seen:
seen.add(path)
unique_paths.append(path)
return [status_counts, unique_paths]Common follow-up questions
- How would you handle gzip-compressed log files? (Tests knowledge of `gzip.open()` for transparent decompression.)
- What if the logs were streaming in real time rather than a fixed list? (Tests incremental processing and bounded memory usage.)
- How would you detect an anomalous spike in 5xx errors? (Tests sliding-window rate calculation and threshold alerting.)
- What if paths contained URL-encoded characters that should be normalized before deduplication? (Tests `urllib.parse.unquote` for normalization before counting.)