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