Skip to content

Instantly share code, notes, and snippets.

@heoelri
Last active August 27, 2026 11:48
Show Gist options
  • Select an option

  • Save heoelri/cd867b03ddc102ad38a7b9c58429e385 to your computer and use it in GitHub Desktop.

Select an option

Save heoelri/cd867b03ddc102ad38a7b9c58429e385 to your computer and use it in GitHub Desktop.
Get-HealthModelMonthlyCost.ps1
<#
.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