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
// 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
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
- Read a real .log file from disk with the node:fs module instead of an array
- Add a warnings count and a per-level breakdown to the report
- Group errors by hour to find when failures spike
- Output the report as JSON or a small HTML table