Skip to content

fix(azure): derive covered monthly cost instead of hardcoding 0 (GUI showed on-demand) - #1105

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/azure-covered-monthly-cost
Jun 8, 2026
Merged

fix(azure): derive covered monthly cost instead of hardcoding 0 (GUI showed on-demand)#1105
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/azure-covered-monthly-cost

Conversation

@cristim

@cristim cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member

Root cause

providers/azure/internal/recommendations/converter.go hardcoded
out.RecurringMonthlyCost = float64Ptr(0) in both extraction paths
(extractLegacy, extractModern). RecurringMonthlyCost is the field the
frontend renders as the covered/effective spend (what you pay WITH the
reservation). With it pinned to 0, the GUI fell back to displaying
OnDemandCost (CostWithNoReservedInstances, the spend WITHOUT any
reservation), so Azure reservation recommendations showed the on-demand spend
instead of the committed cost. The covered total was already in the payload as
TotalCostWithReservedInstances but was never propagated.

Fix

Derive RecurringMonthlyCost from the real data via a shared
deriveCoveredMonthlyCost helper, used identically by both paths:

  • Prefer the provider-reported TotalCostWithReservedInstances (authoritative
    covered cost).
  • If absent, reconstruct as OnDemandCost - NetSavings (covered = on-demand
    minus net savings).
  • If both sources are absent, return nil (renders as data-not-available)
    and log a warning. Never fabricate a 0 (which would falsely claim a free
    recurring charge).

amountValuePtr unwraps Modern's *Amount to a *float64 while preserving
the absent-field nil signal, so the Modern path feeds the same logic as
Legacy's bare *float64 inputs. OnDemandCost / EstimatedSavings extraction
is unchanged. The ExpandPaymentVariants all-upfront/monthly cashflow split is
untouched (separate, intentional mechanism). The figure is treated as a
lookback approximately monthly run-rate, consistent with the other cost fields.

Verification

  • New regression tests in converter_test.go assert the covered cost (not 0)
    for both Legacy and Modern shapes, the reconstruction path, and the
    nil-when-absent path. All six fail against the pre-fix hardcoded-0 code
    (confirmed: covered tests got 0 instead of 70/260, reconstruction got 0
    instead of 75/320, nil tests got a 0-pointer instead of nil) and pass
    after the fix.
  • Downstream compute / managedredis / synapse client tests updated to
    expect the covered cost.
  • go build ./..., go vet, and go test for the touched packages: green
    (197 tests pass across 4 packages). Changed files gofmt-clean.

Closes #1101

Related: #1022 (GCP converter sibling), #1021 (azure)

Summary by CodeRabbit

  • Bug Fixes
    • Azure reservation recommendation costs now accurately reflect covered expenses with reservations applied instead of defaulting to zero. The system calculates recurring monthly costs using available reservation data or reconstructs them from on-demand pricing and savings, providing precise cost visibility.

extractLegacy and extractModern in the Azure reservation recommendation
converter hardcoded out.RecurringMonthlyCost = float64Ptr(0). That field is
the covered/effective spend the GUI renders; with it pinned to 0 the GUI fell
back to displaying OnDemandCost (the spend WITHOUT any reservation), so Azure
recommendations showed on-demand cost instead of the committed cost.

Derive the value from the payload via a shared deriveCoveredMonthlyCost helper:
prefer the provider-reported TotalCostWithReservedInstances; reconstruct as
OnDemandCost - NetSavings when the total is absent; return nil (never a
fabricated 0) and log a warning when neither source is available, so an
unavailable figure renders as data-not-available rather than a false free
recurring charge. amountValuePtr unwraps Modern *Amount while preserving the
absent-field nil signal so both shapes share the same logic.

Regression tests assert the covered cost (not 0) for both Legacy and Modern
shapes, the reconstruction path, and the nil-when-absent path; all six fail
against the pre-fix hardcoded-0 code. Downstream compute/managedredis/synapse
client tests are updated to expect the covered cost.

Closes #1101
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ef7931b-7094-4fad-a19f-ddd04478f5c5

📥 Commits

Reviewing files that changed from the base of the PR and between 276404a and 73a6aa5.

