logagg — the one program, written in every languageThe constant of this series. The languages are the variable; this program and the ten questions are the constant. Every track implements
logaggto the letter of this spec, and the verifier diffs every implementation's output against the same golden file. Agreement is evidence you actually understood the language, not just that you got something to compile.
It was chosen because it forces every language to show its hand on the dimensions that actually differ between languages, and on nothing else:
What logagg needs |
What it reveals |
|---|---|
| Read a file line by line | I/O model, buffering, encoding, what a "string" is |
| Split a line into fields | String handling, allocation, ownership |
| Parse integers and a decimal | Numeric model, parse-failure handling |
| Accumulate into a keyed map | The standard collection, hashing, mutation |
| Sort by a compound key | Comparators, closures, stability |
| Emit a fixed-width report | Formatting, rounding, locale traps |
| Reject bad input without crashing | The error model — the biggest difference of all |
| (stretch) shard the work | The concurrency model |
It is small enough to hold in your head and large enough that an idiomatic Rust version and an idiomatic Erlang version look nothing like each other. That difference is the lesson.
A plain-text log file. One record per line. Fields are separated by runs of whitespace (one or more spaces or tabs). Exactly six fields per record:
<timestamp> <service> <method> <path> <status> <latency_ms>
2026-09-12T08:15:22Z api-gateway GET /v1/accounts 200 12.480
2026-09-12T08:15:22Z ledger POST /v1/postings 201 4.005
2026-09-12T08:15:23Z api-gateway GET /v1/accounts/9931 503 91.200
| Field | Rule |
|---|---|
timestamp |
Opaque. Non-empty. Never parsed — dates are a rabbit hole and this program does not need them. |
service |
The aggregation key. 1–20 chars, no whitespace. |
method |
Opaque, non-empty. |
path |
Opaque, non-empty. |
status |
Exactly three ASCII digits, 100–599. |
latency_ms |
digits, ., then exactly three digits — 12.480, 0.001, 1204.000. No sign, no exponent. |
Blank lines (empty or whitespace-only) are skipped silently and counted in neither ok nor
malformed — they get their own counter.
A line is malformed if any of the following hold, checked in this order — first match wins, and that is the reason reported:
FIELDS — the line does not have exactly six whitespace-separated fieldsSTATUS — status is not exactly three digits, or is outside 100–599LATENCY — latency_ms does not match ^[0-9]+\.[0-9]{3}$A malformed line is counted, its line number and reason recorded, and processing continues. It contributes nothing to any service's statistics.
This ordering is observable and is part of the spec, exactly as the validation order in the COBOL track's
ACCTRPTis. An implementation that reports all failures for a line, or checks latency before status, produces different output and fails the diff — even though it is arguably the better program. Legacy behaviour is like this, and the discipline of matching it byte for byte is most of what modernization work actually is.
All latency arithmetic is done in integer microseconds. A conforming implementation contains no floating point anywhere.
Parse 12.480 by taking the digits before the . as whole milliseconds and the three digits
after as thousandths of a millisecond. Combine:
micros = ms_int * 1000 + frac_int # 12.480 -> 12 * 1000 + 480 = 12480
Accumulate sum_micros as a 64-bit signed integer. Track max_micros the same way.
The mean is an integer division with explicit half-up rounding:
mean_micros = (sum_micros + count / 2) / count # integer division, truncating
The error rate, in tenths of a percent, likewise:
err_tenths = (errors * 1000 + count / 2) / count
Because if you hold latency in a double and format with %.3f, C and Python round
half-to-even while Java rounds half-up. Roughly one value in a thousand differs. You would
lose an afternoon to it and conclude that one of the three languages is broken. None of them
is — IEEE 754 and java.util.Formatter simply made different, defensible choices.
Pinning the arithmetic to integers removes that variable so the language differences stay
visible. It is also exactly why COBOL has COMP-3 and an explicit ROUNDED clause, and why no
banking system on earth keeps money in a double. The first real lesson of the series arrives
before you have written a line of code.
Group by service. Per group track count, errors (records where status >= 500),
sum_micros, max_micros.
Sort for output by:
count descendingservice ascending, byte-wise ASCII — not locale-aware, not case-foldedThe tie-break is mandatory. Without it, two implementations with different hash-map iteration orders disagree and you learn nothing except that hash maps are unordered.
Written to stdout. ASCII only. Unix line endings (\n) — not \r\n, including on
Windows. Exactly this shape:
logagg 1.0
source: access.log
lines: 200 blank: 3 ok: 191 malformed: 6
SERVICE COUNT ERRORS ERR% MEAN_MS MAX_MS
----------------------------------------------------------------------
api-gateway 69 5 7.2 39.115 267.435
ledger 45 0 0.0 5.366 17.028
auth 35 3 8.6 12.802 37.407
search 22 7 31.8 151.582 427.023
notify 14 1 7.1 22.982 66.989
batch-reconcile 6 4 66.7 4788.934 9608.826
----------------------------------------------------------------------
TOTAL 191 20 10.5 187.323 9608.826
malformed:
line 7: FIELDS
line 31: STATUS
line 52: STATUS
line 73: LATENCY
line 98: LATENCY
line 120: FIELDS
| Column | Width | Alignment |
|---|---|---|
SERVICE |
20 | left, space-padded, truncated at 20 |
COUNT |
9 | right |
ERRORS |
9 | right |
ERR% |
8 | right, one decimal |
MEAN_MS |
12 | right, three decimals |
MAX_MS |
12 | right, three decimals |
Columns are separated by nothing — the widths above are the whole story, so the header line is
"SERVICE" left-padded to 20, then "COUNT" right-aligned in 9, and so on. The rule line is
exactly 70 - characters.
Millisecond values are rendered from integer microseconds by printing micros / 1000, a .,
then micros % 1000 zero-padded to three digits. Never by dividing into a float.
The TOTAL row aggregates across all services. Its MEAN_MS is over the whole population
(total_sum_micros against total_count), not the mean of the per-service means. Its
MAX_MS is the max over all services.
The malformed: block — blank line and header included — is omitted entirely when there are
none. When present it lists every malformed line, in line order.
If ok is zero, the table has no data rows and the TOTAL row shows 0, 0, 0.0, 0.000,
0.000.
logagg <path-to-log>
Exactly one argument. The exit code follows the JCL convention from the COBOL track, deliberately:
| Code | Meaning |
|---|---|
0 |
Clean run, no malformed records |
4 |
Completed, but one or more records were malformed. The report is still written to stdout. |
12 |
Fatal: wrong argument count, or the file could not be opened. Message to stderr, nothing on stdout. |
--shard NOnce the single-threaded version passes, add --shard N: split the file across N workers and
merge the partial maps. Output must stay byte-identical to the serial run.
This is where the languages stop resembling each other at all:
| Track | What sharding looks like |
|---|---|
| C | pthread_create, plus either a mutex around one map or per-thread maps merged at join |
| Java 8 | ExecutorService + Future, or parallelStream().collect(...) |
| Python | multiprocessing — and a paragraph on why threading buys nothing here |
| Go | N goroutines, a chan partial, one collector |
| Rust | rayon, or scoped threads with the borrow checker proving the merge sound |
| Erlang | spawn per chunk, gather by message; there is no shared memory to guard |
| COBOL | You don't. Explain what a batch window is instead. |
Skip it on the first pass of a track. Come back after question 6.
python bench/verify.py
Builds and runs every implementation, diffs stdout against the golden file, and checks the exit
code. bench/expected/logagg.txt is generated once from the Python reference implementation
and then frozen — it is checked in, and changing it means changing this spec.
A track is done when its output is byte-identical and its exit code matches.