AG collection: document the VIEW ANY DEFINITION grant, make the lag trap filterable, pin V34 for real - #1691
Merged
Merged
Conversation
…rap filterable, pin V34 for real Review follow-ups to #1688. The grant is the finding that matters in the field. Both AG collectors join the sys.availability_groups / sys.availability_replicas CATALOG VIEWS to the sys.dm_hadr_* DMVs. The DMVs are covered by the VIEW SERVER STATE the product documents, but the catalog views require VIEW ANY DEFINITION - and catalog views enforce that by HIDING ROWS, not by raising an error. So on a real AG cluster a login with only the documented grant returned zero rows, which is byte-identical to what an AG-less server returns: the collectors would look healthy forever while collecting nothing, with no error to notice. Documented in both READMEs (grant script + the Darling permission table's If-missing column) and in both collector headers, together with the fingerprint that identifies it if it is ever worth auto-detecting: the DMV returning rows while the catalog view returns none is unambiguous, and SERVERPROPERTY('IsHadrEnabled') is readable by every login. Verified against MS Learn's own AG monitoring page, which states the split directly. The documented lag trap is now actionable rather than merely documented. secondary_lag_seconds reads 0 (not NULL) while data movement is suspended, so a suspended replica charts as healthy - but nothing exposed the suspension state to a panel, making the misread unavoidable. synchronization_state_desc and suspend_reason_desc are now compose dimensions, so a lag panel can filter suspended replicas out or group by suspend reason. is_suspended itself cannot be a dimension: the compiler binds filter values as text, which would not match a boolean column. And V34 is now genuinely pinned. Its test asserted only that each column NAME appeared somewhere in the DDL, so a V34 with columns reordered, is_local typed text, or a spurious NOT NULL passed - while a comment claimed the shape was pinned elsewhere. It now compares the whole generated CREATE TABLE via the existing Migrations_JobHistoryAndAgentStatus_MatchGeneratedFreshShape idiom; detection power confirmed by planting is_local text and watching it go red. That test's rationale was also wrong and is corrected: it blamed a "positional binary COPY", but PgCollectorRowWriter.CopyCommandFor emits a named column list, so Postgres binds by name. Names and types are the real hazard there; the positional appender is Lite's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikdarlingdata
enabled auto-merge
July 26, 2026 17:48
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review follow-ups to #1688 (the AG health collectors). An adversarial review of that diff cleared the five correctness categories I asked it to attack — column ordering, migration-vs-generated DDL equality, DuckDB golden schema, type mapping, null handling — but turned up four things worth fixing. All four are here.
1. The grant — the one that actually bites in the field
Both AG collectors join the
sys.availability_groups/sys.availability_replicascatalog views to thesys.dm_hadr_*DMVs. The DMVs are covered by theVIEW SERVER STATEthe product documents. The catalog views are not — they requireVIEW ANY DEFINITION, and catalog views enforce that by hiding rows, not by raising an error.So on a fully configured AG cluster, a monitoring login with only the documented grant got zero rows — byte-identical to what an AG-less server returns, which is the case the collectors deliberately treat as normal. The collectors would have looked healthy indefinitely while collecting nothing, with no error anywhere to notice.
Verified against MS Learn, which states the split directly on Monitor availability groups using Transact-SQL:
Corroborated by the SSMS AG dashboard's own documented requirement (
CONNECT,VIEW SERVER STATE, andVIEW ANY DEFINITION), and by Metadata Visibility Configuration ("Queries on system views might only return a subset of rows, or sometimes an empty result set").VIEW ANY DEFINITIONis not implied byVIEW SERVER STATE— per the GRANT server-permissions hierarchy they are separate branches meeting only atCONTROL SERVER, so a login in##MS_ServerStateReader##alone will not see AG rows.Now documented in:
README.md— added to the Lite/Darling grant script, plus a short note explaining the silent-zero-rows symptom and naming it as the fix when AG dashboards are empty.Darling/README.md— added to the grant script and as its own row in the permission table, with "silently zero rows" in the If-missing column (every other row there describes a visible failure; this one needed to say it doesn't produce one).I also recorded the fingerprint that identifies the condition, in case it's ever worth detecting automatically rather than documenting: the DMV returning rows while
sys.availability_replicasreturns none is unambiguous, andSERVERPROPERTY('IsHadrEnabled')is readable by every login ("All users can query the server properties"). I did not build that detection — turning "collected zero rows but shouldn't have" into a health signal is a new pathway through the collection-health subsystem in both apps, which is a design decision rather than a mechanical fix.2. The documented lag trap was unreachable through the surface that ships
secondary_lag_secondsreads 0, not NULL, while data movement is suspended, so a suspended replica charts as perfectly healthy. The original PR documented that carefully and saidis_suspendedwas collected alongside "precisely so a reader can tell the two apart" — but nothing exposed the suspension state to a panel, so the misread the comment warned about was unavoidable in practice.synchronization_state_descandsuspend_reason_descare now compose dimensions onag_database_replica_states, so a lag panel can filter suspended replicas out or group by suspend reason. New test compiles exactly that filter and asserts the value is bound as a parameter rather than inlined.is_suspendeditself can't be a dimension:ComposeCompilerbinds filter values asNpgsqlDbType.Text, which wouldn't match abooleancolumn. The two*_desccolumns were already being collected and carry the same information.3. V34 was not actually pinned — and a comment claimed it was
The migration's test asserted only that each column name appeared somewhere in the DDL:
A V34 with the columns reordered,
is_localtypedtext, or a spuriousNOT NULLpassed every test — while a comment right above it asserted the shape equality "is what PgSchemaGeneratorTests pins". It wasn't; that file had zero AG assertions.Now pinned properly by extending the existing
Migrations_JobHistoryAndAgentStatus_MatchGeneratedFreshShape(the idiom already used for V24/V25), which compares the whole generatedCREATE TABLEagainst the migration body. The weak assertion and the false comment are gone.Detection power confirmed, not assumed: planted
is_local textin V34 and the extended test went red; restored and re-ran green. The old assertion would have passed the planted defect.4. That test's rationale was wrong
The pre-existing comment justified the pin with "the positional binary COPY would mis-bind". That isn't right for the Postgres side —
PgCollectorRowWriter.CopyCommandForemits an explicit column list (COPY t (a, b, ...)) built fromPayloadColumns, so Postgres binds by name. I verified that in the source rather than taking the review's word for it.Corrected to state the real stakes: names and types are the hazard on that side (a renamed/missing column fails the COPY, a retyped one mis-stores), order is pinned so the two provenances stay physically comparable, and the genuinely positional appender is Lite's DuckDB one.
Also
Two CHANGELOG claims in #1688 were slightly wrong and are corrected in place: "in all three cadence presets" implied per-minute everywhere (Low-Impact is 5 min), and "the data is reachable from Custom Views and MCP straight away" is true only of the database-grain table —
ag_replica_statesis all state strings with nothing numeric to aggregate, so it's stored for the coming viewer tab rather than queryable today.Not changed, deliberately
ag_replica_stateshas no read path. Correct for this cut and honestly allow-listed as Tier-1 debt in both apps' coverage ratchets; the viewer tab is the follow-up.DuckDbInitializer.CurrentSchemaVersionstays at 48. Prior new-table PRs bumped it with a log-only block, butSchema.GetAllTableStatements()runs unconditionally withIF NOT EXISTSand bothArchivableTableslists are catalog-derived, so a bump would be a pure no-op.Lite/config/collection_schedule.jsonwas already a stale partial seed missing ~15 shipped collectors;ScheduleManager.MergeNewDefaultsbackfills fromGetDefaultSchedules()regardless, so the two AG entries there are harmless either way.Tests
Installer.Tests not run (live-DB categories clobber real Agent jobs on SQL2022).
🤖 Generated with Claude Code