Private/Show-AACWorkspaceAssessmentView.ps1
|
function Show-AACWorkspaceAssessmentView { <# .SYNOPSIS Renders Invoke-AACLogAnalyticsWorkspaceAssessment's result as a Spectre.Console view. .DESCRIPTION The workspace, tiles, what couldn't be read, then - for the sections asked for - the recommendations (by severity), the billable and the not billable tables (size, share, daily average, plan, retention), the settings by category, and the Workspace Insights tabs: Usage (ingestion per day, per solution, the top resources and computers), Health (operations, latency), Agents, Query Audit, Data Collection Rules and Change Log. Long lists stop at -MaxRow rows with how many more; -PassThru or -HtmlPath has them all. Wrap the call in Invoke-AACPagedOutput to page it. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable] $Assessment, [int] $MaxRow = 25 ) $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $glyph = Get-AACGlyph $stats = $Assessment.Stats $workspace = $Assessment.Workspace $insights = $Assessment.Insights $section = @(if ($Assessment.Contains('Section')) { $Assessment.Section }) $want = { param([string] $Name) -not $section.Count -or $section -contains $Name } $gb = { param($Value) $v = [double]$Value; if ($v -eq 0) { '[grey42]0[/]' } elseif ($v -ge 100) { '{0:N0} GB' -f $v } elseif ($v -ge 1) { '{0:N2} GB' -f $v } else { '{0:N3} GB' -f $v } } $when = { param($Value) if ($Value -is [datetime]) { $Value.ToString('yyyy-MM-dd HH:mm') } elseif ($Value) { [string]$Value } else { '-' } } $severityColor = @{ High = 'red1'; Medium = 'orange1'; Low = 'deepskyblue1' } $newTable = { param([string] $Title, [string] $Color, [object[]] $Columns) $table = [Spectre.Console.Table]::new() $table.Border = [Spectre.Console.TableBorder]::Rounded $table.BorderStyle = [Spectre.Console.Style]::Parse($Color) $table.Expand = $true $table.Title = [Spectre.Console.TableTitle]::new($Title) foreach ($column in $Columns) { $spectre = [Spectre.Console.TableColumn]::new("[grey62]$($column[0])[/]") if ($column[1]) { $spectre.Alignment = [Spectre.Console.Justify]::Right; $spectre.NoWrap = $true } $table.AddColumn($spectre) | Out-Null } $table } $addRow = { param($Table, [string[]] $Cells) [Spectre.Console.TableExtensions]::AddRow($Table, [Spectre.Console.Rendering.IRenderable[]]@($Cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null } $write = { param($Table) [Spectre.Console.AnsiConsole]::Write($Table); [Spectre.Console.AnsiConsole]::WriteLine() } $more = { param([int] $Shown, [int] $Total) if ($Total -gt $Shown) { Write-AACMarkup "[grey50]... and $($Total - $Shown) more: -PassThru or -HtmlPath lists them all.[/]"; [Spectre.Console.AnsiConsole]::WriteLine() } } $heading = { param([string] $Text) Write-AACMarkup "[bold deepskyblue1]$($glyph.Bullet) $(& $escape $Text)[/]"; [Spectre.Console.AnsiConsole]::WriteLine() } # --- The workspace and tiles ------------------------------------------------------------------------------ $facts = @("[white]$(& $escape $workspace.Name)[/]", "workspace ID $(& $escape $workspace.WorkspaceId)", (& $escape $workspace.ResourceGroup), (& $escape $workspace.Location), "last $($workspace.Days) day(s)") Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]" [Spectre.Console.AnsiConsole]::WriteLine() Show-AACTileRow -Tile @( @{ Value = $(if ($workspace.CommitmentTierGB) { "$($workspace.PricingTier) $($workspace.CommitmentTierGB)" } else { [string]$workspace.PricingTier }); Caption = 'pricing tier'; Color = 'mediumpurple2' } @{ Value = '{0:N2} GB' -f $stats.BillableGB; Caption = "billable, $($workspace.Days) days"; Color = 'deepskyblue1' } @{ Value = '{0:N2} GB' -f $stats.AverageDailyGB; Caption = 'billable per day'; Color = 'deepskyblue1' } @{ Value = '{0:N0}' -f $stats.TablesWithData; Caption = 'tables with data'; Color = 'grey85' } @{ Value = "$($stats.HealthyAgents) / $($stats.Agents)"; Caption = 'agents healthy'; Color = $(if ($stats.UnhealthyAgents) { 'orange1' } else { 'green3' }) } @{ Value = '{0:N0}' -f $stats.DataCollectionRules; Caption = 'data collection rules'; Color = 'turquoise2' } @{ Value = "$($stats.High) / $($stats.Medium) / $($stats.Low)"; Caption = 'recommendations H / M / L'; Color = $(if ($stats.High) { 'red1' } elseif ($stats.Medium) { 'orange1' } else { 'green3' }) } ) [Spectre.Console.AnsiConsole]::WriteLine() foreach ($notice in @($Assessment.Notices)) { Write-AACMarkup "[deepskyblue1]i[/] [grey70]$(& $escape $notice)[/]" } foreach ($key in @($Assessment.Errors.Keys)) { Write-AACMarkup "[orange1]![/] [grey70]$(& $escape ($key -replace '^\w+:', '')) couldn't be read: $(& $escape $Assessment.Errors[$key])[/]" } if (@($Assessment.Notices).Count -or $Assessment.Errors.Count) { [Spectre.Console.AnsiConsole]::WriteLine() } # --- Recommendations ------------------------------------------------------------------------------------------ if (& $want 'Recommendations') { $items = @($Assessment.Recommendations) if ($items.Count) { $table = & $newTable "[bold]$($glyph.Bullet) Recommendations[/] [grey58]$($glyph.Dot) $($items.Count)[/]" 'grey42' @(@('Severity', $false), @('Recommendation', $false), @('What to do', $false)) foreach ($item in $items) { $color = $severityColor[$item.Severity] ?? 'grey70' & $addRow $table @( "[bold $color]$($item.Severity)[/]`n[grey58]$(& $escape $item.Category)[/]" "[bold]$(& $escape $item.Recommendation)[/]$(if ($item.Detail) { "`n[grey70]$(& $escape $item.Detail)[/]" })$(if ($item.Source -ne 'Assessment') { "`n[grey50]$(& $escape $item.Source)[/]" })" "$(& $escape $item.Action)$(if ($item.LearnMore) { "`n[grey50 link]$(& $escape $item.LearnMore)[/]" })" ) } & $write $table } else { Show-AACPanel -Content '[bold green3]No recommendations[/] [grey58]for this workspace.[/]' -BorderColor 'green3' -AllowMarkup [Spectre.Console.AnsiConsole]::WriteLine() } } # --- Tables: billable, not billable ---------------------------------------------------------------------------- if (& $want 'Tables') { foreach ($kind in 'Billable', 'Not billable') { $rows = @($Assessment.Tables | Where-Object { if ($kind -eq 'Billable') { $_.Billing -in 'Billable', 'Both' } else { $_.Billing -eq 'Not billable' } }) if (-not $rows.Count) { continue } $color = if ($kind -eq 'Billable') { 'deepskyblue3' } else { 'green3' } $columns = @(@('Table', $false), @('Billable', $true), @('Free', $true), @('Share', $true), @('Per day', $true), @('Plan', $false), @('Retention', $true), @('Last record', $false)) $table = & $newTable "[bold $color]$($glyph.Bullet) $kind tables[/] [grey58]$($glyph.Dot) $($rows.Count) $($glyph.Dot) largest first[/]" $color $columns foreach ($row in $rows | Select-AACFirst ($MaxRow * 2)) { $retention = if ($null -ne $row.RetentionDays) { "$($row.RetentionDays) d$(if ($row.LongTermDays) { " + $($row.LongTermDays) d long-term" })" } else { '-' } & $addRow $table @( "[bold]$(& $escape $row.Table)[/]$(if ($row.Type -ne 'Azure') { " [grey50]$(& $escape $row.Type.ToLowerInvariant())[/]" })" (& $gb $row.BillableGB); (& $gb $row.NonBillableGB) $(if ($row.SharePercent) { "$($row.SharePercent)%" } else { '[grey42]-[/]' }) (& $gb $row.DailyAverageGB) $(if ($row.Plan -and $row.Plan -ne 'Analytics') { "[orchid]$(& $escape $row.Plan)[/]" } else { "[grey70]$(& $escape $(if ($row.Plan) { $row.Plan } else { '-' }))[/]" }) $retention "[grey70]$(& $when $row.LastRecord)[/]" ) } & $write $table & $more ($MaxRow * 2) $rows.Count } $empty = @($Assessment.Tables | Where-Object Billing -EQ 'No data') if ($empty.Count) { Write-AACMarkup "[grey50]$($empty.Count) table(s) listed with no data in the period (custom, or on a non-default plan or retention): $(& $escape (@($empty | Select-AACFirst 12 | ForEach-Object Table) -join ', '))$(if ($empty.Count -gt 12) { ', ...' })[/]"; [Spectre.Console.AnsiConsole]::WriteLine() } } # --- Settings ------------------------------------------------------------------------------------------------------ if (& $want 'Settings') { $table = & $newTable "[bold mediumpurple2]$($glyph.Bullet) Workspace settings[/]" 'mediumpurple2' @(@('Category', $false), @('Setting', $false), @('Value', $false)) $last = '' foreach ($row in $Assessment.Settings) { & $addRow $table @($(if ($row.Category -ne $last) { "[bold]$(& $escape $row.Category)[/]" } else { '' }), (& $escape $row.Setting), "[white]$(& $escape $row.Value)[/]") $last = $row.Category } & $write $table } # --- Usage ----------------------------------------------------------------------------------------------------------- if (& $want 'Usage') { $usage = $insights.Usage & $heading 'Usage' if (@($usage.Daily).Count) { Show-AACBarChart -Title "Billable GB per day (last $($workspace.Days) days)" -Format 'N2' -Suffix ' GB' -Item @($usage.Daily | ForEach-Object { @{ Label = $_.Day.ToString('MM-dd'); Value = [double]$_.BillableGB; Color = 'deepskyblue1' } }) [Spectre.Console.AnsiConsole]::WriteLine() } foreach ($set in @(@('Solutions', 'Solution', 'By solution'), @('Resources', 'Resource', 'By Azure resource, last 24 hours'), @('Computers', 'Computer', 'By computer, last 24 hours'))) { $rows = @($usage[$set[0]]) if (-not $rows.Count) { continue } $table = & $newTable "[bold]$($set[2])[/]" 'grey42' @(@($set[1], $false), @('Billable', $true)) foreach ($row in $rows | Select-AACFirst $MaxRow) { & $addRow $table @("[white]$(& $escape $row.($set[1]))[/]", (& $gb $row.BillableGB)) } & $write $table } } # --- Health -------------------------------------------------------------------------------------------------------- if (& $want 'Health') { & $heading 'Health' $operations = @($insights.Health.Operations) if ($operations.Count) { $levelColor = @{ Error = 'red1'; Warning = 'orange1'; Info = 'grey70'; Informational = 'grey70' } $table = & $newTable "[bold]Operations (_LogOperation)[/] [grey58]$($glyph.Dot) errors and warnings first[/]" 'grey42' @(@('Level', $false), @('Operation', $false), @('Count', $true), @('Last seen', $false), @('Detail', $false)) foreach ($row in $operations | Select-AACFirst $MaxRow) { & $addRow $table @("[$($levelColor[$row.Level] ?? 'grey70')]$(& $escape $row.Level)[/]", "$(& $escape $row.Operation)`n[grey50]$(& $escape $row.Category)[/]", "$($row.Count)", "[grey70]$(& $when $row.LastSeen)[/]", "[grey70]$(& $escape $row.Detail)[/]") } & $write $table & $more $MaxRow $operations.Count } else { Write-AACMarkup '[green3]No operations reported in _LogOperation for the period.[/]'; [Spectre.Console.AnsiConsole]::WriteLine() } $latency = @($insights.Health.Latency) if ($latency.Count) { $table = & $newTable '[bold]Ingestion latency of heartbeats, last 24 hours[/]' 'grey42' @(@('Agent', $false), @('Records', $true), @('Median', $true), @('95th percentile', $true), @('Max', $true)) foreach ($row in $latency) { & $addRow $table @((& $escape $row.AgentType), "$($row.Records)", "$($row.P50Seconds) s", "$($row.P95Seconds) s", "$($row.MaxSeconds) s") } & $write $table } } # --- Agents ---------------------------------------------------------------------------------------------------------- if (& $want 'Agents') { & $heading 'Agents' $byType = @($insights.Agents.ByType) if ($byType.Count) { $table = & $newTable '[bold]Agents by type[/]' 'grey42' @(@('Agent', $false), @('Computers', $true), @('Healthy', $true), @('Unhealthy', $true), @('Not reporting', $true)) foreach ($row in $byType) { & $addRow $table @("$(if ($row.AgentType -like '*MMA*') { '[red1]' } else { '[white]' })$(& $escape $row.AgentType)[/]", "$($row.Computers)", "[green3]$($row.Healthy)[/]", "$(if ($row.Unhealthy) { "[orange1]$($row.Unhealthy)[/]" } else { '[grey42]0[/]' })", "$(if ($row.NotReporting) { "[red1]$($row.NotReporting)[/]" } else { '[grey42]0[/]' })") } & $write $table $silent = @($insights.Agents.Computers | Where-Object State -NE 'Healthy' | Sort-Object MinutesSince) if ($silent.Count) { $table = & $newTable '[bold orange1]Agents without a recent heartbeat[/]' 'orange1' @(@('Computer', $false), @('State', $false), @('Last heartbeat', $false), @('Agent', $false), @('OS', $false)) foreach ($row in $silent | Select-AACFirst $MaxRow) { & $addRow $table @("[bold]$(& $escape $row.Computer)[/]", "$(if ($row.State -eq 'Not reporting') { '[red1]' } else { '[orange1]' })$($row.State)[/]", (& $when $row.LastHeartbeat), (& $escape $row.AgentType), (& $escape (@($row.OSType, $row.OSName) | Where-Object { $_ }) -join ' ')) } & $write $table & $more $MaxRow $silent.Count } } else { Write-AACMarkup "[grey58]No agent sent a heartbeat to this workspace in the period.[/]"; [Spectre.Console.AnsiConsole]::WriteLine() } } # --- Query audit --------------------------------------------------------------------------------------------------- if (& $want 'QueryAudit') { & $heading 'Query audit' $audit = $insights.QueryAudit if (@($audit.Users).Count) { $table = & $newTable '[bold]Queries by user and app[/]' 'grey42' @(@('User', $false), @('App', $false), @('Queries', $true), @('Failed', $true), @('Avg ms', $true), @('Max ms', $true), @('CPU s', $true)) foreach ($row in $audit.Users | Select-AACFirst $MaxRow) { & $addRow $table @("[white]$(& $escape $row.User)[/]", "[grey70]$(& $escape $row.ClientApp)[/]", "$($row.Queries)", "$(if ($row.Failed) { "[red1]$($row.Failed)[/]" } else { '[grey42]0[/]' })", "$($row.AvgDurationMs)", "$($row.MaxDurationMs)", "$($row.CpuSeconds)") } & $write $table if (@($audit.Slowest).Count) { $table = & $newTable '[bold]Slowest queries[/]' 'grey42' @(@('When', $false), @('User', $false), @('ms', $true), @('Query', $false)) foreach ($row in $audit.Slowest | Select-AACFirst 10) { & $addRow $table @((& $when $row.Time), (& $escape $row.User), "$($row.DurationMs)", "[grey70]$(& $escape ($row.Query -replace '\s+', ' '))[/]") } & $write $table } if (@($audit.Failed).Count) { $table = & $newTable '[bold red1]Failed queries[/]' 'red3' @(@('Code', $true), @('User', $false), @('App', $false), @('Count', $true), @('Last seen', $false)) foreach ($row in $audit.Failed | Select-AACFirst $MaxRow) { & $addRow $table @("$($row.ResponseCode)", (& $escape $row.User), (& $escape $row.ClientApp), "$($row.Count)", (& $when $row.LastSeen)) } & $write $table } } else { Write-AACMarkup '[grey58]No query audit records (LAQueryLogs) in the period.[/]'; [Spectre.Console.AnsiConsole]::WriteLine() } } # --- Data collection rules ------------------------------------------------------------------------------------------- if (& $want 'DataCollectionRules') { $rules = @($Assessment.DataCollectionRules) if ($rules.Count) { $table = & $newTable "[bold turquoise2]$($glyph.Bullet) Data collection rules[/] [grey58]$($glyph.Dot) sending to this workspace[/]" 'turquoise2' @(@('Rule', $false), @('Collects', $false), @('Into', $false), @('Associated with', $false)) foreach ($rule in $rules) { & $addRow $table @( "[bold]$(& $escape $rule.Name)[/]`n[grey50]$(& $escape $rule.Kind) $($glyph.Dot) $(& $escape $rule.ResourceGroup)$(if ($rule.WorkspaceTransform) { " $($glyph.Dot) workspace transformation" })[/]" "$(& $escape $(if ($rule.DataSources) { $rule.DataSources } else { $rule.Streams }))$(if ($rule.Transformations) { "`n[grey58]$($rule.Transformations) transformation(s)[/]" })" "[grey70]$(& $escape $(if ($rule.OutputTables) { $rule.OutputTables } else { $rule.Streams }))[/]" $(if ($rule.Associations) { "$($rule.Associations): $(& $escape $rule.AssociatedResources)" } elseif ($rule.WorkspaceTransform -or $rule.Kind -in 'Direct', 'AgentDirectToStore') { '[grey50]-[/]' } else { '[orange1]none[/]' }) ) } & $write $table } else { Write-AACMarkup '[grey58]No data collection rule sends to this workspace.[/]'; [Spectre.Console.AnsiConsole]::WriteLine() } } # --- Change log -------------------------------------------------------------------------------------------------------- if (& $want 'ChangeLog') { $changes = @($Assessment.ChangeLog) if ($changes.Count) { $table = & $newTable "[bold]$($glyph.Bullet) Change log[/] [grey58]$($glyph.Dot) activity log, newest first[/]" 'grey42' @(@('When', $false), @('Operation', $false), @('On', $false), @('Status', $false), @('By', $false)) foreach ($row in $changes | Select-AACFirst $MaxRow) { & $addRow $table @((& $when $row.Time), (& $escape $row.Operation), "[grey70]$(& $escape $row.Resource)[/]", "$(if ($row.Status -eq 'Failed') { '[red1]' } else { '[green3]' })$(& $escape $row.Status)[/]", (& $escape $row.Caller)) } & $write $table & $more $MaxRow $changes.Count } else { Write-AACMarkup "[grey58]No changes to the workspace in the activity log for the period.[/]"; [Spectre.Console.AnsiConsole]::WriteLine() } } Write-AACMarkup '[grey42]Add -PassThru (or pipe the command) for the assessment object - .Tables, .Settings, .Recommendations, .Insights; -CsvPath for the tables, -HtmlPath for every section.[/]' } |