← thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · TypeScript Project

Log Analyser

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

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a log analyser: read a server log file, count how many entries are errors versus normal, find which hour had the most traffic, and list the most frequent error messages. It teaches parsing semi-structured text at scale and aggregating it into useful insight — a daily task in operations.

Where this shows up: monitoring and observability, debugging production issues, security analysis, performance tuning. When something breaks at 3am, the person who can analyse the logs is the one who fixes it.

2 How to Think About It

Think about turning lines into counts, before any code:

The plan — in plain English
1. Read the log file line by line. → 2. Parse each line into its parts: timestamp, level (INFO/ERROR), message. → 3. Count as you go: errors, entries per hour, message frequencies. → 4. Report the totals and the top items.

Read log lines

Parse each: time, level, message

Count errors

Count entries per hour

Count message frequency

Report insights

3 The Build — explained part by part

Here is the complete analyser. Read each part’s note below — you should understand the whole thing from the notes alone.

TypeScriptloganalyse.ts
import * as fs from "node:fs";

interface Entry {
  hour: string;
  level: string;
  message: string;
}

export interface Report {
  total: number;
  errors: number;
  busiestHour: [string, number] | null;
  topErrors: [string, number][];
}

// bump increments a key in a Map-based counter — TypeScript has no built-in
// Counter like Python's collections.Counter, so this tiny helper plays the
// same role.
function bump(counter: Map<string, number>, key: string): void {
  counter.set(key, (counter.get(key) ?? 0) + 1);
}

function topN(counter: Map<string, number>, n: number): [string, number][] {
  return [...counter.entries()].sort((a, b) => b[1] - a[1]).slice(0, n);
}

export function parseLine(line: string): Entry | null {
  // Expected format: "2026-06-24 14:30:00 ERROR Database timeout"
  const parts = line.split(" ");
  if (parts.length < 4) return null;
  const [, time, level, ...rest] = parts;
  return { hour: time.slice(0, 2), level, message: rest.join(" ").trim() };
}

export function analyse(lines: string[]): Report {
  const levels = new Map<string, number>();
  const hours = new Map<string, number>();
  const errors = new Map<string, number>();

  for (const line of lines) {
    const entry = parseLine(line);
    if (!entry) continue;
    bump(levels, entry.level);
    bump(hours, entry.hour);
    if (entry.level === "ERROR") bump(errors, entry.message);
  }

  const total = [...levels.values()].reduce((a, b) => a + b, 0);
  const busiest = topN(hours, 1);

  return {
    total,
    errors: levels.get("ERROR") ?? 0,
    busiestHour: busiest.length ? busiest[0] : null,
    topErrors: topN(errors, 3),
  };
}

function main(): void {
  const lines = fs.readFileSync("server.log", "utf-8").split(/\r?\n/).filter(Boolean);
  const report = analyse(lines);
  console.log(`Total entries: ${report.total}`);
  console.log(`Errors: ${report.errors}`);
  console.log(`Busiest hour: ${report.busiestHour ? report.busiestHour.join(", ") : "null"}`);
  console.log(`Top errors: ${JSON.stringify(report.topErrors)}`);
}

if (require.main === module) {
  main();
}
⚠ No in-browser playground here
Running real, type-checked TypeScript in the browser needs either a full copy of the compiler or a third-party CDN script — the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with Node on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
interface Entry / interface Report — TypeScript interfaces describe the exact shape of the data flowing through the program: an Entry always has an hour, a level, and a message; a Report always has all four summary fields. Mistype a field name anywhere and the compiler catches it.

function bump(counter: Map<string, number>, key: string) — TypeScript has no built-in Counter like Python’s collections.Counter, so this one small helper plays the same role: look up the current count (or 0 if it is new) and add one, using a Map rather than a plain object because the keys are not known ahead of time.

function topN(counter, n): [string, number][] — [string, number] is a tuple type: an array that is always exactly a string followed by a number, used here for a “key and its count” pair, matching the shape of Python’s Counter.most_common().

