← Back to projects
projects

Log Viewer

github.com/aherriot/log-viewer ↗

A log analytics and search dashboard built on Ruby on Rails and PostgreSQL — a working demonstration of how far you can get on nothing but a well-indexed Postgres table, no search engine or analytics database required.

It queries a table of 1,000,000+ synthetic log events by free-text search, time range, log level, HTTP status, user ID, and IP address or CIDR subnet — all sortable, all paginated, all through plain SQL against Postgres.

What this demonstrates

Every filter in the UI is backed by a purpose-picked index, not a generic catch-all:

Filter Index type Why this one
Full-text search over log messages GIN on a generated tsvector column websearch_to_tsquery + ts_rank for relevance-ordered results; a plain LIKE '%...%' can't use an index at all
Time range BRIN Near-zero storage cost on an append-only, time-ordered table — a few kilobytes indexing a million rows
Recent-first default listing B-tree on created_at DESC BRIN can filter ranges but can't serve ORDER BY ... LIMIT; a B-tree alongside it answers "latest 50" without touching the rest of the table
IP address / CIDR subnet GiST with inet_ops Native network-containment operators (<<=) — a plain string index couldn't understand "is this address in this /24"
metadata JSON blob GIN (jsonb_ops) Containment queries (@>) into semi-structured per-request metadata without a rigid schema
user_id + status Composite covering index (INCLUDE) Index-only scans — the query is answered entirely from the index, zero heap fetches
Log level, status, HTTP status B-tree Cheap equality filtering and sorting on low-cardinality columns

The result was measured, not assumed — every choice was validated with EXPLAIN (ANALYZE, BUFFERS) against the real 1M+ row table:

  • IP lookup: full table scan → 0.23ms (about 450x faster)
  • Covering index scan: 0.32ms with zero heap fetches
  • Default "recent logs" listing: full scan + sort → 0.14ms via index-only backward scan
  • Full-text search beats a naive substring scan while supporting relevance ranking

The UI treats query performance as a first-class concern too: every keystroke-driven filter debounces before hitting the database, format-sensitive fields (IP, user ID) wait for you to finish rather than validating half-typed input, and there's no SELECT COUNT(*) anywhere in the pagination path — "is there a next page" is answered by fetching one extra row instead of counting a large filtered result set.

Features

  • Full-text search with relevance ranking
  • Filter by time range, log level, HTTP status, user ID, IP/CIDR, and status — independently or combined
  • Sort by any indexed column, ascending or descending
  • Per-field and clear-all filter resets
  • Real empty, error, and loading states — invalid input surfaces as a clear message, not a silent empty result or a stack trace
  • A JSON API alongside the HTML UI, for every filter and sort combination

Built with Ruby on Rails 8 (server-rendered with Turbo and Stimulus, no SPA framework) and PostgreSQL 18 — BRIN, GIN, GiST, B-tree (plain, composite, and covering), native enum types, generated columns, and tsvector full-text search. Zero external search or analytics infrastructure; everything runs on Postgres alone.

github.com/aherriot/log-viewer ↗