Count log levels per service in JSON-lines logs
Problem statement
A log shipper hands you structured logs as JSON lines: one JSON object per line. Count events per service and level. Print one line per service, sorted by name, with the counts for ERROR, WARN and INFO in that order.
The data is messy, as real shipped logs are:
- Levels arrive in mixed case, and some libraries write
WARNINGinstead ofWARN. Normalise them. - One line was cut in half by a buffer flush.
- One line is valid JSON but is an array, not an object.
- One object has no
servicefield.
Skip bad lines, print the reason for each on stderr with its line number, and print the number skipped at the end.
events.jsonl
{"ts":"2026-09-20T10:00:01Z","service":"api","level":"INFO","msg":"request served"}{"ts":"2026-09-20T10:00:02Z","service":"api","level":"error","msg":"upstream timeout"}{"ts":"2026-09-20T10:00:02Z","service":"worker","level":"INFO","msg":"job 41 done"}{"ts":"2026-09-20T10:00:03Z","service":"api","level":"WARNING","msg":"slow response"}{"ts":"2026-09-20T10:00:04Z","service":"worker","level":"ERROR","msg":"job 42 failed"}{"ts":"2026-09-20T10:00:05Z","service":"api","level":"INFO","msg":"request{"ts":"2026-09-20T10:00:06Z","service":"api","level":"INFO","msg":"request served"}["not", "an", "object"]{"ts":"2026-09-20T10:00:07Z","level":"INFO","msg":"no service field"}{"ts":"2026-09-20T10:00:08Z","service":"worker","level":"INFO","msg":"job 43 done"}Examples
Example 1
Input: python solution.py events.jsonl
Output: api ERROR=1 WARN=1 INFO=2
worker ERROR=1 WARN=0 INFO=2
skipped 3 line(s)
Explanation: stderr shows line 6: invalid JSON, line 8: not a JSON object and line 9: missing service or level. error counts as ERROR and WARNING counts as WARN.
Hints
Approach
Optimal
Validate in layers and report which layer failed.
- Skip blank lines. Parse each line on its own; on
JSONDecodeError, reporte.msgwith the line number. - Require a dict, then require
serviceandlevelto be strings.event.getreturnsNonefor missing keys, so a missing key and anullvalue are handled the same way. - Normalise the level with
.strip().upper()and map aliases such asWARNINGtoWARN. - Increment
counts[service][level]. ACounterreturns 0 for levels a service never logged, soWARN=0prints without special-casing.
Time is linear. Memory is one counter per (service, level) pair, independent of file size.
O(n)Space O(s * l)import jsonimport sysfrom collections import Counter, defaultdict LEVELS = ["ERROR", "WARN", "INFO"]ALIASES = {"WARNING": "WARN", "ERR": "ERROR", "FATAL": "ERROR"} def count_levels(lines): counts = defaultdict(Counter) skipped = 0 for lineno, line in enumerate(lines, 1): if not line.strip(): continue try: event = json.loads(line) except json.JSONDecodeError as e: print(f"line {lineno}: invalid JSON ({e.msg})", file=sys.stderr) skipped += 1 continue if not isinstance(event, dict): print(f"line {lineno}: not a JSON object", file=sys.stderr) skipped += 1 continue service, level = event.get("service"), event.get("level") if not isinstance(service, str) or not isinstance(level, str): print(f"line {lineno}: missing service or level", file=sys.stderr) skipped += 1 continue level = level.strip().upper() counts[service][ALIASES.get(level, level)] += 1 return counts, skipped def main(path): with open(path, encoding="utf-8", errors="replace") as f: counts, skipped = count_levels(f) for service in sorted(counts): c = counts[service] print(service, " ".join(f"{lvl}={c[lvl]}" for lvl in LEVELS)) print(f"skipped {skipped} line(s)") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "events.jsonl")Follow-up questions
- Also report the top 3 error messages per service.
- Some events nest the level under
log.level. How do you look up a dotted path safely? - The file is gzipped and 10 GB. What changes? (Only the
opencall.)
Frequently asked questions
stdout is the data, which another program might parse or a test might compare. stderr is for humans. Keeping them apart means python solution.py events.jsonl > report.txt gives a clean report while you still see the warnings in the terminal.
Most modern services log JSON, and anything that ships logs (Fluent Bit, Vector, a Lambda subscription) sometimes delivers truncated or odd records. The interviewer wants to see that one bad record is skipped and reported, not allowed to crash the whole batch.