Private/Invoke-AACCostQuery.ps1
|
function Invoke-AACCostQuery { <# .SYNOPSIS Runs a Cost Management query for one subscription and returns its rows as objects with one property per column. .DESCRIPTION POST https://management.azure.com/subscriptions/<id>/providers/ Microsoft.CostManagement/query (api-version 2023-11-01), following nextLink paging. The response's column/row arrays become objects, so a query grouped by ServiceName returns rows like @{ Cost = 12.3; ServiceName = 'Storage'; Currency = 'USD' }. Cost Management allows only a few queries per minute per scope, so a throttled (429) or transient (5xx) response is retried up to five times, waiting as long as the response's x-ms-ratelimit-microsoft.costmanagement-*-retry-after or Retry-After header asks (5-60 seconds otherwise). Any other error is thrown with Azure's own message. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string] $SubscriptionId, [Parameter(Mandatory)] [hashtable] $Body ) # No web-request progress bar over the caller's Spectre display. $ProgressPreference = 'SilentlyContinue' $uri = "https://management.azure.com/subscriptions/$SubscriptionId/providers/Microsoft.CostManagement/query?api-version=2023-11-01" $json = $Body | ConvertTo-Json -Depth 10 while ($uri) { $response = $null for ($attempt = 1; ; $attempt++) { try { $headers = @{ Authorization = "Bearer $(Get-AACAccessToken)" } $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -ContentType 'application/json' -Body $json -ErrorAction Stop -Verbose:$false break } catch { $httpResponse = Get-AACPropertyValue -InputObject $_.Exception -Name 'Response' $status = if ($httpResponse) { [int]$httpResponse.StatusCode } else { 0 } if ($attempt -ge 5 -or ($status -ne 429 -and $status -lt 500)) { $message = if ($_.ErrorDetails.Message) { $details = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Ignore $inner = if ($details) { Get-AACPropertyValue -InputObject $details -Name 'error' } if ($inner) { Get-AACPropertyValue -InputObject $inner -Name 'message' } else { $_.ErrorDetails.Message } } else { $_.Exception.Message } throw $message } # The longest wait any rate-limit header asks for. $wait = 0 if ($httpResponse) { foreach ($header in $httpResponse.Headers) { if ($header.Key -match 'retry-after$') { foreach ($value in $header.Value) { $seconds = 0 if ([int]::TryParse($value, [ref]$seconds)) { $wait = [Math]::Max($wait, $seconds) } } } } } if ($wait -le 0) { $wait = 5 * $attempt } Start-Sleep -Seconds ([Math]::Min(60, $wait)) } } $properties = Get-AACPropertyValue -InputObject $response -Name 'properties' $columns = @(Get-AACPropertyValue -InputObject $properties -Name 'columns' | ForEach-Object { $_.name }) foreach ($row in @(Get-AACPropertyValue -InputObject $properties -Name 'rows')) { $item = [ordered]@{} for ($i = 0; $i -lt $columns.Count; $i++) { $item[$columns[$i]] = $row[$i] } [pscustomobject]$item } $uri = Get-AACPropertyValue -InputObject $properties -Name 'nextLink' } } |