Ruby CLI for analyzing Rails application logs (Lograge format) and producing duration statistics aggregated by time windows.
The Lograge parser accepts two common timestamp dialects:
- Gem default with
Logger::Formatter:I, [2026-05-01T03:12:39.681549 #21440] INFO -- app: ... - Simplified variant (custom formatter):
2026-05-01 03:35:20 [INFO] [uuid] [ip] ...
The rest of the line (method=, controller=, action=, duration=,
user_id=, etc.) is identical between the two.
ruby analyze.rb log # directory with .log and/or .gz
ruby analyze.rb log/production.log # single file
ruby analyze.rb log/production.log log/*.gz # multiple paths
ruby analyze.rb -f lograge log # explicit format
ruby analyze.rb -u 831 log # only requests from user_id 831
ruby analyze.rb --top 20 log # show top 20 slow endpoints
ruby analyze.rb --min-entries 5 log # skip endpoints with < 5 entries
ruby analyze.rb --help.gz files are read in-memory — no artifacts left in the directory.
-f, --format FORMAT— log parser (default:lograge)-u, --user-id ID— keep only entries for a singleuser_id--top N— how many slow endpoints to show (default: 10, minimum: 1)--min-entries N— drop endpoints with fewer than N entries from the ranking (default: 1, minimum: 1)
For each run the script prints:
- Read progress (filename, lines, recognized entries)
- Active filter line (when
--user-idis set) # Allblock with average and percentiles (P50, P95, P99) over the entire dataset- One block per window:
Last 30 days,Last 7 days,Last 1 day,Last 1 hour # Top N slow endpoints (by P95)table
Each window compares its metrics against the overall (13% slower vs overall, stable vs overall, etc). Windows without entries report
no entry found.
Windows are computed relative to the last entry's timestamp in the
log (not Time.now). This means fresh and archived logs both produce
meaningful "Last X" results. When --user-id is active, the reference
becomes the user's last activity — so "Last 1 hour" reflects their
last hour, even if the log continues past that.
- Average — simple mean. Can be skewed by long tails.
- P50 — median. Typical experience.
- P95 — 95% of requests finish at or below this value.
- P99 — outlier hunting / worst 1%.
Trend (X% slower / X% faster / stable) compares the window against
the overall average. A ±2% threshold is treated as stable.
The final table groups entries by Controller#action and shows endpoints
sorted by P95 descending. By default it lists the 10 worst. When
--user-id is active, the ranking reflects only that user's requests —
useful for "which endpoints are slow for this customer?".
Use --min-entries N to avoid endpoints with very small samples
inflating the top of the ranking — an endpoint with 2 requests can have
a high P95 by pure chance.
- Create
lib/log_analyzer/parsers/<format>.rbinheriting fromParsers::Base. - Implement
#parse(line)returningEntry.new(timestamp:, duration:, user_id:, endpoint:)ornilwhen the line is invalid. Theuser_idandendpointfields are optional — returnnilfor them if the format doesn't carry them. The--user-idflag and endpoint ranking only work if the parser populates these fields. - Register it in
lib/log_analyzer.rb:
PARSERS = {
"lograge" => Parsers::Lograge,
"my_format" => Parsers::MyFormat,
}.freezeReader, Statistics, and EndpointStats stay untouched.