Tier 3 โ€” Upper-Intermediate

Log Analyser

Parse server log lines to count errors, find the busiest hour, and spot the top offenders. Turn raw logs into insight.

๐Ÿ Python ๐ŸŸจ JavaScript

1. The Problem

Parse server log lines of the form "2026-06-24 14:30:00 ERROR Database timeout" and produce a report: total entries, how many were errors, the busiest hour, and the three most common error messages.

Why this project? Real-world text parsing with a known delimiter, tallying with a frequency map, and ranking by count โ€” exactly what log-analysis and analytics tools do under the hood.

2. How to Think About This

Split each line into at most four parts: date, time, level, and the (possibly space-containing) message โ€” a limited split keeps the message intact. Tally levels, hours, and error messages into plain objects used as counters. To find the busiest hour or top errors, turn the counter into entries and sort by count descending, then take the top N.

3. The Build

analyse.mjs
// Parse one line into { hour, level, message }, or null if malformed.
export function parseLine(line) {
  const parts = line.split(" ");
  if (parts.length < 4) return null;
  const [date, time, level, ...rest] = parts;
  return { hour: time.slice(0, 2), level, message: rest.join(" ").trim() };
}

// Sort a counter object into [key, count] pairs, highest first.
function ranked(counter) {
  return Object.entries(counter).sort((a, b) => b[1] - a[1]);
}

export function analyse(lines) {
  const levels = {}, hours = {}, errors = {};
  for (const line of lines) {
    const entry = parseLine(line);
    if (!entry) continue;
    levels[entry.level] = (levels[entry.level] || 0) + 1;
    hours[entry.hour] = (hours[entry.hour] || 0) + 1;
    if (entry.level === "ERROR") {
      errors[entry.message] = (errors[entry.message] || 0) + 1;
    }
  }
  const total = Object.values(levels).reduce((a, b) => a + b, 0);
  return {
    total,
    errors: levels.ERROR || 0,
    busiestHour: ranked(hours)[0] || null,
    topErrors: ranked(errors).slice(0, 3),
  };
}

// Demo
const log = [
  "2026-06-24 14:30:00 ERROR Database timeout",
  "2026-06-24 14:45:00 INFO  Request served",
  "2026-06-24 14:50:00 ERROR Database timeout",
  "2026-06-24 09:10:00 ERROR Disk full",
];
console.log(analyse(log));

4. Test & Prove

analyse.test.mjs
import { analyse, parseLine } from "./analyse.mjs";

let pass = 0, fail = 0;
function test(desc, fn) {
  try { fn(); console.log("  \u2713", desc); pass++; }
  catch (e) { console.log("  \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }

const log = [
  "2026-06-24 14:30:00 ERROR Database timeout",
  "2026-06-24 14:45:00 INFO Request served",
  "2026-06-24 14:50:00 ERROR Database timeout",
  "2026-06-24 09:10:00 ERROR Disk full",
];

test("parseLine keeps multi-word messages", () => {
  const e = parseLine("2026-06-24 14:30:00 ERROR Database timeout");
  assert(e.message === "Database timeout" && e.hour === "14");
});
test("malformed lines return null", () => {
  assert(parseLine("garbage") === null);
});
test("total counts every valid line", () => {
  assert(analyse(log).total === 4);
});
test("errors are counted", () => {
  assert(analyse(log).errors === 3);
});
test("busiest hour is 14 with 3 hits", () => {
  const [hour, count] = analyse(log).busiestHour;
  assert(hour === "14" && count === 3);
});
test("top error is the repeated timeout", () => {
  assert(analyse(log).topErrors[0][0] === "Database timeout");
});

console.log(`\n${pass} passed, ${fail} failed`);

5. The Interface

node analyse.mjs

{
  total: 4,
  errors: 3,
  busiestHour: [ '14', 3 ],
  topErrors: [ [ 'Database timeout', 2 ], [ 'Disk full', 1 ] ]
}

6. Run It

node analyse.mjs
node analyse.test.mjs

๐ŸŽฏ Try this next

  1. Read a real .log file from disk with the node:fs module instead of an array
  2. Add a warnings count and a per-level breakdown to the report
  3. Group errors by hour to find when failures spike
  4. Output the report as JSON or a small HTML table

What you practised

Text parsing with a limited split, tallying into counter objects, and ranking by sorting Object.entries() by count โ€” plus reducing an array of counts to a total.

Reference: Strings ยท Objects ยท Arrays

Try It Yourself