Skip to content

Feature/added more sql server checks - #1398

Open
mickem wants to merge 15 commits into
mainfrom
feature/added_more_sql_server_checks
Open

Feature/added more sql server checks#1398
mickem wants to merge 15 commits into
mainfrom
feature/added_more_sql_server_checks

Conversation

@mickem

@mickem mickem commented Aug 12, 2026

Copy link
Copy Markdown
Owner

No description provided.

mickem added 9 commits August 12, 2026 07:50
New CheckMSSQL command reporting user sessions and physical connections
aggregated per (database, login) pair from sys.dm_exec_sessions and
sys.dm_exec_connections. Connection-pool exhaustion and runaway session
counts precede most application outages; this shows the growth per
application login before the hard limit is hit.

Keywords: database, login, sessions, running, idle, connections and
max_idle (seconds since the most idle session last completed a request,
with time-unit thresholds like max_idle > 12h to catch leaked
connections; -1 = no completed request yet). Connections are counted
per session with OUTER APPLY so MARS sessions do not multiply the
session rows, and the idle age guards against the 1900-01-01 epoch
default, which would overflow DATEDIFF.

No default thresholds: healthy session counts are workload-specific, so
the check is informational until thresholds are added.

Unit tests cover the unknown-idle -1 contract and the group naming;
integration tests cover REST-style argument parsing (including time
units on max_idle) plus live assertions and perfdata against the docker
SQL Server 2022 container, which also produced the docs samples.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting currently blocked requests from
sys.dm_exec_requests (blocking_session_id <> 0), one row per blocked
session. Blocking chains are the most common "the application is
frozen" root cause on Windows application stacks.

Each row resolves its chain to the root blocker (the session everyone
is ultimately waiting on: 70 -> 60 -> 50 reports root_blocker 50 on
both rows, cycle-safe for in-flight deadlocks) and flags blocker_idle
when the direct blocker is sleeping while holding locks - the classic
orphaned open transaction, which never resolves by itself. Keywords:
session_id, blocking_session_id, root_blocker, database, login,
blocking_login, wait_time (time-unit thresholds), wait_type, command
and blocker_idle.

Defaults: WARNING at wait_time > 30s (user-visible blocking), CRITICAL
at > 5m (frozen application); no blocked sessions is OK. Deadlocks are
resolved by the engine within seconds and are deliberately out of
scope - the deadlock rate belongs to the counters check.

Unit tests cover root-blocker chain resolution, cycle termination and
field pass-through; integration tests cover REST-style parsing with
time units plus the empty contract live. Docs samples were captured
against the docker SQL Server 2022 container during a real three-way
blocking chain.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting the engine health counters from
sys.dm_os_performance_counters over the existing ODBC connection (works
for remote and named instances where local PDH counter sets are
unavailable): buffer cache hit ratio, page life expectancy, batch
requests/sec, SQL (re-)compilations/sec, lazy writes/sec, lock
waits/sec and deadlocks/sec. NSCP could previously say an instance was
up but nothing about whether it was healthy.

Most of these counters are cumulative since instance start, so the
check takes two snapshots bracketing a server-side WAITFOR DELAY of one
second and reports per-second rates over the measured window (the check
takes ~1s longer than the others). The hit ratio divides the deltas of
value/base over the same window: the lifetime ratio converges to ~100%
on any long-running instance and hides a cold or thrashing cache; it
falls back to the lifetime ratio when nothing touched the cache.
Unavailable counters report -1.

All counters are emitted as perfdata by default (extra perf-config), no
threshold required: the check is primarily a graphing source. No
default alert thresholds since healthy values scale with hardware and
workload; the docs suggest starting points.

Unit tests cover the measured-window rate math, the windowed hit ratio
and its lifetime fallback, and the -1 unavailable contract; integration
tests assert live rates under a generated batch workload against the
docker SQL Server 2022 container, which also produced the docs samples.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting Always On availability group health
from sys.dm_hadr_availability_replica_states and
sys.dm_hadr_database_replica_states: one row per replica plus one per
availability database on each replica. Replicas that fall out of
synchronisation silently break the RPO the cluster was built for; this
makes that state page before a failover discovers it.

Keywords cover identity (group/replica/database), role (including
RESOLVING), connected state, the replica/database synchronization
health states (plus a combined "health" that picks the right one per
row), sync state, suspended data movement, and the redo/log-send queue
sizes exposed in bytes so size-unit thresholds (redo_queue > 500M)
gauge RTO/RPO lag before health degrades.

