Skip to content

monthly-cost-change-percentage returns inf when the prior month is zero #2323

Description

🐛 Problem

src/queries/catalog/monthly-cost-change-percentage.kql guards its division with isempty(PreviousBilledCost).

isempty() is true for the null that prev() returns on the first row, but it is false for 0.0. A month whose prior month summed to exactly zero therefore falls through the guard, and KQL evaluates x / 0.0 as inf rather than raising an error — so an infinity is returned to the caller as a percentage.

| project ChargePeriodStart,
    BilledCostChangePct = iff(isempty(PreviousBilledCost), 0.0, toreal((BilledCost - PreviousBilledCost) * 100.0 / PreviousBilledCost)),
    EffectiveCostChangePct = iff(isempty(PreviousEffectiveCost), 0.0, toreal((EffectiveCost - PreviousEffectiveCost) * 100.0 / PreviousEffectiveCost))

👣 Repro steps

No hub required — the shape of the pipeline is enough:

datatable(ChargePeriodStart:datetime, BilledCost:real, EffectiveCost:real)
[
    datetime(2025-01-01), 100.0, 100.0,
    datetime(2025-02-01),   0.0,  80.0,
    datetime(2025-03-01),  50.0, 120.0,
]
| order by ChargePeriodStart asc
| extend PreviousBilledCost = prev(BilledCost)
| project ChargePeriodStart,
    BilledCostChangePct = iff(isempty(PreviousBilledCost), 0.0, toreal((BilledCost - PreviousBilledCost) * 100.0 / PreviousBilledCost))
ChargePeriodStart BilledCostChangePct
2025-01-01 0
2025-02-01 -100
2025-03-01 inf

Is a zero month reachable?

Yes, and BilledCost is the likelier of the two to hit it. On usage rows covered by a commitment discount BilledCost is zero — the charge was billed at purchase time, and the amortized amount lands in EffectiveCost instead. Any scope whose month is entirely commitment-covered therefore sums BilledCost to exactly zero, and the following month's BilledCostChangePct comes back as inf.

Scoping the query with an extra | where is a normal way to use it, which makes this easier to reach than the estate-wide default suggests.

🤔 Expected

The same value the guard already returns for the undefined case — 0.0 — rather than an infinity.

The == 0.0 form is already the convention elsewhere in the catalog, in service-price-benchmarking.kql:

x_CommitmentDiscountPercent = iif(ContractedCost == 0.0, 0.0, toreal(ContractedCost - EffectiveCost) * 100.0 / toreal(ContractedCost)),

🔧 Environment

  • Affected file: src/queries/catalog/monthly-cost-change-percentage.kql on dev
  • FinOps hub version: any — reproducible standalone with datatable, no ingestion involved
  • Billing account type: any
  • Cost Management export: any

ℹ️ Additional context

A separate, arguable point that I have deliberately left out of the fix: returning 0.0 when there is no prior month conflates "no change" with "no data to compare against". real(null) would distinguish them, but that is a behaviour change for existing consumers, so the PR keeps the current convention and only closes the divide-by-zero hole. Happy to follow up if you would prefer the stricter version.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Micro PR 🔬Very small PR that should be especially easy for newcomersSkill: KQLKQL queries and Data Explorer integrationStatus: 🔬 Code reviewIssue is currently in code review with open PRsTool: FinOps hubsData pipeline solutionType: Bug 🐛Something isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions