modules/Invoke-AppInsights.ps1
|
#requires -Version 7.0 <# .SYNOPSIS Wrapper for Application Insights performance and reliability KQL signals. .DESCRIPTION Discovers Microsoft.Insights/components resources and queries telemetry for: - Slow requests - Dependency failures - Exception clusters Query calls are wrapped with Invoke-WithRetry and Invoke-WithTimeout (300s). Optional disk output is sanitized via Remove-Credentials before writing. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $SubscriptionId, [string] $ResourceGroup, [string] $AppInsightsName, [ValidateRange(1, 168)] [int] $TimeRangeHours = 24, [string] $OutputPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $retryPath = Join-Path $PSScriptRoot 'shared' 'Retry.ps1' if (Test-Path $retryPath) { . $retryPath } if (-not (Get-Command Invoke-WithRetry -ErrorAction SilentlyContinue)) { function Invoke-WithRetry { param([scriptblock]$ScriptBlock, [int]$MaxAttempts = 3) & $ScriptBlock } } $sanitizePath = Join-Path $PSScriptRoot 'shared' 'Sanitize.ps1' if (Test-Path $sanitizePath) { . $sanitizePath } if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) { function Remove-Credentials { param([string]$Text) return $Text } } $errorsPath = Join-Path $PSScriptRoot 'shared' 'Errors.ps1' if (Test-Path $errorsPath) { . $errorsPath } if (-not (Get-Command New-FindingError -ErrorAction SilentlyContinue)) { function New-FindingError { param([string]$Source,[string]$Category,[string]$Reason,[string]$Remediation,[string]$Details) return [pscustomobject]@{ Source=$Source; Category=$Category; Reason=$Reason; Remediation=$Remediation; Details=$Details } } } if (-not (Get-Command Format-FindingErrorMessage -ErrorAction SilentlyContinue)) { function Format-FindingErrorMessage { param([Parameter(Mandatory)]$FindingError) $line = "[{0}] {1}: {2}" -f $FindingError.Source, $FindingError.Category, $FindingError.Reason if ($FindingError.Remediation) { $line += " Action: $($FindingError.Remediation)" } return $line } } $installerPath = Join-Path $PSScriptRoot 'shared' 'Installer.ps1' if (Test-Path $installerPath) { . $installerPath } $envelopePath = Join-Path $PSScriptRoot 'shared' 'New-WrapperEnvelope.ps1' if (Test-Path $envelopePath) { . $envelopePath } if (-not (Get-Command New-WrapperEnvelope -ErrorAction SilentlyContinue)) { function New-WrapperEnvelope { param([string]$Source,[string]$Status='Failed',[string]$Message='',[object[]]$FindingErrors=@()) return [PSCustomObject]@{ Source=$Source; SchemaVersion='1.0'; Status=$Status; Message=$Message; Findings=@(); Errors=@($FindingErrors) } } } $timeoutCmd = Get-Command Invoke-WithTimeout -ErrorAction SilentlyContinue if (-not $timeoutCmd -or -not $timeoutCmd.Parameters.ContainsKey('ScriptBlock')) { function Invoke-WithTimeout { param( [Parameter(Mandatory)] [scriptblock] $ScriptBlock, [int] $TimeoutSec = 300 ) return & $ScriptBlock } } $result = [ordered]@{ SchemaVersion = '1.0' Source = 'appinsights' Status = 'Success' Message = '' Findings = @() Errors = @() Subscription = $SubscriptionId Timestamp = (Get-Date).ToUniversalTime().ToString('o') } if (-not (Get-Module -ListAvailable -Name Az.Accounts)) { $result.Status = 'Skipped' $result.Message = 'Az.Accounts module not installed. Run: Install-Module Az.Accounts -Scope CurrentUser' return [PSCustomObject]$result } try { Import-Module Az.Accounts -ErrorAction SilentlyContinue -WarningAction SilentlyContinue Import-Module Az.ApplicationInsights -ErrorAction SilentlyContinue -WarningAction SilentlyContinue Import-Module Az.Monitor -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } catch { Write-Verbose "App Insights module import warning: $(Remove-Credentials -Text ([string]$_.Exception.Message))" } try { $ctx = Get-AzContext -ErrorAction Stop if (-not $ctx) { Write-Error 'No Az context' -ErrorAction Stop } } catch { $result.Status = 'Skipped' $result.Message = 'Not signed in. Run Connect-AzAccount first.' return [PSCustomObject]$result } function Get-QueryCommandName { if (Get-Command Invoke-AzApplicationInsightsQuery -ErrorAction SilentlyContinue) { return 'Invoke-AzApplicationInsightsQuery' } if (Get-Command Invoke-AzOperationalInsightsQuery -ErrorAction SilentlyContinue) { return 'Invoke-AzOperationalInsightsQuery' } return '' } function Convert-AppInsightsQueryResults { param([object] $QueryResult) if ($null -eq $QueryResult) { return @() } if ($QueryResult.PSObject.Properties['Results'] -and $QueryResult.Results) { return @($QueryResult.Results) } if ($QueryResult.PSObject.Properties['value'] -and $QueryResult.value) { return @($QueryResult.value) } if ($QueryResult.PSObject.Properties['Tables'] -and $QueryResult.Tables -and $QueryResult.Tables.Count -gt 0) { $table = $QueryResult.Tables[0] $columns = @($table.Columns | ForEach-Object { [string]$_.Name }) $rows = [System.Collections.Generic.List[object]]::new() foreach ($r in @($table.Rows)) { $obj = [ordered]@{} for ($i = 0; $i -lt $columns.Count; $i++) { $obj[$columns[$i]] = $r[$i] } $rows.Add([pscustomobject]$obj) | Out-Null } return @($rows) } return @() } function ConvertTo-DurationSeconds { param([object] $Value) if ($null -eq $Value) { return 0.0 } if ($Value -is [timespan]) { return [math]::Round($Value.TotalSeconds, 3) } if ($Value -is [double] -or $Value -is [single] -or $Value -is [int] -or $Value -is [long]) { return [math]::Round([double]$Value, 3) } $text = [string]$Value if ([string]::IsNullOrWhiteSpace($text)) { return 0.0 } $ts = [timespan]::Zero if ([timespan]::TryParse($text, [ref]$ts)) { return [math]::Round($ts.TotalSeconds, 3) } $seconds = 0.0 if ([double]::TryParse($text, [ref]$seconds)) { return [math]::Round($seconds, 3) } return 0.0 } function Get-AppInsightsToolVersion { $module = Get-Module -ListAvailable -Name Az.ApplicationInsights | Sort-Object Version -Descending | Select-Object -First 1 if ($module -and $module.Version) { return "Az.ApplicationInsights/$($module.Version)" } if (Get-Command az -ErrorAction SilentlyContinue) { try { $cliVersion = az version --query '"azure-cli"' -o tsv 2>$null if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace([string]$cliVersion)) { return "azure-cli/$([string]$cliVersion)" } } catch { return '' } } return '' } function Get-AppInsightsPillar { param([string] $QueryType) switch ($QueryType) { 'exceptions' { return 'Reliability' } default { return 'PerformanceEfficiency' } } } function Get-AppInsightsImpact { param( [string] $QueryType, [int] $Count, [double] $AvgDurationSeconds = 0.0 ) if ($QueryType -eq 'exceptions') { if ($Count -ge 150) { return 'High' } if ($Count -ge 75) { return 'Medium' } return 'Low' } if ($Count -ge 100 -and $AvgDurationSeconds -ge 10) { return 'High' } if ($Count -ge 20 -or $AvgDurationSeconds -ge 5) { return 'Medium' } return 'Low' } function Get-AppInsightsEffort { param( [string] $QueryType, [double] $AvgDurationSeconds = 0.0 ) switch ($QueryType) { 'exceptions' { return 'Low' } 'dependencies' { if ($AvgDurationSeconds -ge 10) { return 'High' } return 'Medium' } 'requests' { if ($AvgDurationSeconds -ge 15) { return 'High' } return 'Medium' } default { return 'Medium' } } } function Get-AppInsightsBaselineTags { param([string] $QueryType) switch ($QueryType) { 'requests' { return @('AppInsights-SlowRequests', 'metric:latency') } 'dependencies' { return @('AppInsights-SlowDependencies', 'metric:dependency-failures') } 'exceptions' { return @('AppInsights-Exceptions', 'metric:exceptions') } default { return @('AppInsights-Unknown', 'metric:telemetry') } } } function Get-AppInsightsScoreDelta { param( [pscustomobject] $Row, [double] $AvgDurationSeconds = 0.0 ) $baseline = $null foreach ($prop in @('BaselineAvgDurationSeconds', 'baseline_avg_duration_seconds', 'baseline_avg_duration', 'avg_duration_baseline')) { if ($Row.PSObject.Properties[$prop] -and $null -ne $Row.$prop) { $candidate = 0.0 if ([double]::TryParse([string]$Row.$prop, [ref]$candidate)) { $baseline = $candidate break } } } if ($null -ne $baseline) { return [math]::Round(($AvgDurationSeconds - [double]$baseline), 3) } if ($AvgDurationSeconds -gt 0) { return [math]::Round($AvgDurationSeconds, 3) } return $null } function Get-AppInsightsEntityRefs { param( [object] $ResourceItem, [string] $ResourceId ) $refs = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace($ResourceId)) { $refs.Add($ResourceId) | Out-Null } if ($ResourceItem -and $ResourceItem.PSObject.Properties['tags'] -and $ResourceItem.tags) { foreach ($entry in $ResourceItem.tags.PSObject.Properties) { $name = [string]$entry.Name if ($name -notmatch '^hidden-link:(/subscriptions/.+)$') { continue } $linkedId = [string]$Matches[1] if ($linkedId -match '(?i)/providers/Microsoft\.Web/sites/') { if (-not [string]::IsNullOrWhiteSpace($linkedId) -and -not ($refs -contains $linkedId)) { $refs.Add($linkedId) | Out-Null } } } } return @($refs) } function Get-AppInsightsPortalOverviewUrl { param([string] $ResourceId) return "https://portal.azure.com/#@/resource$ResourceId/overview" } function Get-AppInsightsDeepLinkUrl { param( [string] $ResourceId, [string] $QueryType, [string] $FilterValue, [int] $TimeRangeHours ) $safeValue = if ($FilterValue) { $FilterValue.Replace("'", "''") } else { '' } $query = switch ($QueryType) { 'requests' { "requests | where timestamp > ago($($TimeRangeHours)h) | where duration > 5s | where name == '$safeValue' | summarize Count=count(), AvgDurationSeconds=todouble(avg(duration)/1s)" } 'dependencies' { "dependencies | where timestamp > ago($($TimeRangeHours)h) | where success == false | where name == '$safeValue' | summarize Count=count() by type" } 'exceptions' { "exceptions | where timestamp > ago($($TimeRangeHours)h) | where problemId == '$safeValue' | summarize Count=count()" } default { "traces | where timestamp > ago($($TimeRangeHours)h)" } } $resourceIdEncoded = [System.Uri]::EscapeDataString($ResourceId) $queryEncoded = [System.Uri]::EscapeDataString($query) return "https://portal.azure.com/#blade/Microsoft_OperationsManagementSuite_Workspace/AnalyticsBlade/resourceId/$resourceIdEncoded/query/$queryEncoded/timespan/PT$($TimeRangeHours)H" } function Invoke-AppInsightsQuery { param( [Parameter(Mandatory)] [string] $CommandName, [Parameter(Mandatory)] [pscustomobject] $Resource, [Parameter(Mandatory)] [string] $QueryText, [Parameter(Mandatory)] [timespan] $QueryTimeSpan ) return Invoke-WithRetry -MaxAttempts 4 -InitialDelaySeconds 2 -MaxDelaySeconds 30 -ScriptBlock { Invoke-WithTimeout -TimeoutSec 300 -ScriptBlock { if ($CommandName -eq 'Invoke-AzApplicationInsightsQuery') { $cmd = Get-Command Invoke-AzApplicationInsightsQuery -ErrorAction Stop $splat = @{ Query = $QueryText ErrorAction = 'Stop' } if ($cmd.Parameters.ContainsKey('AppInsightsName')) { $splat['AppInsightsName'] = $Resource.Name } elseif ($cmd.Parameters.ContainsKey('ApplicationInsightsName')) { $splat['ApplicationInsightsName'] = $Resource.Name } elseif ($cmd.Parameters.ContainsKey('Name')) { $splat['Name'] = $Resource.Name } if ($cmd.Parameters.ContainsKey('ResourceGroupName')) { $splat['ResourceGroupName'] = $Resource.ResourceGroup } if ($cmd.Parameters.ContainsKey('TimeSpan')) { $splat['TimeSpan'] = $QueryTimeSpan } elseif ($cmd.Parameters.ContainsKey('Timespan')) { $splat['Timespan'] = $QueryTimeSpan } return Invoke-AzApplicationInsightsQuery @splat } if ([string]::IsNullOrWhiteSpace([string]$Resource.WorkspaceId)) { throw (Format-FindingErrorMessage (New-FindingError ` -Source 'wrapper:appinsights' ` -Category 'ConfigurationError' ` -Reason "Resource '$($Resource.Name)' is missing WorkspaceId for Invoke-AzOperationalInsightsQuery fallback." ` -Remediation 'Provide a Log Analytics WorkspaceId on the resource or skip the fallback path.')) } return Invoke-AzOperationalInsightsQuery -WorkspaceId $Resource.WorkspaceId -Query $QueryText -Timespan $QueryTimeSpan -ErrorAction Stop } } } function Add-AppInsightsFinding { param( [System.Collections.Generic.List[object]] $Collection, [string] $Id, [string] $Severity, [bool] $Compliant, [string] $Title, [string] $Detail, [string] $ResourceId, [string] $QueryType, [hashtable] $Extras ) $row = [ordered]@{ Id = $Id Source = 'appinsights' Category = 'Performance' Severity = $Severity Compliant = $Compliant Title = $Title Detail = $Detail Remediation = 'Investigate telemetry trends, correlate with recent changes, and tune performance budgets and alerts.' ResourceId = $ResourceId LearnMoreUrl = (Get-AppInsightsPortalOverviewUrl -ResourceId $ResourceId) QueryType = $QueryType } if ($Extras) { foreach ($key in $Extras.Keys) { $row[$key] = $Extras[$key] } } $Collection.Add([pscustomobject]$row) | Out-Null } try { $commandName = Get-QueryCommandName $toolVersion = Get-AppInsightsToolVersion if (-not $commandName) { $result.Status = 'Skipped' $result.Message = 'No App Insights query cmdlet available. Install Az.ApplicationInsights or Az.Monitor.' return [PSCustomObject]$result } $apiVersion = '2020-02-02' $resourceUri = if ($ResourceGroup -and $AppInsightsName) { "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$([System.Uri]::EscapeDataString($ResourceGroup))/providers/Microsoft.Insights/components/$([System.Uri]::EscapeDataString($AppInsightsName))?api-version=$apiVersion" } elseif ($ResourceGroup) { "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$([System.Uri]::EscapeDataString($ResourceGroup))/providers/Microsoft.Insights/components?api-version=$apiVersion" } else { "https://management.azure.com/subscriptions/$SubscriptionId/providers/Microsoft.Insights/components?api-version=$apiVersion" } $discoveryResponse = Invoke-WithRetry -MaxAttempts 4 -InitialDelaySeconds 2 -MaxDelaySeconds 30 -ScriptBlock { Invoke-AzRestMethod -Method GET -Uri $resourceUri -ErrorAction Stop } if (-not $discoveryResponse -or $discoveryResponse.StatusCode -ge 400) { $statusCode = if ($discoveryResponse) { $discoveryResponse.StatusCode } else { 'null' } $content = if ($discoveryResponse) { [string]$discoveryResponse.Content } else { 'No response' } throw (Format-FindingErrorMessage (New-FindingError ` -Source 'wrapper:appinsights' ` -Category 'TransientFailure' ` -Reason "App Insights discovery failed (HTTP ${statusCode})." ` -Remediation 'Verify Reader access to the App Insights component and retry.' ` -Details (Remove-Credentials -Text $content))) } $payload = $discoveryResponse.Content | ConvertFrom-Json -Depth 30 $resourceItems = if ($payload.PSObject.Properties['value']) { @($payload.value) } else { @($payload) } if ($AppInsightsName -and -not $ResourceGroup) { $resourceItems = @($resourceItems | Where-Object { ([string]$_.name) -ieq $AppInsightsName }) } if ($resourceItems.Count -eq 0) { $result.Status = 'Skipped' $result.Message = 'No Application Insights resources found in the requested scope.' return [pscustomobject]$result } $querySpan = [timespan]::FromHours($TimeRangeHours) $slowRequestQuery = "requests | where timestamp > ago($($TimeRangeHours)h) | where duration > 5s | summarize count(), avg(duration) by name | where count_ > 10" $dependencyFailureQuery = "dependencies | where timestamp > ago($($TimeRangeHours)h) | where success == false | summarize count() by name, type | where count_ > 5" $exceptionRateQuery = "exceptions | where timestamp > ago($($TimeRangeHours)h) | summarize count() by problemId | where count_ > 50" $findings = [System.Collections.Generic.List[object]]::new() foreach ($item in $resourceItems) { $resourceId = [string]$item.id $name = [string]$item.name if (-not $resourceId -or -not $name) { continue } $rg = if ($resourceId -match '/resourceGroups/([^/]+)') { $Matches[1] } else { '' } $workspaceId = '' if ($item.PSObject.Properties['properties'] -and $item.properties) { if ($item.properties.PSObject.Properties['WorkspaceResourceId']) { $workspaceId = [string]$item.properties.WorkspaceResourceId } elseif ($item.properties.PSObject.Properties['workspaceResourceId']) { $workspaceId = [string]$item.properties.workspaceResourceId } } $resourceRef = [pscustomobject]@{ Name = $name ResourceGroup = $rg ResourceId = $resourceId WorkspaceId = $workspaceId } $slowRows = @(Convert-AppInsightsQueryResults (Invoke-AppInsightsQuery -CommandName $commandName -Resource $resourceRef -QueryText $slowRequestQuery -QueryTimeSpan $querySpan)) foreach ($row in $slowRows) { $requestName = [string]$row.name if ([string]::IsNullOrWhiteSpace($requestName)) { $requestName = 'unknown-request' } $count = 0 if ($row.PSObject.Properties['count_'] -and $row.count_ -ne $null) { $count = [int]$row.count_ } if ($count -le 10) { continue } $avgSeconds = ConvertTo-DurationSeconds -Value $(if ($row.PSObject.Properties['avg_duration']) { $row.avg_duration } elseif ($row.PSObject.Properties['avg_duration_']) { $row.avg_duration_ } elseif ($row.PSObject.Properties['average_duration']) { $row.average_duration } else { $null }) $severity = if ($avgSeconds -gt 30) { 'High' } else { 'Medium' } Add-AppInsightsFinding -Collection $findings ` -Id "appinsights/$name/requests/$([System.Uri]::EscapeDataString($requestName))" ` -Severity $severity -Compliant $false ` -Title "Slow request signal for '$requestName'" ` -Detail "Request '$requestName' averaged $avgSeconds second(s) across $count call(s) in the last $TimeRangeHours hour(s)." ` -ResourceId $resourceId -QueryType 'requests' ` -Extras @{ RequestName = $requestName Count = $count AvgDurationSeconds = $avgSeconds TimeRangeHours = $TimeRangeHours Pillar = (Get-AppInsightsPillar -QueryType 'requests') Impact = (Get-AppInsightsImpact -QueryType 'requests' -Count $count -AvgDurationSeconds $avgSeconds) Effort = (Get-AppInsightsEffort -QueryType 'requests' -AvgDurationSeconds $avgSeconds) DeepLinkUrl = (Get-AppInsightsDeepLinkUrl -ResourceId $resourceId -QueryType 'requests' -FilterValue $requestName -TimeRangeHours $TimeRangeHours) BaselineTags = @(Get-AppInsightsBaselineTags -QueryType 'requests') ScoreDelta = (Get-AppInsightsScoreDelta -Row $row -AvgDurationSeconds $avgSeconds) EntityRefs = @(Get-AppInsightsEntityRefs -ResourceItem $item -ResourceId $resourceId) ToolVersion = $toolVersion } $latestFinding = $findings[$findings.Count - 1] $latestEvidenceUris = @($latestFinding.LearnMoreUrl, $latestFinding.DeepLinkUrl | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -Unique) $latestFinding | Add-Member -NotePropertyName EvidenceUris -NotePropertyValue $latestEvidenceUris -Force } $dependencyRows = @(Convert-AppInsightsQueryResults (Invoke-AppInsightsQuery -CommandName $commandName -Resource $resourceRef -QueryText $dependencyFailureQuery -QueryTimeSpan $querySpan)) foreach ($row in $dependencyRows) { $depName = [string]$row.name if ([string]::IsNullOrWhiteSpace($depName)) { $depName = 'unknown-dependency' } $depType = [string]$row.type if ([string]::IsNullOrWhiteSpace($depType)) { $depType = 'unknown' } $count = 0 if ($row.PSObject.Properties['count_'] -and $row.count_ -ne $null) { $count = [int]$row.count_ } if ($count -le 5) { continue } Add-AppInsightsFinding -Collection $findings ` -Id "appinsights/$name/dependencies/$([System.Uri]::EscapeDataString($depName))/$([System.Uri]::EscapeDataString($depType))" ` -Severity 'Medium' -Compliant $false ` -Title "Dependency failure signal for '$depName'" ` -Detail "Dependency '$depName' ($depType) failed $count time(s) in the last $TimeRangeHours hour(s)." ` -ResourceId $resourceId -QueryType 'dependencies' ` -Extras @{ DependencyName = $depName DependencyType = $depType Count = $count TimeRangeHours = $TimeRangeHours Pillar = (Get-AppInsightsPillar -QueryType 'dependencies') Impact = (Get-AppInsightsImpact -QueryType 'dependencies' -Count $count -AvgDurationSeconds 0) Effort = (Get-AppInsightsEffort -QueryType 'dependencies' -AvgDurationSeconds 0) DeepLinkUrl = (Get-AppInsightsDeepLinkUrl -ResourceId $resourceId -QueryType 'dependencies' -FilterValue $depName -TimeRangeHours $TimeRangeHours) BaselineTags = @(Get-AppInsightsBaselineTags -QueryType 'dependencies') ScoreDelta = $null EntityRefs = @(Get-AppInsightsEntityRefs -ResourceItem $item -ResourceId $resourceId) ToolVersion = $toolVersion } $latestFinding = $findings[$findings.Count - 1] $latestEvidenceUris = @($latestFinding.LearnMoreUrl, $latestFinding.DeepLinkUrl | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -Unique) $latestFinding | Add-Member -NotePropertyName EvidenceUris -NotePropertyValue $latestEvidenceUris -Force } $exceptionRows = @(Convert-AppInsightsQueryResults (Invoke-AppInsightsQuery -CommandName $commandName -Resource $resourceRef -QueryText $exceptionRateQuery -QueryTimeSpan $querySpan)) foreach ($row in $exceptionRows) { $problemId = [string]$row.problemId if ([string]::IsNullOrWhiteSpace($problemId)) { $problemId = 'unknown-problem' } $count = 0 if ($row.PSObject.Properties['count_'] -and $row.count_ -ne $null) { $count = [int]$row.count_ } if ($count -le 50) { continue } Add-AppInsightsFinding -Collection $findings ` -Id "appinsights/$name/exceptions/$([System.Uri]::EscapeDataString($problemId))" ` -Severity 'High' -Compliant $false ` -Title "Exception cluster signal for '$problemId'" ` -Detail "Exception problemId '$problemId' occurred $count time(s) in the last $TimeRangeHours hour(s)." ` -ResourceId $resourceId -QueryType 'exceptions' ` -Extras @{ ProblemId = $problemId Count = $count TimeRangeHours = $TimeRangeHours Pillar = (Get-AppInsightsPillar -QueryType 'exceptions') Impact = (Get-AppInsightsImpact -QueryType 'exceptions' -Count $count -AvgDurationSeconds 0) Effort = (Get-AppInsightsEffort -QueryType 'exceptions' -AvgDurationSeconds 0) DeepLinkUrl = (Get-AppInsightsDeepLinkUrl -ResourceId $resourceId -QueryType 'exceptions' -FilterValue $problemId -TimeRangeHours $TimeRangeHours) BaselineTags = @(Get-AppInsightsBaselineTags -QueryType 'exceptions') ScoreDelta = $null EntityRefs = @(Get-AppInsightsEntityRefs -ResourceItem $item -ResourceId $resourceId) ToolVersion = $toolVersion } $latestFinding = $findings[$findings.Count - 1] $latestEvidenceUris = @($latestFinding.LearnMoreUrl, $latestFinding.DeepLinkUrl | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Select-Object -Unique) $latestFinding | Add-Member -NotePropertyName EvidenceUris -NotePropertyValue $latestEvidenceUris -Force } } $result.Findings = @($findings) $result.Message = "Scanned Application Insights telemetry for the last $TimeRangeHours hour(s); emitted $($findings.Count) finding(s)." } catch { $result.Status = 'Failed' $result.Message = "Application Insights query failed: $(Remove-Credentials -Text ([string]$_.Exception.Message))" return [pscustomobject]$result } if ($OutputPath) { try { if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } $raw = Join-Path $OutputPath "appinsights-$SubscriptionId-$(Get-Date -Format yyyyMMddHHmmss).json" Set-Content -Path $raw -Value (Remove-Credentials ($result | ConvertTo-Json -Depth 30)) -Encoding utf8 } catch { Write-Warning "Failed to write App Insights raw JSON: $(Remove-Credentials -Text ([string]$_.Exception.Message))" } } return [pscustomobject]$result |