Defaults alert on Microsoft's own health evaluation: WARNING on
PARTIALLY_HEALTHY, CRITICAL on NOT_HEALTHY / DISCONNECTED / suspended /
RESOLVING. The replica-state join is INNER on purpose: a secondary has
no state rows for remote replicas, and keeping the catalog-only rows
would misreport them as DISCONNECTED on every secondary - the docs
recommend checking via the primary/listener for the full picture.

empty-state is OK ("No availability groups found") so the check deploys
fleet-wide; hosts that must have an AG can set empty-state=critical,
covered by an integration test. Live behaviour (healthy, suspended ->
CRITICAL, queue perfdata) was verified against a clusterless AG on a
HADR-enabled SQL Server 2022 container, which also produced the docs
samples; the docker-gated suite pins the no-AG contract.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting wait statistics by category from
sys.dm_os_wait_stats plus scheduler pressure from sys.dm_os_schedulers.
Wait categories are how you tell a storage problem from a CPU or
locking problem without a DBA: the category with the highest rate is
where the instance's time is going.

The wait DMV is cumulative since instance start, so the check takes two
snapshots bracketing a server-side WAITFOR DELAY of one second (the
same pattern as check_mssql_counters) and reports each category as ms
of wait accumulated per second of wall clock over the measured window.
Classification happens client-side where it is unit-testable: cpu, io,
log, lock, latch, memory, network and other, with the community
benign-wait suspects (LAZYWRITER_SLEEP, CHECKPOINT_QUEUE, XE_*, HADR_*
timers, including this check's own WAITFOR) excluded so 0 really means
nothing waited. signal_wait_pct (time spent runnable after the resource
arrived) flags CPU pressure; runnable_tasks and work_queue from the
scheduler DMV are point-in-time, with work_queue > 0 the canonical
THREADPOOL-starvation signal.

The whole profile is emitted as perfdata by default (graphing source).
No default alert thresholds: wait rates only mean something against the
workload's own baseline, but work_queue > 0 and sustained
signal_wait_pct > 25 are documented as universal starting points.

Unit tests cover the category mapping, the benign exclusions, the
measured-window rate math and the quiet-window -1 contract; integration
tests assert the live profile and deterministic scheduler thresholds
against the docker SQL Server 2022 container, which also produced the
docs samples (captured under a write-heavy workload showing WRITELOG).

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting tempdb space from
tempdb.sys.dm_db_file_space_usage, split by consumer: version store
(a long-running snapshot transaction pinning cleanup), user objects
(temp tables and table variables) and internal objects (sort/hash
spills). tempdb is the instance-wide shared resource - when it fills,
every database on the instance starts failing - and the split says
what is filling it before anyone has to guess. A volume_free keyword
(MIN of sys.dm_os_volume_stats across the tempdb data volumes, since
the fullest volume hits the wall first) exposes the real ceiling when
autogrowth is enabled; if the DMF is unavailable the check still works
and reports -1.

Everything is emitted as perfdata by default (trending the split is
how tempdb sizing is diagnosed). No default alert thresholds: used_pct
measures the current allocation, which is soft under autogrowth, so
the docs give used_pct/volume_free/version_store starting points and
point at check_mssql_transactions to find a pinning transaction.

Unit tests cover the used/percent derivation and the empty-tempdb
division guard; integration tests assert the live split, default
perfdata, size-unit thresholds and a real volume_free against the
docker SQL Server 2022 container, which also produced the docs samples
(captured under real temp-table pressure that grew tempdb 64MB->576MB).

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command closing the gap the backup check leaves open: a
backup of a corrupt database restores a corrupt database. One row per
online database (tempdb excluded) with two signals: suspect_pages
(unresolved 823/824/825 pages from msdb.dbo.suspect_pages - the engine
has already seen corruption; restored/repaired event types are excluded
so fixed damage clears the alert) and checkdb_age (seconds since the
last successful DBCC CHECKDB via dbi_dbccLastKnownGood, -1 = never,
-2 = unknown/no access).

Defaults: CRITICAL on suspect_pages > 0 (act while the backups that can
repair it still exist), WARNING on checkdb_age > 14d or never checked.
The DBCC DBINFO timestamp needs sysadmin, so -2 stays quiet by default
and the suspect-pages half keeps working for low-privilege logins.

The DBINFO result is read positionally like the LOGSPACE parsing, one
DBCC per database with per-database failure tolerance. Ages are
computed against the server's own clock (returned in the same query and
diffed with a pure timezone-free civil-calendar parser), so an agent in
a different timezone does not skew them; the 1900-01-01 never-sentinel
parses to a pre-1970 value and is recognized before the validity check.

Unit tests cover the datetime parser, the never/unknown/age mapping and
the sentinel ordering; integration tests assert the never-checked
WARNING on a fresh instance and a seconds-fresh age after a real DBCC
CHECKDB against the docker SQL Server 2022 container, which also
produced the docs samples.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
New CheckMSSQL command reporting open user transactions from
sys.dm_tran_session_transactions / sys.dm_tran_active_transactions, one
row per transaction. An old open transaction blocks log truncation (the
log grows until the disk fills) and pins version-store cleanup (tempdb
grows) - the precursor to two different outages, hours before either
happens, and check_mssql_blocking only sees it once another session
collides with it.

Keywords: session_id, login, database, transaction_name,
transaction_age and request_age (time-unit thresholds), is_idle (open
transaction with no active request - the classic leaked transaction: an
application that crashed or forgot to COMMIT, which never resolves by
itself) and command. The check excludes its own session, whose
autocommit transaction would otherwise appear in every result.

Defaults: WARNING at transaction_age > 30m, or > 5m for idle
transactions (the short fuse is deliberate - nothing will ever commit
those); CRITICAL at > 2h. Long batch jobs can be excluded with a
filter. Long-running queries surface here too, since every user request
runs inside a transaction, with request_age separating statement
runtime from transaction lifetime.

A unit test pins that the or/and default threshold expression parses
(a precedence regression would surface as UNKNOWN); integration tests
cover REST-style parsing with time units, the empty-state contract and
the keywords live. Docs samples were captured against the docker SQL
Server 2022 container with a real leaked (idle) transaction, produced
by holding a BEGIN TRAN session open over a stdin pipe, alongside an
active long-running one.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
check_mssql_databases reported current data/log sizes but nothing about
remaining capacity: a file hitting its max_size (or its volume running
out of space) is an outage the size keywords alone cannot predict. Two
new per-database keywords close that:

- data_headroom: the smallest remaining growth room among the data
  files in bytes - distance to max_size for capped files (the log's
  default 2TB cap is a real limit and treated as one), free space on
  the file's volume (sys.dm_os_volume_stats) for uncapped files, and 0
  when autogrowth is off.
- log_headroom: the same for the log files.

MIN across files because SQL Server cannot move allocations between
files - the most constrained file is the one that errors first. The
query is a separate nice-to-have pass like LOGSPACE: if
dm_os_volume_stats is unavailable the check keeps working and reports
-1; negative headroom (a file shrunk below a former cap) clamps to 0.
A fixed-size pre-allocated file reports headroom 0 by design - free
space inside the files is a different measure (log_used_pct covers it
for logs).

The keywords are not type_size but a custom type with a new
mssql_filter::parse_size converter: the built-in size type cannot
compare against plain integers at all (can_convert(type_size,
type_int) is false), which would make the -1 unknown sentinel
inexpressible - and worse, "data_headroom < 1G" would silently match
-1 with no way to exclude it. The converter accepts plain integers
(including negatives) plus the usual single-letter size suffixes, so
"data_headroom < 1G and data_headroom >= 0" and "data_headroom = -1"
both work as expected.

Unit tests cover the merge, the -1 unknown default, the clamp, the
data/log split and the size-unit multipliers; integration tests assert
live volume-derived headroom, the sentinel expression and size-unit
thresholds against the docker SQL Server 2022 container - including a
file capped at 100MB tripping a 200M warning - which also produced the
refreshed docs samples.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds eight SQL Server health checks and database growth-headroom monitoring to CheckMSSQL.

Changes:

  • Adds session, blocking, counter, AG, wait, tempdb, integrity, and transaction checks.
  • Adds database file-growth headroom metrics and size parsing.
  • Adds command registration, documentation, unit tests, and live integration coverage.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/checkmssql-commands.test.ts Adds command and live SQL Server tests.
