Last active
August 27, 2026 11:48
-
-
Save heoelri/cd867b03ddc102ad38a7b9c58429e385 to your computer and use it in GitHub Desktop.
Get-HealthModelMonthlyCost.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Estimates the monthly signal cost for one or more Microsoft.CloudHealth health models. | |
| .DESCRIPTION | |
| Uses Azure CLI ARM requests to retrieve the health model, its entities, and its | |
| signal definitions. Signal instance overrides and referenced signal definition | |
| defaults are resolved before applying the following monthly list-price estimates: | |
| - Metric signal: $0.30 | |
| - Metric signal with a dynamic threshold: $0.40 | |
| - Prometheus signal: $0.30 | |
| - Log signal evaluated every 1 minute: $3.00 | |
| - Log signal evaluated every 5 minutes: $1.50 | |
| - Log signal evaluated every 10 minutes: $1.00 | |
| - Log signal evaluated every 15 minutes or slower: $0.50 | |
| The result is an estimate, not a bill or pricing quote. Actual charges can vary | |
| because of negotiated agreements, discounts, currency conversion, taxes, price | |
| changes, and other billing factors. Confirm current pricing in your Azure agreement. | |
| Requires Azure CLI and an authenticated account with read access to the health models. | |
| .PARAMETER HealthModelResourceId | |
| One or more full ARM resource IDs of Microsoft.CloudHealth/healthModels resources. | |
| .PARAMETER ApiVersion | |
| The Microsoft.CloudHealth ARM API version. Defaults to 2026-05-01-preview. | |
| .PARAMETER SelfTest | |
| Runs the built-in pricing checks without calling Azure. | |
| .EXAMPLE | |
| .\Get-HealthModelMonthlyCost.ps1 -HealthModelResourceId "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-rg/providers/Microsoft.CloudHealth/healthModels/example-model" | |
| .EXAMPLE | |
| .\Get-HealthModelMonthlyCost.ps1 -HealthModelResourceId $modelId1, $modelId2 | |
| .EXAMPLE | |
| .\Get-HealthModelMonthlyCost.ps1 -SelfTest | |
| .NOTES | |
| External, dependency, and Azure Resource Health signals are not | |
| included because no prices for them are defined by this script. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [ValidateNotNullOrEmpty()] | |
| [string[]]$HealthModelResourceId, | |
| [string]$ApiVersion = "2026-05-01-preview", | |
| [switch]$SelfTest | |
| ) | |
| $ErrorActionPreference = "Stop" | |
| function Get-MonthlySignalPrice { | |
| param( | |
| [Parameter(Mandatory)] | |
| [string]$SignalKind, | |
| [bool]$UsesDynamicThreshold, | |
| [string]$RefreshInterval = "PT1M" | |
| ) | |
| if ($SignalKind -eq "AzureResourceMetric") { | |
| return [decimal]$(if ($UsesDynamicThreshold) { 0.40 } else { 0.30 }) | |
| } | |
| if ($SignalKind -eq "PrometheusMetricsQuery") { | |
| return [decimal]0.30 | |
| } | |
| if ($SignalKind -eq "LogAnalyticsQuery") { | |
| return [decimal]$(switch ($RefreshInterval) { | |
| "PT1M" { 3.00 } | |
| "PT5M" { 1.50 } | |
| "PT10M" { 1.00 } | |
| "PT15M" { 0.50 } | |
| "PT30M" { 0.50 } | |
| "PT1H" { 0.50 } | |
| "PT2H" { 0.50 } | |
| default { throw "No log signal price is defined for refresh interval '$RefreshInterval'." } | |
| }) | |
| } | |
| return [decimal]0 | |
| } | |
| if ($SelfTest) { | |
| $checks = @( | |
| ((Get-MonthlySignalPrice "AzureResourceMetric" $false) -eq [decimal]0.30) | |
| ((Get-MonthlySignalPrice "AzureResourceMetric" $true) -eq [decimal]0.40) | |
| ((Get-MonthlySignalPrice "PrometheusMetricsQuery" $false) -eq [decimal]0.30) | |
| ((Get-MonthlySignalPrice "LogAnalyticsQuery" $false "PT1M") -eq [decimal]3.00) | |
| ((Get-MonthlySignalPrice "LogAnalyticsQuery" $false "PT5M") -eq [decimal]1.50) | |
| ((Get-MonthlySignalPrice "LogAnalyticsQuery" $false "PT10M") -eq [decimal]1.00) | |
| ((Get-MonthlySignalPrice "LogAnalyticsQuery" $false "PT15M") -eq [decimal]0.50) | |
| ((Get-MonthlySignalPrice "LogAnalyticsQuery" $false "PT30M") -eq [decimal]0.50) | |
| ) | |
| if ($checks -contains $false) { | |
| throw "Pricing self-test failed." | |
| } | |
| Write-Host "Pricing self-test passed." | |
| return | |
| } | |
| if (!$HealthModelResourceId) { | |
| throw "HealthModelResourceId is required. Pass one or more ARM resource IDs or use -SelfTest." | |
| } | |
| function Invoke-ArmGet { | |
| param([Parameter(Mandatory)][string]$Url) | |
| $json = az rest --method get --url $Url --output json | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Azure CLI request failed: $Url" | |
| } | |
| return $json | ConvertFrom-Json | |
| } | |
| function Get-ArmCollection { | |
| param([Parameter(Mandatory)][string]$Url) | |
| $items = @() | |
| while ($Url) { | |
| $page = Invoke-ArmGet $Url | |
| $items += @($page.value) | |
| $Url = $page.nextLink | |
| } | |
| return $items | |
| } | |
| function Get-HealthModelEstimate { | |
| param([Parameter(Mandatory)][string]$ResourceId) | |
| $armUrl = "https://management.azure.com$($ResourceId.TrimEnd('/'))" | |
| $healthModel = Invoke-ArmGet "$armUrl`?api-version=$ApiVersion" | |
| $definitions = Get-ArmCollection "$armUrl/signaldefinitions?api-version=$ApiVersion" | |
| $entities = Get-ArmCollection "$armUrl/entities?api-version=$ApiVersion" | |
| $definitionsByName = @{} | |
| foreach ($definition in $definitions) { | |
| $definitionsByName[$definition.name] = $definition.properties | |
| } | |
| $totals = [ordered]@{ | |
| "Metric" = @{ Count = 0; UnitCost = [decimal]0.30; Cost = [decimal]0 } | |
| "Metric (dynamic thresholds)" = @{ Count = 0; UnitCost = [decimal]0.40; Cost = [decimal]0 } | |
| "Prometheus" = @{ Count = 0; UnitCost = [decimal]0.30; Cost = [decimal]0 } | |
| "Log (1 minute)" = @{ Count = 0; UnitCost = [decimal]3.00; Cost = [decimal]0 } | |
| "Log (5 minutes)" = @{ Count = 0; UnitCost = [decimal]1.50; Cost = [decimal]0 } | |
| "Log (10 minutes)" = @{ Count = 0; UnitCost = [decimal]1.00; Cost = [decimal]0 } | |
| "Log (15+ minutes)" = @{ Count = 0; UnitCost = [decimal]0.50; Cost = [decimal]0 } | |
| } | |
| foreach ($entity in $entities) { | |
| $signals = @($entity.properties.signalGroups.azureResource.signals) + | |
| @($entity.properties.signalGroups.azureLogAnalytics.signals) + | |
| @($entity.properties.signalGroups.prometheus.signals) | | |
| Where-Object { $null -ne $_ } | |
| foreach ($signal in $signals) { | |
| $definition = $null | |
| if ($signal.signalDefinitionName) { | |
| if (!$definitionsByName.ContainsKey($signal.signalDefinitionName)) { | |
| throw "Signal '$($signal.name)' references missing definition '$($signal.signalDefinitionName)'." | |
| } | |
| $definition = $definitionsByName[$signal.signalDefinitionName] | |
| } | |
| $rules = if ($signal.evaluationRules) { $signal.evaluationRules } else { $definition.evaluationRules } | |
| $isDynamic = $rules.degradedRule.operator -eq "Dynamic" -or | |
| $rules.unhealthyRule.operator -eq "Dynamic" | |
| $interval = if ($signal.refreshInterval) { | |
| $signal.refreshInterval | |
| } | |
| elseif ($definition.refreshInterval) { | |
| $definition.refreshInterval | |
| } | |
| else { | |
| "PT1M" | |
| } | |
| $price = Get-MonthlySignalPrice $signal.signalKind $isDynamic $interval | |
| if ($price -eq 0) { | |
| continue | |
| } | |
| $category = if ($signal.signalKind -eq "AzureResourceMetric") { | |
| if ($isDynamic) { "Metric (dynamic thresholds)" } else { "Metric" } | |
| } | |
| elseif ($signal.signalKind -eq "PrometheusMetricsQuery") { | |
| "Prometheus" | |
| } | |
| else { | |
| "Log ($(@{ | |
| "PT1M" = "1 minute" | |
| "PT5M" = "5 minutes" | |
| "PT10M" = "10 minutes" | |
| "PT15M" = "15+ minutes" | |
| "PT30M" = "15+ minutes" | |
| "PT1H" = "15+ minutes" | |
| "PT2H" = "15+ minutes" | |
| }[$interval]))" | |
| } | |
| $totals[$category].Count++ | |
| $totals[$category].Cost += $price | |
| } | |
| } | |
| $rows = foreach ($category in $totals.Keys) { | |
| [pscustomobject]@{ | |
| Category = $category | |
| Count = $totals[$category].Count | |
| UnitCostUsd = $totals[$category].UnitCost | |
| MonthlyCostUsd = $totals[$category].Cost | |
| } | |
| } | |
| Write-Host "`nHealth model: $($healthModel.name)" | |
| $rows | Format-Table -AutoSize | Out-Host | |
| $modelTotal = ($rows.MonthlyCostUsd | Measure-Object -Sum).Sum | |
| Write-Host "Estimated monthly cost: `$$($modelTotal.ToString("N2", [System.Globalization.CultureInfo]::InvariantCulture)) USD" | |
| return $modelTotal | |
| } | |
| Write-Warning "Estimate only. Actual charges may vary due to discounts, agreements, taxes, currency, price changes, and other billing factors." | |
| $grandTotal = [decimal]0 | |
| foreach ($resourceId in $HealthModelResourceId) { | |
| $grandTotal += Get-HealthModelEstimate $resourceId | |
| } | |
| if ($HealthModelResourceId.Count -gt 1) { | |
| Write-Host "`nEstimated total for $($HealthModelResourceId.Count) health models: `$$($grandTotal.ToString("N2", [System.Globalization.CultureInfo]::InvariantCulture)) USD" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment