Count errors per minute, including quiet minutes
Problem statement
An application writes one line per event: date, time, level, component and message. For a dashboard you need the number of ERROR lines in each minute, from the first minute in the log to the last.
A minute with no errors must still appear with a count of 0. Otherwise a graph built from your output draws a straight line across the gap and hides the quiet period. Lines whose timestamp cannot be parsed are skipped and reported on stderr.
app.log
2026-09-20 10:00:05 INFO api request served2026-09-20 10:00:17 ERROR api upstream timeout2026-09-20 10:00:41 ERROR api upstream timeout2026-09-20 10:01:02 INFO api request served2026-09-20 10:02:10 ERROR db connection reset2026-09-20 10:02:11 ERROR db connection reset2026-09-20 10:02:59 ERROR api upstream timeout2026-09-20 10:03:30 WARN api retry budget lowPrint one line per minute: YYYY-MM-DD HH:MM count.
Examples
Example 1
Input: python solution.py app.log
Output: 2026-09-20 10:00 2
2026-09-20 10:01 0
2026-09-20 10:02 3
2026-09-20 10:03 0
Explanation: 10:01 and 10:03 have log lines but no errors, so they are printed with 0. The range runs from the first minute in the log to the last, not from the first error to the last.
Hints
Approach
Optimal
Make one pass over the lines, then one pass over the minute range.
- Split each line into at most four parts: date, time, level, rest. If the timestamp does not parse, count the line as skipped.
- Truncate the timestamp to the minute. Update
firstandlastwithmin/maxinstead of assuming the file is sorted; multi-threaded loggers often write lines slightly out of order. - If the level is
ERROR, incrementcounts[minute]. - Walk from
firsttolastone minute at a time and emit(minute, counts[minute]).
n is the number of lines and m the number of minutes covered. Memory only depends on the time span, so a day of logs needs at most 1,440 counters however many lines it has.
O(n + m)Space O(m)import sysfrom collections import Counterfrom datetime import datetime, timedelta def errors_per_minute(lines): counts = Counter() first = last = None skipped = 0 for line in lines: parts = line.split(maxsplit=3) if not parts: continue # blank line try: ts = datetime.strptime(f"{parts[0]} {parts[1]}", "%Y-%m-%d %H:%M:%S") level = parts[2] except (IndexError, ValueError): skipped += 1 continue minute = ts.replace(second=0) # min/max rather than "first line wins": lines can arrive slightly out of order first = minute if first is None else min(first, minute) last = minute if last is None else max(last, minute) if level == "ERROR": counts[minute] += 1 rows = [] minute = first while minute is not None and minute <= last: rows.append((minute, counts[minute])) # Counter returns 0 for quiet minutes minute += timedelta(minutes=1) return rows, skipped def main(path): with open(path, encoding="utf-8", errors="replace") as f: rows, skipped = errors_per_minute(f) for minute, n in rows: print(f"{minute:%Y-%m-%d %H:%M} {n}") if skipped: print(f"skipped {skipped} unparseable line(s)", file=sys.stderr) if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "app.log")Follow-up questions
- Group by component as well, so you get errors per minute per component.
- The log spans a daylight-saving change in local time. What goes wrong, and why should logs be written in UTC?
- Make it streaming: print each minute as soon as it is complete instead of at the end. What do you do about late, out-of-order lines?
Frequently asked questions
Slicing works for well-formed lines and is faster. But 25:61 would pass as a valid minute, and you could not step to the next minute to fill gaps. Parsing into a datetime validates the timestamp and gives you arithmetic. For very large files you can slice first and parse only the distinct minute strings.
Turning raw logs into a time series is the first step of most incident timelines and of every log-based alert. The zero-filling detail shows whether you think about how the output will be used, not only whether the counts are right.