modules/CheckMSSQL/mssql_filter_helpers.hpp Adds sentinel-aware size conversion.
modules/CheckMSSQL/module.json Registers new commands.
modules/CheckMSSQL/CMakeLists.txt Builds new checks and tests.
modules/CheckMSSQL/CheckMSSQL.h Declares command handlers.
modules/CheckMSSQL/CheckMSSQL.cpp Dispatches new commands.
modules/CheckMSSQL/check_mssql_waits.hpp Defines wait-check data structures.
modules/CheckMSSQL/check_mssql_waits.cpp Implements wait and scheduler monitoring.
modules/CheckMSSQL/check_mssql_transactions.hpp Defines transaction-check structures.
modules/CheckMSSQL/check_mssql_transactions.cpp Implements open-transaction monitoring.
modules/CheckMSSQL/check_mssql_test.cpp Adds unit coverage.
modules/CheckMSSQL/check_mssql_tempdb.hpp Defines tempdb metrics.
modules/CheckMSSQL/check_mssql_tempdb.cpp Implements tempdb monitoring.
modules/CheckMSSQL/check_mssql_sessions.hpp Defines session metrics.
modules/CheckMSSQL/check_mssql_sessions.cpp Implements session monitoring.
modules/CheckMSSQL/check_mssql_integrity.hpp Defines integrity metrics.
modules/CheckMSSQL/check_mssql_integrity.cpp Implements CHECKDB and suspect-page monitoring.
modules/CheckMSSQL/check_mssql_databases.hpp Adds headroom fields.
modules/CheckMSSQL/check_mssql_databases.cpp Queries and exposes growth headroom.
modules/CheckMSSQL/check_mssql_counters.hpp Defines performance-counter metrics.
modules/CheckMSSQL/check_mssql_counters.cpp Implements sampled engine counters.
modules/CheckMSSQL/check_mssql_blocking.hpp Defines blocking-chain data.
modules/CheckMSSQL/check_mssql_blocking.cpp Implements blocking detection.
modules/CheckMSSQL/check_mssql_availability_groups.hpp Defines AG health data.
modules/CheckMSSQL/check_mssql_availability_groups.cpp Implements AG monitoring.
docs/samples/CheckMSSQL_check_mssql_waits_samples.md Adds wait-check examples.
docs/samples/CheckMSSQL_check_mssql_waits_desc.md Documents wait monitoring.
docs/samples/CheckMSSQL_check_mssql_transactions_samples.md Adds transaction examples.
docs/samples/CheckMSSQL_check_mssql_transactions_desc.md Documents transaction monitoring.
docs/samples/CheckMSSQL_check_mssql_tempdb_samples.md Adds tempdb examples.
docs/samples/CheckMSSQL_check_mssql_tempdb_desc.md Documents tempdb monitoring.
docs/samples/CheckMSSQL_check_mssql_sessions_samples.md Adds session examples.
docs/samples/CheckMSSQL_check_mssql_sessions_desc.md Documents session monitoring.
docs/samples/CheckMSSQL_check_mssql_integrity_samples.md Adds integrity examples.
docs/samples/CheckMSSQL_check_mssql_integrity_desc.md Documents integrity monitoring.
docs/samples/CheckMSSQL_check_mssql_databases_samples.md Adds headroom examples.
docs/samples/CheckMSSQL_check_mssql_databases_desc.md Documents headroom metrics.
docs/samples/CheckMSSQL_check_mssql_counters_samples.md Adds counter examples.
docs/samples/CheckMSSQL_check_mssql_counters_desc.md Documents performance counters.
docs/samples/CheckMSSQL_check_mssql_blocking_samples.md Adds blocking examples.
docs/samples/CheckMSSQL_check_mssql_blocking_desc.md Documents blocking checks.
docs/samples/CheckMSSQL_check_mssql_availability_groups_samples.md Adds AG examples.
docs/samples/CheckMSSQL_check_mssql_availability_groups_desc.md Documents AG monitoring.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread modules/CheckMSSQL/check_mssql_sessions.cpp Outdated
Comment thread modules/CheckMSSQL/check_mssql_tempdb.cpp Outdated
Comment thread modules/CheckMSSQL/check_mssql_waits.cpp Outdated
mickem added 6 commits August 12, 2026 21:20
Address the Copilot review findings on #1398 plus one more found while
re-reviewing:

- check_mssql_sessions: only sleeping/dormant sessions feed max_idle. A
  running session's last_request_end_time describes its previous request,
  so a session busy on one long request for hours would have tripped an
  idle-connection threshold while actively working.

