Functions/Alert-SdtDiskSpace.ps1

function Alert-SdtDiskSpace
{
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [Alias('ServerName','MachineName')]
        [string[]]$ComputerName,
        [Parameter(Mandatory=$false)]
        [string[]]$ExcludeDrive,
        [Parameter(Mandatory=$false)]
        [decimal]$WarningThresholdPercent = 80.0,
        [Parameter(Mandatory=$false)]
        [decimal]$CriticalThresholdPercent = 90.0,
        [Parameter(Mandatory=$false)]
        [string[]]$EmailTo = @($SdtDBAMailId),
        [Parameter(Mandatory=$false)]
        [int]$DelayMinutes = 60
    )

    # Set Initial Variables
    $startTime = Get-Date
    $dtmm = $startTime.ToString('yyyy-MM-dd HH.mm.ss')
    $script = $MyInvocation.MyCommand.Name
    if([String]::IsNullOrEmpty($Script)) {
        $Script = 'Alert-SdtDiskSpace'
    }

    Try 
    {
        $isCustomError = $false

        #1/0;

        # Start Actual Work
        $blockDbaDiskSpace = {
            $ComputerName = $_
            $FriendlyName = $ComputerName.Split('.')[0]
            $r = Get-DbaDiskSpace -ComputerName $ComputerName -EnableException
            $r | Add-Member -NotePropertyName FriendlyName -NotePropertyValue $FriendlyName
            $r | Add-Member -MemberType ScriptProperty -Name "PercentUsed" -Value {[math]::Round((100.00 - $this.PercentFree), 2)}
            $r
        }

        "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Start RSJobs with $SdtDOP threads.." | Write-Output
        $jobs = @()
        $jobs += $ComputerName | Start-RSJob -Name {$_} -ScriptBlock $blockDbaDiskSpace -Throttle $SdtDOP
        "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Waiting for RSJobs to complete.." | Write-Verbose
        $jobs | Wait-RSJob -ShowProgress -Timeout 1200 -Verbose:$false | Out-Null

        $jobs_timedout = @()
        $jobs_timedout += $jobs | Where-Object {$_.State -in ('NotStarted','Running','Stopping')}
        $jobs_success = @()
        $jobs_success += $jobs | Where-Object {$_.State -eq 'Completed' -and $_.HasErrors -eq $false}
        $jobs_fail = @()
        $jobs_fail += $jobs | Where-Object {$_.HasErrors -or $_.State -in @('Disconnected')}

        $jobsResult = @()
        $jobsResult += $jobs_success | Receive-RSJob -Verbose:$false
    
        if($jobs_success.Count -gt 0) {
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Below jobs finished without error.." | Write-Output
            $jobs_success | Select-Object Name, State, HasErrors | Format-Table -AutoSize | Out-String | Write-Output
        }

        if($jobs_timedout.Count -gt 0)
        {
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(ERROR)","Some jobs timed out. Could not completed in 20 minutes." | Write-Output
            $jobs_timedout | Format-Table -AutoSize | Out-String | Write-Output
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Stop timedout jobs.." | Write-Output
            $jobs_timedout | Stop-RSJob
        }

        if($jobs_fail.Count -gt 0)
        {
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(ERROR)","Some jobs failed." | Write-Output
            $jobs_fail | Format-Table -AutoSize | Out-String | Write-Output
            "--"*20 | Write-Output
        }

        $jobs_exception = @()
        $jobs_exception += $jobs_timedout + $jobs_fail
        [System.Collections.ArrayList]$jobErrMessages = @()
        if($jobs_exception.Count -gt 0 ) {   
            $alertHost = $jobs_exception | Select-Object -ExpandProperty Name -First 1
            $isCustomError = $true
            $errMessage = "`nBelow jobs either timed or failed-`n$($jobs_exception | Select-Object Name, State, HasErrors | Format-Table -AutoSize | Out-String -Width 700)"
            $failCount = $jobs_fail.Count
            $failCounter = 0
            foreach($job in $jobs_fail) {
                $failCounter += 1
                $jobErrMessage = ''
                if($failCounter -eq 1) {
                    $jobErrMessage = "`n$("_"*20)`n" | Write-Output
                }
                $jobErrMessage += "`nError Message for server [$($job.Name)] => `n`n$($job.Error | Out-String)"
                $jobErrMessage += "$("_"*20)`n`n" | Write-Output
                $jobErrMessages.Add($jobErrMessage) | Out-Null;
            }
            $errMessage += ($jobErrMessages -join '')
            #throw $errMessage
        }
        $jobs | Remove-RSJob -Verbose:$false

        $subject = "Alert-SdtDiskSpace"
        $footer = "<br><p>Report Generated @ $(Get-Date -format 'yyyy-MM-dd HH.mm.ss')</p>"

        
        # Get alert rules for the alert key
        "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Get rules for Alert Key '$Subject'.." | Write-Output
        $currentAlertRules = @()
        $currentAlertRules += Invoke-DbaQuery -SqlInstance $SdtInventoryInstance -Database $SdtInventoryDatabase `
                    -Query "select * from $SdtAlertRulesTable ar with (nolock) where alert_key = '$Subject' and is_active = 1";

        # Add Warning & Critical threshold inline with Details
        "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Add inline properties like receiver, thresholds, delay etc based on alert rules.." | Write-Output
        $jobsResultExtended = @()
        foreach($srvGroup in $($jobsResult | Group-Object ComputerName)) {
            [decimal]$alertWarningThreshold = $WarningThresholdPercent
            [decimal]$alertCriticalThreshold = $CriticalThresholdPercent
            [System.Array]$alertReceiver = $EmailTo
            $alertReceiverName = 'DBA'
            $alertDelayMinutes = $DelayMinutes

            $alertServerName = $srvGroup.Name
            $alertDiskDetails = $srvGroup.Group

            $srvRule = @()
            $srvRule += $currentAlertRules | Where-Object {$_.server_friendly_name -eq $alertServerName}
            if($srvRule.Count -eq 1) {
                [system.Array]$alertReceiverRules = if(-not [String]::IsNullOrEmpty($srvRule.alert_receiver)){((($srvRule.alert_receiver) -split ';') -split ',')}

                [decimal]$alertWarningThreshold = if([String]::IsNullOrEmpty($srvRule.severity_high_threshold)){$alertWarningThreshold}else{$srvRule.severity_high_threshold}
                [decimal]$alertCriticalThreshold = if([String]::IsNullOrEmpty($srvRule.severity_critical_threshold)){$alertCriticalThreshold}else{$srvRule.severity_critical_threshold}
                $alertReceiver += $alertReceiverRules
                $alertReceiverName = if([String]::IsNullOrEmpty($srvRule.alert_receiver_name)){$alertReceiverName}else{$srvRule.alert_receiver_name}
                $alertDelayMinutes = if([String]::IsNullOrEmpty($srvRule.delay_minutes)){$alertDelayMinutes}else{$srvRule.delay_minutes}
            }

            $srvDiskDetails = @()
            $srvDiskDetails += $($srvGroup.Group)
            $srvDiskDetails | Add-Member -NotePropertyName WarningThreshold -NotePropertyValue $alertWarningThreshold -Force
            $srvDiskDetails | Add-Member -NotePropertyName CriticalThreshold -NotePropertyValue $alertCriticalThreshold -Force
            $srvDiskDetails | Add-Member -NotePropertyName Receiver -NotePropertyValue $alertReceiver -Force
            $srvDiskDetails | Add-Member -NotePropertyName ReceiverName -NotePropertyValue $alertReceiverName -Force
            $srvDiskDetails | Add-Member -NotePropertyName DelayMinutes -NotePropertyValue $alertDelayMinutes -Force

            $jobsResultExtended += $srvDiskDetails
        }
        
        $jobsResultFiltered = @()
        $jobsResultFiltered += $jobsResultExtended | Where-Object {$_.PercentUsed -ge $_.WarningThreshold}
        if($jobsResultFiltered.Count -gt 0) {
            $jobsResultFiltered | Add-Member -MemberType ScriptProperty -Name "Severity" -Value { if($this.PercentUsed -ge $this.CriticalThreshold) {'Critical'} else {'Warning'} }
        }

        # Raise alert
        $alertsCreated = @()
        foreach($alertGroup in $($jobsResultFiltered | Group-Object -Property ReceiverName, Severity))
        {
            $receiverName = ($alertGroup.Name -split ',')[0].Trim()
            $severity = ($alertGroup.Name -split ',')[1].Trim()
            [string[]]$receiver = $alertGroup.Group | Select-Object -ExpandProperty Receiver -Unique
            $groupAlertDelayMinutes = $alertGroup.Group | Select-Object -ExpandProperty DelayMinutes -First 1
    
            $alertResult = @()
            $alertResult += $alertGroup.Group | Select-Object @{l='Server';e={$_.FriendlyName}}, @{l='DiskVolume';e={$_.Name}}, Severity, `
                                                @{l='FreePercent';e={"$($_.PercentFree)% ($($_.Free)/$($_.Capacity))"}}, `
                                                @{l='WarningPercent';e={[math]::Round($_.WarningThreshold,2)}}, @{l='CriticalPercent';e={[math]::Round($_.CriticalThreshold,2)}}, `
                                                Receiver, DelayMinutes, @{l='DashboardURL';e={"http://$SdtGrafanaBaseURL"}} 
        
            "`n{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Below disk(s) are found with [$severity] space issue for receiver '$receiverName'- " | Write-Output
            $alertResult | ft -AutoSize | Out-String

            $alertServers = @()
            $alertServers += $alertResult | Select-Object -ExpandProperty Server -Unique
            $serverCounts = $alertServers.Count

            $title = "<h2>Alert-SdtDiskSpace - $(if($serverCounts -gt 1){"$serverCounts Servers"}else{"[$alertServers]"}) - $($alertGroup.Count) $severity</h2>"
            $params = @{
                        'As'='Table';
                        'PreContent'= "<p>Hi $receiverName,<br><br>Kindly take corrective action.</p><br><h3 class=`"blue`">Disk Space Utilization</h3>";
                        'EvenRowCssClass' = 'even';
                        'OddRowCssClass' = 'odd';
                        'MakeTableDynamic' = $true;
                        'TableCssClass' = 'grid';
                        'Properties' = 'Server', 'DiskVolume', @{n='Severity';e={$_.Severity};css={if ($_.Severity -eq 'Critical') { 'red' }}},
                                        @{n='Warning %';e={$($_.WarningPercent).ToString("#.00")}}, @{n='Critical %';e={$($_.CriticalPercent).ToString("#.00")}},
                                        @{n='Free Space %';e={$_.FreePercent}}, 'DashboardURL'
                    }
            $content = $alertResult | Sort-Object -Property Severity, Server | ConvertTo-EnhancedHTMLFragment @params
            $body = "<html><head>$SdtCssStyle</head><body> $title $content $footer </body></html>" | Out-String

            if($severity -eq 'Critical') { $priority = 'High' } else { $priority = 'Normal'; $severity = 'HIGH' }
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Calling 'Raise-SdtAlert' with alert key '$subject' for receiver '$receiverName'.." | Write-Output
            Raise-SdtAlert -To $receiver -Subject $subject -Body $body -ServersAffected $alertServers -Priority $priority -Severity $severity -BodyAsHtml -DelayMinutes $groupAlertDelayMinutes
            
            # Create alerted list to clear other combinations
            $alertsCreated += [PSCustomObject]@{
                                    ReceiverName = $receiverName;
                                    Receiver = $receiver;
                                    Severity = $severity;
                                    IsAlerted = $true;
                                    JoinKey = "$receiverName | $severity";
                                }
        }

        # Get all alert combinations
        $alertCombinations = @()
        foreach($alertGroup in $($jobsResultExtended | Group-Object -Property ReceiverName)) {
            $receiverName = $alertGroup.Name
            $severity = @('Critical','High') # Supported Severities for Alert-Key
            [string[]]$receiver = $alertGroup.Group | Select-Object -ExpandProperty Receiver -Unique
            $alertDelay = $alertGroup.Group | Select-Object -ExpandProperty DelayMinutes -First 1

            foreach($svt in $severity) {
                $alertCombinations += [PSCustomObject]@{
                                    ReceiverName = $receiverName;
                                    Receiver = $receiver;
                                    Severity = $svt;
                                    JoinKey = "$receiverName | $svt";
                                }
            }
        }

        # Get alerts to clear
        $alerts2Clear = @()
        if($alertsCreated.Count -eq 0) {
            $alerts2Clear += $alertCombinations
        } else {
            $alerts2Clear += Join-SdtObject -Left $alertCombinations -Right $alertsCreated -LeftJoinProperty JoinKey -RightJoinProperty JoinKey `
                                    -Type AllInLeft -RightProperties IsAlerted | Where-Object {[String]::IsNullOrEmpty($_.IsAlerted)}
        }
        
        # Clear the alerts if pending
        "`n{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Checking existing alerts to be cleared.." | Write-Output
        foreach($alert in $alerts2Clear) {
            $content = '<p style="color:blue">Alert has cleared. No action pending</p>'
            $body = "$SdtCssStyle $content $footer" | Out-String
            "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(INFO)","Calling 'Raise-SdtAlert' to clear [$($alert.Severity)] alert for [$($alert.ReceiverName)] (if any).." | Write-Output
            Raise-SdtAlert -To $alert.Receiver -Subject $subject -Body $body -Priority 'Normal' -Severity $alert.Severity -BodyAsHtml -ClearAlert -DelayMinutes $DelayMinutes
        }
    }
    catch {
        $errMessage = $_;
        "{0} {1,-10} {2}" -f "($((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')))","(ERROR)","Something went wrong. Inside catch block of '$script'." | Write-Output
        $isCustomError = $true
        $_ | Write-Warning
    }
    finally {
        if($isCustomError) {
            throw $errMessage
        }
    }
<#
.SYNOPSIS
    Check Disk Space on Computer, and send Alert
.DESCRIPTION
    This function analyzes disk space on Computer, and send an email alert for Critical & Warning state.
.PARAMETER ComputerName
    Server name where disk space has to be analyzed.
.PARAMETER ExcludeDrive
    List of drives that should not be part of alert
.PARAMETER WarningThresholdPercent
    Used space warning threshold. Default 80 percent.
.PARAMETER CriticalThresholdPercent
    Used space critical threshold. Default 90 percent.
.PARAMETER ThresholdTable
    Table containing more specific threshold for server & disk drive at percentage & size level.
.PARAMETER EmailTo
    Email ids that should receive alert email.
.EXAMPLE
    Alert-SdtDiskSpace -ComputerName 'SqlProd1','SqlDr1' -WarningThresholdPercent 70 -CriticalThresholdPercent 85
       
    Analyzes SqlProd1 & SqlDr1 servers for disk drives having used space above 70 percent.
.LINK
    https://github.com/imajaydwivedi/SQLDBATools
#>

}