Fixes collate#2985: bound the audit log count and index its filters - #32622
Conversation
GET /api/v1/audit/logs?limit=25&q=admin spent 150s in the database over 27 round trips. The deferred-join fix in #28851 removed the full-row scan; this takes the follow-ups that issue listed as out of scope. - Count is bounded at 10k rows. Paging is keyset-based, so the exact total only renders a page count, but computing it visited every matching row on every request. 744ms -> 30ms on Postgres, 594ms -> 155ms on MySQL at 400k rows. - Filter predicates are emitted only when set. The (:p IS NULL OR col = :p) form kept all nine filters in the plan and none of the (col, event_ts DESC) indexes was reachable. entityFQN now filters on its indexed hash instead of the unindexed entity_fqn: 120ms -> 0.19ms on MySQL. - A missing entity FQN is read from the stored change event, or from the acting user for auth events, instead of one lookup per row. Auth events now record the FQN at write time. 27 db round trips per request -> 2. - Export counts once, bounded by the export limit, instead of once per batch, and its 1000-row batches are no longer capped to 200 by the page-size guard. - isBot lookups are cached (1000 entries, 10 min), so the write path stops querying the user table once per change event. - Agent detection matches whole name parts: "management" contains "agent". The migration recreates the full-text index where it is missing. 1.12.1's ALTER TABLE ... ADD COLUMN search_text has no IF NOT EXISTS, so re-running it aborts the script before CREATE FULLTEXT INDEX, and every q= search since has been a full table scan. It also adds the (event_type, event_ts DESC) and (entity_type, event_ts DESC) indexes #28850 called for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
There was a problem hiding this comment.
🟢 Approval recommended
The changes are coherent with the stated performance goals, update both DAO + callers consistently, include targeted unit test coverage for the new query-shaping logic, and migrations are idempotent on both engines.
Pull request overview
This PR improves audit log API performance and scalability by reducing database work in the list/count/export paths and by adding missing audit-log indexes via migrations, targeting the latency + round-trip issues described in collate#2985.
Changes:
- Replace the unbounded
COUNTwith a bounded count (ceiling 10k for list, bounded by export limit for exports) to avoid scanning the full match set on each request. - Emit SQL filter predicates only when corresponding filters are set (and route
entityFQNfiltering throughentity_fqn_hash) to make composite indexes usable. - Remove per-row entity reference lookups by resolving missing FQNs from stored change events / auth event context, and cache
isBotlookups on the write path.
File summaries
| File | Description |
|---|---|
| openmetadata-service/src/main/java/org/openmetadata/service/audit/AuditLogRepository.java | Implements bounded counts, conditional predicate emission, avoids per-row lookups, optimizes export batching, and adds bounded isBot caching + improved agent detection. |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ActivityAuditDAOs.java | Updates the DAO count query to a bounded derived-table count and threads countLimit through the DAO contract. |
| openmetadata-service/src/test/java/org/openmetadata/service/audit/AuditLogRepositoryTest.java | Adds unit tests covering conditional predicate emission, hash-based entityFQN filtering, bounded totals, and auth-event FQN resolution without per-row lookups. |
| bootstrap/sql/migrations/native/2.1.0/mysql/schemaChanges.sql | Adds idempotent creation of the missing FULLTEXT index and adds composite indexes for (event_type, event_ts) and (entity_type, event_ts) on MySQL. |
| bootstrap/sql/migrations/native/2.1.0/postgres/schemaChanges.sql | Adds idempotent GIN full-text index on search_text and composite indexes for (event_type, event_ts) and (entity_type, event_ts) on Postgres. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
✅ Playwright Results — workflow succeededValidated commit ✅ 4479 passed · ❌ 0 failed · 🟡 9 flaky · ⏭️ 1 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 56m 30s ⏱️ Max setup 4m 23s · max shard execution 20m 18s · max shard-job elapsed before upload 24m 33s · reporting 19s 🌐 217.93 requests/attempt · 2.31 app boots/UI scenario · 36.86% common-shard skew Optimization targets still in progress:
🟡 9 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
The justification was wrong: "management" is managem-ent, not man-agent, so the substring form never over-matched it and there was nothing to fix. Worse, splitting on [^a-z0-9]+ regressed real cases — "AutoClassificationAgent" lowercases to a single token and stops being detected as an AGENT at all. Restores the original substring matching. Agent classification was never part of the latency fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code Review ✅ Approved 1 resolved / 1 findingsFixes a critical audit log performance issue where ✅ 1 resolved✅ Edge Case: Agent detection now misses concatenated/camelCase names
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
Describe your changes:
Fixes open-metadata/openmetadata-collate#2985
GET /api/v1/audit/logs?limit=25&q=adminspent 150s in the database across 27 round trips:dbOps: 27decomposes exactly: 1 list + 1 count + 25 per-row entity-reference lookups.#28851 already removed the full-row scan with a deferred join. This picks up the three items #28850 explicitly listed as "Follow-ups (not part of this fix)" and adds what the profile turned up alongside them.
What changed
(:param IS NULL OR col = :param)form kept all nine filters in the plan, so none of the(col, event_ts DESC)composite indexes was reachable.entityFQNalso filtered on the unindexedentity_fqncolumn while its indexed hash sat unused.userLogin/userLogoutrows — exactly whatq=adminreturns — always had a nullentity_fqn. Auth events now record the FQN at write time.isBotlookups are cached (1000 entries / 10 min), so the write path stops querying the user table once per change event.Migration.
1.12.1'sALTER TABLE audit_log_event ADD COLUMN search_texthas noIF NOT EXISTS, so on any deployment where the column already existed the script aborted beforeCREATE FULLTEXT INDEX— and everyq=search since has been a full table scan. The migration recreates it idempotently, and adds the(event_type, event_ts DESC)/(entity_type, event_ts DESC)indexes #28850 called for.Measured — 400k rows, MySQL 8.0.46 and PostgreSQL 15.19, plans confirmed with
EXPLAIN ANALYZE/EXPLAIN FORMAT=TREE:q=admin+ time filtereventTypefilterentityFQNfilterKnown limitation, not addressed here. On MySQL the list query for a common search term is still ~580ms at 400k rows and is what scales to the 150s in the report. MySQL always drives from the FULLTEXT index and sorts the whole match set;
FORCE INDEX (idx_audit_log_event_ts)makes common terms 5.5× faster (104ms) but makes rare terms ~10× slower, so there is no safe hint. The real fix is indexing audit events into the search cluster — notesearchOps: 0in the report, the cluster is idle. Happy to open a follow-up issue.Review response. A first revision also changed agent detection to match whole name parts, justified by a claim that
"management".contains("agent"). That is false —managementismanagem-ent— so there was nothing to fix, and the change regressed real cases (AutoClassificationAgentlowercases to a single token and stopped being detected). Reverted in d86a49a; agent classification was never part of the latency fix.Type of change:
High-level design:
The list path builds its
WHEREclause as a string that JDBI splices into<condition>, so making the filters conditional is a change tobuildBaseConditionalone — every caller (list, count, export) inherits it. Verified that JDBI tolerates the now-unused bindings: it only rejects superfluous named parameters when the query declares zero, and:limit/:countLimitalways survive. Confirmed empirically against a running Postgres through the realAuditLogDAO, not just by readingArgumentBinder.The bounded count is a derived table with a
LIMIT, which both engines plan as "stop after N matching rows":list()splits into a public wrapper that counts and a private one that does not, so the export loop stops paying for a count per batch. The wrapper is also wheresanitizeLimit's 200-row cap now lives, which is what un-caps the export's 1000-row batches.Alternatives rejected:
totalentirely. Cursor paging does not need it, butNextPreviousrenders a page count frompaging.total. Bounding it keeps the control working and degrades tocurrentPage + 1, which is what the component already falls back to.q. Would bound the MySQL scan and is how every audit product behaves, but it silently changes what a search returns. Left for a product call.FORCE INDEXon the list query. Helps common terms, hurts rare ones by ~10×. Not a trade worth hard-coding.Compatibility.
paging.totalnow saturates at 10 000 — the one user-visible change.Include-style filtering, cursors, and ordering are untouched. The migration is idempotent on both engines and safe on deployments that already have the indexes.Tests:
Use cases covered
entityFQNroutes through the indexedentity_fqn_hashtotalis bounded rather than exactuserLoginevents resolves its entity FQN without a per-row database lookup2.1.0migration on a table that already has the indexes is a no-opUnit tests
openmetadata-service/src/test/java/org/openmetadata/service/audit/AuditLogRepositoryTest.java(+4 tests)AuditLogRepository.java, from unit tests only: 23.0% → 44.8% line, 12.1% → 30.0% branch (mvn -P static-code-analysis jacoco:prepare-agent test jacoco:report, measured with a cleanjacoco.execon each side)AuditLogResourceITrather than by unit tests.AuditLogRepositoryTest7/7,AuditLogConsumerTest22/22 pass.Backend integration tests
paging.totalsaturating.entity_fqn→entity_fqn_hash) is already covered:AuditLogResourceIT.waitForAuditLogEntryMultipleTypeshard-fail()s if anentityFQN+entityType+eventTypequery returns nothing, and many tests route through it. CI's integration suites passed on mysql-elasticsearch, postgres-elasticsearch-redis and postgres-opensearch.Ingestion integration tests
Playwright (UI) tests
Manual testing performed
Benchmarked against the dev Docker databases rather than the full stack:
docker compose -f docker/development/docker-compose.yml up -d(MySQL 8.0.46, PostgreSQL 15.19)audit_log_eventwith the production index set, ~1/3 of rows attributed toadminand 1 in 5 auserLoginEXPLAIN ANALYZE(Postgres) andEXPLAIN FORMAT=TREE+ timed execution (MySQL), 2–3 warm repetitions each — numbers in the table aboveAuditLogDAO.list/.countthrough real JDBI against Postgres to confirm the unused bindings and the derived-table count both work outside of mocks2.1.0migration block twice on a fresh table on both engines, confirming all three indexes appear and the second pass is a no-opmvn spotless:checkclean; recompiled and re-ran the audit tests after rebasing onto currentmainNot done: I did not click through the audit log page in a running UI. The one thing worth eyeballing there is the page count now saturating at 400 pages.
UI screen recording / screenshots:
Not applicable — no UI changes.
Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.