Private/Lead.ps1
|
# Leads: what several Findings point to together. # # A blue screen from a read error, eleven disk errors and a drive that reports wear are # three Findings in three places, and seeing that they are one story is the step that # takes experience. Once every Check has run, a Run reads its Findings together and says # what several of them point to. A Lead is not a Finding: it observes nothing, has no # Severity and is not counted. See ADR-0005. # # A rule is a name and the Signals that support it. It goes by Signals and never by what # a Finding says, so that rewording a Finding cannot stop a Lead from forming. $script:LeadRules = @( [pscustomobject]@{ Name = 'drive'; Signals = @('disk-errors', 'drive-health-warning', 'stop-code-disk', 'disk-write-slow', 'program-not-paged-in') } [pscustomobject]@{ Name = 'memory'; Signals = @('stop-code-memory', 'hardware-errors', 'access-violations-spread') } [pscustomobject]@{ Name = 'graphics'; Signals = @('live-kernel-graphics', 'stop-code-graphics', 'graphics-driver-old') } # Several Servers affected is this machine's connection; one Server affected is that # Server. The two go by different Signals, so that neither is taken for the other. [pscustomobject]@{ Name = 'connection'; Signals = @('link-dropped', 'servers-lost', 'loss-on-own-network', 'usb-network-adapter', 'crash-with-network-disruption') } # One Lead for each Server: its Findings are read together by what they are about. [pscustomobject]@{ Name = 'server'; PerSubject = $true; Signals = @('server-lost', 'server-slow', 'server-unreachable', 'dependency-on-server') } ) # How many Leads a Report shows. More than this at the top of a Report is a list again, # and a list is what a Lead is there to spare the reader. $script:LeadMaximum = 3 function Get-LeadText { <# .SYNOPSIS What a Lead is called and what to do about it. A rule this has no words for is called by its name, which shows that the words are missing. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)][string]$Name, [AllowNull()][AllowEmptyString()][string]$Subject, [AllowNull()]$Situation) switch ($Name) { 'connection' { return [pscustomobject]@{ Title = Get-Text 'Lead.Connection.Title' Hint = (Get-Text 'Lead.Connection.Hint') -f (Get-SituationLinkText -Situation $Situation) } } 'server' { return [pscustomobject]@{ Title = (Get-Text 'Lead.Server.Title') -f $Subject; Hint = (Get-Text 'Lead.Server.Hint') -f $Subject } } 'drive' { return [pscustomobject]@{ Title = Get-Text 'Lead.Drive.Title'; Hint = Get-Text 'Lead.Drive.Hint' } } 'memory' { return [pscustomobject]@{ Title = Get-Text 'Lead.Memory.Title'; Hint = Get-Text 'Lead.Memory.Hint' } } 'graphics' { return [pscustomobject]@{ Title = Get-Text 'Lead.Graphics.Title'; Hint = Get-Text 'Lead.Graphics.Hint' } } } [pscustomobject]@{ Title = $Name; Hint = '' } } function ConvertTo-LeadSubject { <# .SYNOPSIS What a Finding is about, as something two Findings can be compared by. Pure. .DESCRIPTION One Check names a Server FS01 and another fs01.contoso-example.de. A name is taken by its first label, in lower case; an address is taken as it is, because its first part is not a name for it. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$Subject) $text = "$Subject".Trim().ToLowerInvariant() if (-not $text) { return '' } if ($text -match '^\d{1,3}(\.\d{1,3}){3}$' -or $text.Contains(':')) { return $text } ($text -split '\.')[0] } function Get-Lead { <# .SYNOPSIS The Leads a Run's Findings support. Pure. .DESCRIPTION A Lead takes two Findings that observed two different things its rule names. One Finding has its own Meaning and Hint and needs no reading-together. The same thing observed twice - two drives that report wear, two Servers that are slow - is two Findings about two things, and no evidence that either is what the other means. A Finding that is in order observed nothing that supports anything. Every rule that fits is stated, the one resting on more Findings first, and no more than a few: a Report that showed one of two would claim to know which it is. #> [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()][AllowEmptyCollection()]$Finding, [AllowNull()][AllowEmptyCollection()]$Rule = $script:LeadRules, # What a Lead's Hint may assume about the machine, like any Hint. [AllowNull()]$Situation ) $all = @($Finding | Where-Object { $_ }) $candidates = @($all | Where-Object { "$(Get-DataProperty $_ 'Severity')" -ne 'OK' }) $formed = New-Object 'System.Collections.Generic.List[object]' $open = New-Object 'System.Collections.Generic.List[object]' $order = 0 foreach ($one in @($Rule | Where-Object { $_ })) { $order++ $wanted = @((Get-DataCollection $one 'Signals') | ForEach-Object { "$_" }) $mine = @($candidates | Where-Object { @((Get-DataCollection $_ 'Signals') | Where-Object { $_ -in $wanted }).Count }) if (-not $mine.Count) { continue } # A rule about one thing among several is asked once for each of them, and a # Finding that is about nothing in particular takes part in none. $groups = @([pscustomobject]@{ Subject = ''; Findings = $mine }) if ([bool](Get-DataProperty $one 'PerSubject')) { $groups = @($mine | Where-Object { ConvertTo-LeadSubject "$(Get-DataProperty $_ 'Subject')" } | Group-Object { ConvertTo-LeadSubject "$(Get-DataProperty $_ 'Subject')" } | Sort-Object Name | ForEach-Object { [pscustomobject]@{ Subject = "$(Get-DataProperty $_.Group[0] 'Subject')"; Findings = @($_.Group) } }) } foreach ($group in $groups) { $support = @($group.Findings) $seen = @{} foreach ($f in $support) { foreach ($signal in @((Get-DataCollection $f 'Signals') | Where-Object { $_ -in $wanted })) { $seen["$signal"] = $true } } # What could have supported it and did not look: a Check that was skipped, # rights that were not given. Its Value says why. It supports nothing. Of a # rule about one thing, only what is about that thing or about none. $key = ConvertTo-LeadSubject $group.Subject $notChecked = @(foreach ($f in $all) { if (-not @((Get-DataCollection $f 'Unobserved') | Where-Object { $_ -in $wanted }).Count) { continue } $about = ConvertTo-LeadSubject "$(Get-DataProperty $f 'Subject')" if ($key -and $about -and $about -ne $key) { continue } [pscustomobject]@{ Check = "$(Get-DataProperty $f 'Check')"; Reason = "$(Get-DataProperty $f 'Value')" } }) $isLead = $support.Count -ge 2 -and $seen.Count -ge 2 # One observation and nobody looked for the second: not a Lead, and not nothing. if (-not $isLead -and -not $notChecked.Count) { continue } $text = Get-LeadText -Name "$(Get-DataProperty $one 'Name')" -Subject $group.Subject -Situation $Situation $lead = [pscustomobject]@{ PSTypeName = 'Gutcheck.Lead' Name = "$(Get-DataProperty $one 'Name')" Subject = "$($group.Subject)" Title = $text.Title Hint = $text.Hint Findings = $support Signals = @($seen.Keys | Sort-Object) NotChecked = $notChecked # $false: what was observed is one thing only, and what could have made a # Lead of it was not looked at. Said under the Leads, so that it is not # read as none. Formed = $isLead Order = $order } if ($isLead) { $formed.Add($lead) } else { $open.Add($lead) } } } # Sort-Object is not stable on Windows PowerShell 5.1, so the order rules were given # in is part of the key. @($formed | Sort-Object @{ Expression = { $_.Findings.Count }; Descending = $true }, Order | Select-Object -First $script:LeadMaximum) @($open | Sort-Object Order | Select-Object -First $script:LeadMaximum) } |