Step 2 of 6
LOG-102: the daily traffic report
LOG-102: Daily traffic report
Product managers want a morning summary of yesterday's traffic: how many requests, how many failed, which endpoints are busy and slow. The log comes in on standard input. Bad lines must not crash the report; count them instead.
This is the bread and butter of backend work: read records, aggregate, print a table. The tools:
- One struct per group you report on, updated as you read (count, total, max), kept in a BTreeMap so the rows come out sorted and the report is the same on every run.
- Iterator adapters for one-off questions:
filter(...).count(),max_by_key,map(...).sum(). - Format specifiers for aligned columns:
{:<14}pads on the right,{:>5}on the left,{:.1}rounds to one decimal.
use std::collections::BTreeMap;
#[derive(Default)]
struct RegionStats {
orders: u32,
total_cents: u64,
biggest_cents: u64,
}
fn main() {
let sales = [("north", 1250), ("south", 300), ("north", 4990), ("east", 2100), ("south", 700)];
let mut by_region: BTreeMap<&str, RegionStats> = BTreeMap::new();
for &(region, cents) in &sales {
let stats = by_region.entry(region).or_default();
stats.orders += 1;
stats.total_cents += cents;
stats.biggest_cents = stats.biggest_cents.max(cents);
}
println!("{:<8}{:>7}{:>10}{:>10}", "region", "orders", "average", "biggest");
for (region, s) in &by_region {
let average = s.total_cents as f64 / s.orders as f64 / 100.0;
println!("{:<8}{:>7}{:>10.2}{:>10.2}", region, s.orders, average, s.biggest_cents as f64 / 100.0);
}
let big = sales.iter().filter(|&&(_, cents)| cents >= 1000).count();
println!("{big} orders of 10.00 or more ({:.1}%)", big as f64 * 100.0 / sales.len() as f64);
}
region orders average biggest
east 1 21.00 21.00
north 2 31.20 49.90
south 2 5.00 7.00
3 orders of 10.00 or more (60.0%)
#[derive(Default)] gives the struct a zeroed starting value, so entry(...).or_default() can create a fresh one the first time a region appears.
Common mistakes
- Printing a HashMap. The rows come out in a different order on every run, which makes reports impossible to diff and tests impossible to write.
- Integer percentages.
2 * 100 / 7is 28, not 28.6. Convert tof64first. - Dividing by zero. An empty log means 0 requests: handle it before computing averages.
- Ties in
max_by_key. When several items share the maximum,max_by_keyreturns the last one. To get the first, iterate in reverse (.rev().max_by_key(...)) or compare explicitly.
Your task
Write the report. The seed has the LogEntry parser from LOG-101 (simplified to String errors). Read every line of standard input; lines that don't parse (blank lines included) are skipped and counted. Print exactly:
requests: 7
skipped: 1
errors: 2 (28.6%)
methods: GET 6, POST 1
endpoints:
/api/orders 3 req avg 240 ms max 300 ms
/api/users 2 req avg 45 ms max 50 ms
/health 2 req avg 1 ms max 1 ms
slowest: /api/orders 300 ms at 2026-10-01T09:00:07Z
errorscounts status codes of 500 and above; the percentage is of all valid requests, with one decimal.methodslists each method with its count, alphabetically, separated by,.- Endpoint rows are sorted by path and printed with
println!(" {:<14}{:>3} req avg {:>5} ms max {:>5} ms", path, count, avg, max), whereavgis the integer average (total divided by count, rounded down). slowestis the request with the highest latency; on a tie, the first one in the log.- If there are no valid requests at all, print only the
requestsandskippedlines and thenno data.
Challenge 2
LOG-105: Requests per hour
The capacity planning team wants a quick text histogram of traffic per hour of the day.
Write a program that reads the same log format from standard input and prints one line per hour that had traffic, in order: the hour as HH:00, a bar with one # per request, and the count:
09:00 ### 3
14:00 # 1
A line counts if it has exactly 5 fields and its timestamp has a two-digit hour at character positions 11 and 12, counting from 0 (the 09 in 2026-10-01T09:00:03Z), from 00 to 23; skip everything else. Use a BTreeMap keyed by hour. If nothing counts, print no traffic.
Challenge 3
LOG-108: Page the on-call engineer on error bursts
One 5xx error is noise. Several within a minute means something is broken. We want an alert tool for the log stream.
This log uses Unix timestamps (seconds) in the first field: 1727773200 GET /api/orders 503 120. Timestamps never go backwards. The alert threshold is the program's first command-line argument (for example 3). If it's missing or not a positive number, print usage: alerts THRESHOLD to standard error and exit with code 2.
Keep the times of recent server errors (status 500 or above) in a VecDeque. For each error at time t, first drop errors from the front that are 60 or more seconds older than t, then add t. When the window holds THRESHOLD errors, print ALERT at T: N server errors in 60s and clear the window (so one burst gives one alert). Skip malformed lines. At the end print alerts: K.
Previous: LOG-101: parse the access log Next: SHOP-210: orders with validation