- check_mssql_tempdb: volume_free now uses the custom size converter
  (like the databases headroom keywords) instead of plain type_size.
  type_size cannot compare against plain integers, so the -1 unknown
  sentinel was inexpressible and, worse, `volume_free < 1G` silently
  matched -1 - a host without dm_os_volume_stats access would have
  raised a false capacity alert. The documented threshold now guards
  with `volume_free >= 0`.

- check_mssql_waits: replace the blanket HADR_ benign prefix with an
  explicit housekeeping allowlist (HADR_TIMER_TASK, HADR_WORK_QUEUE,
  HADR_LOGCAPTURE_WAIT, HADR_NOTIFICATION_DEQUEUE, HADR_CLUSAPI_CALL,
  HADR_CLUSTER_INTEGRATION, HADR_FAILOVER_PARTNER, HADR_FILESTREAM_*).
  HADR_SYNC_COMMIT - the primary synchronous-AG commit-latency signal -
  now counts towards other_waits/total_waits instead of being suppressed
  during exactly the incident the check should surface.

- check_mssql_databases: a file with max_size = 0 (no growth allowed)
  was treated as uncapped and reported its volume's free space as
  headroom; it now reports 0 like growth = 0.

Docs and captured samples updated to match; new unit tests pin the
HADR_SYNC_COMMIT categorization and new integration tests cover the
volume_free sentinel expression end-to-end.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
Fetch the data in three flat selects and do the rolling up in C++ instead
of one aggregating query. The old HEADROOM_SQL took a MIN over a CASE
across a CROSS APPLY, which no unit test could reach, and it got the
capacity question wrong in three separate ways:

- A file capped by max_size reported its distance to the cap without
  regard for the disk. Log files always carry a cap (the engine's 2TB
  limit even with unlimited growth), so log_headroom reported ~2.2TB on
  a volume with under a terabyte free - blind to the disk exhaustion
  that ends in error 9002 and a database that stops accepting writes,
  which is the outage the keyword exists to predict.

- The MIN across files assumed the most constrained file limits growth,
  but proportional fill keeps allocating in sibling files until every
  file in the filegroup is full. One legacy fixed-size file pinned
  data_headroom to 0 permanently, so `crit=data_headroom < 5G` became a
  stuck CRITICAL nothing could clear.

- A NULL headroom (dm_os_volume_stats returning nothing for a file) came
  back through result::get_int as 0, which reads as "cannot grow", not
  as the -1 unknown sentinel the keyword documents.

Headroom is now whichever limit binds first, the file's cap or its
volume, rolled up in three steps: files sharing a volume can only add
that volume's free space between them, volumes sum within a filegroup,
and the most constrained filegroup wins. The volume step matters on
every default install - a tempdb with one data file per core on one
volume reported eight times the free space on disk (7.9TB against a
924GB volume in the dev container).

Files are identified as sharing a volume by volume_mount_point, falling
back to total_bytes because Linux SQL Server returns NULL for all three
of volume_mount_point, volume_id and logical_volume_name. That fallback
can only merge two distinct volumes of exactly equal size, which
under-reports headroom rather than multiplying it.

Splitting the queries also improves degradation: dm_os_volume_stats
needs VIEW SERVER STATE while sys.master_files needs only VIEW ANY
DEFINITION, so a login without the former now still reports sizes and
degrades only headroom to -1. sys.databases gains an explicit ORDER BY
to keep the output ordering the dropped GROUP BY used to impose.

Ten unit tests cover the rules that were previously unreachable in SQL
(cap versus volume, volume sharing, filegroup roll-up, the unknown
sentinel), and a live test pins tempdb and log headroom against the
volume so neither regression can come back silently. Docs and captured
samples updated.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
… blocking

check_mssql_blocking treated `blocking_session_id <> 0` as "another session is
blocking this one", which it is not always:

- A parallel query waiting on its own threads reports its own session id
  (CXPACKET/CXCONSUMER). Anything running longer than the default 30s
  wait_time threshold raised a WARNING claiming the session was blocked by
  itself, on a query that was working normally.
- The documented negative values name no session at all (-2 orphaned
  distributed transaction, -3 deferred recovery, -4 latch state
  undetermined). Those rendered as "blocked by session -2 ()" and fed the
  root-blocker chain walk an id that cannot exist.

Both are now excluded, and the check fetches every request rather than only
the blocked ones so that build_blocking() can answer blocker_idle from the
request set. That replaces a LEFT JOIN back onto sys.dm_exec_requests which
multiplied the blocked row whenever the blocker had several requests in
flight - a MultipleActiveResultSets connection - inflating the count and
duplicating perfdata keys.

