FinOpsToolkitExt.BenefitRecommendations.psm1
|
function Get-BenefitRecommendation { <# .SYNOPSIS Queries the Azure Cost Management Benefit Recommendations API. .DESCRIPTION Retrieves savings plan purchase recommendations for a billing account. Returns the raw API response value array as-is. .PARAMETER BillingAccountId The billing account ID (e.g., "7249999" or the full GUID format). .PARAMETER LookBackPeriod The look-back period for usage analysis. Default: Last60Days. .PARAMETER Term The commitment term. Default: P3Y. .PARAMETER Scope Recommendation scope. Default: Shared. .PARAMETER ExpandUsage Include hourly usage data in the response. .PARAMETER ExpandAllRecommendationDetails Include all recommendation detail tiers in the response. .EXAMPLE Get-BenefitRecommendation -BillingAccountId "7249999" -LookBackPeriod Last30Days -Term P3Y -ExpandUsage -ExpandAllRecommendationDetails #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BillingAccountId, [ValidateSet('Last7Days', 'Last30Days', 'Last60Days')] [string]$LookBackPeriod = 'Last60Days', [ValidateSet('P1Y', 'P3Y')] [string]$Term = 'P3Y', [ValidateSet('Single', 'Shared')] [string]$Scope = 'Shared', [switch]$ExpandUsage, [switch]$ExpandAllRecommendationDetails ) $token = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token $billingScope = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountId" $baseUri = "https://management.azure.com$billingScope/providers/Microsoft.CostManagement/benefitRecommendations" $filter = "properties/lookBackPeriod eq '$LookBackPeriod' AND properties/term eq '$Term' AND properties/scope eq '$Scope'" $expandParts = @() if ($ExpandUsage) { $expandParts += 'properties/usage' } if ($ExpandAllRecommendationDetails) { $expandParts += 'properties/allRecommendationDetails' } $encodedFilter = [Uri]::EscapeDataString($filter) $uri = "${baseUri}?api-version=2025-03-01&`$filter=${encodedFilter}" if ($expandParts.Count -gt 0) { $encodedExpand = [Uri]::EscapeDataString($expandParts -join ',') $uri += "&`$expand=${encodedExpand}" } Write-Verbose "Calling: $uri" $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $token" 'Content-Type' = 'application/json' } -Method Get $count = ($response.value | Measure-Object).Count Write-Host "Retrieved $count benefit recommendation(s) for billing account $BillingAccountId" -ForegroundColor Green return $response.value } function Publish-BenefitRecommendationToKusto { <# .SYNOPSIS Ingests benefit recommendations into an Azure Data Explorer table. .DESCRIPTION Takes benefit recommendation objects (from Get-BenefitRecommendation) and ingests them into the SavingsPlanRecommendations_raw table as a single row containing the full array in the Recommendation column. .PARAMETER Recommendations The recommendation array from Get-BenefitRecommendation. Accepts pipeline input. .PARAMETER ClusterUri The ADX cluster URI (e.g., "https://str-finops-hub.israelcentral.kusto.windows.net"). .PARAMETER Database The database name. Default: Hub. .PARAMETER Table The target table name. Default: SavingsPlanRecommendations_raw. .EXAMPLE Get-BenefitRecommendation -BillingAccountId "7249999" | Publish-BenefitRecommendationToKusto -ClusterUri "https://str-finops-hub.israelcentral.kusto.windows.net" .EXAMPLE $recs = Get-BenefitRecommendation -BillingAccountId "7249999" Publish-BenefitRecommendationToKusto -Recommendations $recs -ClusterUri "https://str-finops-hub.israelcentral.kusto.windows.net" #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [object]$Recommendations, [Parameter(Mandatory)] [string]$ClusterUri, [string]$Database = 'Hub', [string]$Table = 'SavingsPlanRecommendations_raw' ) begin { $collected = [System.Collections.Generic.List[object]]::new() } process { if ($Recommendations -is [System.Collections.IEnumerable] -and $Recommendations -isnot [string]) { foreach ($item in $Recommendations) { $collected.Add($item) } } else { $collected.Add($Recommendations) } } end { if ($collected.Count -eq 0) { Write-Warning "No recommendations to publish." return } $token = (Get-AzAccessToken -ResourceUrl $ClusterUri).Token # Build the JSON payload - single row with the full array as the Recommendation column $jsonArray = $collected | ConvertTo-Json -Depth 100 -Compress # Wrap in a single-row array for ingestion: [{"Recommendation": [...]}] $payload = "[{`"Recommendation`":$jsonArray}]" $ingestUri = "$($ClusterUri.TrimEnd('/'))/v1/rest/ingest/$Database/$($Table)?streamFormat=JSON" Write-Verbose "Ingesting $($collected.Count) recommendation(s) into $Table via streaming ingestion" Write-Verbose "URI: $ingestUri" $response = Invoke-RestMethod -Uri $ingestUri -Method Post -Headers @{ Authorization = "Bearer $token" 'Content-Type' = 'application/json; charset=utf-8' } -Body ([System.Text.Encoding]::UTF8.GetBytes($payload)) Write-Host "Published $($collected.Count) recommendation(s) to $Database.$Table" -ForegroundColor Green return $response } } |