parts.split(" ") then destructuring const [, time, level, ...rest] = parts — skips the date (the leading comma), grabs time and level by position, and collects everything else into rest with the rest operator, rejoining it with rest.join(" ") to rebuild the message — equivalent to Python’s line.split(" ", 3).
Common mistakes — and how to avoid them
✗ Reaching for a plain {} object as a counter.
✓ A Map<string, number> is the right tool when keys are only known at runtime — it also avoids subtle bugs from inherited object properties like toString colliding with a real key.
✗ Declaring topErrors as string[][] instead of [string, number][].
✓ string[][] says “an array of arrays of strings” — it would silently accept ["a", "b", "c"] as one entry. The tuple type [string, number] pins down exactly two elements of exactly those types.
✗ Assuming every line has exactly four space-separated parts.
✓ Check parts.length < 4 and return null for malformed lines, the same guard the Python version uses, so one bad line does not crash the whole report.

4 Test & Prove Each Part

How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.

A line parses into its parts
Two errors are counted
Hour 14 has the most entries
TypeScriptloganalyse.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseLine, analyse } from "./log-analyser";

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

test("a line parses into its parts", () => {
  const entry = parseLine(LINES[0]);
  assert.equal(entry?.hour, "14");
  assert.equal(entry?.level, "ERROR");
});

test("two errors are counted", () => {
  assert.equal(analyse(LINES).errors, 2);
});

test("hour 14 has the most entries", () => {
  assert.equal(analyse(LINES).busiestHour?.[0], "14");
});

Compile with npx tsc then run node --test loganalyse.test.js. parseLine and analyse take plain strings and arrays, so the tests check known log lines directly, without touching a real server.log file.

5 The Interface

INPUTINPUTlog file
What it expects
2026-06-24 14:30:00 ERROR Database timeout
OUTPUTOUTPUTreport
What it returns
Total: 6  Errors: 3
Busiest hour: ("14", 3)
Top errors: [("Database timeout", 2), ...]

6 Run It & Automate It

Save the code as loganalyse.ts, compile with npx tsc, and run with node loganalyse.js against a real server.log file in the same folder — or run it directly with npx tsx loganalyse.ts.

Run it locally
npx tsc loganalyse.ts && node loganalyse.js
Reads server.log in the current folder and prints the summary.

A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
Total entries: 3
Errors: 2
Busiest hour: 14, 2
Top errors: [["Database timeout",1],["Disk full",1]]
If it breaks — how to fix it
🚨 ENOENT: no such file or directory, open 'server.log'
The program expects server.log in the current working directory. Create one (or copy a real one) before running it.
🚨 Every line comes back as “malformed” (counted as 0 total)
Check the log format matches DATE TIME LEVEL MESSAGE with single spaces between the first three fields — parseLine relies on that exact shape.
GroovyJenkinsfile
// Jenkinsfile &mdash; runs the type-checker and tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Node') {
            steps {
                sh 'node --version'                              // confirm Node is installed
                sh 'npm install -D typescript @types/node'       // zero runtime deps &mdash; just the compiler and its Node types
            }
        }
        stage('Type-check and test') {
            steps {
                sh 'npx tsc --noEmit'                 // catch type errors before anything runs
                sh 'npx tsc'                           // compile to plain JavaScript
                sh 'node --test loganalyse.test.js'            // Node's built-in test runner, no extra install needed
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working log analyser. Extend it:

  1. Stream instead of loading the whole file. Use readline.createInterface over a file stream for logs too large to fit in memory. (Teaches: streaming line-by-line processing.)
  2. Support more levels. Break out WARN counts alongside ERROR. (Teaches: extending the Report interface.)
  3. Parse real timestamps. Use the Date object instead of just the hour substring. (Teaches: Date parsing and its pitfalls.)
  4. Output JSON. Print JSON.stringify(report, null, 2) for a machine-readable report. (Teaches: pretty-printing JSON.)
What you learned
You learned interfaces for describing data shape, a Map-based counter standing in for Python’s Counter, and tuple types for fixed-shape pairs like [key, count]. Related reference: Tuples, Generics.