Aggregate Structured Log Lines
Problem Statement
Structured logging exists so that log lines are data, not prose: one JSON object per line, parsed by the aggregator rather than by a regex someone wrote at 3am. Aggregate a batch of such lines into the numbers a dashboard actually shows.
Background
A well-behaved line looks like:
{"ts": "2026-08-17T10:00:01Z", "level": "INFO", "status": 200, "path": "/predict", "latency_ms": 12.5}
Real log streams are never that clean — a library writes a plain-text line, a crash truncates a record. Log hygiene means counting those separately instead of letting them silently vanish or crash the parser.
Rules:
- A line is valid when it parses as JSON, is an object, and has a "status" key. Anything else is malformed and counted, not aggregated.
- Bucket each valid status by its class: 200 -> "2xx", 404 -> "4xx", 503 -> "5xx".
- error_rate is the fraction of valid lines whose status is a 5xx (server errors only — a 404 is the caller's problem), rounded to 4 decimals.
- top_error_path is the path with the most 5xx responses; break ties alphabetically. None when there were no 5xx lines. Use "unknown" when a 5xx line has no path.
Dict key order in the output must be deterministic, so by_class is built with its keys sorted.
Your Task
Implement:
def analyze_logs(lines):
Return a dict with keys "total", "malformed", "by_class", "error_rate", "top_error_path", in that order.
Input Format
- lines: list of strings, each intended to be one JSON log record.
Output Format
- The dict described above. "total" counts only valid lines.
Sample
lines = ['{"status": 200, "path": "/predict"}',
'{"status": 500, "path": "/predict"}',
'oops not json',
'{"status": 404, "path": "/v1/missing"}']
print(analyze_logs(lines))
Output:
{'total': 3, 'malformed': 1, 'by_class': {'2xx': 1, '4xx': 1, '5xx': 1}, 'error_rate': 0.3333, 'top_error_path': '/predict'}
Three lines parse; the plain-text line is counted as malformed rather than dropped. One of the three valid lines is a 5xx, so the error rate is 0.3333.
Example
lines = ['{"status": 200, "path": "/predict"}', '{"status": 500, "path": "/predict"}', 'oops not json', '{"status": 404, "path": "/v1/missing"}']
print(analyze_logs(lines)){'total': 3, 'malformed': 1, 'by_class': {'2xx': 1, '4xx': 1, '5xx': 1}, 'error_rate': 0.3333, 'top_error_path': '/predict'}oops not json fails to parse, so it is counted as malformed and excluded from total. The three valid lines bucket to 2xx, 5xx and 4xx by integer-dividing the status by 100. Only the 500 counts toward the error rate: 1 / 3 = 0.3333, and its path is the only 5xx path, so it is the top offender.
Constraints
- 0 <= len(lines) <= 100000
- A line is valid only if it parses as a JSON object containing a
"status"key; everything else counts as malformed by_classkeys are"1xx".."5xx", present only when observed, and must be inserted in sorted ordererror_ratecounts 5xx only, over valid lines, rounded to 4 decimalstop_error_pathbreaks ties alphabetically and isNonewhen there are no 5xx lines- A 5xx line with no
"path"is attributed to"unknown"