Frontend Labs
⋯

42. Capstone: a streaming log analyzer for multi-GB files

Goal: parse and aggregate a large access log in constant memory, concurrently, using everything from Chapters 22, 27, and 29.

package main

import (
	"bufio"
	"flag"
	"fmt"
	"os"
	"regexp"
	"sort"
	"sync"
)

var logLineRe = regexp.MustCompile(`^(\S+) \S+ \S+ \[.*?\] "(\S+) (\S+).*?" (\d{3}) (\d+)`)

type stats struct {
	mu         sync.Mutex
	byStatus   map[string]int
	byPath     map[string]int
	totalBytes int64
}

func newStats() *stats {
	return &stats{byStatus: map[string]int{}, byPath: map[string]int{}}
}

func (s *stats) record(status, path string, bytes int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.byStatus[status]++
	s.byPath[path]++
	s.totalBytes += bytes
}

func processLine(line string, s *stats) {
	m := logLineRe.FindStringSubmatch(line)
	if m == nil {
		return
	}
	status := m[4]
	path := m[3]
	var bytes int64
	fmt.Sscanf(m[5], "%d", &bytes)
	s.record(status, path, bytes)
}

func main() {
	path := flag.String("file", "", "log file to analyze")
	workers := flag.Int("workers", 4, "concurrent parsing workers")
	flag.Parse()

	f, err := os.Open(*path)
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	defer f.Close()

	s := newStats()
	lines := make(chan string, 1000)

	var wg sync.WaitGroup
	for i := 0; i < *workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for line := range lines {
				processLine(line, s)
			}
		}()
	}

	scanner := bufio.NewScanner(f)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // handle long lines
	for scanner.Scan() {
		lines <- scanner.Text()
	}
	close(lines)
	wg.Wait()

	printReport(s)
}

func printReport(s *stats) {
	fmt.Println("=== Status codes ===")
	for status, count := range sortedByCount(s.byStatus) {
		fmt.Printf("%-6s %d\n", status, count)
	}
	fmt.Println("\n=== Top paths ===")
	top := sortedByCount(s.byPath)
	i := 0
	for path, count := range top {
		if i >= 10 {
			break
		}
		fmt.Printf("%-40s %d\n", path, count)
		i++
	}
	fmt.Printf("\nTotal bytes served: %d\n", s.totalBytes)
}

func sortedByCount(m map[string]int) map[string]int {
	type kv struct{ k string; v int }
	pairs := make([]kv, 0, len(m))
	for k, v := range m {
		pairs = append(pairs, kv{k, v})
	}
	sort.Slice(pairs, func(i, j int) bool { return pairs[i].v > pairs[j].v })
	out := make(map[string]int, len(pairs))
	for _, p := range pairs {
		out[p.k] = p.v
	}
	return out
}
go run . -file access.log -workers 8

The file is read line-by-line (never fully loaded), lines fan out across worker goroutines through a buffered channel, and a single mutex guards the shared aggregation maps — the same architecture as Chapter 25's URL checker, applied to a CPU-bound (regex parsing) workload instead of an I/O-bound one. (sortedByCount's Go-map-preserves-insertion-illusion is sidestepped by returning a slice-derived ordering for display, not by relying on map order — see Chapter 7.3.)


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36