├── .gitignore ├── Invoke-MorningHealthChecks.ps1 ├── README.md ├── SQLMorningHealthChecks.pssproj └── SQLMorningHealthChecks.sln /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs/SQLMorningHealthChecks/v14/.suo 6 | -------------------------------------------------------------------------------- /Invoke-MorningHealthChecks.ps1: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Author: Patrick Keisler, Microsoft 3 | # Date: Nov 2017 4 | # 5 | # File Name: Invoke-MorningHealthChecks.ps1 6 | # 7 | # Purpose: PowerShell script to automate morning health checks. 8 | # 9 | # History: 10 | # Date Comment 11 | # ----------- ---------------------------------------------------------------- 12 | # 06 Nov 2017 Created 13 | # 27 May 2020 Changed error trapping to continue processing when a server is offline. 14 | # 27 May 2020 Added "is enabled" check for the failed jobs function. 15 | # 27 May 2020 Fixed bug in the database status funtion for mirrored databases. 16 | # 27 May 2020 Added a new check for Windows Cluster node status. 17 | # 27 May 2020 Added a new check for SQL service(s) status. 18 | # 19 Mar 2024 Added support for the latest SMO binaries. 19 | # ----------------------------------------------------------------------------- 20 | # 21 | # Copyright (C) 2020 Microsoft Corporation 22 | # 23 | # Disclaimer: 24 | # This is SAMPLE code that is NOT production ready. It is the sole intention of this code to provide a proof of concept as a 25 | # learning tool for Microsoft Customers. Microsoft does not provide warranty for or guarantee any portion of this code 26 | # and is NOT responsible for any affects it may have on any system it is executed on or environment it resides within. 27 | # Please use this code at your own discretion! 28 | # Additional legalese: 29 | # This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. 30 | # THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, 31 | # INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 32 | # We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute 33 | # the object code form of the Sample Code, provided that You agree: 34 | # (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; 35 | # (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and 36 | # (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys' fees, 37 | # that arise or result from the use or distribution of the Sample Code. 38 | # ----------------------------------------------------------------------------- 39 | # 40 | # Paramaters: 41 | # $cmsServer - Name of the CMS server where your list of SQL Servers is registered. 42 | # $cmsGroup - Name of the CMS group that will be evaluated. 43 | # $serverList - Comma delimited list of SQL Servers that will be evaluated. 44 | # 45 | # Important note: 46 | # Either "$cmsServer/$cmsGroup" or "$serverList" parameter should have values specified, but NOT BOTH. 47 | # 48 | # Example 1 uses the CMS parameters to check servers in the 'SQL2012' CMS group that is a subfolder of 'PROD': 49 | # Invoke-MorningHealthChecks.ps1 -cmsServer 'SOLO\CMS' -cmsGroup 'PROD\SQL2012' 50 | # 51 | # Example 2 uses the $serverList paramenter to check 4 different SQL Servers: 52 | # Invoke-MorningHealthChecks.ps1 -serverList 'CHEWIE','CHEWIE\SQL01','LUKE\SKYWALKER','LANDO\CALRISSIAN' 53 | # 54 | # ----------------------------------------------------------------------------- 55 | 56 | #################### SCRIPT-LEVEL PARAMETERS ######################## 57 | param( 58 | [CmdletBinding()] 59 | [Parameter(ParameterSetName='Set1',Position=0,Mandatory=$true)][String]$cmsServer, 60 | [parameter(ParameterSetName='Set1',Position=1,Mandatory=$false)][String]$cmsGroup, 61 | [parameter(ParameterSetName='Set2',Position=2,Mandatory=$true)][String[]]$serverList 62 | ) 63 | 64 | #################### LOAD ASSEMBLIES ######################## 65 | 66 | #Attempt to load assemblies by name starting with the latest version 67 | try { 68 | #SMO v15 - SQL Server 2019+ 69 | Add-Type -AssemblyName 'Microsoft.SqlServer.ConnectionInfo, Version=15.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 70 | Add-Type -AssemblyName 'Microsoft.SqlServer.Management.RegisteredServers, Version=15.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 71 | } 72 | catch { 73 | try { 74 | #SMO v14 - SQL Server 2017 75 | Add-Type -AssemblyName 'Microsoft.SqlServer.ConnectionInfo, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 76 | Add-Type -AssemblyName 'Microsoft.SqlServer.Management.RegisteredServers, Version=14.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 77 | } 78 | catch { 79 | try { 80 | #SMO v13 - SQL Server 2016 81 | Add-Type -AssemblyName 'Microsoft.SqlServer.ConnectionInfo, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 82 | Add-Type -AssemblyName 'Microsoft.SqlServer.Management.RegisteredServers, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 83 | } 84 | catch { 85 | try { 86 | #SMO v12 - SQL Server 2014 87 | Add-Type -AssemblyName 'Microsoft.SqlServer.ConnectionInfo, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 88 | Add-Type -AssemblyName 'Microsoft.SqlServer.Management.RegisteredServers, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 89 | } 90 | catch { 91 | try { 92 | #SMO v11 - SQL Server 2012 93 | Add-Type -AssemblyName 'Microsoft.SqlServer.ConnectionInfo, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 94 | Add-Type -AssemblyName 'Microsoft.SqlServer.Management.RegisteredServers, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' -ErrorAction Stop 95 | } 96 | catch { 97 | Write-Warning 'SMO components not installed. Download from https://goo.gl/E700bG' 98 | Break 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | #################### FUNCTIONS ######################## 106 | function Get-Error { 107 | <# 108 | .SYNOPSIS 109 | Processes errors encoutered in PowerShell code. 110 | .DESCRIPTION 111 | The Get-SqlConnection function processes either PowerShell errors or application errors defined within your code. 112 | .INPUTS 113 | None 114 | .OUTPUTS 115 | None 116 | .EXAMPLE 117 | try { 1/0 } catch { Get-Error $Error } 118 | This passes the common error object (System.Management.Automation.ErrorRecord) for processing. 119 | .EXAMPLE 120 | try { 1/0 } catch { Get-Error "You attempted to divid by zero. Try again." } 121 | This passes a string that is output as an error message. 122 | .LINK 123 | Get-SqlConnection 124 | #> 125 | param( 126 | [CmdletBinding()] 127 | [Parameter(Position=0,ParameterSetName='PowerShellError',Mandatory=$true)] [System.Management.Automation.ErrorRecord]$PSError, 128 | [Parameter(Position=0,ParameterSetName='ApplicationError',Mandatory=$true)] [string]$AppError, 129 | [Parameter(Position=1,Mandatory=$false)] [switch]$ContinueAfterError 130 | ) 131 | 132 | if ($PSError) { 133 | #Process a PowerShell error 134 | Write-Host '******************************' 135 | Write-Host "Error Count: $($PSError.Count)" 136 | Write-Host '******************************' 137 | 138 | $err = $PSError.Exception 139 | Write-Host $err.Message 140 | $err = $err.InnerException 141 | while ($err.InnerException) { 142 | Write-Host $err.InnerException.Message 143 | $err = $err.InnerException 144 | } 145 | if ($ContinueAfterError) { Continue } 146 | else { Throw } 147 | } 148 | elseif ($AppError) { 149 | #Process an application error 150 | Write-Host '******************************' 151 | Write-Host 'Error Count: 1' 152 | Write-Host '******************************' 153 | Write-Host $AppError 154 | if ($ContinueAfterError) { Continue } 155 | else { Throw } 156 | } 157 | } #Get-Error 158 | 159 | function Get-SqlConnection { 160 | <# 161 | .SYNOPSIS 162 | Gets a ServerConnection. 163 | .DESCRIPTION 164 | The Get-SqlConnection function gets a ServerConnection to the specified SQL Server. 165 | .INPUTS 166 | None 167 | You cannot pipe objects to Get-SqlConnection 168 | .OUTPUTS 169 | Microsoft.SqlServer.Management.Common.ServerConnection 170 | Get-SqlConnection returns a Microsoft.SqlServer.Management.Common.ServerConnection object. 171 | .EXAMPLE 172 | Get-SqlConnection "Z002\sql2K8" 173 | This command gets a ServerConnection to SQL Server Z002\SQL2K8. 174 | .EXAMPLE 175 | Get-SqlConnection "Z002\sql2K8" "sa" "Passw0rd" 176 | This command gets a ServerConnection to SQL Server Z002\SQL2K8 using SQL authentication. 177 | .LINK 178 | Get-SqlConnection 179 | #> 180 | param( 181 | [CmdletBinding()] 182 | [Parameter(Mandatory=$true)] [string]$sqlserver, 183 | [string]$username, 184 | [string]$password, 185 | [Parameter(Mandatory=$false)] [string]$applicationName='Morning Health Checks' 186 | ) 187 | 188 | Write-Verbose "Get-SqlConnection $sqlserver" 189 | 190 | if($Username -and $Password){ 191 | try { $con = new-object ('Microsoft.SqlServer.Management.Common.ServerConnection') $sqlserver,$username,$password } 192 | catch { Get-Error $_ } 193 | } 194 | else { 195 | try { $con = new-object ('Microsoft.SqlServer.Management.Common.ServerConnection') $sqlserver } 196 | catch { Get-Error $_ } 197 | } 198 | 199 | $con.ApplicationName = $applicationName 200 | try { 201 | $con.Connect() 202 | } 203 | catch { 204 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $targetServer`n" 205 | Get-Error $_ -ContinueAfterError 206 | } 207 | 208 | Write-Output $con 209 | 210 | } #Get-ServerConnection 211 | 212 | function Get-CmsServer { 213 | <# 214 | .SYNOPSIS 215 | Returns a list of SQL Servers from a CMS server. 216 | 217 | .DESCRIPTION 218 | Parses registered servers in CMS to return a list of SQL Servers for processing. 219 | 220 | .INPUTS 221 | None 222 | You cannot pipe objects to Get-CmsServer 223 | 224 | .OUTPUTS 225 | Get-CmsServer returns an array of strings. 226 | 227 | .PARAMETER cmsServer 228 | The name of the CMS SQL Server including instance name. 229 | 230 | .PARAMETER cmsGroup 231 | OPTIONAL. The name of a group (and path) in the CMS server. 232 | 233 | .PARAMETER recurse 234 | OPTIONAL. Return all servers that may exist in subfolders below cmsFolder. 235 | 236 | .PARAMETER unique 237 | OPTIONAL. Returns a unique list of servers. This is helpful if you have the same SQL server registered in multiple groups. 238 | 239 | .NOTES 240 | Includes code from Chrissy LeMarie (@cl). 241 | https://blog.netnerds.net/smo-recipes/central-management-server/ 242 | 243 | .EXAMPLE 244 | Get-CmsServer -cmsServer "SOLO\CMS" 245 | Returns a list of all registered servers that are on the CMS server. 246 | 247 | .EXAMPLE 248 | Get-CmsServer -cmsServer "SOLO\CMS" -cmsFolder "SQL2012" -recurse 249 | Returns a list of all registered servers that are in the SQL2012 folder and any subfolders that exist below it. 250 | 251 | .EXAMPLE 252 | Get-CmsServer -cmsServer "SOLO\CMS" -cmsFolder "SQL2012\Cluster" -unique 253 | Returns a list of all unique (distinct) registered servers that are in the folder for this exact path "SQL2012\Cluster". 254 | 255 | .LINK 256 | http://www.patrickkeisler.com/ 257 | #> 258 | Param ( 259 | [CmdletBinding()] 260 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$cmsServer, 261 | [parameter(Position=1)][String]$cmsGroup, 262 | [parameter(Position=2)][Switch]$recurse, 263 | [parameter(Position=3)][Switch]$unique 264 | ) 265 | 266 | switch ($cmsServer.GetType().Name) { 267 | 'String' { 268 | try { 269 | $sqlConnection = Get-SqlConnection -sqlserver $cmsServer 270 | $cmsStore = new-object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($sqlConnection) 271 | } 272 | catch { 273 | Get-Error $_ 274 | } 275 | } 276 | 'RegisteredServersStore' { $cmsStore = $cmsServer } 277 | default { Get-Error "Get-CmsGroup:Param `$cmsStore must be a String or ServerConnection object." } 278 | } 279 | 280 | Write-Verbose "Get-CmsServer $($cmsStore.DomainInstanceName) $cmsGroup $recurse $unique" 281 | 282 | ############### Declarations ############### 283 | 284 | $collection = @() 285 | $newcollection = @() 286 | $serverList = @() 287 | $cmsFolder = $cmsGroup.Trim('\') 288 | 289 | ############### Functions ############### 290 | 291 | Function Parse-ServerGroup { 292 | Param ( 293 | [CmdletBinding()] 294 | [parameter(Position=0)][Microsoft.SqlServer.Management.RegisteredServers.ServerGroup]$serverGroup, 295 | [parameter(Position=1)][System.Object]$collection 296 | ) 297 | 298 | #Get registered instances in this group. 299 | foreach ($instance in $serverGroup.RegisteredServers) { 300 | $urn = $serverGroup.Urn 301 | $group = $serverGroup.Name 302 | $fullGroupName = $null 303 | 304 | for ($i = 0; $i -lt $urn.XPathExpression.Length; $i++) { 305 | $groupName = $urn.XPathExpression[$i].GetAttributeFromFilter('Name') 306 | if ($groupName -eq 'DatabaseEngineServerGroup') { $groupName = $null } 307 | if ($i -ne 0 -and $groupName -ne 'DatabaseEngineServerGroup' -and $groupName.Length -gt 0 ) { 308 | $fullGroupName += "$groupName\" 309 | } 310 | } 311 | 312 | #Add a new object for each registered instance. 313 | $object = New-Object PSObject -Property @{ 314 | Server = $instance.ServerName 315 | Group = $groupName 316 | FullGroupPath = $fullGroupName 317 | } 318 | $collection += $object 319 | } 320 | 321 | #Loop again if there are more sub groups. 322 | foreach($group in $serverGroup.ServerGroups) 323 | { 324 | $newobject = (Parse-ServerGroup -serverGroup $group -collection $newcollection) 325 | $collection += $newobject 326 | } 327 | return $collection 328 | } 329 | 330 | ############### Main Execution Get-CmsServer ############### 331 | 332 | #Get a list of all servers in the CMS store 333 | foreach ($serverGroup in $cmsStore.DatabaseEngineServerGroup) { 334 | $serverList = Parse-ServerGroup -serverGroup $serverGroup -collection $newcollection 335 | } 336 | 337 | #Set default to recurse if $cmsFolder is blank 338 | if ($cmsFolder -eq '') {$recurse = $true} 339 | 340 | if(($cmsFolder.Split('\')).Count -gt 1) { 341 | if($recurse.IsPresent) { 342 | #Return ones in this folder and subfolders 343 | $cmsFolder = "*$cmsFolder\*" 344 | if($unique.IsPresent) { 345 | $output = $serverList | Where-Object {$_.FullGroupPath -like $cmsFolder} | Select-Object Server -Unique 346 | } 347 | else { 348 | $output = $serverList | Where-Object {$_.FullGroupPath -like $cmsFolder} | Select-Object Server 349 | } 350 | } 351 | else { 352 | #Return only the ones in this folder 353 | $cmsFolder = "$cmsFolder\" 354 | if($unique.IsPresent) { 355 | $output = $serverList | Where-Object {$_.FullGroupPath -eq $cmsFolder} | Select-Object Server -Unique 356 | } 357 | else { 358 | $output = $serverList | Where-Object {$_.FullGroupPath -eq $cmsFolder} | Select-Object Server 359 | } 360 | } 361 | } 362 | elseif (($cmsFolder.Split('\')).Count -eq 1 -and $cmsFolder.Length -ne 0) { 363 | if($recurse.IsPresent) { 364 | #Return ones in this folder and subfolders 365 | $cmsFolder = "*$cmsFolder\*" 366 | if($unique.IsPresent) { 367 | $output = $serverList | Where-Object {$_.FullGroupPath -like $cmsFolder} | Select-Object Server -Unique 368 | } 369 | else { 370 | $output = $serverList | Where-Object {$_.FullGroupPath -like $cmsFolder} | Select-Object Server 371 | } 372 | } 373 | else { 374 | #Return only the ones in this folder 375 | if($unique.IsPresent) { 376 | $output = $serverList | Where-Object {$_.Group -eq $cmsFolder} | Select-Object Server -Unique 377 | } 378 | else { 379 | $output = $serverList | Where-Object {$_.Group -eq $cmsFolder} | Select-Object Server 380 | } 381 | } 382 | } 383 | elseif ($cmsFolder -eq '' -or $cmsFolder -eq $null) { 384 | if($recurse.IsPresent) { 385 | if($unique.IsPresent) { 386 | $output = $serverList | Select-Object Server -Unique 387 | } 388 | else { 389 | $output = $serverList | Select-Object Server 390 | } 391 | } 392 | else { 393 | if($unique.IsPresent) { 394 | $output = $serverList | Where-Object {$_.Group -eq $null} | Select-Object Server -Unique 395 | } 396 | else { 397 | $output = $serverList | Where-Object {$_.Group -eq $null} | Select-Object Server 398 | } 399 | } 400 | } 401 | 402 | #Convert the output a string array 403 | [string[]]$outputArray = $null 404 | $output | ForEach-Object {$outputArray += $_.Server} 405 | Write-Output $outputArray 406 | } #Get-CmsServer 407 | 408 | function Get-SqlUpTime { 409 | Param ( 410 | [CmdletBinding()] 411 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 412 | ) 413 | 414 | $server = Get-SqlConnection $targetServer 415 | 416 | #Get startup time 417 | $cmd = "SELECT sqlserver_start_time FROM sys.dm_os_sys_info;" 418 | try { 419 | $sqlStartupTime = $server.ExecuteScalar($cmd) 420 | } 421 | catch { 422 | Get-Error $_ -ContinueAfterError 423 | } 424 | 425 | $upTime = (New-TimeSpan -Start ($sqlStartupTime) -End ($script:startTime)) 426 | 427 | #Display the results to the console 428 | if ($upTime.Days -eq 0 -and $upTime.Hours -le 6) { 429 | #Critical if uptime is less than 6 hours 430 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 431 | Write-Host "Uptime: $($upTime.Days).$($upTime.Hours):$($upTime.Minutes):$($upTime.Seconds)" 432 | } 433 | elseif ($upTime.Days -lt 1 -and $upTime.Hours -gt 6) { 434 | #Warning if uptime less than 1 day but greater than 6 hours 435 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 436 | Write-Host "Uptime: $($upTime.Days).$($upTime.Hours):$($upTime.Minutes):$($upTime.Seconds)" 437 | } 438 | else { 439 | #Good if uptime is greater than 1 day 440 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 441 | Write-Host "Uptime: $($upTime.Days).$($upTime.Hours):$($upTime.Minutes):$($upTime.Seconds)" 442 | } 443 | } #Get-SqlUptime 444 | 445 | function Get-DatabaseStatus { 446 | Param ( 447 | [CmdletBinding()] 448 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 449 | ) 450 | 451 | #Get status of each database 452 | $server = Get-SqlConnection $targetServer 453 | 454 | $cmd = @" 455 | SELECT [name] AS [database_name], state_desc FROM sys.databases d 456 | JOIN sys.database_mirroring dm ON d.database_id = dm.database_id 457 | WHERE dm.mirroring_role_desc <> 'MIRROR' 458 | OR dm.mirroring_role_desc IS NULL; 459 | "@ 460 | try { 461 | $results = $server.ExecuteWithResults($cmd) 462 | } 463 | catch { 464 | Get-Error $_ -ContinueAfterError 465 | } 466 | 467 | #Display the results to the console 468 | if ($results.Tables[0] | Where-Object {$_.state_desc -eq 'SUSPECT'}) { 469 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 470 | } 471 | elseif ($results.Tables[0] | Where-Object {$_.state_desc -in 'RESTORING','RECOVERING','RECOVERY_PENDING','EMERGENCY','OFFLINE','COPYING','OFFLINE_SECONDARY'}) { 472 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 473 | } 474 | else { Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" } 475 | 476 | $results.Tables[0] | Where-Object {$_.state_desc -in 'SUSPECT','RESTORING','RECOVERING','RECOVERY_PENDING','EMERGENCY','OFFLINE','COPYING','OFFLINE_SECONDARY'} | Select-Object database_name,state_desc | Format-Table -AutoSize 477 | } #Get-DatabaseStatus 478 | 479 | function Get-AGStatus { 480 | Param ( 481 | [CmdletBinding()] 482 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 483 | ) 484 | 485 | $server = Get-SqlConnection $targetServer 486 | 487 | $cmd = @" 488 | SELECT 489 | ag.name AS ag_name 490 | ,ar.replica_server_name 491 | ,ars.role_desc AS role 492 | ,ar.availability_mode_desc 493 | ,ar.failover_mode_desc 494 | ,adc.[database_name] 495 | ,drs.synchronization_state_desc AS synchronization_state 496 | ,drs.synchronization_health_desc AS synchronization_health 497 | FROM sys.dm_hadr_database_replica_states AS drs WITH (NOLOCK) 498 | INNER JOIN sys.availability_databases_cluster AS adc WITH (NOLOCK) ON drs.group_id = adc.group_id AND drs.group_database_id = adc.group_database_id 499 | INNER JOIN sys.availability_groups AS ag WITH (NOLOCK) ON ag.group_id = drs.group_id 500 | INNER JOIN sys.availability_replicas AS ar WITH (NOLOCK) ON drs.group_id = ar.group_id AND drs.replica_id = ar.replica_id 501 | INNER JOIN sys.dm_hadr_availability_replica_states AS ars ON ar.replica_id = ars.replica_id 502 | WHERE ars.is_local = 1 503 | ORDER BY ag.name, ar.replica_server_name, adc.[database_name] OPTION (RECOMPILE); 504 | "@ 505 | 506 | #If one exists, get status of each Availability Group 507 | try { 508 | $results = $server.ExecuteWithResults($cmd) 509 | } 510 | catch { 511 | Get-Error $_ -ContinueAfterError 512 | } 513 | 514 | #Display the results to the console 515 | if ($results.Tables[0].Rows.Count -ne 0) { 516 | if ($results.Tables[0] | Where-Object {$_.synchronization_health -ne 'HEALTHY'}) { 517 | if ($_.synchronization_health -eq 'NOT_HEALTHY') { 518 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 519 | } 520 | elseif ($_.synchronization_health -eq 'PARTIALLY_HEALTHY') { 521 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 522 | } 523 | } 524 | else { 525 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 526 | } 527 | 528 | $results.Tables[0] | Where-Object {$_.synchronization_health -in 'NOT_HEALTHY','PARTIALLY_HEALTHY'} | Select-Object ag_name,role,database_name,synchronization_state,synchronization_health | Format-Table -AutoSize 529 | } 530 | else { 531 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 532 | Write-Host '*** No Availabiliy Groups detected ***' 533 | } 534 | } #Get-AGStatus 535 | 536 | function Get-DatabaseBackupStatus { 537 | Param ( 538 | [CmdletBinding()] 539 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 540 | ) 541 | 542 | #Get status of each database 543 | $server = Get-SqlConnection $targetServer 544 | 545 | $cmd = @" 546 | SELECT 547 | name AS [database_name] 548 | ,recovery_model_desc 549 | ,[D] AS last_full_backup 550 | ,[I] AS last_differential_backup 551 | ,[L] AS last_tlog_backup 552 | ,CASE 553 | /* These conditions below will cause a CRITICAL status */ 554 | WHEN [D] IS NULL THEN 'CRITICAL' -- if last_full_backup is null then critical 555 | WHEN [D] < DATEADD(DD,-1,CURRENT_TIMESTAMP) AND [I] IS NULL THEN 'CRITICAL' -- if last_full_backup is more than 2 days old and last_differential_backup is null then critical 556 | WHEN [D] < DATEADD(DD,-7,CURRENT_TIMESTAMP) AND [I] < DATEADD(DD,-2,CURRENT_TIMESTAMP) THEN 'CRITICAL' -- if last_full_backup is more than 7 days old and last_differential_backup more than 2 days old then critical 557 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] IS NULL THEN 'CRITICAL' -- if recovery_model_desc is SIMPLE and last_tlog_backup is null then critical 558 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] < DATEADD(HH,-6,CURRENT_TIMESTAMP) THEN 'CRITICAL' -- if last_tlog_backup is more than 6 hours old then critical 559 | --/* These conditions below will cause a WARNING status */ 560 | WHEN [D] < DATEADD(DD,-1,CURRENT_TIMESTAMP) AND [I] < DATEADD(DD,-1,CURRENT_TIMESTAMP) THEN 'WARNING' -- if last_full_backup is more than 1 day old and last_differential_backup is greater than 1 days old then warning 561 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] < DATEADD(HH,-3,CURRENT_TIMESTAMP) THEN 'WARNING' -- if last_tlog_backup is more than 3 hours old then warning 562 | /* Everything else will return a GOOD status */ 563 | ELSE 'GOOD' 564 | END AS backup_status 565 | ,CASE 566 | /* These conditions below will cause a CRITICAL status */ 567 | WHEN [D] IS NULL THEN 'No FULL backups' -- if last_full_backup is null then critical 568 | WHEN [D] < DATEADD(DD,-1,CURRENT_TIMESTAMP) AND [I] IS NULL THEN 'FULL backup > 1 day; no DIFF backups' -- if last_full_backup is more than 2 days old and last_differential_backup is null then critical 569 | WHEN [D] < DATEADD(DD,-7,CURRENT_TIMESTAMP) AND [I] < DATEADD(DD,-2,CURRENT_TIMESTAMP) THEN 'FULL backup > 7 day; DIFF backup > 2 days' -- if last_full_backup is more than 7 days old and last_differential_backup more than 2 days old then critical 570 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] IS NULL THEN 'No LOG backups' -- if recovery_model_desc is SIMPLE and last_tlog_backup is null then critical 571 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] < DATEADD(HH,-6,CURRENT_TIMESTAMP) THEN 'LOG backup > 6 hours' -- if last_tlog_backup is more than 6 hours old then critical 572 | --/* These conditions below will cause a WARNING status */ 573 | WHEN [D] < DATEADD(DD,-1,CURRENT_TIMESTAMP) AND [I] < DATEADD(DD,-1,CURRENT_TIMESTAMP) THEN 'FULL backup > 7 day; DIFF backup > 1 day' -- if last_full_backup is more than 1 day old and last_differential_backup is greater than 1 days old then warning 574 | WHEN recovery_model_desc <> 'SIMPLE' AND name <> 'model' AND [L] < DATEADD(HH,-3,CURRENT_TIMESTAMP) THEN 'LOG backup > 3 hours' -- if last_tlog_backup is more than 3 hours old then warning 575 | /* Everything else will return a GOOD status */ 576 | ELSE 'No issues' 577 | END AS status_desc 578 | FROM ( 579 | SELECT 580 | d.name 581 | ,d.recovery_model_desc 582 | ,bs.type 583 | ,MAX(bs.backup_finish_date) AS backup_finish_date 584 | FROM master.sys.databases d 585 | LEFT JOIN msdb.dbo.backupset bs ON d.name = bs.database_name 586 | WHERE (bs.type IN ('D','I','L') OR bs.type IS NULL) 587 | AND d.database_id <> 2 -- exclude tempdb 588 | AND d.source_database_id IS NULL -- exclude snapshot databases 589 | AND d.state NOT IN (1,6,10) -- exclude offline, restoring, or secondary databases 590 | AND d.is_in_standby = 0 -- exclude log shipping secondary databases 591 | GROUP BY d.name, d.recovery_model_desc, bs.type 592 | ) AS SourceTable 593 | PIVOT 594 | ( 595 | MAX(backup_finish_date) 596 | FOR type IN ([D],[I],[L]) 597 | ) AS PivotTable 598 | ORDER BY database_name; 599 | "@ 600 | 601 | try { 602 | $results = $server.ExecuteWithResults($cmd) 603 | } 604 | catch { 605 | Get-Error $_ -ContinueAfterError 606 | } 607 | 608 | #Display the results to the console 609 | if ($results.Tables[0] | Where-Object {$_.backup_status -eq 'CRITICAL'}) { 610 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 611 | } 612 | elseif ($results.Tables[0] | Where-Object {$_.backup_status -eq 'WARNING'}) { 613 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 614 | } 615 | else { 616 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 617 | } 618 | 619 | $results.Tables[0] | Where-Object {$_.backup_status -in 'CRITICAL','WARNING'} | Select-Object database_name,backup_status,status_desc | Format-Table -AutoSize 620 | 621 | } #Get-DatabaseBackupStatus 622 | 623 | function Get-DiskSpace { 624 | Param ( 625 | [CmdletBinding()] 626 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 627 | ) 628 | 629 | $server = Get-SqlConnection $targetServer 630 | 631 | $cmd = @" 632 | SELECT DISTINCT 633 | vs.volume_mount_point 634 | ,vs.logical_volume_name 635 | ,CONVERT(DECIMAL(18,2), vs.total_bytes/1073741824.0) AS total_size_gb 636 | ,CONVERT(DECIMAL(18,2), vs.available_bytes/1073741824.0) AS available_size_gb 637 | ,CONVERT(DECIMAL(18,2), vs.available_bytes * 1. / vs.total_bytes * 100.) AS free_space_pct 638 | FROM sys.master_files AS f WITH (NOLOCK) 639 | CROSS APPLY sys.dm_os_volume_stats(f.database_id, f.[file_id]) AS vs 640 | ORDER BY vs.volume_mount_point OPTION (RECOMPILE); 641 | "@ 642 | 643 | #Get disk space and store it in the repository 644 | try { 645 | $results = $server.ExecuteWithResults($cmd) 646 | } 647 | catch { 648 | Get-Error $_ -ContinueAfterError 649 | } 650 | 651 | #Display the results to the console 652 | if ($results.Tables[0] | Where-Object {$_.free_space_pct -lt 10.0}) { 653 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 654 | } 655 | elseif ($results.Tables[0] | Where-Object {$_.free_space_pct -lt 20.0 -and $_.free_space_pct -gt 10.0}) { 656 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 657 | } 658 | else { Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" } 659 | 660 | $results.Tables[0] | Where-Object {$_.free_space_pct -lt 20.0} | Select-Object volume_mount_point,total_size_gb,available_size_gb,free_space_pct | Format-Table -AutoSize 661 | } #Get-DiskSpace 662 | 663 | function Get-FailedJobs { 664 | Param ( 665 | [CmdletBinding()] 666 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 667 | ) 668 | 669 | $server = Get-SqlConnection $targetServer 670 | 671 | $cmd = @" 672 | SELECT 673 | j.name AS job_name 674 | ,CASE 675 | WHEN a.start_execution_date IS NULL THEN 'Not Running' 676 | WHEN a.start_execution_date IS NOT NULL AND a.stop_execution_date IS NULL THEN 'Running' 677 | WHEN a.start_execution_date IS NOT NULL AND a.stop_execution_date IS NOT NULL THEN 'Not Running' 678 | END AS 'current_run_status' 679 | ,a.start_execution_date AS 'last_start_date' 680 | ,a.stop_execution_date AS 'last_stop_date' 681 | ,CASE h.run_status 682 | WHEN 0 THEN 'Failed' 683 | WHEN 1 THEN 'Succeeded' 684 | WHEN 2 THEN 'Retry' 685 | WHEN 3 THEN 'Canceled' 686 | END AS 'last_run_status' 687 | ,h.message AS 'job_output' 688 | FROM msdb.dbo.sysjobs j 689 | INNER JOIN msdb.dbo.sysjobactivity a ON j.job_id = a.job_id 690 | LEFT JOIN msdb.dbo.sysjobhistory h ON a.job_history_id = h.instance_id 691 | WHERE a.session_id = (SELECT MAX(session_id) FROM msdb.dbo.sysjobactivity) 692 | AND j.enabled = 1 693 | ORDER BY j.name; 694 | "@ 695 | 696 | #Get the failed jobs and store it in the repository 697 | try { 698 | $results = $server.ExecuteWithResults($cmd) 699 | } 700 | catch { 701 | Get-Error $_ -ContinueAfterError 702 | } 703 | 704 | #Display the results to the console 705 | if ($results.Tables[0] | Where-Object {$_.last_run_status -eq 'Failed'}) { 706 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 707 | } 708 | elseif ($results.Tables[0] | Where-Object {$_.last_run_status -in 'Retry','Canceled'}) { 709 | Write-Host "`nWARNING:" -BackgroundColor Yellow -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 710 | } 711 | else { 712 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 713 | } 714 | 715 | $results.Tables[0] | Where-Object {$_.last_run_status -in 'Failed','Retry','Canceled'} | Select-Object job_name,current_run_status,last_run_status,last_stop_date | Format-Table -AutoSize 716 | } #Get-FailedJobs 717 | 718 | function Get-AppLogEvents { 719 | Param ( 720 | [CmdletBinding()] 721 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 722 | ) 723 | 724 | <# 725 | NOTE: If SQL is using the "-n" startup paramenter, then SQL does not 726 | write to the Windows Application log, and this will always return no errors. 727 | https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/database-engine-service-startup-options 728 | #> 729 | 730 | #Get the physical hostname 731 | $server = Get-SqlConnection $targetServer 732 | 733 | if($server.TrueName.Split('\')[1]) { 734 | $source = "MSSQL`$$($server.TrueName.Split('\')[1])" 735 | } 736 | else { 737 | $source = 'MSSQLSERVER' 738 | } 739 | 740 | $cmd = "SELECT SERVERPROPERTY('ComputerNamePhysicalNetBIOS');" 741 | try { 742 | $computerName = $server.ExecuteScalar($cmd) 743 | } 744 | catch { 745 | Get-Error $_ 746 | } 747 | 748 | #ErrorAction = SilentlyConintue to prevent "No events were found" 749 | $events = $null 750 | $events = Get-WinEvent -ComputerName $computerName -FilterHashtable @{LogName='Application';Level=2;StartTime=((Get-Date).AddDays(-1));ProviderName=$source} -ErrorAction SilentlyContinue 751 | 752 | if ($events) { 753 | #Display the results to the console 754 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 755 | Write-Host "Found $($events.Count) error(s)! Showing only the most recent events:" 756 | $events | Select-Object TimeCreated,@{Label='EventID';Expression={$_.Id}},Message | Format-Table -AutoSize 757 | } 758 | else { Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" } 759 | } #Get-AppLogEvents 760 | 761 | function Get-ServiceStatus { 762 | Param ( 763 | [CmdletBinding()] 764 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 765 | ) 766 | 767 | $cmd = "SELECT servicename,CASE SERVERPROPERTY('IsClustered') WHEN 0 THEN startup_type_desc WHEN 1 THEN 'Automatic' END AS startup_type_desc,status_desc FROM sys.dm_server_services;" 768 | 769 | #Get status of each SQL service 770 | $server = Get-SqlConnection $targetServer 771 | try { 772 | $results = $server.ExecuteWithResults($cmd) 773 | } 774 | catch { 775 | Get-Error $_ -ContinueAfterError 776 | } 777 | 778 | #Display the results to the console 779 | if ($results.Tables[0] | Where-Object {$_.status_desc -ne 'Running' -and $_.startup_type_desc -eq 'Automatic'}) { 780 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 781 | } 782 | else { Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" } 783 | 784 | #Display the results to the console 785 | if ($results.Tables[0] | Where-Object {$_.status_desc -ne 'Running' -and $_.startup_type_desc -eq 'Automatic'}) { 786 | $results.Tables[0] | ForEach-Object { 787 | Write-Host "$($_.servicename): $($_.status_desc)" 788 | } 789 | } 790 | } #Get-ServiceStatus 791 | 792 | function Get-ClusterStatus { 793 | Param ( 794 | [CmdletBinding()] 795 | [parameter(Position=0,Mandatory=$true)][ValidateNotNullOrEmpty()]$targetServer 796 | ) 797 | 798 | $cmd = @" 799 | SELECT 800 | NodeName AS cluster_node_name 801 | ,UPPER(status_description) AS cluster_node_status 802 | FROM sys.dm_os_cluster_nodes 803 | UNION 804 | SELECT 805 | member_name AS cluster_node_name 806 | ,member_state_desc AS cluster_node_status 807 | FROM sys.dm_hadr_cluster_members 808 | WHERE member_type = 0; 809 | "@ 810 | 811 | #If one exists, get status of each Availability Group 812 | $server = Get-SqlConnection $targetServer 813 | try { 814 | $results = $server.ExecuteWithResults($cmd) 815 | } 816 | catch { 817 | Get-Error $_ -ContinueAfterError 818 | } 819 | 820 | #Display the results to the console 821 | if ($results.Tables[0].Rows.Count -ne 0) { 822 | if ($results.Tables[0] | Where-Object {$_.cluster_node_status -ne 'UP'}) { 823 | Write-Host "`nCRITICAL:" -BackgroundColor Red -ForegroundColor White -NoNewline; Write-Host " $($server.TrueName)" 824 | } 825 | else { Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" } 826 | } 827 | 828 | #Display the results to the console 829 | if ($results.Tables[0] | Where-Object {$_.cluster_node_status -ne 'UP'}) { 830 | $results.Tables[0] | ForEach-Object { 831 | if ($_.cluster_node_status -ne 'UP') { 832 | Write-Host "$($_.cluster_node_name): $($_.cluster_node_status)" -BackgroundColor Red -ForegroundColor White 833 | } 834 | else { 835 | Write-Host "$($_.cluster_node_name): $($_.cluster_node_status)" 836 | } 837 | } 838 | } 839 | if ($results.Tables[0].Rows.Count -eq 0) { 840 | Write-Host "`nGOOD:" -BackgroundColor Green -ForegroundColor Black -NoNewline; Write-Host " $($server.TrueName)" 841 | Write-Host '*** No cluster detected ***' 842 | } 843 | } #Get-ClusterStatus 844 | 845 | #################### MAIN ######################## 846 | Clear-Host 847 | 848 | $startTime = Get-Date 849 | 850 | [string[]]$targetServerList = $null 851 | 852 | #Get the server list from the CMS group, only if one was specified 853 | if($cmsServer) { 854 | $targetServerList = Get-CmsServer -cmsServer $cmsServer -cmsGroup $cmsGroup -recurse 855 | } 856 | else { 857 | $targetServerList = $serverList 858 | } 859 | 860 | #Check uptime of each SQL Server 861 | Write-Host "########## SQL Server Uptime Report (DD.HH:MM:SS): ##########" -BackgroundColor Black -ForegroundColor Green 862 | ForEach ($targetServer in $targetServerList) { Get-SqlUptime -targetServer $targetServer} 863 | 864 | #Get the status of each SQL service 865 | Write-Host "`n########## SQL Service(s) Status Report: ##########" -BackgroundColor Black -ForegroundColor Green 866 | ForEach ($targetServer in $targetServerList) { Get-ServiceStatus -targetServer $targetServer } 867 | 868 | #Get the state of each Windows cluster node 869 | Write-Host "`n########## Windows Cluster Node Status Report: ##########" -BackgroundColor Black -ForegroundColor Green 870 | ForEach ($targetServer in $targetServerList) { Get-ClusterStatus -targetServer $targetServer } 871 | 872 | #Get status of each database for each server 873 | Write-Host "`n########## Database Status Report: ##########" -BackgroundColor Black -ForegroundColor Green 874 | ForEach ($targetServer in $targetServerList) { Get-DatabaseStatus -targetServer $targetServer} 875 | 876 | #Get status of each Availability Group for each server 877 | Write-Host "`n########## Availability Groups Report: ##########" -BackgroundColor Black -ForegroundColor Green 878 | ForEach ($targetServer in $targetServerList) { Get-AGStatus -targetServer $targetServer} 879 | 880 | #Get the most recent backup of each database 881 | Write-Host "`n########## Database Backup Report: ##########" -BackgroundColor Black -ForegroundColor Green 882 | ForEach ($targetServer in $targetServerList) { Get-DatabaseBackupStatus -targetServer $targetServer} 883 | 884 | #Get the disk space info for each server 885 | Write-Host "`n########## Disk Space Report: ##########" -BackgroundColor Black -ForegroundColor Green 886 | ForEach ($targetServer in $targetServerList) { Get-DiskSpace -targetServer $targetServer} 887 | 888 | #Get the failed jobs for each server 889 | Write-Host "`n########## Failed Jobs Report: ##########" -BackgroundColor Black -ForegroundColor Green 890 | ForEach ($targetServer in $targetServerList) { Get-FailedJobs -targetServer $targetServer} 891 | 892 | #Check the Application event log for SQL errors 893 | Write-Host "`n########## Application Event Log Report: ##########" -BackgroundColor Black -ForegroundColor Green 894 | ForEach ($targetServer in $targetServerList) { Get-AppLogEvents -targetServer $targetServer} 895 | 896 | Write-Host "`nElapsed Time: $(New-TimeSpan -Start $startTime -End (Get-Date))" 897 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLMorningHealthChecks 2 | 3 | ### Supported On-prem Versions: SQL Server 2012 and higher 4 | 5 | #### This script answers the following questions about your SQL Servers. 6 | 7 | 1. What is the uptime of each SQL Server? 8 | 1. CRITICAL = < 6 hours 9 | 2. WARNING = > 6 hours and < 24 hours 10 | 3. GOOD = > 24 hours 11 | 2. What is the status of each SQL service (engine, agent, full text, etc)? 12 | 1. CRITICAL = Not running with automatic startup 13 | 2. GOOD = Running 14 | 3. What is the status of each cluster node (AG or FCI)? 15 | 1. CRITICAL = Down 16 | 2. GOOD = Up 17 | 4. What is the status of each database? 18 | 1. CRITICAL = Suspect 19 | 2. WARNING = Restoring, recovering, recoery_pending, emergency, offline, copying, or offline_secondary 20 | 3. GOOD = Normal 21 | 5. What is the status of each Availability Group? 22 | 1. CRITICAL = Not_healthy 23 | 2. WARNING = Partially_healthy 24 | 3. GOOD = Healthy 25 | 6. What is the backup status of each database? 26 | 1. CRITICAL = No FULL/DIFF/LOG, FULL > 7 days and DIFF > 2 days, LOG > 6 hours 27 | 2. WARNING = FULL > 7 days and DIFF > 1 day, LOG > 3 hours 28 | 3. GOOD = Normal 29 | 7. What is the available disk space? 30 | 1. CRITICAL = < 10% 31 | 2. WARNING = > 10% and < 20% 32 | 3. GOOD = > 20% 33 | 8. Are there any SQL Agent failed jobs in the last 24 hours? 34 | 1. CRITICAL = Failed 35 | 2. WARNING = Retry or Canceled 36 | 3. GOOD = Succeeded 37 | 9. What errors appeared in the SQL errorlog in the last 24 hours? 38 | 1. CRITICAL = Errors logged 39 | 2. GOOD = No errors logged 40 | 41 | For a full description of how this script works, read the article here. 42 | 43 | https://docs.microsoft.com/en-us/archive/blogs/samlester/sql-server-dba-morning-health-checks 44 | -------------------------------------------------------------------------------- /SQLMorningHealthChecks.pssproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | 6CAFC0C6-A428-4d30-A9F9-700E829FEA51 7 | Exe 8 | MyApplication 9 | MyApplication 10 | SQLMorningHealthChecks 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug\ 17 | DEBUG;TRACE 18 | prompt 19 | 4 20 | 21 | 22 | pdbonly 23 | true 24 | bin\Release\ 25 | TRACE 26 | prompt 27 | 4 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /SQLMorningHealthChecks.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F5034706-568F-408A-B7B3-4D38C6DB8A32}") = "SQLMorningHealthChecks", "SQLMorningHealthChecks.pssproj", "{6CAFC0C6-A428-4D30-A9F9-700E829FEA51}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | --------------------------------------------------------------------------------