Skip to content

Add full-text search over build failure logs - #656

Merged
hsbt merged 11 commits into
masterfrom
log-excerpt-search
Jul 28, 2026
Merged

Add full-text search over build failure logs#656
hsbt merged 11 commits into
masterfrom
log-excerpt-search

Conversation

@hsbt

@hsbt hsbt commented Jul 28, 2026

Copy link
Copy Markdown
Member

chkbuild keeps its failure logs on S3, which offers no full text search. S3 Select is closed to new users, and Athena and CloudWatch sit outside the Heroku stack. This imports the failing excerpt into Postgres and searches it with pg_trgm, which fits log output better than tsvector because the content is not natural language.

log_excerpts stores the fail.txt and diff.txt of failed builds only, capped at 100KB per report and split between the two so a large fail.txt cannot crowd the diff out. Measured against production that is 11,861 builds over the last year and roughly 270MB before the GIN index. Indexing diffs for successful builds as well would pass 10GB a year, since different_sections is set on almost every build, so those are left out.

Report.scan_recent_ltsv captures new failures as they arrive, so fetch_recent already covers ongoing builds and no new scheduled job is needed. A rake task backfills past builds and another drops old excerpts.

/search narrows by server, branch and date range on the existing indexes before the trigram match, and pages at 50 rows to stay inside the Heroku router timeout. revision works as a query parameter for machine clients but has no visible field.

Development and test run on SQLite, where pg_trgm does not exist, so both the migration and the query branch on the adapter.

heroku run rake log_excerpts:backfill -a rubyci
heroku run rake log_excerpts:backfill FROM=2024-01-01 TO=2025-01-01 -a rubyci
heroku run rake log_excerpts:prune KEEP_DAYS=366 DRY_RUN=1 -a rubyci

log_excerpts:prune needs to be added to Heroku Scheduler after deploy.

hsbt and others added 11 commits July 28, 2026 09:49
Store fail.txt and diff.txt excerpts of failed builds in Postgres so
they can be searched with pg_trgm, since S3 offers no full-text search.
Excerpts are capped at 100KB per report and captured right after
scan_recent_ltsv creates a report; S3 errors are logged and skipped so
they never break report ingestion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
log_excerpts:backfill fills excerpts for past failed builds (default
last 1 year, FROM/TO to override) and resumes from the max stored
report_id when interrupted. log_excerpts:prune deletes excerpts older
than KEEP_DAYS for Heroku Scheduler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search failed builds by excerpt text, revision prefix, server, branch
and date range. Reports are narrowed by existing indexes before the
trigram match, and results are paginated at 50 rows to stay within the
Heroku router timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the revision query parameter for machine clients (a planned MCP
server needs to check whether reports exist for a commit), but drop the
visible input since it is not useful for humans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Put the submit button right of the text field and spread the filter row
so both rows share the same right edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The resume point came from the highest already-captured report_id, but
scan_recent_ltsv captures new failures as they arrive, so that id is
always the newest report and the task skipped everything behind it. It
printed "backfilling 0 reports" on every run after the first live
capture, which is exactly when the past year still needed importing.

Select reports that have no excerpt instead, so an interrupted run
resumes and a report whose fetch failed is retried. FORCE=1 refetches
reports that already have one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KEEP_DAYS= or KEEP_DAYS=abc took the empty-string branch and fell to 0
via to_i, which made prune delete every excerpt. FROM and TO parsed in
the local zone while reports.datetime is UTC, and an unparseable date
raised a bare backtrace. Every option now aborts with a message naming
the offending value.

Backfill counted loop iterations, so a run where every fetch raised
still finished "done: N/N" and exited 0. Count captured, empty and
failed separately and exit nonzero when anything failed. Add DRY_RUN
to prune, and report progress from both tasks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two sections were joined and then truncated as a whole, so a
fail.txt over the limit left no room for the diff at all. Six of 25
sampled reports hit the cap, meaning a quarter of them were searchable
only through their fail output.

Give each section an equal share and let one donate its unused share to
the other, so the common case where one side is small is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
?page[]=1 made params[:page] an Array, which has no to_i, so the
request reached the browser as a 500. A page number past the bigint
range produced an OFFSET PostgreSQL rejects, with the same result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The results table reuses table.reports, whose failure colour targets
columns 6 through 9. Search has a different column order, so Match and
Diff came out red on every row while meaning nothing. Scope the table
with its own class and clear those two, leaving Summary red.

Also give the Match snippet a width and a title, matching how Summary
and Diff already truncate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hsbt
hsbt merged commit d10152a into master Jul 28, 2026
2 checks passed
@hsbt
hsbt deleted the log-excerpt-search branch July 28, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant