Troubleshooting
Common failure modes and what to do. If you hit one not listed, capture
/health/detailed output plus the last 200 log lines and open a support ticket.
App won't start
SqlException (53): A network-related or instance-specific error occurred…
The app can't reach the monitoring SQL Server. Verify:
ConnectionStrings:DefaultConnectionis correct- The SQL Server is reachable from the host (
Test-NetConnection -ComputerName <db> -Port 1433) - TLS: connection string includes
TrustServerCertificate=trueorEncrypt=falseif the DB doesn't have a trusted cert
License: Missing — license.lic not found
appsettings.json:Licensing:LicenseFilePath points to a file that doesn't
exist. See license. The app still starts in read-only mode.
Migration error on startup
Microsoft.EntityFrameworkCore.DbUpdateException: ... INSERT statement conflicted with the FOREIGN KEY constraint
You're upgrading across a destructive migration without a clean baseline. Stop the app, restore the DB from before the upgrade, and contact support — don't keep restarting hoping it resolves itself.
All servers show as "Offline" / "Unreachable"
After a restore from backup
Most likely the restored host is not using the same Encryption:Key as the one
the backup was written under, so the connection strings cannot be decrypted. Check
appsettings.json on both hosts: either both set the same key, or neither sets one
(which falls back to the built-in default). The startup log names every row it could
not decrypt. See
backup-and-restore.
Losing the DataProtection keys folder does not cause this — it only signs users out.
After working previously
Test connection from the app host:
Test-NetConnection -ComputerName <monitored-server> -Port 1433
If reachable, log into the monitored server and verify the monitoring account
still has VIEW SERVER STATE. Permissions sometimes get rebuilt during patches.
Index Health shows nothing, or every count is a dash
/performance/index-health reads persisted scan runs, so an empty page means the
last scan found nothing or could not look. The two are different, and the page
distinguishes them: a scan that could not read a single database shows — in the
count cards rather than 0, plus a banner listing every skipped database with its
reason. Check that banner first, then the log (Skipped DB … Cause: …).
The three reasons that account for almost every case:
| Reason in the banner | What it means | Fix |
|---|---|---|
SQL error 300: … VIEW SERVER PERFORMANCE STATE … |
SQL Server 2022+ split VIEW SERVER STATE; the login has the old permission only. |
GRANT VIEW SERVER PERFORMANCE STATE TO [login] — see deployment.md. |
Permission denied (a few ms) |
The login has no user in that database, so USE [db] fails outright. |
CREATE USER … FOR LOGIN … in that database, plus VIEW DATABASE (PERFORMANCE) STATE. |
Timeout (~300 000 ms) |
dm_db_index_physical_stats did not finish within the 5-minute per-database budget. Normal for very large databases. |
Exclude that database from the scan, or scan it during a quiet window. |
Note that 0 % fragmentation is a perfectly normal value — a freshly rebuilt or
reorganized index reports exactly that. A page full of dashes means "not scanned";
a page full of 0.0 % rows means "scanned, and healthy".
The error log page says "Permission denied"
/error-log calls sys.xp_readerrorlog and sys.xp_enumerrorlogs, neither of which is
covered by VIEW SERVER STATE. Both live in master, so the login needs a user there for
the grants to land on — a GRANT alone fails if that user does not exist:
USE master;
CREATE USER [monitoring_login] FOR LOGIN [monitoring_login];
GRANT EXECUTE ON sys.xp_readerrorlog TO [monitoring_login];
GRANT EXECUTE ON sys.xp_enumerrorlogs TO [monitoring_login];
If the log itself loads but the File picker only offers "Current", the second grant is the one missing.
Do not reach for ALTER SERVER ROLE securityadmin ADD MEMBER unless you want the account
to be able to create logins and reset passwords. That advice circulates because the
sp_readerrorlog wrapper needs the role; the monitor calls the extended procedure
underneath it for exactly this reason.
On Azure SQL Database the page reports that there is no instance-level log rather than an error — that platform genuinely has none. Managed Instance works normally.
If the page is slow rather than empty, narrow the time range or add a search term: both are applied inside SQL Server, so a narrower query really does less work. A default error log on a busy instance is easily tens of megabytes.
Notifications don't fire
Alert is "active" on the dashboard but no email arrived
Check in order:
- Channels configured on the rule?
/Settings/Alerts→ edit the rule → "Notification channels" — if blank, the alert falls back to the global severity routing. Check/Settings/Notifications→ severity routing → confirm the severity has at least one channel ticked. - Channel actually wired up?
/Settings/Notifications→ for each channel the rule routes to, click "Send test". Test must succeed before live alerts work. - Silence active?
/settings/silences— if any silence matches (server × rule), the trigger is suppressed silently. Look for an Info-level log entry "Alert suppressed by maintenance window".
Test email fails with "Authentication required"
Office 365 tenants increasingly have SMTP AUTH disabled. Either re-enable
SMTP AUTH for the monitoring mailbox or switch to the Graph API path:
/Settings/Notifications → Email → Provider = "Office 365 / Graph API" →
fill in Tenant ID / Client ID / Client Secret / Sender mailbox.
PagerDuty silently drops events
Verify the routing key matches your Events API v2 integration. If you suspect
the call isn't even leaving the box: check the log around the alert trigger.
A successful call logs PagerDuty event accepted for dedup_key {key}. Errors
log the HTTP status and response body.
2FA: user is locked out
A user lost their authenticator app. There's no self-service recovery in the current UI — an admin must reset 2FA for that account.
Workaround until a UI exists: disable 2FA on the user record directly in the database, then have them re-enroll on next login.
UPDATE AspNetUsers SET TwoFactorEnabled = 0 WHERE UserName = 'alice';
Audit-log this manually (the action bypasses the app's audit pipeline).
Dashboard is slow / sluggish
The "Disk" column on the health matrix shows —
The monitored server doesn't expose sys.dm_os_volume_stats (Azure SQL DB
behaviour, or missing VIEW SERVER STATE). Disk-free data is unavailable; the
rest of the matrix is unaffected.
Whole dashboard takes seconds to render
Likely the InfrastructureSnapshotService is fetching from a slow / unreachable
monitored server and the cycle is timing out. Check the log for
Snapshot pipeline failed: … entries naming a specific server. Disable that
server temporarily under /Servers.
"Stale" badges on every server card
The DashboardRefreshService background worker isn't completing cycles. Check
the log for thrown exceptions from ProbeServerAsync. Common cause: every
monitored server has the same credentials and the credentials have expired.
Logs are noisy
Default log level is Information. To quiet down:
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"SqlServerHealthMonitor": "Information"
}
}
}
This keeps our code at Info level while suppressing the framework's chatter.
To dig deeper into a specific subsystem, raise that namespace to Debug (e.g.
"SqlServerHealthMonitor.Services.AlertingService": "Debug").
Health-check endpoints
/health— single-wordHealthy/Unhealthyfor load-balancer probes/health/detailed— JSON with per-check status, useful for support tickets
Both endpoints skip authentication.