thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · Python 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. Each part is explained below.

Pythonloganalyse.py
from collections import Counter

def parse_line(line):
    # Expected format: "2026-06-24 14:30:00 ERROR Database timeout"
    parts = line.split(" ", 3)
    if len(parts) < 4:
        return None
    date, time, level, message = parts
    return {&quot;hour&quot;: time[:2], &quot;level&quot;: level, &quot;message&quot;: message.strip()}

def analyse(lines):
    levels = Counter()
    hours = Counter()
    errors = Counter()
    for line in lines:
        entry = parse_line(line)
        if not entry:
            continue
        levels[entry[&quot;level&quot;]] += 1
        hours[entry[&quot;hour&quot;]] += 1
        if entry[&quot;level&quot;] == &quot;ERROR&quot;:
            errors[entry[&quot;message&quot;]] += 1
    return {
        &quot;total&quot;: sum(levels.values()),
        &quot;errors&quot;: levels[&quot;ERROR&quot;],
        &quot;busiest_hour&quot;: hours.most_common(1)[0] if hours else None,
        &quot;top_errors&quot;: errors.most_common(3),
    }

if __name__ == &quot;__main__&quot;:
    with open(&quot;server.log&quot;) as f:
        report = analyse(f)
    print(f&quot;Total entries: {report[&#x27;total&#x27;]}&quot;)
    print(f&quot;Errors: {report[&#x27;errors&#x27;]}&quot;)
    print(f&quot;Busiest hour: {report[&#x27;busiest_hour&#x27;]}&quot;)
    print(f&quot;Top errors: {report[&#x27;top_errors&#x27;]}&quot;)
What each part does — in plain words
from collections import Counter — a special dictionary built for counting. counter[key] += 1 just works, and it has handy methods like most_common.

parse_line — split each line into its parts. line.split(" ", 3) splits on the first 3 spaces only, so the message (which may contain spaces) stays whole. We return None for malformed lines.

time[:2] — the first two characters of the time ("14:30:00""14") give the hour, for grouping traffic by hour.

three Counters — we tally levels, hours, and error messages in one pass through the file. Counting everything in a single loop is efficient even for huge logs.

hours.most_common(1) — the busiest hour. errors.most_common(3) — the top 3 error messages. Counter does the sorting for us.
Common mistakes — and how to avoid them
✗ Using plain split() — the message gets chopped at every space.
✓ Use split(" ", 3) to keep the message whole.
✗ Counting with a manual dictionary and forgetting to initialise keys — KeyError.
✓ Use Counter, which starts every key at 0.
✗ Loading the whole file into memory for a huge log — wasteful.
✓ Iterate line by line (the file object is already iterable).

4 Test & Prove Each Part

We test parsing a log line and the aggregation, using a few known lines.

A line parses into hour, level, and message
Errors are counted correctly
The busiest hour is identified
Pythontest_loganalyse.py
from collections import Counter


def parse_line(line):
    parts = line.split(&quot; &quot;, 3)
    if len(parts) < 4:
        return None
    date, time, level, message = parts
    return {&quot;hour&quot;: time[:2], &quot;level&quot;: level, &quot;message&quot;: message.strip()}


def analyse(lines):
    levels, hours = Counter(), Counter()
    for line in lines:
        entry = parse_line(line)
        if entry:
            levels[entry[&quot;level&quot;]] += 1
            hours[entry[&quot;hour&quot;]] += 1
    return {&quot;errors&quot;: levels[&quot;ERROR&quot;],
            &quot;busiest_hour&quot;: hours.most_common(1)[0] if hours else None}


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


def test_parse():
    &quot;&quot;&quot;A line parses into its parts.&quot;&quot;&quot;
    entry = parse_line(LINES[0])
    assert entry[&quot;hour&quot;] == &quot;14&quot;
    assert entry[&quot;level&quot;] == &quot;ERROR&quot;


def test_error_count():
    &quot;&quot;&quot;Two errors are counted.&quot;&quot;&quot;
    assert analyse(LINES)[&quot;errors&quot;] == 2


def test_busiest_hour():
    &quot;&quot;&quot;Hour 14 has the most entries.&quot;&quot;&quot;
    assert analyse(LINES)[&quot;busiest_hour&quot;][0] == &quot;14&quot;

Run with pytest -v. We feed analyse a few known log lines so every count can be checked by hand. This is how you trust an analyser before running it on millions of real lines.

5 The Interface

INPUTlog filetimestamped log lines
Format
2026-06-24 14:30:00 ERROR Database timeout
OUTPUTreporttotals + top items
Returns
Total: 1024  Errors: 37
Busiest hour: ("14", 210)
Top errors: [("Database timeout", 12), ...]

6 Run It & Automate It

Run it locally
python3 loganalyse.py
Point it at a server.log file to get a full report.

Jenkins runs the tests automatically — each line explained below.

What you should see when it works
Terminala real run
Total entries: 1024
Errors: 37
Busiest hour: (&#x27;14&#x27;, 210)
Top errors: [(&#x27;Database timeout&#x27;, 12), (&#x27;Disk full&#x27;, 8)]
If it breaks — how to fix it
🚨 ValueError: not enough values to unpack.
A line did not have all parts. The len(parts) < 4 guard handles malformed lines — make sure it is there.
🚨 Counts look wrong.
Check your log format matches the parser, especially the position of the level field.
GroovyJenkinsfile
// Jenkinsfile — automated tests on every change.
pipeline {
    agent any
    stages {
        stage(&#x27;Get the code&#x27;) { steps { checkout scm } }       // download the code
        stage(&#x27;Install&#x27;) { steps { sh &#x27;python3 -m venv venv && . venv/bin/activate && pip install pytest&#x27; } }  // test tool
        stage(&#x27;Test&#x27;) { steps { sh &#x27;. venv/bin/activate && pytest -v&#x27; } }   // run every test
    }
    post {
        success { echo &#x27;All tests passed.&#x27; }
        failure { echo &#x27;A test failed — look above.&#x27; }
    }
}
🎯 Try this next — make it yours
  1. Date filtering. Analyse only a chosen day. (Teaches: filtering by date.)
  2. Regex parsing. Handle varied log formats with regular expressions. (Teaches: regex.)
  3. Live tail. Watch a log as new lines arrive. (Teaches: following a file.)
What you learned
You learned to parse semi-structured text at scale and aggregate it with Counter — counting categories, grouping by time, and finding top items in a single pass. Turning raw logs into insight is a vital operations skill. Related: collections, logging.