A text pipeline

The input

A pipeline step is a program that reads a file, does one job and prints a result another program can read. The input here is a small access log, one request per line, with an occasional line that is not a request:

2026-11-05 12:00:01 GET /api/users 200 12 2026-11-05 12:00:02 GET /api/users 500 31 2026-11-05 12:00:03 POST /api/orders 502 44 2026-11-05 12:00:04 GET /api/users 200 9 2026-11-05 12:00:05 GET /health 200 1 2026-11-05 12:00:06 broken line 2026-11-05 12:00:07 GET /api/orders 500 27

The job is the one from a real incident: which paths fail, and how often. A line that does not parse is counted separately instead of being silently dropped, because a parser that hides bad input hides the bug.

The script

File.foreach reads one line at a time, so the size of the log does not matter. The pattern is the validating regex from lesson 124; the hash counts the paths with a 5xx status:

pattern = /\A(?<time>\S+ \S+) (?<method>[A-Z]+) (?<path>\S+) (?<status>\d{3}) (?<ms>\d+)\z/ errors = Hash.new(0) total = 0 skipped = 0 File.foreach(ARGV.fetch(0)) do |line| total += 1 m = line.chomp.match(pattern) if m.nil? skipped += 1 next end next unless m[:status].start_with?("5") errors[m[:path]] += 1 end puts "lines: #{total}, skipped: #{skipped}" errors.sort_by { |path, count| [-count, path] }.each do |path, count| puts format("%-12s %d", path, count) end

Two details carry a pipeline: ARGV.fetch(0) makes a missing argument an error instead of reading standard input by accident, and sort_by with a two-element key makes the output stable even when two paths have the same count.

Running it

ruby filter.rb access.log

lines: 7, skipped: 1 /api/orders 2 /api/users 1

The summary line goes first, so a reader sees that one line was not counted. The two failing paths follow, most frequent first, which is the order an on-call engineer reads.

In a pipeline the same script is a stage whose output is captured to a file, and a CI job diffs that file against a known good one. The exit code is the other interface; a version that must fail the build when errors are present ends with exit 1 unless errors.empty?.

The shape of a pipeline step

What to watch for

line.chomp is required before \z: the newline at the end of a line otherwise fails the anchor. The alternative is chomp: true on readlines, which foreach does not take.

A hash with a default of zero is the counter; a hash of lists is for a report that needs the lines themselves, and it holds every matched line in memory. Pick the aggregate the report prints, not the data behind it.

Next: Formatting tables in the terminal

Article author: Arthur Isaev