Public/Get-AACResourceHealth.ps1
|
function Get-AACResourceHealth { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Checks the health of your Azure resources - Resource Health's availability of each (Available, Unavailable, Degraded), why and since when, its latest platform event, the Azure outages and maintenance that hit it, and with -IncludeHistory its availability over the last days. .DESCRIPTION Reads, in one Azure Resource Graph batch: Resource Health every resource's current availability, the reason (platform or user initiated, planned or not), its summary and since when - and its latest health event (a reboot, a live migration, a host repair) in the last -Days Service Health Azure outages, planned maintenance and advisories active or updated in the last -Days, with the regions and services they name and the resources in scope they hit -IncludeHistory reads Resource Health's history of the resources that aren't available, had an event in the window, or were given with -ResourceId (up to 200), and works out each one's availability over the last -Days (time Available out of the time known) and how often its state changed. Resources come worst first: Unavailable, Degraded, Unknown, Available, the longest down first. -ServiceHealth returns the Service Health events instead of the resources. Read-only; Reader is enough. .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (at any depth). .PARAMETER ResourceGroupName Only resources in these resource groups. .PARAMETER ResourceType Only resources of these types (for example 'microsoft.compute/virtualmachines', or '*sql*'); wildcards work. .PARAMETER ResourceId Only these resources. Takes ResourceId or Id from the pipeline. .PARAMETER State Only resources in these states (Available, Unavailable, Degraded, Unknown). .PARAMETER Days The window for health events, Service Health and the history. Default 7; up to 30. .PARAMETER IncludeHistory Read the Resource Health history of the resources that aren't available, had an event, or were given, for their availability. .PARAMETER ServiceHealth Return the Service Health events (AAC.ServiceHealthEvent) instead of the resources. .PARAMETER CsvPath Write the resources to this CSV file. .PARAMETER HtmlPath Write an interactive HTML report. .PARAMETER PdfPath Write a PDF report. .PARAMETER Title The reports' title. .PARAMETER PassThru Show the view and also return the objects. .PARAMETER NoDisplay Return the objects without showing the view. .PARAMETER NoPaging Show the whole view at once. .EXAMPLE Get-AACResourceHealth Every resource's health, worst first, and the Azure events of the last week. .EXAMPLE Get-AACResourceHealth -State Unavailable, Degraded -IncludeHistory -Days 30 What is down now, and how available each was over the last 30 days. .EXAMPLE Get-AACResourceHealth -ResourceType 'microsoft.compute/virtualmachines' -NoDisplay | Where-Object LastEvent | Select-Object Resource, LastEvent, LastEventTime, LastEventContext The VMs Azure rebooted, migrated or repaired this week. .EXAMPLE Get-AACResourceHealth -ServiceHealth -NoDisplay | Where-Object Status -EQ 'Active' The Azure outages and maintenance active now. .OUTPUTS AAC.ResourceHealth .OUTPUTS AAC.ServiceHealthEvent #> [CmdletBinding()] [OutputType('AAC.ResourceHealth', 'AAC.ServiceHealthEvent')] param( [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [string[]] $ManagementGroupId, [string[]] $ResourceGroupName, [SupportsWildcards()] [string[]] $ResourceType, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Id')] [string[]] $ResourceId, [ValidateSet('Available', 'Unavailable', 'Degraded', 'Unknown')] [string[]] $State, [ValidateRange(1, 30)] [int] $Days = 7, [switch] $IncludeHistory, [switch] $ServiceHealth, [string] $CsvPath, [string] $HtmlPath, [string] $PdfPath, [string] $Title = 'Azure resource health', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) begin { $wanted = [System.Collections.Generic.List[string]]::new() } process { foreach ($id in @($ResourceId | Where-Object { $_ })) { $wanted.Add($id.TrimEnd('/').ToLowerInvariant()) } } end { trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $request = @{ SubscriptionId = @($SubscriptionId | Where-Object { $_ }); ManagementGroupId = @($ManagementGroupId | Where-Object { $_ }); ResourceGroupName = @($ResourceGroupName | Where-Object { $_ }); Days = $Days; IncludeHistory = [bool]$IncludeHistory; Wanted = $wanted.ToArray() } $isWanted = { param([string] $Id) foreach ($w in $request.Wanted) { if ($Id -eq $w -or $Id.StartsWith("$w/")) { return $true } }; $false } $null = Get-AACAccessToken if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Resource health' -Color 'springgreen3' } $outcome = Invoke-AACProgress -ScriptBlock { Update-AACProgress -Id 'scope' -Indeterminate -Description 'Finding the subscriptions' $scope = Resolve-AACScope -SubscriptionId $request.SubscriptionId -ManagementGroupId $request.ManagementGroupId Update-AACProgress -Id 'scope' -Complete -Description "Scope: $($scope.Label)" $queries = Get-AACResourceHealthQuery -ResourceGroupName $request.ResourceGroupName -Days $request.Days Update-AACProgress -Id 'read' -Total $queries.Count -Description 'Reading Resource Health and Service Health' $read = Invoke-AACGraphBatch -Query $queries -SubscriptionId $scope.GraphScope -AllowFailure @('events', 'service', 'impacted') -OnProgress { param($QueryName, $Done, $Total) Update-AACProgress -Id 'read' -Increment 1 -Description "Read the $QueryName ($Done of $Total)" } Update-AACProgress -Id 'read' -Complete -Description ('Read the health of {0:N0} resource(s) and {1:N0} Service Health event(s)' -f @($read.Rows['statuses']).Count, @($read.Rows['service']).Count) $history = @{} $historyNotices = [System.Collections.Generic.List[string]]::new() if ($request.IncludeHistory) { $recent = @{} foreach ($e in @($read.Rows['events'] | Where-Object { $_ })) { $recent[[string]$e['resourceId']] = $true } $ids = @(foreach ($row in @($read.Rows['statuses'] | Where-Object { $_ })) { $id = [string]$row['resourceId'] if ($id -notmatch '^/subscriptions/([^/]+)' -or -not $scope.Names.Contains($Matches[1].ToLowerInvariant())) { continue } if ($(if ($request.Wanted.Count) { & $isWanted $id } else { $recent.Contains($id) -or [string]$row['state'] -ne 'Available' })) { $id } }) | Select-Object -Unique if ($ids.Count -gt 200) { $historyNotices.Add("The history of $($ids.Count) resources was asked for; the first 200 were read."); $ids = @($ids | Select-Object -First 200) } if ($ids.Count) { $uris = [ordered]@{} foreach ($id in $ids) { $uris[$id] = "$id/providers/Microsoft.ResourceHealth/availabilityStatuses?api-version=2022-10-01" } Update-AACProgress -Id 'history' -Total $uris.Count -Description "Reading the Resource Health history of $($uris.Count) resource(s)" $answers = Invoke-AACArmParallel -Uri @($uris.Values) -OnProgress { param($HistoryDone, $HistoryTotal) Update-AACProgress -Id 'history' -Increment 1 } $failed = 0 foreach ($id in $uris.Keys) { $answer = $answers[$uris[$id]] if (-not $answer -or $answer.Error) { $failed++; continue } $history[$id] = @($answer.Items | Where-Object { $_ }) } if ($failed) { $historyNotices.Add("The Resource Health history of $failed resource(s) couldn't be read.") } Update-AACProgress -Id 'history' -Complete -Description ('Read the history of {0:N0} resource(s)' -f $history.Count) } } $result = ConvertTo-AACResourceHealth -Read $read -SubscriptionName $scope.Names -History $history -Days $request.Days @{ Result = $result; Scope = $scope; Notices = @($result.Notices) + $historyNotices.ToArray() } } $resources = @($outcome.Result.Resources) if ($request.Wanted.Count) { $resources = @($resources | Where-Object { & $isWanted $_.ResourceId }) } if ($ResourceType) { $resources = @($resources | Where-Object { $t = $_.ResourceType; @($ResourceType | Where-Object { $t -like $_ }).Count }) } if ($State) { $resources = @($resources | Where-Object { $State -contains $_.State }) } $events = @($outcome.Result.Events) $unavailable = @($resources | Where-Object State -EQ 'Unavailable').Count $degraded = @($resources | Where-Object State -EQ 'Degraded').Count $available = @($resources | Where-Object State -EQ 'Available').Count $unknown = @($resources | Where-Object State -EQ 'Unknown').Count $issues = @($events | Where-Object { $_.Status -eq 'Active' -and $_.EventType -eq 'Service issue' }).Count $maintenance = @($events | Where-Object { $_.Status -eq 'Active' -and $_.EventType -eq 'Planned maintenance' }).Count $withEvents = @($resources | Where-Object LastEvent).Count $measured = @($resources | Where-Object { $null -ne $_.Availability }) $stateTones = @{ Available = 'good'; Unavailable = 'bad'; Degraded = 'warn'; Unknown = 'neutral' } $eventTones = @{ 'Service issue' = 'bad'; 'Planned maintenance' = 'warn'; 'Health advisory' = 'info'; 'Security advisory' = 'violet' } $report = @{ Subtitle = 'Resource Health and Azure Service Health' Facts = [ordered]@{ Scope = $outcome.Scope.Label; Window = "the last $Days day(s)"; History = $(if ($IncludeHistory) { "$($measured.Count) resource(s)" } else { 'not read (-IncludeHistory)' }) } Status = $(if ($unavailable -or $issues) { 'Failed' } elseif ($degraded -or $maintenance) { 'Warning' } else { 'Success' }) Headline = $(if ($resources.Count) { "$unavailable unavailable, $degraded degraded, $available available of $($resources.Count) resource(s); $issues active Azure service issue(s)$(if ($maintenance) { ", $maintenance planned maintenance" })" } else { 'No Resource Health statuses in scope.' }) Tiles = @( @{ Value = '{0:N0}' -f $unavailable; Label = 'unavailable'; Tone = $(if ($unavailable) { 'bad' } else { 'good' }); Table = 'health'; Filters = @{ State = 'Unavailable' } } @{ Value = '{0:N0}' -f $degraded; Label = 'degraded'; Tone = $(if ($degraded) { 'warn' } else { 'good' }); Table = 'health'; Filters = @{ State = 'Degraded' } } @{ Value = '{0:N0}' -f $available; Label = 'available'; Tone = 'good'; Table = 'health'; Filters = @{ State = 'Available' } } @{ Value = '{0:N0}' -f $unknown; Label = 'unknown'; Tone = 'neutral'; Table = 'health'; Filters = @{ State = 'Unknown' } } @{ Value = '{0:N0}' -f $issues; Label = 'active service issues'; Tone = $(if ($issues) { 'bad' } else { 'good' }); Table = 'events' } @{ Value = '{0:N0}' -f $withEvents; Label = "with a health event ($Days d)"; Tone = $(if ($withEvents) { 'info' } else { 'good' }) } ) Notices = @($outcome.Notices | ForEach-Object { @{ Status = 'Warning'; Text = $_ } }) Charts = @( @{ Title = 'By state'; Kind = 'donut'; CenterLabel = 'resources'; Items = @(foreach ($s in 'Unavailable', 'Degraded', 'Unknown', 'Available') { $n = @($resources | Where-Object State -EQ $s).Count; if ($n) { @{ Label = $s; Value = $n; Tone = $stateTones[$s]; Filter = $s } } }); Table = 'health'; Column = 'State' } @{ Title = 'Not available, by type'; Items = @($resources | Where-Object { $_.State -in 'Unavailable', 'Degraded' } | Group-Object ResourceType | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'health'; Column = 'ResourceType'; Tone = 'bad'; Console = $true } @{ Title = 'Health events, by kind'; Items = @($resources | Where-Object LastEvent | Group-Object LastEvent | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'health'; Column = 'LastEvent'; Tone = 'info'; Console = $true } @{ Title = 'Least available'; Items = @($measured | Sort-Object Availability | Select-Object -First 10 | ForEach-Object { @{ Label = $_.Resource; Value = $_.Availability; Filter = $_.Resource } }); Table = 'health'; Column = 'Resource'; Tone = 'warn' } ) Tables = @( @{ Id = 'health'; Title = 'Resource health'; Section = 'Resources'; Rows = $resources; Noun = 'resources'; GroupBy = @('State', 'ResourceType', 'Subscription', 'ResourceGroup'); ConsoleLimit = 25 Empty = 'No Resource Health statuses in scope.' Columns = @( @{ Key = 'State'; Label = 'State'; Type = 'badge'; Tones = $stateTones; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Resource'; Label = 'Resource'; Type = 'resource'; Console = $true; Pdf = $true } @{ Key = 'ResourceType'; Label = 'Type'; Type = 'type'; Facet = $true; Console = $true } @{ Key = 'Summary'; Label = 'Summary'; Type = 'wide'; Console = $true; Pdf = $true } @{ Key = 'Hours'; Label = 'Hours'; Type = 'number'; Console = $true; Pdf = $true } @{ Key = 'Availability'; Label = 'Availability %'; Type = 'number'; Console = [bool]$IncludeHistory; Pdf = [bool]$IncludeHistory } @{ Key = 'Reason'; Label = 'Reason'; Facet = $true } @{ Key = 'Since'; Label = 'Since'; Type = 'datetime' } @{ Key = 'LastEvent'; Label = 'Last event'; Facet = $true } @{ Key = 'LastEventTime'; Label = 'Event time'; Type = 'datetime' } @{ Key = 'LastEventContext'; Label = 'Initiated'; Facet = $true } @{ Key = 'ServiceIssues'; Label = 'Service issues'; Type = 'mono' } @{ Key = 'StateChanges'; Label = 'State changes'; Type = 'number' } @{ Key = 'ResourceGroup'; Label = 'Resource group'; Facet = $true } @{ Key = 'Subscription'; Label = 'Subscription'; Facet = $true } ) } @{ Id = 'events'; Title = 'Azure Service Health'; Section = 'Service Health'; Rows = $events; Noun = 'events'; ConsoleLimit = 10 Empty = "No Service Health events in the last $Days day(s)."; EmptyStatus = 'Success' Columns = @( @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = @{ Active = 'bad'; Resolved = 'good' }; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'EventType'; Label = 'Kind'; Type = 'badge'; Tones = $eventTones; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Title'; Label = 'Event'; Type = 'wide'; Console = $true; Pdf = $true } @{ Key = 'Regions'; Label = 'Regions'; Console = $true } @{ Key = 'ImpactedResources'; Label = 'Your resources'; Type = 'number'; Console = $true; Pdf = $true } @{ Key = 'Services'; Label = 'Services'; Type = 'wide' } @{ Key = 'Started'; Label = 'Started'; Type = 'datetime'; Pdf = $true } @{ Key = 'Mitigated'; Label = 'Mitigated'; Type = 'datetime' } @{ Key = 'LastUpdate'; Label = 'Last update'; Type = 'datetime' } @{ Key = 'TrackingId'; Label = 'Tracking ID'; Type = 'mono' } @{ Key = 'Subscriptions'; Label = 'Subscriptions' } @{ Key = 'Summary'; Label = 'Summary'; Type = 'wide' } @{ Key = 'Link'; Label = 'Details'; Type = 'link'; Text = 'Open' } ) } ) Hint = '-State Unavailable, Degraded narrows the list; -IncludeHistory adds availability over -Days; -ServiceHealth returns the Azure events; -HtmlPath, -PdfPath or -CsvPath for a report.' } $objects = if ($ServiceHealth) { $events } else { $resources } Invoke-AACReportOutput -Report $report -Title $Title -CsvObject $objects -Noun $(if ($ServiceHealth) { 'event' } else { 'resource' }) -CsvPath (& $resolve $CsvPath) -HtmlPath (& $resolve $HtmlPath) -PdfPath (& $resolve $PdfPath) ` -ShowView:$interactive -NoPaging:$NoPaging -Object $objects -ReturnObject:($PassThru -or $NoDisplay -or $pipedOnward) } } |