📒 Files selected for processing (5)
  • providers/azure/internal/recommendations/converter.go
  • providers/azure/internal/recommendations/converter_test.go
  • providers/azure/services/compute/client_test.go
  • providers/azure/services/managedredis/client_test.go
  • providers/azure/services/synapse/client_test.go

📝 Walkthrough

Walkthrough

This PR fixes hardcoded zero values in Azure reservation recommendation cost calculations. The RecurringMonthlyCost field now derives covered cost from provider data (preferring TotalCostWithReservedInstances, reconstructing from on-demand minus savings, or returning nil when unavailable) instead of always showing 0. Both Legacy and Modern extraction paths and all downstream service tests are updated.

Changes

Azure RecurringMonthlyCost Derivation

Layer / File(s) Summary
RecurringMonthlyCost derivation specification and helpers
providers/azure/internal/recommendations/converter.go
Field documentation updated to define covered/effective cost semantics; new deriveCoveredMonthlyCost helper prefers TotalCostWithReservedInstances, reconstructs from on-demand minus net savings, and returns nil (never 0) when sources are unavailable; new amountValuePtr utility preserves absent-vs-zero distinction for Modern amount types.
Legacy and Modern extraction path integration
providers/azure/internal/recommendations/converter.go
Both extractLegacy and extractModern now populate RecurringMonthlyCost using the new derivation helper; Modern variant uses pointer-preserving unwrap to correctly propagate missing amount fields as nil.
Core converter tests for RecurringMonthlyCost
providers/azure/internal/recommendations/converter_test.go
Legacy and Modern test suites replaced with three-case validation: populated covered cost when total-with-RI is present, reconstruction from on-demand minus net savings when total-with-RI is absent, and nil (never fabricated zero) when all required cost sources are missing.
Service-level test expectation updates
providers/azure/services/compute/client_test.go, providers/azure/services/managedredis/client_test.go, providers/azure/services/synapse/client_test.go
Azure VM, Managed Redis, and Synapse recommendation tests updated to assert non-zero RecurringMonthlyCost values (70.0, ~700.0, and ~3500.0 respectively) reflecting correct covered/effective reserved monthly cost instead of hardcoded 0.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#256: Earlier change that enforced non-nil pointer-to-zero for RecurringMonthlyCost; this PR reverses that by deriving actual covered cost and returning nil when unavailable.
  • LeanerCloud/CUDly#682: Directly modifies RecurringMonthlyCost computation in the same converter file with accompanying test updates.
  • LeanerCloud/CUDly#1022: GCP converter equivalent that applies similar covered-cost derivation logic to GCP reservation recommendations.

Poem

🐰 Azure's costs now show their true face—
No more zeros hiding the reserved grace!
From total-with-RI or savings we trace,
The covered amount finds its rightful place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly describes the main fix: deriving covered monthly cost instead of hardcoding 0, which directly addresses the GUI display issue showing on-demand spend.
Linked Issues check ✅ Passed The PR fully implements all requirements from #1101: derives RecurringMonthlyCost using TotalCostWithReservedInstances or reconstructed logic, returns nil when data unavailable, applies identical logic to both extraction paths, and includes comprehensive regression tests.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to fixing the RecurringMonthlyCost derivation in converter.go and updating related test expectations; no unrelated modifications introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/azure-covered-monthly-cost

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect labels Jun 7, 2026
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cristim Sure, I'll review the latest changes on this PR.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.069Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.433Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.433Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.445Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` no longer hardcodes `Term = "1yr"` (fixed in PR `#1047` commit 8f9a787, H-3 audit finding). It propagates `params.Term` with a `"1yr"` default, validates via `termPlan`, and returns nil on unknown term. This is stricter than memorystore/cloudstorage/cloudsql (which default and continue): computeengine returns nil on unknown term because it is on the purchase path. Regression tests: `TestConvertGCPRecommendation_PropagatesParamsTerm`, `TestConvertGCPRecommendation_RejectsUnknownTerm`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:34:23.754Z
Learning: In LeanerCloud/CUDly Azure pricing extractors (compute, cache, database, cosmosdb, search, managedredis), Azure's Retail Prices API returns "1 Year" (singular) for 1-year reservation terms and "3 Years" (plural) for 3-year terms. All 6 pricing extractors use `azureTermString(termYears int) string` helper (added in PR `#1045` commit 5e938d9) to produce the correct singular/plural form for term matching. Prior to this fix, "%d Years" format produced "1 Years" which never matched Azure API responses, causing all 1-year reservation pricing lookups to silently fail. Regression tests: `TestExtractVMPricing_SingularOneYear`, `TestExtractVMPricing_PluralThreeYears`, `TestExtractRedisPricing_SingularOneYear`, `TestExtractRedisPricing_PluralThreeYears`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.445Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, the `memMBPerVCPU = 4096` const was removed in PR `#1047` commit 8f9a787. MEMORY MB is now extracted directly from the Recommender payload via `memoryMBFromOperationGroups` (sibling of `vcpuCountFromOperationGroups`). `buildInsertRequest` and `GroupCommitments` call `memoryMBFromDetails(rec)` which returns an error (no silent fallback) when `Details.MemoryGB` is absent. This ensures high-memory families (e.g. N2-highmem) get the correct MB from the payload rather than the GENERAL_PURPOSE approximation. Regression test: `TestBuildInsertRequest_RefusesMissingMemory`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:28:28.139Z
Learning: In LeanerCloud/CUDly `providers/gcp/recommendations.go`, `collectRegion` fans out to all four GCP service clients (computeengine, cloudsql, memorystore, cloudstorage) concurrently (fixed in PR `#1047` commit 95a390f, H-2 finding). `regionResult` gains `cache` and `storage` slices; merge is updated accordingly. The decision was to wire (not trim) memorystore and cloudstorage because both have complete `GetRecommendations` implementations and their no-op `PurchaseCommitment` paths are orthogonal to surfacing advisory recommendations. Regression tests: `TestRegionResult_HasCacheAndStorageFields` and `TestShouldIncludeService_Cache_Storage`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@cristim

cristim commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@cristim
cristim merged commit 14e953a into feat/multicloud-web-frontend Jun 8, 2026
4 checks passed
cristim added a commit that referenced this pull request Jun 9, 2026
…1126)

The Home dashboard showed a non-zero "current / committed" figure for Azure
(e.g. $166) while the Coverage tab reported $0 / "No usage detected" for the
same subscription. The two surfaces read different fields of the same active
commitments, and the Coverage path silently dropped Azure rows.

Root cause: getCoverageBreakdown summed only the recurring MonthlyCost and
guarded it with `if p.MonthlyCost != nil`. Azure all-upfront RIs carry
MonthlyCost == nil (no recurring charge at the commitment layer, documented on
config.PurchaseHistoryRecord.MonthlyCost), so every such row was skipped and the
provider collapsed to nil Services / nil OverallCoveragePct. The dashboard, by
contrast, aggregates EstimatedSavings (always populated), so it still counted the
commitment. PR #1105 only fixed the recommendation converter's RecurringMonthlyCost
for future rows; it never touched the Coverage handler, so the divergence persisted.

Fix: derive the effective covered monthly spend via a shared commitmentCoveredMonthly
helper = recurring MonthlyCost (when present) + amortised upfront
(UpfrontCost / (Term * MonthsPerYear)), matching the canonical effective-monthly
formula already used by analytics.Collector and exchange_lookup. An all-upfront
commitment now contributes its amortised upfront instead of being silently dropped,
so Coverage reflects the same active commitments the dashboard does. A nil
MonthlyCost is treated as a real $0 recurring component (not a fabricated total),
and Term <= 0 is guarded against division by zero, mirroring the collector's
skip-bad-term defence.

Regression test TestHandler_getCoverageBreakdown_AzureAllUpfrontConsistency seeds
an active Azure compute all-upfront RI (MonthlyCost nil, $1200 upfront / 1y =
$100/mo) and asserts both that the dashboard primitive counts it AND that the
Coverage tab now reports $100/mo covered (100% coverage) instead of nil. It fails
pre-fix (azure.Services nil) and passes post-fix.
@cristim
cristim deleted the fix/azure-covered-monthly-cost branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant