← thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · Go 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. Go has no built-in equivalent of Python’s collections.Counter, so this project builds its counting and “top N” logic explicitly with a plain map and sort.Slice. Each part is explained below.

Gologanalyse.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"
)

// Entry is one parsed log line.
type Entry struct {
	Hour    string
	Level   string
	Message string
}

// parseLine expects: "2026-06-24 14:30:00 ERROR Database timeout"
func parseLine(line string) (Entry, bool) {
	parts := strings.SplitN(line, " ", 4)
	if len(parts) < 4 {
		return Entry{}, false
	}
	timePart, level, message := parts[1], parts[2], parts[3]
	hour := timePart
	if len(hour) >= 2 {
		hour = hour[:2]
	}
	return Entry{Hour: hour, Level: level, Message: strings.TrimSpace(message)}, true
}

// CountPair is one key and how many times it occurred.
type CountPair struct {
	Key   string
	Count int
}

// topN returns the N highest-count entries, most frequent first — Go has no
// built-in Counter.most_common, so this is the explicit equivalent.
func topN(counts map[string]int, n int) []CountPair {
	pairs := make([]CountPair, 0, len(counts))
	for k, v := range counts {
		pairs = append(pairs, CountPair{k, v})
	}
	sort.Slice(pairs, func(i, j int) bool { return pairs[i].Count > pairs[j].Count })
	if len(pairs) > n {
		pairs = pairs[:n]
	}
	return pairs
}

// Report is the finished analysis.
type Report struct {
	Total        int
	Errors       int
	BusiestHour  string
	BusiestCount int
	TopErrors    []CountPair
}

func analyse(lines []string) Report {
	levels := map[string]int{}
	hours := map[string]int{}
	errors := map[string]int{}
	total := 0

	for _, line := range lines {
		entry, ok := parseLine(line)
		if !ok {
			continue
		}
		levels[entry.Level]++
		hours[entry.Hour]++
		total++
		if entry.Level == "ERROR" {
			errors[entry.Message]++
		}
	}

	report := Report{Total: total, Errors: levels["ERROR"], TopErrors: topN(errors, 3)}
	if busiest := topN(hours, 1); len(busiest) > 0 {
		report.BusiestHour = busiest[0].Key
		report.BusiestCount = busiest[0].Count
	}
	return report
}

func main() {
	file, err := os.Open("server.log")
	if err != nil {
		fmt.Println("Could not open server.log:", err)
		return
	}
	defer file.Close()

	var lines []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}

	report := analyse(lines)
	fmt.Printf("Total entries: %d\n", report.Total)
	fmt.Printf("Errors: %d\n", report.Errors)
	fmt.Printf("Busiest hour: (%q, %d)\n", report.BusiestHour, report.BusiestCount)
	fmt.Print("Top errors: [")
	for i, e := range report.TopErrors {
		if i > 0 {
			fmt.Print(", ")
		}
		fmt.Printf("(%q, %d)", e.Key, e.Count)
	}
	fmt.Println("]")
}
⚠ No in-browser playground here
Go compiles to a real binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once Go is installed.
What each part does — in plain words
strings.SplitN(line, " ", 4) — split on spaces, but stop after 4 pieces, so the message (which may itself contain spaces, like “Database timeout”) stays whole as the last piece instead of being split further — the N is doing real work here.

map[string]int used as a counter — Go has no Counter class, but a map from key to int does the same job: counts[key]++ works even the first time (a missing key reads as its zero value, 0, then increments to 1).

type CountPair struct { Key string; Count int } plus topN — since a Go map has no defined iteration order and no built-in “most common” method, we copy the counts into a slice of pairs and sort it ourselves with sort.Slice, comparing by Count in descending order. This is exactly what Python’s Counter.most_common(n) does for you automatically — here it is one small function you write once and reuse for both the busiest hour and the top errors.

bufio.Scanner — read the log file line by line without loading the whole thing into memory at once, the right approach for a file that could be gigabytes long in a real system.
Common mistakes — and how to avoid them
✗ Splitting with plain strings.Split(line, " ") instead of SplitN(line, " ", 4) — a multi-word error message gets chopped into extra pieces and the field count check breaks.
✓ Always cap the split count when the last field can contain the separator character.
✗ Assuming a Go map iterates in the order you inserted keys — for k, v := range counts visits entries in a random order every run.
✓ If order matters (like “top 3”), always sort explicitly afterward — never rely on range order.
✗ Not guarding against a line with fewer than 4 parts — indexing parts[3] on a malformed line panics.
✓ Check len(parts) < 4 and skip the line first, as parseLine does.

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
Gologanalyse_test.go
package main

import "testing"

var testLines = []string{
	"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",
}

func TestParse(t *testing.T) {
	entry, ok := parseLine(testLines[0])
	if !ok {
		t.Fatalf("parseLine(%q) failed to parse", testLines[0])
	}
	if entry.Hour != "14" {
		t.Errorf("entry.Hour = %q; want %q", entry.Hour, "14")
	}
	if entry.Level != "ERROR" {
		t.Errorf("entry.Level = %q; want %q", entry.Level, "ERROR")
	}
}

func TestErrorCount(t *testing.T) {
	report := analyse(testLines)
	if report.Errors != 2 {
		t.Errorf("report.Errors = %d; want 2", report.Errors)
	}
}

func TestBusiestHour(t *testing.T) {
	report := analyse(testLines)
	if report.BusiestHour != "14" {
		t.Errorf("report.BusiestHour = %q; want %q", report.BusiestHour, "14")
	}
}

Run with go test -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

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.go and run it with go run loganalyse.go — Go compiles and executes in one step, no separate build needed while you are experimenting.

Run it locally
go run loganalyse.go
Point it at a server.log file in the same folder to get a full report.

A CI tool like Jenkins runs go test automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
Total entries: 6
Errors: 3
Busiest hour: ("14", 3)
Top errors: [("Database timeout", 2), ("Disk full", 1)]
If it breaks — how to fix it
🚨 Could not open server.log: open server.log: no such file or directory
Create a server.log file in the same directory as the program, with lines in the expected format.
🚨 Counts look wrong or entries are silently skipped.
Check your log format matches the parser exactly — especially that the level (INFO/ERROR) is the third space-separated field.
🚨 panic: runtime error: index out of range [1] with length 1
A line had fewer than 4 space-separated parts and the guard was skipped or removed. Make sure len(parts) < 4 returns false, nil before indexing further.
GroovyJenkinsfile
// Jenkinsfile — runs the 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 Go') {
            steps {
                sh 'go version'                                 // confirm Go is installed
                sh 'test -f go.mod || go mod init log_analyser'  // create a module if none exists
            }
        }
        stage('Run the tests') {
            steps {
                sh 'go vet ./...'                    // catch obvious mistakes before running
                sh 'go test -v ./...'                // run every test, show each result
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed — look above.' }
    }
}
🎯 Try this next — make it yours
  1. Date filtering. Parse the date with time.Parse and only include one day. (Teaches: the time package.)
  2. Regex parsing. Handle varied log formats with regexp instead of a fixed split. (Teaches: regular expressions.)
  3. Live tail. Keep reading a log file as new lines arrive, like tail -f. (Teaches: following a growing file.)
What you learned
You learned to parse semi-structured text at scale and aggregate it with a map used as a counter — counting categories, grouping by time, and finding top items with your own sort.Slice-based “most common” helper, since Go has no built-in Counter. Turning raw logs into insight is a vital operations skill in any language. Related: Maps, Standard Library.