Project: a log analyzer

The tool

The first project takes one idea from most of the course at once: read a file, split a line, count in two hashes, sort, print. An access log in the common format is the input, and the output is the list of paths and status codes that a reader wants first.

lib/logstat.rb

class Logstat def initialize(lines) @lines = lines end def report paths = Hash.new(0) codes = Hash.new(0) @lines.each do |line| parts = line.split next if parts.length < 9 paths[parts[6]] += 1 codes[parts[8]] += 1 end { paths: paths.sort_by { |path, count| [-count, path] }.first(3).to_h, codes: codes.sort_by { |code, count| [-count, code] }.to_h } end end

The two field numbers are the ones that matter in this format: the sixth field is the request path inside the quotes, the eighth is the status code after them. A line with fewer fields is skipped rather than guessed at.

bin/logstat

#!/usr/bin/env ruby require "json" require_relative "../lib/logstat" path = ARGV[0] || "access.log" report = Logstat.new(File.readlines(path)).report if ARGV.include?("--json") puts JSON.generate(report) else puts "Top paths:" report[:paths].each { |name, count| puts " #{name} #{count}" } puts "Status codes:" report[:codes].each { |code, count| puts " #{code} #{count}" } end

Run it on a log

A five-line sample is enough to see the shape. The same log format as the comparison lesson, with one path hit twice and two server errors:

access.log

ruby bin/logstat access.log

Top paths:
  /index.html 2
  /boom 1
  /missing 1
Status codes:
  200 2
  500 2
  404 1

The order is by count and then by name, so two runs on the same file print the same lines. That is worth the extra sort_by comparator: a report that reorders itself is hard to compare over time.

The JSON flag

A person wants a table; a pipeline wants data. The same report goes out as one JSON object when the flag is on:

ruby bin/logstat access.log --json

{"paths":{"/index.html":2,"/boom":1,"/missing":1},"codes":{"200":2,"500":2,"404":1}}

The test

The counting is the part that can be wrong, so the test builds three lines in memory and checks both hashes:

require "minitest/autorun" require_relative "../lib/logstat" class LogstatTest < Minitest::Test def test_counts_paths_and_codes lines = [ '127.0.0.1 - - [04/Nov/2026:10:00:00 +0000] "GET /a HTTP/1.1" 200 1', '127.0.0.1 - - [04/Nov/2026:10:00:01 +0000] "GET /a HTTP/1.1" 500 0', '127.0.0.1 - - [04/Nov/2026:10:00:02 +0000] "GET /b HTTP/1.1" 200 1' ] report = Logstat.new(lines).report assert_equal({ "/a" => 2, "/b" => 1 }, report[:paths]) assert_equal({ "200" => 2, "500" => 1 }, report[:codes]) end end

ruby -Itest test/logstat_test.rb

Run options: --seed 12345

# Running:

.

Finished in 0.000848s, 1179.6404 runs/s, 2359.2807 assertions/s.

1 runs, 2 assertions, 0 failures, 0 errors, 0 skips

What to watch for

A log format is a contract, and this tool reads one fixed format. When the log is JSON or a custom line, the counting stays and the split changes; keep the split in one method so the test only has to change there.

A tool that reads a whole file fits a five-line sample and not a gigabyte. File.foreach with the same loop works line by line; the report is small either way.

Next: Project: a link checker

Article author: Arthur Isaev