check_mssql_transactions had the same join shape for the same reason, and it
is fixed the same way: an OUTER APPLY that takes the session's
longest-running request. Both checks now report one row per session (the
blocked session's longest wait, the transaction session's oldest
transaction), which is what the summary text has always claimed to count and
what keeps the ${session_id} perfdata keys unique. The oldest transaction is
also the one that actually pins log truncation and version-store cleanup.

Both checks move their joins into unit-testable pure functions, which is
where the new tests for the sentinels, the self-blocking case and the
per-session collapse live. Verified against a real blocking scenario on a
live server (an idle-in-transaction blocker and a blocked UPDATE): logins,
blocker_idle and the chain root all resolve as before the restructure.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
The benign-wait filter excluded the whole PREEMPTIVE_ prefix. Most of those
waits are SQLOS calling out of the engine and idle-accumulate, but a handful
are precisely the external stalls the check exists to surface, and they were
being dropped from every category including other_waits and total_waits.

Autogrow on slow storage (PREEMPTIVE_OS_WRITEFILEGATHER), a backup hanging on
a URL (PREEMPTIVE_HTTP_REQUEST) or a stalled domain lookup
(PREEMPTIVE_OS_AUTHENTICATIONOPS) could accumulate seconds of wait per second
of wall clock and the check would report a quiet server in the middle of the
incident.

Those, plus PREEMPTIVE_OS_FLUSHFILEBUFFERS, PREEMPTIVE_OS_CRYPTOPS,
PREEMPTIVE_ODBCOPS and PREEMPTIVE_OLEDBOPS, are now carved back out of the
prefix - the same treatment HADR_ already gets for HADR_SYNC_COMMIT. The two
file-level ones are classified as io_waits so they show up where a DBA looks
for storage trouble; the rest land in other_waits.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
…C calls

Two problems, both in how the check gathers its data.

The suspect_pages count lived in the same query as the database list, so the
cross-database reference to msdb.dbo.suspect_pages was load-bearing for the
whole check. On Azure SQL Database (error 40515) or with a monitoring login
that has no msdb user, that query throws and the check returns UNKNOWN
"Query failed" - even though the CHECKDB half needs nothing from msdb, and
even though the check already knew how to degrade the sysadmin-only DBCC part
to a sentinel. The two are now separate queries: suspect_pages reports -1
when msdb is out of reach, checkdb_age keeps reporting per database, and the
check still answers. Neither sentinel trips the default thresholds, since
missing permission is not a finding.

The CHECKDB timestamps took one round trip per database, each shipping back
the whole ~100-250-row boot-page dump to keep a single row. On a hosting
instance with several hundred databases the check could not finish inside the
command timeout, so it returned UNKNOWN every interval on exactly the
instances that most need CHECKDB monitoring. The loop now runs server-side in
one batch that returns one row per database, with a per-database TRY/CATCH so
one inaccessible database does not cost the rest. INSERT ... EXEC is refused
in a few contexts and not every DBCC error is catchable server-side, so the
old per-database walk stays as a fallback.

The ok-message also stopped claiming "All N databases checked recently, no
suspect pages". That phrasing was safe while an msdb failure meant UNKNOWN,
but now that both keywords can be sentinels at once it would report a clean
bill of health that nothing had verified.

Verified on a live server that the batch path is the one taken (no fallback),
and that a login without sysadmin or msdb access reports -2/-1 per keyword
and still returns OK where it previously returned UNKNOWN. New integration
test covers that degradation end to end.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
Two cleanups with no behaviour change.

check_mssql_counters listed the counters it reads twice, in two
character-identical WHERE clauses either side of the sampling WAITFOR. If they
ever drifted, the second read would find no matching first-read row for the
added counter, prev_value would stay NULL, and the counter would report the -1
"unavailable" sentinel forever - no error, just a plausible wrong value that
monitoring would trust. The predicate is now a single string interpolated into
both snapshots.

mssql_filter::apply_size_unit re-implemented str::format::decode_byte_units,
which the header already includes, with the same semantics - two unit tables to
keep in sync for no reason. It now delegates. The surrounding converter stays,
because the string-splitting overload of decode_byte_units does not handle the
leading sign that the -1 sentinel needs.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Michael Medin <michael@medin.name>
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.

2 participants