Collectors/Observability.ps1
|
function Get-AerObservability { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]] $SubscriptionIds, [Parameter(Mandatory)] $SubscriptionMap ) $subLookup = @{} if ($SubscriptionMap -is [hashtable]) { $subLookup = $SubscriptionMap } elseif ($SubscriptionMap) { $SubscriptionMap.PSObject.Properties | ForEach-Object { $subLookup[$_.Name] = $_.Value } } function SubName($id) { if (-not $id) { return 'Unknown' } $n = $subLookup[$id.ToLowerInvariant()] if ($n) { $n } else { $id } } function Pctg($part, $total) { if ($total -gt 0) { [int][math]::Round($part / $total * 100) } else { $null } } # Expand-AerRows + Invoke-AerArmBatch come from Core\ResourceGraph.ps1. function ArgRows($query) { Expand-AerRows (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query $query) } # ── Inventory counts by type (cheap) ───────────────────────────────────── $cnt = @{} try { $types = "'microsoft.operationalinsights/workspaces','microsoft.insights/components','microsoft.insights/datacollectionrules','microsoft.insights/datacollectionendpoints','microsoft.insights/actiongroups','microsoft.insights/metricalerts','microsoft.insights/scheduledqueryrules','microsoft.insights/activitylogalerts','microsoft.insights/workbooks','microsoft.portal/dashboards','microsoft.dashboard/grafana','microsoft.automation/automationaccounts'" foreach ($r in (ArgRows "resources | where type in~ ($types) | summarize c = count() by t = tolower(type)")) { if ($r.t) { $cnt[$r.t] = [int]$r.c } } } catch { Write-Warning "[Observability.counts] $($_.Exception.Message)" } function C($t) { [int]($cnt[$t] ?? 0) } $totalObs = 0; foreach ($v in $cnt.Values) { $totalObs += [int]$v } # Diagnostic Settings coverage now lives in its own collector # (Get-AerDiagnosticSettings); the Overview reads it from d.diagnosticSettings. # ── AMA (Azure Monitor Agent) coverage on VM + Arc machines ────────────── $machines = 0; $withAma = 0 try { $machines = (@(ArgRows "resources | where type in~ ('microsoft.compute/virtualmachines','microsoft.hybridcompute/machines') | project id = tolower(id)")).Count $amaParents = @{} foreach ($e in (ArgRows "resources | where type in~ ('microsoft.compute/virtualmachines/extensions','microsoft.hybridcompute/machines/extensions') | where name has 'AzureMonitorWindowsAgent' or name has 'AzureMonitorLinuxAgent' or tostring(properties.type) in~ ('AzureMonitorWindowsAgent','AzureMonitorLinuxAgent') | project id = tolower(id)")) { if ($e.id) { $parent = ($e.id -split '/extensions/')[0] if ($parent) { $amaParents[$parent] = $true } } } $withAma = $amaParents.Count } catch { Write-Warning "[Observability.ama] $($_.Exception.Message)" } # ── Application Insights coverage on Web / Function apps ───────────────── # An app is "covered" if it references App Insights either via the portal # 'hidden-link:<appId>' tag on the component (legacy) OR via an # APPINSIGHTS_INSTRUMENTATIONKEY / APPLICATIONINSIGHTS_CONNECTION_STRING app # setting (the modern, far more common path — read via ARM batch). $apps = 0; $appsCovered = 0 try { $appIds = @{} # id -> covered $appList = [System.Collections.Generic.List[string]]::new() foreach ($a in (ArgRows "resources | where type =~ 'microsoft.web/sites' | project id = tolower(id)")) { if ($a.id -and -not $appIds.ContainsKey($a.id)) { $appIds[$a.id] = $false; $appList.Add($a.id) } } $apps = $appIds.Count # Signal 1 — hidden-link tags on App Insights components (cheap, ARG) $viaTag = 0 foreach ($c in (ArgRows "resources | where type =~ 'microsoft.insights/components' | where isnotempty(tags) | project tags")) { if (-not $c.tags) { continue } foreach ($p in $c.tags.PSObject.Properties) { if ($p.Name -like 'hidden-link:*' -and $p.Name -like '*/sites/*') { $linked = ($p.Name -replace '^hidden-link:', '').ToLowerInvariant() if ($appIds.ContainsKey($linked) -and -not $appIds[$linked]) { $appIds[$linked] = $true; $viaTag++ } } } } # Signal 2 — App Insights app settings (authoritative, ARM batch POST). # Requires the 'Microsoft.Web/sites/config/list/Action' permission; # plain Reader/Monitoring Reader gets HTTP 403 here (→ undetectable). $aiKeys = @('APPINSIGHTS_INSTRUMENTATIONKEY','APPLICATIONINSIGHTS_CONNECTION_STRING','APPLICATIONINSIGHTS_CONNECTIONSTRING','APPLICATIONINSIGHTSAGENT_EXTENSION_VERSION','APPLICATIONINSIGHTS_CONNECTION_STRING__AccountEndpoint') $viaSettings = 0; $s2xx = 0; $s403 = 0; $sOther = 0 if ($appList.Count) { $reqs = [System.Collections.Generic.List[object]]::new() $idByName = @{} for ($i = 0; $i -lt $appList.Count; $i++) { $idByName[$i.ToString()] = $appList[$i] $reqs.Add([ordered]@{ httpMethod = 'POST' name = $i.ToString() url = "$($appList[$i])/config/appsettings/list?api-version=2022-03-01" }) } $responses = Invoke-AerArmBatch $reqs foreach ($key in $responses.Keys) { $rr = $responses[$key] if ($rr.httpStatusCode -ge 200 -and $rr.httpStatusCode -lt 300 -and $rr.content -and $rr.content.properties) { $s2xx++ foreach ($pp in $rr.content.properties.PSObject.Properties) { if (($aiKeys -contains $pp.Name) -and "$($pp.Value)".Trim()) { if (-not $appIds[$idByName[$key]]) { $appIds[$idByName[$key]] = $true; $viaSettings++ } break } } } elseif ($rr.httpStatusCode -eq 403) { $s403++ } else { $sOther++ } } } $appsCovered = (@($appIds.Values | Where-Object { $_ })).Count Write-Verbose "[Observability.appInsights] apps=$apps covered=$appsCovered viaTag=$viaTag viaSettings=$viaSettings | appsettings: 2xx=$s2xx 403(no-permission)=$s403 other=$sOther" } catch { Write-Warning "[Observability.appInsights] $($_.Exception.Message)" } return [pscustomobject]@{ Total = $totalObs SubscriptionsEvaluated = @($SubscriptionIds).Count Workspaces = C 'microsoft.operationalinsights/workspaces' AmaCoverage = [pscustomobject]@{ Machines = $machines WithAma = $withAma Percent = (Pctg $withAma $machines) } AppInsightsCoverage = [pscustomobject]@{ Apps = $apps Covered = $appsCovered Percent = (Pctg $appsCovered $apps) } Counts = [pscustomobject]@{ Workspaces = C 'microsoft.operationalinsights/workspaces' AppInsights = C 'microsoft.insights/components' DataCollectionRules = C 'microsoft.insights/datacollectionrules' DataCollectionEndpoints = C 'microsoft.insights/datacollectionendpoints' ActionGroups = C 'microsoft.insights/actiongroups' AlertRules = (C 'microsoft.insights/metricalerts') + (C 'microsoft.insights/scheduledqueryrules') + (C 'microsoft.insights/activitylogalerts') Workbooks = C 'microsoft.insights/workbooks' Dashboards = C 'microsoft.portal/dashboards' Grafana = C 'microsoft.dashboard/grafana' AutomationAccounts = C 'microsoft.automation/automationaccounts' } } } |