From 72b6d20c64c91ab9ed218c5c0fca7d65f9823701 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 12 Aug 2026 10:30:58 -0700 Subject: [PATCH 01/28] Fix Data Explorer dashboard savings formulas and FX fallback (#2093) Replaces inline sum(ListCost) - sum(EffectiveCost) style savings calculations in dashboard.json with sum() of the canonical, row-level clamped columns (x_TotalSavings, x_CommitmentDiscountSavings, x_NegotiatedDiscountSavings) across 21 tiles. Raw subtraction of aggregated sums can diverge from the canonical clamped values. Also removes the silent 1:1 exchange-rate fallback (coalesce(x_BillingExchangeRate, real(1))) used when deriving CommitmentDiscountQuantity for spend-based commitments in HubSetup_v1_2.kql and IngestionSetup_v1_2.kql. When the rate is missing, the quantity (and any utilization built on it) is now left unset rather than silently distorted by the true FX factor. Addresses the remaining Phase 1.3 and savings-plan FX distortion items from #2093; BillingCurrency visibility in detail tables and the Phase 2 USD-normalization toggle remain out of scope. --- src/templates/finops-hub/dashboard.json | 42 +++++++++---------- .../Analytics/scripts/HubSetup_v1_2.kql | 5 ++- .../Analytics/scripts/IngestionSetup_v1_2.kql | 5 ++- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/templates/finops-hub/dashboard.json b/src/templates/finops-hub/dashboard.json index dab4f6b27..93e43b961 100644 --- a/src/templates/finops-hub/dashboard.json +++ b/src/templates/finops-hub/dashboard.json @@ -2873,31 +2873,31 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost)\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| extend CommitmentDiscountSavings = ContractedCost - EffectiveCost\n| extend NegotiatedDiscountSavings = ListCost - ContractedCost\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", "id": "dcc1f533-e5f9-4855-9e4c-21e21fcdf943", "usedVariables": ["CostsPlus"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = materialize(\n CostsByMonth\n | where ChargePeriodStart >= startofmonth(now(), -1)\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost)\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend CommitmentDiscountSavings = ContractedCost - EffectiveCost\n | extend NegotiatedDiscountSavings = ListCost - ContractedCost\n | extend Month = monthname[monthofyear(ChargePeriodStart)]\n | project json = todynamic(strcat('[{ \"Type\":\"Billed cost\", \"Count\":', BilledCost, ' }, { \"Type\":\"Effective cost\", \"Count\":', EffectiveCost, ' }, { \"Type\":\"Commitment savings\", \"Count\":', CommitmentDiscountSavings, ' }, { \"Type\":\"Negotiated savings\", \"Count\":', NegotiatedDiscountSavings, ' }]')), Month, IsThisMonth = ChargePeriodStart >= startofmonth(now())\n | mv-expand json\n | project Label = strcat(json.Type, ' (', Month, ')'), Count = tolong(json.Count), IsThisMonth\n);\ndata", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = materialize(\n CostsByMonth\n | where ChargePeriodStart >= startofmonth(now(), -1)\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend Month = monthname[monthofyear(ChargePeriodStart)]\n | project json = todynamic(strcat('[{ \"Type\":\"Billed cost\", \"Count\":', BilledCost, ' }, { \"Type\":\"Effective cost\", \"Count\":', EffectiveCost, ' }, { \"Type\":\"Commitment savings\", \"Count\":', CommitmentDiscountSavings, ' }, { \"Type\":\"Negotiated savings\", \"Count\":', NegotiatedDiscountSavings, ' }]')), Month, IsThisMonth = ChargePeriodStart >= startofmonth(now())\n | mv-expand json\n | project Label = strcat(json.Type, ' (', Month, ')'), Count = tolong(json.Count), IsThisMonth\n);\ndata", "id": "e8b343dc-7430-4487-8d44-ef48ac454f2d", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n ContractedCost = todouble(round(sum(ContractedCost), 2)),\n ListCost = todouble(round(sum(ListCost), 2))\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend CommitmentDiscountSavings = todouble(round(ContractedCost - EffectiveCost, 2))\n | extend NegotiatedDiscountSavings = todouble(round(ListCost - ContractedCost, 2))\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sum(x_CommitmentDiscountSavings), 2)),\n NegotiatedDiscountSavings = todouble(round(sum(x_NegotiatedDiscountSavings), 2))\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n", "id": "c077a6d4-719f-42fe-b39b-36f77dc68976", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n ContractedCost = todouble(round(sum(ContractedCost), 2)),\n ListCost = todouble(round(sum(ListCost), 2))\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart)\n | order by ChargePeriodStart asc\n | extend CommitmentDiscountSavings = todouble(round(ContractedCost - EffectiveCost, 2))\n | extend NegotiatedDiscountSavings = todouble(round(ListCost - ContractedCost, 2))\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n//, BilledCost, NegotiatedDiscountSavings, CommitmentDiscountSavings)\n//| project ChargePeriod, BilledCost = round(BilledCost, 2), EffectiveCost = round(EffectiveCost, 2), NegotiatedDiscountSavings, CommitmentDiscountSavings\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sum(x_CommitmentDiscountSavings), 2)),\n NegotiatedDiscountSavings = todouble(round(sum(x_NegotiatedDiscountSavings), 2))\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart)\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n//, BilledCost, NegotiatedDiscountSavings, CommitmentDiscountSavings)\n//| project ChargePeriod, BilledCost = round(BilledCost, 2), EffectiveCost = round(EffectiveCost, 2), NegotiatedDiscountSavings, CommitmentDiscountSavings\n", "id": "3d947a7c-edca-46b5-a862-de77a725c85f", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend ContractedCost = todouble(round(ContractedCost, 2))\n | extend ListCost = todouble(round(ListCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(ContractedCost - EffectiveCost, 2))\n | extend NegotiatedDiscountSavings = todouble(round(ListCost - ContractedCost, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(CommitmentDiscountSavings, 2))\n | extend NegotiatedDiscountSavings = todouble(round(NegotiatedDiscountSavings, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", "id": "3886b5cd-34a8-42d7-9e16-33ea4d236953", "usedVariables": ["CostsByMonth"] }, @@ -2933,7 +2933,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart,\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend ContractedCost = todouble(round(ContractedCost, 2))\n | extend ListCost = todouble(round(ListCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(ContractedCost - EffectiveCost, 2))\n | extend NegotiatedDiscountSavings = todouble(round(ListCost - ContractedCost, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart,\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(CommitmentDiscountSavings, 2))\n | extend NegotiatedDiscountSavings = todouble(round(NegotiatedDiscountSavings, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", "id": "90502e9a-2d0d-4ae4-8d9d-cc21f9b72d5d", "usedVariables": ["CostsByMonth"] }, @@ -3011,13 +3011,13 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost)\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n // NegotiatedSavings = round(ListCost - ContractedCost, 2),\n // CommitmentSavings = round(ContractedCost - EffectiveCost, 2),\n Savings = round(ListCost - EffectiveCost, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_TotalSavings)\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", "id": "2d7b6447-2769-40b8-958a-f252dab68b1e", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sum(ListCost - EffectiveCost) by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", + "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sum(x_TotalSavings) by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", "id": "f062533b-8c94-412f-bf83-0eb5cd06063d", "usedVariables": ["CostsByDay", "CostsPlus", "numberOfDays"] }, @@ -3071,25 +3071,25 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n ListCost = round(sum(ListCost), 2),\n ContractedCost = round(sum(ContractedCost), 2),\n EffectiveCost = round(sum(EffectiveCost), 2)\n by\n Period\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', ListCost - EffectiveCost, ' }, { \"Label\":\"Negotiated\", \"Value\":', ListCost - ContractedCost, ' }, { \"Label\":\"Commitment\", \"Value\":', ContractedCost - EffectiveCost, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", + "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n TotalSavings = round(sum(x_TotalSavings), 2),\n NegotiatedDiscountSavings = round(sum(x_NegotiatedDiscountSavings), 2),\n CommitmentDiscountSavings = round(sum(x_CommitmentDiscountSavings), 2)\n by\n Period\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', TotalSavings, ' }, { \"Label\":\"Negotiated\", \"Value\":', NegotiatedDiscountSavings, ' }, { \"Label\":\"Commitment\", \"Value\":', CommitmentDiscountSavings, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", "id": "f5f240a8-a818-4f27-bdfb-e96fcfe433bd", "usedVariables": ["CostsByDay", "CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where ChargeCategory == 'Usage'\n| where isnotempty(CommitmentDiscountId)\n//\n| extend x_CommitmentDiscountUtilizationPotential = case(\n ProviderName == 'Microsoft', EffectiveCost,\n CommitmentDiscountCategory == 'Usage', ConsumedQuantity,\n CommitmentDiscountCategory == 'Spend', EffectiveCost,\n real(0)\n)\n| extend x_CommitmentDiscountUtilizationAmount = iff(CommitmentDiscountStatus == 'Used', x_CommitmentDiscountUtilizationPotential, real(0))\n//\n| summarize\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType),\n x_SkuTerm = take_any(x_SkuTerm),\n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost),\n x_CommitmentDiscountUtilizationAmount = sum(x_CommitmentDiscountUtilizationAmount),\n x_CommitmentDiscountUtilizationPotential = sum(x_CommitmentDiscountUtilizationPotential)\n by\n CommitmentDiscountId\n| order by EffectiveCost desc\n| project\n CommitmentDiscountName,\n CommitmentDiscountType,\n Term = case(isempty(x_SkuTerm) or x_SkuTerm <= 0, '', x_SkuTerm < 12, strcat(x_SkuTerm, ' month', iff(x_SkuTerm != 1, 's', '')), strcat(x_SkuTerm / 12, ' year', iff(x_SkuTerm != 12, 's', ''))),\n Utilization = round(x_CommitmentDiscountUtilizationAmount / x_CommitmentDiscountUtilizationPotential * 100, 1),\n Cost = round(EffectiveCost, 2),\n Savings = round(ListCost - EffectiveCost, 2)\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where ChargeCategory == 'Usage'\n| where isnotempty(CommitmentDiscountId)\n//\n| extend x_CommitmentDiscountUtilizationPotential = case(\n ProviderName == 'Microsoft', EffectiveCost,\n CommitmentDiscountCategory == 'Usage', ConsumedQuantity,\n CommitmentDiscountCategory == 'Spend', EffectiveCost,\n real(0)\n)\n| extend x_CommitmentDiscountUtilizationAmount = iff(CommitmentDiscountStatus == 'Used', x_CommitmentDiscountUtilizationPotential, real(0))\n//\n| summarize\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType),\n x_SkuTerm = take_any(x_SkuTerm),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_TotalSavings),\n x_CommitmentDiscountUtilizationAmount = sum(x_CommitmentDiscountUtilizationAmount),\n x_CommitmentDiscountUtilizationPotential = sum(x_CommitmentDiscountUtilizationPotential)\n by\n CommitmentDiscountId\n| order by EffectiveCost desc\n| project\n CommitmentDiscountName,\n CommitmentDiscountType,\n Term = case(isempty(x_SkuTerm) or x_SkuTerm <= 0, '', x_SkuTerm < 12, strcat(x_SkuTerm, ' month', iff(x_SkuTerm != 1, 's', '')), strcat(x_SkuTerm / 12, ' year', iff(x_SkuTerm != 12, 's', ''))),\n Utilization = round(x_CommitmentDiscountUtilizationAmount / x_CommitmentDiscountUtilizationPotential * 100, 1),\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2)\n", "id": "d7f31381-ba44-46a1-ad3e-dc6b8826706d", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n | extend CommitmentDiscountSavings = ContractedCost - EffectiveCost\n | extend NegotiatedDiscountSavings = ListCost - ContractedCost\n | extend TotalSavings = ListCost - EffectiveCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n TotalSavings = sum(x_TotalSavings)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e17346d1-0227-4aa4-8765-f9b4bf43bed5", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n | extend CommitmentDiscountSavings = ContractedCost - EffectiveCost\n | extend NegotiatedDiscountSavings = ListCost - ContractedCost\n | extend TotalSavings = ListCost - EffectiveCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n TotalSavings = sum(x_TotalSavings)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "f1ef29df-7a1d-4dd0-8619-0c0164707b31", "usedVariables": ["CostsByDay"] }, @@ -3113,25 +3113,25 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(ListCost - EffectiveCost, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_TotalSavings)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5bbb5369-ac95-45fc-853c-a6a2ce6a9e7b", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(ListCost - EffectiveCost), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_TotalSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "93f0eb9f-fa4d-4c54-96c2-8c1d3387d13a", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(ListCost - EffectiveCost, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_TotalSavings)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5c442903-65b0-4b53-9e7c-3ea9c1af7be5", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(ListCost - EffectiveCost), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_TotalSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "9a9ee4b0-a37d-475f-bf1d-f51573243491", "usedVariables": ["CostsByDay"] }, @@ -3143,7 +3143,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | where ChargeCategory == 'Usage'\n | where isnotempty(CommitmentDiscountId)\n | where EffectiveCost != 0\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_anyif(SubAccountName, isnotempty(SubAccountId)),\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType)\n by\n ChargePeriodStart,\n SubAccountId,\n CommitmentDiscountId,\n CommitmentDiscountStatus\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n ContractedCost = sum(ContractedCost),\n ListCost = sum(ListCost),\n SubAccountName = take_anyif(SubAccountName, isnotempty(SubAccountId)),\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType)\n by\n SubAccountId,\n CommitmentDiscountId,\n CommitmentDiscountStatus\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend ContractedCost = todouble(round(ContractedCost, 2))\n | extend ListCost = todouble(round(ListCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(ContractedCost - EffectiveCost, 2))\n | extend NegotiatedDiscountSavings = todouble(round(ListCost - ContractedCost, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n | extend SubAccountName = iff(isempty(SubAccountName) and CommitmentDiscountStatus == 'Unused', '(Unused)', SubAccountName)\n);\npercent((\n data | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName, CommitmentDiscountName, CommitmentDiscountType)\n | extend Count = tolong(Total * 1000)\n))\n| project-away Count\n| order by SubAccountName asc, Total desc", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | where ChargeCategory == 'Usage'\n | where isnotempty(CommitmentDiscountId)\n | where EffectiveCost != 0\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n SubAccountName = take_anyif(SubAccountName, isnotempty(SubAccountId)),\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType)\n by\n ChargePeriodStart,\n SubAccountId,\n CommitmentDiscountId,\n CommitmentDiscountStatus\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(NegotiatedDiscountSavings),\n SubAccountName = take_anyif(SubAccountName, isnotempty(SubAccountId)),\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType)\n by\n SubAccountId,\n CommitmentDiscountId,\n CommitmentDiscountStatus\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(CommitmentDiscountSavings, 2))\n | extend NegotiatedDiscountSavings = todouble(round(NegotiatedDiscountSavings, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n | extend SubAccountName = iff(isempty(SubAccountName) and CommitmentDiscountStatus == 'Unused', '(Unused)', SubAccountName)\n);\npercent((\n data | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName, CommitmentDiscountName, CommitmentDiscountType)\n | extend Count = tolong(Total * 1000)\n))\n| project-away Count\n| order by SubAccountName asc, Total desc", "id": "13cad52d-91e7-4ab4-aad5-4aae21c1019a", "usedVariables": ["CostsByMonth"] }, @@ -3251,19 +3251,19 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n | extend TotalSavings = ListCost - EffectiveCost\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n TotalSavings = sum(x_TotalSavings)\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e1bc2d51-44af-4dd9-8b0d-a71088b551f5", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| extend x_CommitmentDiscountSavings = iff(ContractedCost == 0, real(0), ContractedCost - EffectiveCost)\n| extend x_NegotiatedDiscountSavings = iff(ListCost == 0, real(0), ListCost - ContractedCost)\n| extend x_TotalSavings = iff(ListCost == 0, real(0), ListCost - EffectiveCost)\n| summarize\n ['List cost'] = round(sumif(ListCost, x_AmortizationClass != 'Principal'), 2),\n ['Effective cost'] = round(sum(EffectiveCost), 2),\n Savings = round(sum(x_TotalSavings), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| extend ESR = percentstring(Savings/ ['List cost'])\n| order by Month desc", + "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n ['List cost'] = round(sumif(ListCost, x_AmortizationClass != 'Principal'), 2),\n ['Effective cost'] = round(sum(EffectiveCost), 2),\n Savings = round(sum(x_TotalSavings), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| extend ESR = percentstring(Savings/ ['List cost'])\n| order by Month desc", "id": "3b3f0a58-2d84-4e3e-bebc-3e747a7d5ede", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost)\n | extend TotalSavings = ListCost - EffectiveCost\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n TotalSavings = sum(x_TotalSavings)\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "cb34f22b-9370-460c-9658-3e73d220bbc7", "usedVariables": ["CostsByDay"] }, @@ -3275,7 +3275,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| extend x_CommitmentDiscountSavings = iff(ContractedCost == 0, real(0), ContractedCost - EffectiveCost)\n| extend x_NegotiatedDiscountSavings = iff(ListCost == 0, real(0), ListCost - ContractedCost)\n| extend x_TotalSavings = iff(ListCost == 0, real(0), ListCost - EffectiveCost)\n| summarize\n ['On-demand'] = round(sumif(EffectiveCost, PricingCategory == 'Standard'), 2),\n Spot = round(sumif(EffectiveCost, PricingCategory == 'Dynamic'), 2),\n Reservation = round(sumif(EffectiveCost, CommitmentDiscountType == 'Reservation'), 2),\n ['Savings plan'] = round(sumif(EffectiveCost, CommitmentDiscountType == 'Savings Plan'), 2),\n ['Other'] = round(sumif(EffectiveCost, PricingCategory !in ('Standard', 'Dynamic') and isempty(CommitmentDiscountType)), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| order by Month desc", + "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n ['On-demand'] = round(sumif(EffectiveCost, PricingCategory == 'Standard'), 2),\n Spot = round(sumif(EffectiveCost, PricingCategory == 'Dynamic'), 2),\n Reservation = round(sumif(EffectiveCost, CommitmentDiscountType == 'Reservation'), 2),\n ['Savings plan'] = round(sumif(EffectiveCost, CommitmentDiscountType == 'Savings Plan'), 2),\n ['Other'] = round(sumif(EffectiveCost, PricingCategory !in ('Standard', 'Dynamic') and isempty(CommitmentDiscountType)), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| order by Month desc", "id": "0341b3e4-eccc-4924-9555-9835b128c543", "usedVariables": ["CostsByMonth"] }, diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql index 912b1ff8a..cc943e2b0 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql @@ -114,7 +114,10 @@ Costs_v1_2() ) | extend CommitmentDiscountQuantity = case( isempty(CommitmentDiscountStatus), real(null), - CommitmentDiscountCategory == 'Spend', EffectiveCost / coalesce(x_BillingExchangeRate, real(1)), + // Don't assume a 1:1 exchange rate when it's missing -- that silently distorts + // the derived quantity (and any utilization built on it) by the true FX factor. + // Leave it unset like the other not-derivable cases below instead. + CommitmentDiscountCategory == 'Spend' and isnotempty(x_BillingExchangeRate), EffectiveCost / x_BillingExchangeRate, CommitmentDiscountCategory == 'Usage' and isnotempty(x_CommitmentDiscountNormalizedRatio), PricingQuantity / coalesce(x_PricingBlockSize, real(1)) * x_CommitmentDiscountNormalizedRatio, real(null) ) diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql index da41cafa4..dc730e1a5 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql @@ -529,7 +529,10 @@ Costs_transform_v1_2() isnotempty(CommitmentDiscountQuantity), CommitmentDiscountQuantity, // FOCUS 1.0-preview, 1.0 isempty(CommitmentDiscountStatus), real(null), - CommitmentDiscountCategory == 'Spend', EffectiveCost / coalesce(x_BillingExchangeRate, real(1)), + // Don't assume a 1:1 exchange rate when it's missing -- that silently distorts + // the derived quantity (and any utilization built on it) by the true FX factor. + // Leave it unset like the other not-derivable cases below instead. + CommitmentDiscountCategory == 'Spend' and isnotempty(x_BillingExchangeRate), EffectiveCost / x_BillingExchangeRate, CommitmentDiscountCategory == 'Usage' and isnotempty(x_CommitmentDiscountNormalizedRatio), PricingQuantity / coalesce(x_PricingBlockSize, real(1)) * x_CommitmentDiscountNormalizedRatio, real(null) ) From 0e05f98b0a211c798167a32ff5e8871e1691c2ad Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Thu, 13 Aug 2026 18:33:48 -0700 Subject: [PATCH 02/28] Revert FX-fallback change in CommitmentDiscountQuantity derivation The edited branch is only reached by FOCUS-sourced rows where a null x_BillingExchangeRate means single-currency billing (PricingCurrency == BillingCurrency, so Cost Management doesn't populate a rate) -- not the EA/legacy null-source scenario the change was justified against. Those EA/legacy paths also null CommitmentDiscountQuantity upstream, so they short-circuit at the first case() branch and never reach this one. For single-currency accounts, a rate of 1.0 is correct, not a guess. Requiring isnotempty(x_BillingExchangeRate) instead silently drops these rows (and their CommitmentDiscountUnit) from utilization aggregates -- trading a bounded, visible number for a silent hole in sum()-based denominators. Restoring coalesce(x_BillingExchangeRate, real(1)). Co-Authored-By: Claude Sonnet 5 --- .../Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql | 5 +---- .../Analytics/scripts/IngestionSetup_v1_2.kql | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql index cc943e2b0..912b1ff8a 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/HubSetup_v1_2.kql @@ -114,10 +114,7 @@ Costs_v1_2() ) | extend CommitmentDiscountQuantity = case( isempty(CommitmentDiscountStatus), real(null), - // Don't assume a 1:1 exchange rate when it's missing -- that silently distorts - // the derived quantity (and any utilization built on it) by the true FX factor. - // Leave it unset like the other not-derivable cases below instead. - CommitmentDiscountCategory == 'Spend' and isnotempty(x_BillingExchangeRate), EffectiveCost / x_BillingExchangeRate, + CommitmentDiscountCategory == 'Spend', EffectiveCost / coalesce(x_BillingExchangeRate, real(1)), CommitmentDiscountCategory == 'Usage' and isnotempty(x_CommitmentDiscountNormalizedRatio), PricingQuantity / coalesce(x_PricingBlockSize, real(1)) * x_CommitmentDiscountNormalizedRatio, real(null) ) diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql index dc730e1a5..da41cafa4 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_2.kql @@ -529,10 +529,7 @@ Costs_transform_v1_2() isnotempty(CommitmentDiscountQuantity), CommitmentDiscountQuantity, // FOCUS 1.0-preview, 1.0 isempty(CommitmentDiscountStatus), real(null), - // Don't assume a 1:1 exchange rate when it's missing -- that silently distorts - // the derived quantity (and any utilization built on it) by the true FX factor. - // Leave it unset like the other not-derivable cases below instead. - CommitmentDiscountCategory == 'Spend' and isnotempty(x_BillingExchangeRate), EffectiveCost / x_BillingExchangeRate, + CommitmentDiscountCategory == 'Spend', EffectiveCost / coalesce(x_BillingExchangeRate, real(1)), CommitmentDiscountCategory == 'Usage' and isnotempty(x_CommitmentDiscountNormalizedRatio), PricingQuantity / coalesce(x_PricingBlockSize, real(1)) * x_CommitmentDiscountNormalizedRatio, real(null) ) From 00f51dd038c243bd2b40b486593891e4187d55cc Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 26 Aug 2026 12:19:26 -0700 Subject: [PATCH 03/28] fix(hubs): reconcile displayed operands with clamped savings on 6 dashboard tiles Roland Krummenacher and MSBrett both flagged that summing clamped, row-level savings columns (x_TotalSavings, x_CommitmentDiscountSavings, x_NegotiatedDiscountSavings) for the result side of 6 tiles, while leaving ListCost/ContractedCost/EffectiveCost as raw aggregates on the operand side, broke the on-screen equations those tiles visually promise (e.g. "List - Contracted = Negotiated savings"). Clamping doesn't distribute over sums, so the two sides no longer reconciled. Rather than reverting to raw subtraction (which would silently reintroduce the original per-row clamping bug for just these tiles), derive the displayed List/Contracted cost operands from the same clamped basis as the results, anchored on EffectiveCost (the only value that's never negative-clamped): ContractedCost' = EffectiveCost + sum(x_CommitmentDiscountSavings) ListCost' = ContractedCost' + sum(x_NegotiatedDiscountSavings) TotalSavings' = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings) Applied to: - Savings breakdown waterfall, monthly (e17346d1) and daily (f1ef29df) - Total/Negotiated/Commitment savings bar (f5f240a8) - Cost + savings stacked area chart (dcc1f533) - already reconciled by construction; added a clarifying comment only - Effective Savings Rate, monthly (e1bc2d51) and daily (cb34f22b) This changes the ESR definition on the two ESR tiles, since TotalSavings and ListCost are now derived consistently rather than mixing a clamped numerator with a raw denominator. Added a release note under FinOps hubs v15 in docs-mslearn/toolkit/changelog.md per review feedback. Verified against the counterexample from review: rows (List, Contracted, Effective) = (100,120,80) and (100,80,60) give NegotiatedDiscountSavings=20, CommitmentDiscountSavings=60, Effective=140 -> ContractedCost'=200 -> ListCost'=220; all displayed equations hold exactly. The other ~15 tiles' clamped-sum fix from the original PR is unaffected. Co-Authored-By: Claude Sonnet 5 --- docs-mslearn/toolkit/changelog.md | 3 ++- src/templates/finops-hub/dashboard.json | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 8849f945f..3df45dcc4 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 08/12/2026 +ms.date: 08/26/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -95,6 +95,7 @@ _Released June 2026_ - **Changed** - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)). + - Changed the Data Explorer dashboard's savings-breakdown, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)). - **Fixed** - Fixed Data Factory ingestion memory pressure during emptiness filtering. - Replaced `isnotempty(strcat(x_SkuMeterId, x_SkuOfferId))` with separate `isnotempty()` checks in FinOps hub ingestion scripts to avoid temporary string allocation. diff --git a/src/templates/finops-hub/dashboard.json b/src/templates/finops-hub/dashboard.json index 93e43b961..119d422cc 100644 --- a/src/templates/finops-hub/dashboard.json +++ b/src/templates/finops-hub/dashboard.json @@ -2873,7 +2873,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\n//\n// Stacked area: EffectiveCost is the unclamped base layer; CommitmentDiscountSavings and\n// NegotiatedDiscountSavings are clamped, row-level sums layered on top. The stack top\n// (Effective + Commitment + Negotiated) is therefore a derived 'cost without discounts'\n// figure built on the same clamped basis as the savings layers -- it reconciles with this\n// chart's own layers by construction, but will not always equal the raw sum(ListCost) KPI\n// shown elsewhere on the page. See the savings-breakdown tiles for the same derivation.\n//\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", "id": "dcc1f533-e5f9-4855-9e4c-21e21fcdf943", "usedVariables": ["CostsPlus"] }, @@ -3071,7 +3071,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n TotalSavings = round(sum(x_TotalSavings), 2),\n NegotiatedDiscountSavings = round(sum(x_NegotiatedDiscountSavings), 2),\n CommitmentDiscountSavings = round(sum(x_CommitmentDiscountSavings), 2)\n by\n Period\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', TotalSavings, ' }, { \"Label\":\"Negotiated\", \"Value\":', NegotiatedDiscountSavings, ' }, { \"Label\":\"Commitment\", \"Value\":', CommitmentDiscountSavings, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", + "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n NegotiatedDiscountSavings = round(sum(x_NegotiatedDiscountSavings), 2),\n CommitmentDiscountSavings = round(sum(x_CommitmentDiscountSavings), 2)\n by\n Period\n //\n // Total savings is derived as the sum of the two clamped components (not the\n // independently row-clamped x_TotalSavings) so the Total bar always reconciles\n // with Negotiated + Commitment shown alongside it.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', TotalSavings, ' }, { \"Label\":\"Negotiated\", \"Value\":', NegotiatedDiscountSavings, ' }, { \"Label\":\"Commitment\", \"Value\":', CommitmentDiscountSavings, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", "id": "f5f240a8-a818-4f27-bdfb-e96fcfe433bd", "usedVariables": ["CostsByDay", "CostsByMonth"] }, @@ -3083,13 +3083,13 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n TotalSavings = sum(x_TotalSavings)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive displayed cost operands from the same clamped, row-level savings basis as the\n // results so the on-screen equations reconcile (List - Contracted = Negotiated savings,\n // Contracted - Effective = Commitment savings). EffectiveCost is the only unclamped value;\n // Contracted and List are built up from it so both sides of each equation match exactly.\n | extend ContractedCost = EffectiveCost + CommitmentDiscountSavings\n | extend ListCost = ContractedCost + NegotiatedDiscountSavings\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e17346d1-0227-4aa4-8765-f9b4bf43bed5", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n //\n | summarize \n ListCost = sum(ListCost),\n ContractedCost = sum(ContractedCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n TotalSavings = sum(x_TotalSavings)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive displayed cost operands from the same clamped, row-level savings basis as the\n // results so the on-screen equations reconcile (List - Contracted = Negotiated savings,\n // Contracted - Effective = Commitment savings). EffectiveCost is the only unclamped value;\n // Contracted and List are built up from it so both sides of each equation match exactly.\n | extend ContractedCost = EffectiveCost + CommitmentDiscountSavings\n | extend ListCost = ContractedCost + NegotiatedDiscountSavings\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "f1ef29df-7a1d-4dd0-8619-0c0164707b31", "usedVariables": ["CostsByDay"] }, @@ -3251,7 +3251,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n TotalSavings = sum(x_TotalSavings)\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e1bc2d51-44af-4dd9-8b0d-a71088b551f5", "usedVariables": ["CostsByMonth"] }, @@ -3263,7 +3263,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n ListCost = sum(ListCost),\n TotalSavings = sum(x_TotalSavings)\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "cb34f22b-9370-460c-9658-3e73d220bbc7", "usedVariables": ["CostsByDay"] }, From 60c147ef2b6d483ebe2c9864dc41fd3e9439398f Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 26 Aug 2026 11:38:24 -0700 Subject: [PATCH 04/28] docs: prepare v15 release documentation - Fix stale changelog: correct release date, promote the fully-merged Unreleased content into the v15 section, add FinOps alerts and Open data tool sections. - Update what's new blurbs across all marketing pages for v15. - Add the missing InstanceSizeFlexibility table to the hubs data model. - Set up docs for the new FinOps toolkit agent plugin: MS Learn overview page, TOC entry, toolkit overview listing, marketing page and gallery tile, and an advisory council seat. - Standardize tool ordering (Hubs, Power BI, Workbooks, Alerts, Optimization Engine, Agent plugin, PowerShell, Bicep, Open data) consistently across the TOC, toolkit overview, and marketing gallery. Co-Authored-By: Claude Sonnet 5 --- docs-mslearn/TOC.yml | 6 +- .../finops-toolkit-agent-plugin-overview.md | 86 +++++++++++++++ docs-mslearn/toolkit/changelog.md | 103 ++++++------------ .../toolkit/finops-toolkit-overview.md | 4 +- docs-mslearn/toolkit/hubs/data-model.md | 8 +- docs-wiki/Advisory-council.md | 1 + docs/README.md | 15 ++- docs/agent-plugin.md | 55 ++++++++++ docs/alerts.md | 6 +- docs/guide.md | 4 +- docs/hubs.md | 4 +- docs/open-data.md | 4 +- docs/optimization-engine.md | 4 +- docs/power-bi.md | 4 +- docs/powershell.md | 4 +- docs/workbooks.md | 4 +- 16 files changed, 218 insertions(+), 94 deletions(-) create mode 100644 docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md create mode 100644 docs/agent-plugin.md diff --git a/docs-mslearn/TOC.yml b/docs-mslearn/TOC.yml index 052ef93eb..206ba4c28 100644 --- a/docs-mslearn/TOC.yml +++ b/docs-mslearn/TOC.yml @@ -216,8 +216,8 @@ href: toolkit/optimization-engine/troubleshooting.md - name: Reference href: toolkit/optimization-engine/reference.md - - name: Open data - href: toolkit/open-data.md + - name: Agent plugins + href: toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md - name: PowerShell items: - name: All commands @@ -278,6 +278,8 @@ href: toolkit/bicep-registry/modules.md - name: Scheduled actions href: toolkit/bicep-registry/scheduled-actions.md + - name: Open data + href: toolkit/open-data.md - name: Data Lake Storage connectivity href: toolkit/data-lake-storage-connectivity.md - name: Help + support diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md new file mode 100644 index 000000000..001eb536b --- /dev/null +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -0,0 +1,86 @@ +--- +title: FinOps toolkit agent plugin overview +description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. +author: flanakin +ms.author: micflan +ms.date: 08/14/2026 +ms.topic: concept-article +ms.service: finops +ms.subservice: finops-toolkit +ms.reviewer: msbrett +#customer intent: As a FinOps practitioner, I need to learn about the FinOps toolkit agent plugin. +--- + +# FinOps toolkit agent plugin + +The FinOps toolkit agent plugin brings AI-powered cloud financial management to your coding agent. It works with both [Claude Code](https://claude.com/claude-code) and [GitHub Copilot CLI](https://docs.github.com/copilot/how-tos/set-up/install-copilot-cli), pairing role-specific agents, ready-to-run commands, and a FinOps hubs query skill with a read-only [Azure MCP server](https://github.com/Azure/azure-mcp) so you can analyze cost data, review recommendations, and manage FinOps hubs without leaving your terminal. + +## Installation + +Install the plugin from the FinOps toolkit repository: + +```bash +# Claude Code +claude plugin add microsoft/finops-toolkit + +# GitHub Copilot CLI +copilot plugin install microsoft/finops-toolkit +``` + +The plugin registers an Azure MCP server scoped to the Kusto namespace in read-only mode, so it can query FinOps hubs data without being able to modify your environment. + +## What's included + +### Agents + +| Agent | Description | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Chief Financial Officer** | Executive-level guidance across financial strategy, capital allocation, risk management, budgeting, forecasting, and cost optimization. | +| **FinOps practitioner** | Domain expertise on FinOps principles, cost allocation, showback/chargeback models, and the FinOps Framework maturity model. | +| **FinOps hubs database query** | Queries cost data, resource metadata, pricing, regional data, and service mappings from a FinOps hub's Data Explorer database. | +| **FinOps hubs agent** | Deploys, upgrades, configures, and troubleshoots FinOps hubs, including Cost Management exports and hub architecture questions. | +| **Azure capacity manager** | Azure capacity evidence for FinOps work: quota analysis, capacity reservation groups, SKU availability, region and zone access, and coordinating capacity guarantees with pricing commitments. | + +### Commands + +| Command | Description | +| ----------------------- | -------------------------------------------------------------------------------------------------------------- | +| `/ftk-hubs-connect` | Discover FinOps hub instances via Azure Resource Graph, connect to a cluster, and save environment settings. | +| `/ftk-hubs-healthCheck` | Check a deployed hub's version against the latest stable and dev releases and validate data freshness. | +| `/ftk-mom-report` | Autonomous month-over-month cost analysis with anomaly detection, forecasting, and actionable recommendations. | +| `/ftk-ytd-report` | Comprehensive fiscal year-to-date cost analysis with a forecast through your organization's fiscal year end. | + +### Skill and query catalog + +The **finops-toolkit** skill triggers on FinOps hubs, KQL, and Azure Data Explorer topics and provides task routing, a query catalog, and schema guidance for working with a FinOps hub's `Costs()`, `Prices()`, `Recommendations()`, and `Transactions()` functions. + +## Required permissions + +- [Azure CLI](/cli/azure/install-azure-cli) authenticated (`az login`). +- Azure RBAC permissions for the Cost Management APIs you want to query or manage. +- For FinOps hubs queries: Database Viewer access to the hub's Azure Data Explorer cluster. + +## Give feedback + +Let us know how we're doing with a quick review. We use these reviews to improve and expand FinOps tools and resources. + + +> [!div class="nextstepaction"] +> [Give feedback](https://portal.azure.com/#view/HubsExtension/InProductFeedbackBlade/extensionName/FinOpsToolkit/cesQuestion/How%20easy%20or%20hard%20is%20it%20to%20use%20the%20FinOps%20toolkit%20agent%20plugin%3F/cvaQuestion/How%20valuable%20is%20the%20FinOps%20toolkit%20agent%20plugin%3F/surveyId/FTK/bladeName/AgentPlugin/featureName/Overview) + + +If you're looking for something specific, vote for an existing or create a new idea. Share ideas with others to get more votes. We focus on ideas with the most votes. + + +> [!div class="nextstepaction"] +> [Vote on or suggest ideas](https://github.com/microsoft/finops-toolkit/issues?q=is%3Aissue%20is%3Aopen%20sort%3Areactions-%2B1-desc) + + +
+ +## Related content + +- [FinOps hubs](../hubs/finops-hubs-overview.md) +- [FinOps hubs data model](../hubs/data-model.md) + +
diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index c9a66fcd1..5ef117a7e 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 09/07/2026 +ms.date: 09/02/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -25,85 +25,26 @@ This article summarizes the features and enhancements in each release of the Fin The following section lists features and enhancements that are currently in development. -### [FinOps hubs](hubs/finops-hubs-overview.md) - -- **Added** - - Added VNet and private network modes, including opt-in NAT Gateway support for private mode; NAT Gateway incurs additional cost when enabled ([#2163](https://github.com/microsoft/finops-toolkit/pull/2163)). -- **Changed** - - Clarified that the FinOps toolkit exclusively manages the FinOps hub virtual network and documented customer-managed private endpoints as the preferred private-access topology, with virtual network peering as a secondary option ([#2156](https://github.com/microsoft/finops-toolkit/issues/2156)). - - Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)). - - Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)). -- **Fixed** - - Fixed managed exports failing with an `Unauthorized` error because the Role Based Access Control Administrator role was never assigned to the Data Factory identity. Roles requested by a hub app are now assigned on the publisher storage account even when the app doesn't create the storage account itself ([#2253](https://github.com/microsoft/finops-toolkit/issues/2253)). - - Fixed private-network deployments that Azure Policy blocked when `defaultOutboundAccess` was omitted. Private mode subnets now set it to `false`, while an Azure Files private endpoint supports deployment-script storage and the NAT Gateway provides required container egress ([#2258](https://github.com/microsoft/finops-toolkit/issues/2258), [#2259](https://github.com/microsoft/finops-toolkit/pull/2259)). - - Fixed the `ContractedCost` recompute guard to compare with a null-safe tolerance instead of exact float equality, eliminating millions of no-op rewrites that polluted the `x_SourceValues` audit trail while preserving the null-cost backfill and no longer overwriting an existing cost when the unit price is missing ([#2216](https://github.com/microsoft/finops-toolkit/issues/2216)). - - Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - - Switched dimension enrichment in the v1_0/v1_2 ingestion transforms (`PricingUnits`, `Regions`, `ResourceTypes`, `Services`) from `join` to the broadcast-optimized `lookup` operator and deduplicated the `Services` mapping per resource type to prevent cost row fan-out ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - - Fixed the v1_0 and v1_2 price transforms setting `x_CommitmentDiscountSpendEligibility` from reservation meters and `x_CommitmentDiscountUsageEligibility` from savings plan meters, the opposite of the FOCUS `CommitmentDiscountCategory` value the same transform assigns. The spend column now reports savings plan pricing and the usage column reports reservation pricing. Prices ingested after upgrading carry the corrected values; rows ingested earlier keep the old values until they are reingested ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). - -### [FinOps workbooks](workbooks/finops-workbooks-overview.md) - -- **Fixed** - - Fixed the savings plan workbook summary and details queries silently dropping all but one recommendation per subscription due to an implicit `innerunique` join ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - - Fixed the AHB workbook "VM Latest Change Last 7 days" tile, which joined the `resourcechanges` record id against the resource id and always returned no rows, and the SQL VM queries, which never matched VMs with uppercase names ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - - Changed the savings plan workbook subscription name joins to `leftouter` so a recommendation is still shown when its subscription is missing from `resourcecontainers` ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - -### [FinOps alerts](alerts/finops-alerts-overview.md) - -- **Fixed** - - Made the idle application gateway and idle public IP query join kinds explicit so they no longer rely on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - -### [Power BI reports](power-bi/reports.md) - -- **Changed** - - Switched the InstanceSizeFlexibility table in the storage and KQL shared datasets from the retired `ccmstorageprod` AutofitComboMeterData.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file, joined to reservation recommendations on the unique ARM SKU name ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - -### [Optimization Engine](optimization-engine/overview.md) - -- **Added** - - Added a comprehensive [Azure Optimization Engine reference](optimization-engine/reference.md) for runbooks, schedules, variables, Log Analytics tables, and SQL Database tables ([#1271](https://github.com/microsoft/finops-toolkit/issues/1271)). -- **Changed** - - Switched the reservations and benefits workbooks from the retired `ccmstorageprod` isfratioblob.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). -- **Fixed** - - Fixed a regression in the `AzureOptimizationConsumptionV1_CL` schema that was breaking the Reservations Usage workbook ([#2301](https://github.com/microsoft/finops-toolkit/issues/2301)). - -### [PowerShell module](powershell/powershell-commands.md) - -- **Fixed** - - Fixed [Start-FinOpsCostExport](powershell/cost/start-finopscostexport.md) exporting the wrong period for anyone running in a positive UTC offset. `-StartDate` and `-EndDate` are now treated as UTC calendar dates instead of being time zone converted, so the days you request are the days that get exported. Previously, local midnight converted to the previous UTC day, which moved the period back a day and made `-Backfill` run one extra month ([#2255](https://github.com/microsoft/finops-toolkit/issues/2255)). - -### [Open data](open-data.md) updates - -**[Instance size flexibility](open-data.md#instance-size-flexibility)** - -- **Added** - - Added a new [Instance size flexibility](open-data.md#instance-size-flexibility) dataset that maps each ARM SKU to its instance size flexibility group and ratio, sourced from the Azure Reservations Catalogs API. It replaces the deprecated ISF ratio files hosted on `ccmstorageprod.blob.core.windows.net` ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - -**[Commitment discount eligibility](open-data.md#commitment-discount-eligibility)** - -- **Fixed** - - Fixed the commitment discount eligibility dataset refresh so it is reproducible and complete; retired meters now age out and previously missed meters are included ([#2164](https://github.com/microsoft/finops-toolkit/pull/2164)). - - Fixed the weekly commitment discount eligibility refresh timing out before it could publish, which left the dataset unchanged since it first shipped in v14. The refresh now walks each price type directly instead of sharding by service family, and verifies completeness by comparing two independent traversals before writing ([#2251](https://github.com/microsoft/finops-toolkit/pull/2251)). - - Fixed `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` carrying each other's values. FOCUS classifies a reservation as a usage commitment (committed to a quantity of usage) and a savings plan as a spend commitment (committed to an amount of money), so the spend column now reports savings plan pricing and the usage column reports reservation pricing. The dataset shipped with the two reversed from v14 ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). - -->
## v15 -_Released June 2026_ +_Released August 2026_ ### [Implementing FinOps guide](../implementing-finops-guide.md) v15 - **Added** + - Added a [FinOps toolkit ecosystem page](https://microsoft.github.io/finops-toolkit/ecosystem) listing independent tools and organizations that build on or support the FinOps toolkit. - **Changed** + - Aligned FinOps Framework capability names and links across guidance to current FinOps Framework terminology, including Usage optimization and Governance, policy, and risk ([#2170](https://github.com/microsoft/finops-toolkit/pull/2170)). - **Fixed** - - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)). + - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2. ### FinOps toolkit agent plugins v15 @@ -116,17 +57,28 @@ _Released June 2026_ - **Added** - Added a [Run hubs locally](hubs/run-hubs-locally.md) guide to stand up a FinOps hub in a local Kusto emulator container and ingest cost data using the same KQL, transforms, and open data as a deployed hub. - Added a build-generated `finops-hub-local-opendata.kql` release artifact that loads the open data reference tables from CSV, so the local hub guide stays in sync with published open data instead of hard-coding schemas. + - Added VNet and private network modes, including opt-in NAT Gateway support for private mode; NAT Gateway incurs additional cost when enabled ([#2163](https://github.com/microsoft/finops-toolkit/pull/2163)). - **Changed** - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)). + - Clarified that the FinOps toolkit exclusively manages the FinOps hub virtual network and documented customer-managed private endpoints as the preferred private-access topology, with virtual network peering as a secondary option ([#2156](https://github.com/microsoft/finops-toolkit/issues/2156)). + - Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)). + - Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)). + - Switched dimension enrichment in the v1_0/v1_2 ingestion transforms (`PricingUnits`, `Regions`, `ResourceTypes`, `Services`) from `join` to the broadcast-optimized `lookup` operator and deduplicated the `Services` mapping per resource type to prevent cost row fan-out. - **Fixed** - Fixed Data Factory ingestion memory pressure during emptiness filtering. - Replaced `isnotempty(strcat(x_SkuMeterId, x_SkuOfferId))` with separate `isnotempty()` checks in FinOps hub ingestion scripts to avoid temporary string allocation. - - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings ([#2189](https://github.com/microsoft/finops-toolkit/pull/2189)). + - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings. + - Fixed the `ContractedCost` recompute guard to compare with a null-safe tolerance instead of exact float equality, eliminating millions of no-op rewrites that polluted the `x_SourceValues` audit trail while preserving the null-cost backfill and no longer overwriting an existing cost when the unit price is missing ([#2216](https://github.com/microsoft/finops-toolkit/issues/2216)). + - Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default. + - Fixed managed exports failing with an `Unauthorized` error because the Role Based Access Control Administrator role was never assigned to the Data Factory identity. Roles requested by a hub app are now assigned on the publisher storage account even when the app doesn't create the storage account itself ([#2253](https://github.com/microsoft/finops-toolkit/issues/2253)). + - Fixed private-network deployments that Azure Policy blocked when `defaultOutboundAccess` was omitted. Private mode subnets now set it to `false`, while an Azure Files private endpoint supports deployment-script storage and the NAT Gateway provides required container egress ([#2258](https://github.com/microsoft/finops-toolkit/issues/2258), [#2259](https://github.com/microsoft/finops-toolkit/pull/2259)). + - Fixed the v1_0 and v1_2 price transforms setting `x_CommitmentDiscountSpendEligibility` from reservation meters and `x_CommitmentDiscountUsageEligibility` from savings plan meters, the opposite of the FOCUS `CommitmentDiscountCategory` value the same transform assigns. The spend column now reports savings plan pricing and the usage column reports reservation pricing. Prices ingested after upgrading carry the corrected values; rows ingested earlier keep the old values until they are reingested ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). ### [Power BI reports](power-bi/reports.md) v15 - **Changed** - Updated report labels and documentation to use current FinOps Framework capability names, including Usage optimization and Governance, policy, and risk ([#2170](https://github.com/microsoft/finops-toolkit/pull/2170)). + - Switched the InstanceSizeFlexibility table in the storage and KQL shared datasets from the retired `ccmstorageprod` AutofitComboMeterData.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file, joined to reservation recommendations on the unique ARM SKU name ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - **Fixed** - Fixed Power BI storage report refresh errors caused by ISO 8601 duration `x_SkuTerm` values (like `P3Y`) and empty strings in cost exports ([#2174](https://github.com/microsoft/finops-toolkit/issues/2174)). - Paginate Azure Resource Graph queries by subscription to mitigate [payload size limit](help/errors.md#response-payload-size-is-and-has-exceeded-the-limit) errors in the [Governance](power-bi/governance.md) and [Workload optimization](power-bi/workload-optimization.md) reports ([#1768](https://github.com/microsoft/finops-toolkit/issues/1768)). Queries still surface a payload size error (rather than silently returning truncated results) if a single batch of subscriptions exceeds the limit; see [Reduce the batch size](help/errors.md#option-1-reduce-the-batch-size) for the mitigation. As part of this change, the `AdvisorRecommendations` and `AdvisorReservationRecommendations` tables now return complete (non-truncated) results per batch, matching every other batched table; previously these two tables silently dropped rows beyond the payload limit instead of erroring. @@ -135,16 +87,23 @@ _Released June 2026_ - **Changed** - Updated workbook labels and documentation to use current FinOps Framework capability names, including Usage optimization and Governance, policy, and risk ([#2170](https://github.com/microsoft/finops-toolkit/pull/2170)). +- **Fixed** + - Fixed the savings plan workbook summary and details queries silently dropping all but one recommendation per subscription due to an implicit `innerunique` join ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). + - Fixed the AHB workbook "VM Latest Change Last 7 days" tile, which joined the `resourcechanges` record id against the resource id and always returned no rows, and the SQL VM queries, which never matched VMs with uppercase names ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). + - Fixed the savings plan workbook subscription name join to use `leftouter` so a recommendation is still shown when its subscription is missing from `resourcecontainers` ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). -### [Data dictionary](help/data-dictionary.md) updates +### [FinOps alerts](alerts/finops-alerts-overview.md) v15 - **Fixed** - - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)). + - Made the idle application gateway and idle public IP query join kinds explicit so they no longer rely on the `innerunique` default. ### [Optimization engine](optimization-engine/overview.md) v15 +- **Added** + - Added a comprehensive [Azure Optimization Engine reference](optimization-engine/reference.md) for runbooks, schedules, variables, Log Analytics tables, and SQL Database tables ([#1271](https://github.com/microsoft/finops-toolkit/issues/1271)). - **Changed** - Migrated Log Analytics ingestion from the Data Collection API (to be deprecated in September 2026) to a Data Collection Rule, Ingestion API-based solution. + - Switched the reservations and benefits workbooks from the retired `ccmstorageprod` isfratioblob.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - **Fixed** - Removed call to Azure Classic administrators endpoint (deprecated on May 1, 2026) from Azure RBAC assignments exports ([#2142](https://github.com/microsoft/finops-toolkit/issues/2142)). @@ -157,6 +116,16 @@ _Released June 2026_ - **Fixed** - Fixed [Get-FinOpsCostExport](powershell/cost/get-finopscostexport.md) `-RunHistory` to return the complete run history ([#2063](https://github.com/microsoft/finops-toolkit/issues/2063)). - Bumped the `Az.Accounts` required-module minimum to 2.17.0 so dependency resolution can't land on a version missing the `Get-AzAccessToken -AsSecureString` parameter that `Invoke-Rest` relies on ([#2185](https://github.com/microsoft/finops-toolkit/issues/2185)). + - Fixed [Start-FinOpsCostExport](powershell/cost/start-finopscostexport.md) exporting the wrong period for anyone running in a positive UTC offset. `-StartDate` and `-EndDate` are now treated as UTC calendar dates instead of being time zone converted, so the days you request are the days that get exported. Previously, local midnight converted to the previous UTC day, which moved the period back a day and made `-Backfill` run one extra month ([#2255](https://github.com/microsoft/finops-toolkit/issues/2255)). + +### [Open data](open-data.md) v15 + +- **Added** + - Added a new [Instance size flexibility](open-data.md#instance-size-flexibility) dataset that maps each ARM SKU to its instance size flexibility group and ratio, sourced from the Azure Reservations Catalogs API. It replaces the deprecated ISF ratio files hosted on `ccmstorageprod.blob.core.windows.net` ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). +- **Fixed** + - Fixed the [commitment discount eligibility](open-data.md#commitment-discount-eligibility) dataset refresh so it's reproducible and complete; retired meters now age out and previously missed meters are included ([#2164](https://github.com/microsoft/finops-toolkit/pull/2164)). + - Fixed the weekly commitment discount eligibility refresh timing out before it could publish, which left the dataset unchanged since it first shipped in v14. The refresh now walks each price type directly instead of sharding by service family, and verifies completeness by comparing two independent traversals before writing ([#2251](https://github.com/microsoft/finops-toolkit/pull/2251)). + - Fixed `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` carrying each other's values. FOCUS classifies a reservation as a usage commitment (committed to a quantity of usage) and a savings plan as a spend commitment (committed to an amount of money), so the spend column now reports savings plan pricing and the usage column reports reservation pricing. The dataset shipped with the two reversed from v14 ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). > [!div class="nextstepaction"] diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index 22f68c7fb..99e1cc6e4 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 08/05/2026 +ms.date: 08/14/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit @@ -31,7 +31,9 @@ The FinOps toolkit is an ever-evolving collection of tools and resources. The fo - [FinOps workbooks](./workbooks/finops-workbooks-overview.md) – Central hub for cost optimization. - [Cost optimization workbook](./workbooks/optimization.md) – Central hub for cost optimization. - [Governance workbook](./workbooks/governance.md) – Central hub for governance. +- [FinOps alerts](./alerts/finops-alerts-overview.md) – Automate cost optimization by managing idle resources. - [Azure Optimization Engine](./optimization-engine/overview.md) – Extensible solution for custom optimization recommendations. +- [Agent plugin](./agent-plugin/finops-toolkit-agent-plugin-overview.md) – AI-powered cloud financial management for Claude Code and GitHub Copilot CLI. - [PowerShell module](./powershell/powershell-commands.md) – Automate and manage FinOps solutions and capabilities. - [Bicep Registry modules](./bicep-registry/modules.md) – Official repository for Bicep modules. - [Open data](open-data.md) – Data available for anyone to access, use, and share without restriction. diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index e06a19519..aeb54180d 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 08/14/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -473,6 +473,12 @@ Examples:
+## InstanceSizeFlexibility table + +The **InstanceSizeFlexibility** table in Power BI and Data Explorer is populated from the [Instance size flexibility open data file](../open-data.md#instance-size-flexibility). This table is used to identify reservation-eligible SKUs that share instance size flexibility with a purchased reservation. + +
+ ## ManagementGroups table The **ManagementGroups** table in Power BI reports that queries Azure management groups from Azure Resource Graph. diff --git a/docs-wiki/Advisory-council.md b/docs-wiki/Advisory-council.md index dbbd5eebc..874f24737 100644 --- a/docs-wiki/Advisory-council.md +++ b/docs-wiki/Advisory-council.md @@ -61,6 +61,7 @@ This currently includes the following people: | Seat | Member | | ------------------------------------ | --------------------- | +| Agent plugin | Brett Wilson | | Azure Data Explorer | Brad Watts | | Azure Optimization Engine | Helder Pinto | | Bicep Registry | Vacant | diff --git a/docs/README.md b/docs/README.md index 03ac79baf..6852359a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,9 +18,9 @@ Automate and extend the Microsoft Cloud with starter kits, scripts, and advanced ---
-

What's new in April 2026v14

+

What's new in August 2026v15

- April adds recommendations ingestion and a Copilot Studio agent template for FinOps hubs, a new commitment discount eligibility open dataset, simplified hub deployment UI, Azure Optimization Engine pricesheet API upgrade, and minor improvements in Power BI, FinOps workbooks, and the PowerShell module. + August adds a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI, private networking support for FinOps hubs, a new Instance size flexibility open dataset, several KQL performance and correctness fixes across hubs, workbooks, and alerts, and minor improvements in Power BI, the Optimization Engine, and the PowerShell module.

See all changes

@@ -61,9 +61,9 @@ Automate and extend the Microsoft Cloud with starter kits, scripts, and advanced Learn more
-
🌐 Open data
-
Useful data for anyone to access, use, and share.
- Learn more +
🤖 Agent plugin
+
AI-powered cloud financial management for Claude Code and GitHub Copilot CLI.
+ Learn more
🖥️ PowerShell
@@ -75,6 +75,11 @@ Automate and extend the Microsoft Cloud with starter kits, scripts, and advanced
Official repository for Bicep modules.
Learn more
+
+
🌐 Open data
+
Useful data for anyone to access, use, and share.
+ Learn more +
Download FinOps toolkit {% include ftktag.txt %} diff --git a/docs/agent-plugin.md b/docs/agent-plugin.md new file mode 100644 index 000000000..3ab7a24f0 --- /dev/null +++ b/docs/agent-plugin.md @@ -0,0 +1,55 @@ +--- +layout: default +title: Agent plugin +browser: FinOps toolkit agent plugin - Automate your FinOps efforts +nav_order: 65 +description: 'The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI.' +permalink: /agent-plugin +#customer intent: As a Finops practitioner, I need to learn about the FinOps toolkit agent plugin +--- + +FinOps toolkit agent plugin +Bring AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. +{: .fs-6 .fw-300 } + +Install +Documentation + +--- + +The FinOps toolkit agent plugin pairs role-specific agents, ready-to-run commands, and a FinOps hubs query skill with a read-only Azure MCP server, so you can analyze cost data, review recommendations, and manage FinOps hubs without leaving your terminal. + +
+

What's new in August 2026v15

+

+ The FinOps toolkit agent plugin is new this release: a shared plugin for Claude Code and GitHub Copilot CLI with 5 agents (CFO, FinOps practitioner, database query, hubs agent, and Azure capacity manager), 4 commands, a FinOps hubs KQL skill, and a read-only Azure MCP server. +

+

See all changes

+
+ + + +## What's included + +- **Agents** – Chief Financial Officer, FinOps practitioner, FinOps hubs database query, FinOps hubs agent, and Azure capacity manager. +- **Commands** – `/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, and `/ftk-ytd-report`. +- **Skill** – A FinOps hubs KQL skill with task routing, a pre-built query catalog, and schema guidance. +- **MCP server** – A read-only [Azure MCP server](https://github.com/Azure/azure-mcp) scoped to the Kusto namespace for querying FinOps hubs data. + + + +## Install + +```bash +# Claude Code +claude plugin add microsoft/finops-toolkit + +# GitHub Copilot CLI +copilot plugin install microsoft/finops-toolkit +``` + +
+ +## Learn more + +[Read the full documentation](https://learn.microsoft.com/cloud-computing/finops/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview) for required permissions and setup details. diff --git a/docs/alerts.md b/docs/alerts.md index 24e06851b..2d33581e0 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -19,15 +19,13 @@ Accelerate your cost optimization efforts with scheduled notifications that cont Designed with flexibility in mind, FinOps alerts run on a fully configurable schedule, continuously scanning your selected subscriptions for idle resources. - ## Actionable insights for smarter cloud management diff --git a/docs/guide.md b/docs/guide.md index abe565fdb..85bd3ef4b 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -16,9 +16,9 @@ Your guide to implementing FinOps in the Microsoft Cloud. FinOps is an expansive area with branches that extend out into many cloud management and governance activities. Cost especially is a part of many cloud and business initiatives, like security, resiliency, and adopting cloud-native solutions like AI. And to streamline cloud adoption in a way that prioritizes business value, you need the right guidance to help you make the right data-driven decisions. The Implementing FinOps guide is a collection of resources that help you facilitate your FinOps goals.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, the Implementing FinOps guide updated the FinOps overview video and error handling documentation. + In August, the Implementing FinOps guide added a FinOps toolkit ecosystem page and aligned capability names and links to current FinOps Framework terminology.

See all changes

diff --git a/docs/hubs.md b/docs/hubs.md index c3ff69557..dae52d916 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -20,9 +20,9 @@ Open, extensible, and scalable cost governance for the enterprise. FinOps hubs are a reliable, trustworthy platform for cost analytics, insights, and optimization – virtual command centers for leaders throughout the organization to report on, monitor, and optimize cost based on their organizational needs.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, FinOps hubs added recommendations ingestion from Azure Advisor and custom ARG queries, a Copilot Studio agent template, simplified the deployment UI, and fixed deployment and ETL pipeline issues. + In August, FinOps hubs added VNet and private network modes with optional NAT Gateway support, a guide to run FinOps hubs using a local Kusto emulator, and fixed several KQL join and cost recompute issues.

See all changes

diff --git a/docs/open-data.md b/docs/open-data.md index ed191680b..97702ca4d 100644 --- a/docs/open-data.md +++ b/docs/open-data.md @@ -20,9 +20,9 @@ Leverage open data to normalize and enhance your FinOps reporting. FinOps toolkit open data is used to transform Cost Management actual and amortized data into FOCUS. Use the same mappings to clean your FinOps datasets.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, open data added a new commitment discount eligibility dataset with pre-computed reservation and savings plan eligibility per meter, sourced from the Azure Retail Prices API. + In August, open data added a new Instance size flexibility dataset sourced from the Azure Reservations Catalogs API, replacing the deprecated ISF ratio files, and fixed the commitment discount eligibility dataset refresh to be reproducible and complete.

See all changes

diff --git a/docs/optimization-engine.md b/docs/optimization-engine.md index 878b982d0..ae9b90fa9 100644 --- a/docs/optimization-engine.md +++ b/docs/optimization-engine.md @@ -20,9 +20,9 @@ Optimize your Azure environment. The Azure Optimization Engine (AOE) is an extensible solution designed to generate optimization recommendations for your Azure environment. See it like a fully customizable Azure Advisor.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, Azure Optimization Engine upgraded the EA/MCA pricesheet API version (the previous version is deprecated June 1, 2026) and improved retail prices export resilience with retry logic, streamed CSV writes, and progress logging. + In August, Azure Optimization Engine migrated Log Analytics ingestion to a Data Collection Rule-based solution, switched to the new Instance size flexibility open dataset, and removed a deprecated Azure Classic administrators API call.

diff --git a/docs/power-bi.md b/docs/power-bi.md index b64c6fd4a..55f4a9e60 100644 --- a/docs/power-bi.md +++ b/docs/power-bi.md @@ -20,9 +20,9 @@ Accelerate your analytics efforts with simple, targeted reports. Summarize and b FinOps toolkit Power BI reports provide a great starting point for FinOps reporting. Customize and augment reports with your own data to facilitate organizational requirements.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, Power BI reports fixed storage report refresh errors caused by unrecognized Term values like P10Y in pricesheet exports. + In August, Power BI reports switched to the new Instance size flexibility open dataset, fixed storage report refresh errors from ISO 8601 duration values, and paginated Azure Resource Graph queries to avoid payload size errors.

See all changes

diff --git a/docs/powershell.md b/docs/powershell.md index 26cd4b910..2db1bcd74 100644 --- a/docs/powershell.md +++ b/docs/powershell.md @@ -20,9 +20,9 @@ Automate and scale your FinOps efforts with PowerShell commands that streamline The FinOps toolkit PowerShell module helps you automate and scale common Cost Management and FinOps toolkit management operations and work with FinOps toolkit open data.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, the PowerShell module added WhatIf support for resource provider registration in New-FinOpsCostExport and fixed inverted verbose logging in Start-FinOpsCostExport. + In August, the PowerShell module added Initialize-FinOpsHubLocal for setting up a local FinOps hub with one command, updated test compatibility with Pester 6, and fixed Get-FinOpsCostExport -RunHistory to return the complete run history.

See all changes

diff --git a/docs/workbooks.md b/docs/workbooks.md index 7559f8d03..3210c5d09 100644 --- a/docs/workbooks.md +++ b/docs/workbooks.md @@ -20,9 +20,9 @@ Engineering hub to maximize cloud ROI through FinOps. FinOps workbooks are Azure workbooks that provide a series of tools to help engineers perform targeted FinOps tasks, modeled after the Well-Architected Framework guidance.
-

What's new in April 2026v14

+

What's new in August 2026v15

- In April, FinOps workbooks fixed the broken feedback link in the Optimization workbook, excluded dev/test subscriptions from Azure Hybrid Benefit reports, and fixed AHB reports to include Windows VMs from all publishers. + In August, FinOps workbooks fixed savings plan and Azure Hybrid Benefit queries that were silently dropping or missing recommendations due to implicit join behavior.

See all changes

From 843a102027247f9f994a39010622251a8fdebff2 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 26 Aug 2026 15:13:05 -0700 Subject: [PATCH 05/28] docs: add DeploymentScriptACIProvisioningTimeout mitigation Documents the known retry and SecurityControl=Ignore mitigations for deployment scripts stuck provisioning their Azure Container Instance, per the diagnosis worked out on #2241. Co-Authored-By: Claude Sonnet 5 --- docs-mslearn/toolkit/help/errors.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index f9adc25eb..00311561c 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 08/13/2026 +ms.date: 08/14/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit @@ -376,6 +376,26 @@ We are following up with the Azure Data Explorer team to identify the correct re
+## DeploymentScriptACIProvisioningTimeout + +Severity: Major + +FinOps hub deployments use `Microsoft.Resources/deploymentScripts` resources to run PowerShell setup scripts. Each deployment script provisions a temporary Azure Container Instance (ACI) to execute its script. If that container instance doesn't start in time, the deployment script — and the overall deployment — fails with `DeploymentScriptACIProvisioningTimeout`. + +This error isn't caused by the script itself; it means the underlying ACI never finished provisioning. We've seen two causes: + +- **Transient ACI capacity or scheduling delay.** The container instance service is momentarily unable to place the container. This usually resolves on its own. +- **Restrictive tenant security policies (for example, Microsoft SFI) blocking the deployment script's use of a storage account key.** The container gets stuck in a `Waiting to run` or `Creating` state and never progresses. + +**Mitigation**: + +1. **Retry the deployment.** Most instances of this error are transient — simply redeploying resolves it. +2. **If it keeps failing, set the `SecurityControl` tag to `Ignore` on the target resource group before deploying.** Some tenant security policies disable the shared storage account key that deployment scripts rely on; this tag bypasses that restriction for the deployment. + +Report unresolved issues at . + +
+ ## ExportDataNotFound Severity: Critical From 44293c23043b1fe7eda3388fa7d63967a84ac0f9 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 2 Sep 2026 09:00:15 -0700 Subject: [PATCH 06/28] chore: bump version to 15.0.0 Co-Authored-By: Claude Opus 5 --- .claude-plugin/marketplace.json | 2 +- .github/plugin/marketplace.json | 6 +++--- docs/_includes/ftkver.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/optimization-engine/ftkver.txt | 2 +- src/powershell/Private/Get-VersionNumber.ps1 | 2 +- .../agent-plugin/.claude-plugin/plugin.json | 15 ++++++++++++--- src/templates/agent-plugin/plugin.json | 15 ++++++++++++--- src/templates/finops-alerts/modules/ftkver.txt | 2 +- src/templates/finops-hub/modules/fx/ftkver.txt | 2 +- src/templates/finops-workbooks/ftkver.txt | 2 +- src/workbooks/.scaffold/ftkver.txt | 2 +- 13 files changed, 38 insertions(+), 20 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4f8d4ba2a..4ad827dc5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "plugins": [ { "name": "microsoft-finops-toolkit", - "version": "15.0.0", + "version": "15.0", "source": "./plugins/microsoft-finops-toolkit", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "category": "finops", diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 49876e25b..46096669a 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -5,12 +5,12 @@ }, "metadata": { "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", - "version": "15.0.0" + "version": "15.0" }, "plugins": [ { "name": "microsoft-finops-toolkit", - "version": "15.0.0", + "version": "15.0", "source": "./plugins/microsoft-finops-toolkit", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "category": "finops", @@ -27,4 +27,4 @@ "homepage": "https://learn.microsoft.com" } ] -} +} \ No newline at end of file diff --git a/docs/_includes/ftkver.txt b/docs/_includes/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/docs/_includes/ftkver.txt +++ b/docs/_includes/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d1993e489..2b84a1d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ftk", - "version": "15.0.0-dev.0", + "version": "15.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ftk", - "version": "15.0.0-dev.0", + "version": "15.0.0", "license": "MIT", "devDependencies": { "all-contributors-cli": "^6.26.1" diff --git a/package.json b/package.json index 898ab1baa..6c0a6b93b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ftk", - "version": "15.0.0-dev.0", + "version": "15.0.0", "description": "Starter kits, scripts, and advanced solutions to accelerate your FinOps journey in the Microsoft Cloud.", "main": "index.js", "directories": { diff --git a/src/optimization-engine/ftkver.txt b/src/optimization-engine/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/src/optimization-engine/ftkver.txt +++ b/src/optimization-engine/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file diff --git a/src/powershell/Private/Get-VersionNumber.ps1 b/src/powershell/Private/Get-VersionNumber.ps1 index 097964a44..c14edf29b 100644 --- a/src/powershell/Private/Get-VersionNumber.ps1 +++ b/src/powershell/Private/Get-VersionNumber.ps1 @@ -4,5 +4,5 @@ function Get-VersionNumber { param() - return '15.0-dev' + return '15.0' } diff --git a/src/templates/agent-plugin/.claude-plugin/plugin.json b/src/templates/agent-plugin/.claude-plugin/plugin.json index 109c206d7..09d2a04d0 100644 --- a/src/templates/agent-plugin/.claude-plugin/plugin.json +++ b/src/templates/agent-plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "microsoft-finops-toolkit", - "version": "15.0.0", + "version": "15.0", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "author": { "name": "FinOps Toolkit Team", @@ -9,10 +9,19 @@ "homepage": "https://aka.ms/finops/toolkit", "repository": "https://github.com/microsoft/finops-toolkit", "license": "MIT", - "keywords": ["finops", "cost-management", "reservations", "savings-plans", "cloud-optimization", "commitments", "credits", "macc"], + "keywords": [ + "finops", + "cost-management", + "reservations", + "savings-plans", + "cloud-optimization", + "commitments", + "credits", + "macc" + ], "commands": "./commands/", "skills": [ "./skills/" ], "outputStyles": "./output-styles/" -} +} \ No newline at end of file diff --git a/src/templates/agent-plugin/plugin.json b/src/templates/agent-plugin/plugin.json index bc7d4da20..946292328 100644 --- a/src/templates/agent-plugin/plugin.json +++ b/src/templates/agent-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "microsoft-finops-toolkit", - "version": "15.0.0", + "version": "15.0", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "author": { "name": "FinOps Toolkit Team", @@ -9,11 +9,20 @@ "homepage": "https://aka.ms/finops/toolkit", "repository": "https://github.com/microsoft/finops-toolkit", "license": "MIT", - "keywords": ["finops", "cost-management", "reservations", "savings-plans", "cloud-optimization", "commitments", "credits", "macc"], + "keywords": [ + "finops", + "cost-management", + "reservations", + "savings-plans", + "cloud-optimization", + "commitments", + "credits", + "macc" + ], "commands": "./commands/", "agents": "./agents/", "skills": [ "skills/" ], "mcpServers": ".mcp.json" -} +} \ No newline at end of file diff --git a/src/templates/finops-alerts/modules/ftkver.txt b/src/templates/finops-alerts/modules/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/src/templates/finops-alerts/modules/ftkver.txt +++ b/src/templates/finops-alerts/modules/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file diff --git a/src/templates/finops-hub/modules/fx/ftkver.txt b/src/templates/finops-hub/modules/fx/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/src/templates/finops-hub/modules/fx/ftkver.txt +++ b/src/templates/finops-hub/modules/fx/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file diff --git a/src/templates/finops-workbooks/ftkver.txt b/src/templates/finops-workbooks/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/src/templates/finops-workbooks/ftkver.txt +++ b/src/templates/finops-workbooks/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file diff --git a/src/workbooks/.scaffold/ftkver.txt b/src/workbooks/.scaffold/ftkver.txt index e6e484c9f..3d3be3c32 100644 --- a/src/workbooks/.scaffold/ftkver.txt +++ b/src/workbooks/.scaffold/ftkver.txt @@ -1 +1 @@ -15.0-dev \ No newline at end of file +15.0 \ No newline at end of file From 327eafb927a5ee293518ce0dfb34bb5c5cd76e98 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Mon, 7 Sep 2026 09:20:46 -0700 Subject: [PATCH 07/28] docs: finalize v15 changelog, what's new blurbs, and hubs docs - Set release month to September 2026 across changelog and what's new blurbs - Link the agent plugins changelog section to its docs page - Add missing issue links (#2176, #2225) and fix past-tense verb usage - Trim verbose and implementation-detail changelog entries - Fold in dev entries merged after the prep branch was cut (#2301) - Document the v15 commitment discount eligibility column swap in the upgrade guide, data model, and data processing docs Co-Authored-By: Claude Opus 5 --- docs-mslearn/toolkit/changelog.md | 22 +- docs-mslearn/toolkit/hubs/data-model.md | 450 +++++++++---------- docs-mslearn/toolkit/hubs/data-processing.md | 9 +- docs-mslearn/toolkit/hubs/upgrade.md | 4 +- docs/README.md | 4 +- docs/agent-plugin.md | 2 +- docs/alerts.md | 4 +- docs/guide.md | 4 +- docs/hubs.md | 4 +- docs/open-data.md | 4 +- docs/optimization-engine.md | 4 +- docs/power-bi.md | 4 +- docs/powershell.md | 4 +- docs/workbooks.md | 4 +- 14 files changed, 265 insertions(+), 258 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 5ef117a7e..1eb3a3fb1 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -31,22 +31,18 @@ The following section lists features and enhancements that are currently in deve ## v15 -_Released August 2026_ +_Released September 2026_ ### [Implementing FinOps guide](../implementing-finops-guide.md) v15 - **Added** - - Added a [FinOps toolkit ecosystem page](https://microsoft.github.io/finops-toolkit/ecosystem) listing independent tools and organizations that build on or support the FinOps toolkit. - - **Changed** - - Aligned FinOps Framework capability names and links across guidance to current FinOps Framework terminology, including Usage optimization and Governance, policy, and risk ([#2170](https://github.com/microsoft/finops-toolkit/pull/2170)). - - **Fixed** - Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2. -### FinOps toolkit agent plugins v15 +### [FinOps toolkit agent plugins](agent-plugin/finops-toolkit-agent-plugin-overview.md) v15 - **Added** - Added a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI with 5 agents (CFO, FinOps practitioner, database query, hubs agent, and Azure capacity manager), 4 commands (`/ftk/hubs-connect`, `/ftk/hubs-health-check`, `/ftk/mom-report`, and `/ftk/ytd-report`), a FinOps hubs KQL skill, and a read-only Azure MCP server. @@ -63,13 +59,12 @@ _Released August 2026_ - Clarified that the FinOps toolkit exclusively manages the FinOps hub virtual network and documented customer-managed private endpoints as the preferred private-access topology, with virtual network peering as a secondary option ([#2156](https://github.com/microsoft/finops-toolkit/issues/2156)). - Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)). - Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)). - - Switched dimension enrichment in the v1_0/v1_2 ingestion transforms (`PricingUnits`, `Regions`, `ResourceTypes`, `Services`) from `join` to the broadcast-optimized `lookup` operator and deduplicated the `Services` mapping per resource type to prevent cost row fan-out. + - Switched dimension enrichment in the v1_0/v1_2 ingestion transforms (`PricingUnits`, `Regions`, `ResourceTypes`, `Services`) from `join` to the broadcast-optimized `lookup` operator and deduplicated the `Services` mapping per resource type to prevent cost row fan-out ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - **Fixed** - Fixed Data Factory ingestion memory pressure during emptiness filtering. - - Replaced `isnotempty(strcat(x_SkuMeterId, x_SkuOfferId))` with separate `isnotempty()` checks in FinOps hub ingestion scripts to avoid temporary string allocation. - - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings. + - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings ([#2176](https://github.com/microsoft/finops-toolkit/issues/2176)). - Fixed the `ContractedCost` recompute guard to compare with a null-safe tolerance instead of exact float equality, eliminating millions of no-op rewrites that polluted the `x_SourceValues` audit trail while preserving the null-cost backfill and no longer overwriting an existing cost when the unit price is missing ([#2216](https://github.com/microsoft/finops-toolkit/issues/2216)). - - Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default. + - Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). - Fixed managed exports failing with an `Unauthorized` error because the Role Based Access Control Administrator role was never assigned to the Data Factory identity. Roles requested by a hub app are now assigned on the publisher storage account even when the app doesn't create the storage account itself ([#2253](https://github.com/microsoft/finops-toolkit/issues/2253)). - Fixed private-network deployments that Azure Policy blocked when `defaultOutboundAccess` was omitted. Private mode subnets now set it to `false`, while an Azure Files private endpoint supports deployment-script storage and the NAT Gateway provides required container egress ([#2258](https://github.com/microsoft/finops-toolkit/issues/2258), [#2259](https://github.com/microsoft/finops-toolkit/pull/2259)). - Fixed the v1_0 and v1_2 price transforms setting `x_CommitmentDiscountSpendEligibility` from reservation meters and `x_CommitmentDiscountUsageEligibility` from savings plan meters, the opposite of the FOCUS `CommitmentDiscountCategory` value the same transform assigns. The spend column now reports savings plan pricing and the usage column reports reservation pricing. Prices ingested after upgrading carry the corrected values; rows ingested earlier keep the old values until they are reingested ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). @@ -81,7 +76,9 @@ _Released August 2026_ - Switched the InstanceSizeFlexibility table in the storage and KQL shared datasets from the retired `ccmstorageprod` AutofitComboMeterData.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file, joined to reservation recommendations on the unique ARM SKU name ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - **Fixed** - Fixed Power BI storage report refresh errors caused by ISO 8601 duration `x_SkuTerm` values (like `P3Y`) and empty strings in cost exports ([#2174](https://github.com/microsoft/finops-toolkit/issues/2174)). - - Paginate Azure Resource Graph queries by subscription to mitigate [payload size limit](help/errors.md#response-payload-size-is-and-has-exceeded-the-limit) errors in the [Governance](power-bi/governance.md) and [Workload optimization](power-bi/workload-optimization.md) reports ([#1768](https://github.com/microsoft/finops-toolkit/issues/1768)). Queries still surface a payload size error (rather than silently returning truncated results) if a single batch of subscriptions exceeds the limit; see [Reduce the batch size](help/errors.md#option-1-reduce-the-batch-size) for the mitigation. As part of this change, the `AdvisorRecommendations` and `AdvisorReservationRecommendations` tables now return complete (non-truncated) results per batch, matching every other batched table; previously these two tables silently dropped rows beyond the payload limit instead of erroring. + - Paginated Azure Resource Graph queries by subscription to mitigate [payload size limit](help/errors.md#response-payload-size-is-and-has-exceeded-the-limit) errors in the [Governance](power-bi/governance.md) and [Workload optimization](power-bi/workload-optimization.md) reports ([#1768](https://github.com/microsoft/finops-toolkit/issues/1768)). + - The `AdvisorRecommendations` and `AdvisorReservationRecommendations` tables now return complete results per batch instead of silently dropping rows beyond the payload limit. + - A batch that still exceeds the limit surfaces an error rather than truncating; see [Reduce the batch size](help/errors.md#option-1-reduce-the-batch-size). ### [FinOps workbooks](workbooks/finops-workbooks-overview.md) v15 @@ -95,7 +92,7 @@ _Released August 2026_ ### [FinOps alerts](alerts/finops-alerts-overview.md) v15 - **Fixed** - - Made the idle application gateway and idle public IP query join kinds explicit so they no longer rely on the `innerunique` default. + - Made the idle application gateway and idle public IP query join kinds explicit so they no longer rely on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). ### [Optimization engine](optimization-engine/overview.md) v15 @@ -106,6 +103,7 @@ _Released August 2026_ - Switched the reservations and benefits workbooks from the retired `ccmstorageprod` isfratioblob.csv to the FinOps toolkit [Instance size flexibility](open-data.md#instance-size-flexibility) open data file ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - **Fixed** - Removed call to Azure Classic administrators endpoint (deprecated on May 1, 2026) from Azure RBAC assignments exports ([#2142](https://github.com/microsoft/finops-toolkit/issues/2142)). + - Fixed a regression in the `AzureOptimizationConsumptionV1_CL` schema that was breaking the Reservations Usage workbook ([#2301](https://github.com/microsoft/finops-toolkit/issues/2301)). ### [PowerShell module](powershell/powershell-commands.md) v15 diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index aeb54180d..e835f887b 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 08/14/2026 +ms.date: 09/07/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -227,159 +227,159 @@ FinOps hubs does not support directly pulling data from other clouds, but if dat Columns in the **Costs** managed dataset include: -| Column | Type | Notes | -| ---------------------------------------- | --------- | ------------------------------------------------------ | -| AvailabilityZone | String | | -| BilledCost | Real | | -| BillingAccountId | String | | -| BillingAccountName | String | | -| BillingAccountType | String | | -| BillingCurrency | String | | -| BillingPeriodEnd | Date/time | | -| BillingPeriodStart | Date/time | | -| CapacityReservationId | String | New in FOCUS 1.2. | -| CapacityReservationStatus | String | New in FOCUS 1.2. | -| ChargeCategory | String | | -| ChargeClass | String | | -| ChargeDescription | String | | -| ChargeFrequency | String | | -| ChargePeriodEnd | Date/time | | -| ChargePeriodStart | Date/time | | -| CommitmentDiscountCategory | String | | -| CommitmentDiscountId | String | | -| CommitmentDiscountName | String | | -| CommitmentDiscountQuantity | Real | New in FOCUS 1.2. | -| CommitmentDiscountStatus | String | | -| CommitmentDiscountType | String | | -| CommitmentDiscountUnit | String | New in FOCUS 1.2. | -| ConsumedQuantity | Real | | -| ConsumedUnit | String | | -| ContractedCost | Real | | -| ContractedUnitPrice | Real | | -| EffectiveCost | Real | | -| InvoiceId | String | New in FOCUS 1.2. Renamed from x_InvoiceId. | -| InvoiceIssuerName | String | | -| ListCost | Real | | -| ListUnitPrice | Real | | -| PricingCategory | String | | -| PricingCurrency | String | New in FOCUS 1.2. Renamed from x_PricingCurrency. | -| PricingQuantity | Real | | -| PricingUnit | String | | -| ProviderName | String | | -| PublisherName | String | | -| RegionId | String | | -| RegionName | String | | -| ResourceId | String | | -| ResourceName | String | | -| ResourceType | String | | -| ServiceCategory | String | | -| ServiceName | String | | -| ServiceSubcategory | String | New in FOCUS 1.2. | -| SkuId | String | | -| SkuMeter | String | New in FOCUS 1.2. Renamed from x_SkuMeterName. | -| SkuPriceDetails | Dynamic | New in FOCUS 1.2. Transformed version of x_SkuDetails. | -| SkuPriceId | String | | -| SubAccountId | String | | -| SubAccountName | String | | -| SubAccountType | String | | -| Tags | Dynamic | | -| x_AccountId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_AccountName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_AccountOwnerId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_AmortizationClass | String | Source: Microsoft 1.2-preview+. | -| x_BilledCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_BilledUnitPrice | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingAccountAgreement | String | Source: Hubs add-on. | -| x_BillingAccountId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingAccountName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingExchangeRate | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingExchangeRateDate | Date/time | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingItemCode | String | Source: Alibaba 1.0. | -| x_BillingItemName | String | Source: Alibaba 1.0. | -| x_BillingProfileId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_BillingProfileName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ChargeId | String | Source: Microsoft 1.0-preview(v1) only. | -| x_CommitmentDiscountNormalizedRatio | Real | Source: Microsoft 1.2-preview+. | -| x_CommitmentDiscountPercent | String | Source: Hubs add-on. | -| x_CommitmentDiscountSavings | String | Source: Hubs add-on. | -| x_CommitmentDiscountSpendEligibility | String | Source: Hubs add-on. | -| x_CommitmentDiscountUsageEligibility | String | Source: Hubs add-on. | -| x_CommitmentDiscountUtilizationAmount | String | Source: Hubs add-on. | -| x_CommitmentDiscountUtilizationPotential | String | Source: Hubs add-on. | -| x_CommodityCode | String | Source: Alibaba 1.0. | -| x_CommodityName | String | Source: Alibaba 1.0. | -| x_ConsumedCoreHours | Real | Source: Hubs add-on. | -| x_ContractedCostInUsd | Real | Source: Microsoft 1.0+. | -| x_CostAllocationRuleName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_CostCategories | Dynamic | Source: AWS 1.0 (JSON). | -| x_CostCenter | String | Source: Microsoft 1.0-preview(v1)+. | -| x_CostType | String | Source: GCP Jan 2024. | -| x_Credits | Dynamic | Source: GCP Jan 2024. | -| x_CurrencyConversionRate | Real | Source: GCP Jun 2024. | -| x_CustomerId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_CustomerName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_Discount | Dynamic | Source: AWS 1.0 (JSON). | -| x_EffectiveCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_EffectiveUnitPrice | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_ExportTime | Date/time | Source: GCP Jan 2024. | -| x_IngestionTime | Date/time | Source: Hubs add-on. | -| x_InstanceID | String | Source: Alibaba 1.0. | -| x_InvoiceIssuerId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_InvoiceSectionId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_InvoiceSectionName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ListCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_Location | String | Source: GCP Jan 2024. | -| x_NegotiatedDiscountSavings | String | Source: Hubs add-on. | -| x_NegotiatedDiscountPercent | String | Source: Hubs add-on. | -| x_Operation | String | Source: AWS 1.0. | -| x_PartnerCreditApplied | String | Source: Microsoft 1.0-preview(v1)+. | -| x_PartnerCreditRate | String | Source: Microsoft 1.0-preview(v1)+. | -| x_PricingBlockSize | Real | Source: Microsoft 1.0-preview(v1)+. | -| x_PricingSubcategory | String | Source: Microsoft 1.0-preview(v1)+. | -| x_PricingUnitDescription | String | Source: Microsoft 1.0-preview(v1)+. | -| x_Project | String | Source: GCP Jan 2024. | -| x_PublisherCategory | String | Source: Microsoft 1.0-preview(v1)+. | -| x_PublisherId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ResellerId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ResellerName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ResourceGroupName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ResourceType | String | Source: Microsoft 1.0-preview(v1)+. | -| x_ServiceCode | String | Source: AWS 1.0. | -| x_ServiceId | String | Source: GCP Jan 2024. | -| x_ServiceModel | String | Source: Microsoft 1.2-preview+. | -| x_ServicePeriodEnd | Date/time | Source: Microsoft 1.0-preview(v1)+. | -| x_ServicePeriodStart | Date/time | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuCoreCount | String | Source: Hubs add-on. | -| x_SkuDescription | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuDetails | Dynamic | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuInstanceType | String | Source: Hubs add-on. | -| x_SkuIsCreditEligible | Bool | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuLicenseQuantity | String | Source: Hubs add-on. | -| x_SkuLicenseStatus | String | Source: Hubs add-on. | -| x_SkuLicenseType | String | Source: Hubs add-on. | -| x_SkuLicenseUnit | String | Source: Hubs add-on. | -| x_SkuMeterCategory | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuMeterId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuMeterSubcategory | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuOfferId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuOperatingSystem | String | Source: Hubs add-on. | -| x_SkuOrderId | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuOrderName | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuPartNumber | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuPlanName | String | Source: Microsoft 1.2-preview+. | -| x_SkuRegion | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuServiceFamily | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuTerm | Int | Source: Microsoft 1.0-preview(v1)+. | -| x_SkuTier | String | Source: Microsoft 1.0-preview(v1)+. | -| x_SourceChanges | String | Source: Hubs add-on. | -| x_SourceName | String | Source: Hubs add-on. | -| x_SourceProvider | String | Source: Hubs add-on. | -| x_SourceType | String | Source: Hubs add-on. | -| x_SourceValues | Dynamic | Source: Hubs add-on. | -| x_SourceVersion | String | Source: Hubs add-on. | -| x_TotalDiscountPercent | String | Source: Hubs add-on. | -| x_TotalSavings | String | Source: Hubs add-on. | -| x_UsageType | String | Source: AWS 1.0. | +| Column | Type | Notes | +| ---------------------------------------- | --------- | ------------------------------------------------------------ | +| AvailabilityZone | String | | +| BilledCost | Real | | +| BillingAccountId | String | | +| BillingAccountName | String | | +| BillingAccountType | String | | +| BillingCurrency | String | | +| BillingPeriodEnd | Date/time | | +| BillingPeriodStart | Date/time | | +| CapacityReservationId | String | New in FOCUS 1.2. | +| CapacityReservationStatus | String | New in FOCUS 1.2. | +| ChargeCategory | String | | +| ChargeClass | String | | +| ChargeDescription | String | | +| ChargeFrequency | String | | +| ChargePeriodEnd | Date/time | | +| ChargePeriodStart | Date/time | | +| CommitmentDiscountCategory | String | | +| CommitmentDiscountId | String | | +| CommitmentDiscountName | String | | +| CommitmentDiscountQuantity | Real | New in FOCUS 1.2. | +| CommitmentDiscountStatus | String | | +| CommitmentDiscountType | String | | +| CommitmentDiscountUnit | String | New in FOCUS 1.2. | +| ConsumedQuantity | Real | | +| ConsumedUnit | String | | +| ContractedCost | Real | | +| ContractedUnitPrice | Real | | +| EffectiveCost | Real | | +| InvoiceId | String | New in FOCUS 1.2. Renamed from x_InvoiceId. | +| InvoiceIssuerName | String | | +| ListCost | Real | | +| ListUnitPrice | Real | | +| PricingCategory | String | | +| PricingCurrency | String | New in FOCUS 1.2. Renamed from x_PricingCurrency. | +| PricingQuantity | Real | | +| PricingUnit | String | | +| ProviderName | String | | +| PublisherName | String | | +| RegionId | String | | +| RegionName | String | | +| ResourceId | String | | +| ResourceName | String | | +| ResourceType | String | | +| ServiceCategory | String | | +| ServiceName | String | | +| ServiceSubcategory | String | New in FOCUS 1.2. | +| SkuId | String | | +| SkuMeter | String | New in FOCUS 1.2. Renamed from x_SkuMeterName. | +| SkuPriceDetails | Dynamic | New in FOCUS 1.2. Transformed version of x_SkuDetails. | +| SkuPriceId | String | | +| SubAccountId | String | | +| SubAccountName | String | | +| SubAccountType | String | | +| Tags | Dynamic | | +| x_AccountId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_AccountName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_AccountOwnerId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_AmortizationClass | String | Source: Microsoft 1.2-preview+. | +| x_BilledCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_BilledUnitPrice | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingAccountAgreement | String | Source: Hubs add-on. | +| x_BillingAccountId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingAccountName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingExchangeRate | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingExchangeRateDate | Date/time | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingItemCode | String | Source: Alibaba 1.0. | +| x_BillingItemName | String | Source: Alibaba 1.0. | +| x_BillingProfileId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_BillingProfileName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ChargeId | String | Source: Microsoft 1.0-preview(v1) only. | +| x_CommitmentDiscountNormalizedRatio | Real | Source: Microsoft 1.2-preview+. | +| x_CommitmentDiscountPercent | String | Source: Hubs add-on. | +| x_CommitmentDiscountSavings | String | Source: Hubs add-on. | +| x_CommitmentDiscountSpendEligibility | String | Reports savings plan pricing as of v15. Source: Hubs add-on. | +| x_CommitmentDiscountUsageEligibility | String | Reports reservation pricing as of v15. Source: Hubs add-on. | +| x_CommitmentDiscountUtilizationAmount | String | Source: Hubs add-on. | +| x_CommitmentDiscountUtilizationPotential | String | Source: Hubs add-on. | +| x_CommodityCode | String | Source: Alibaba 1.0. | +| x_CommodityName | String | Source: Alibaba 1.0. | +| x_ConsumedCoreHours | Real | Source: Hubs add-on. | +| x_ContractedCostInUsd | Real | Source: Microsoft 1.0+. | +| x_CostAllocationRuleName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_CostCategories | Dynamic | Source: AWS 1.0 (JSON). | +| x_CostCenter | String | Source: Microsoft 1.0-preview(v1)+. | +| x_CostType | String | Source: GCP Jan 2024. | +| x_Credits | Dynamic | Source: GCP Jan 2024. | +| x_CurrencyConversionRate | Real | Source: GCP Jun 2024. | +| x_CustomerId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_CustomerName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_Discount | Dynamic | Source: AWS 1.0 (JSON). | +| x_EffectiveCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_EffectiveUnitPrice | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_ExportTime | Date/time | Source: GCP Jan 2024. | +| x_IngestionTime | Date/time | Source: Hubs add-on. | +| x_InstanceID | String | Source: Alibaba 1.0. | +| x_InvoiceIssuerId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_InvoiceSectionId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_InvoiceSectionName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ListCostInUsd | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_Location | String | Source: GCP Jan 2024. | +| x_NegotiatedDiscountSavings | String | Source: Hubs add-on. | +| x_NegotiatedDiscountPercent | String | Source: Hubs add-on. | +| x_Operation | String | Source: AWS 1.0. | +| x_PartnerCreditApplied | String | Source: Microsoft 1.0-preview(v1)+. | +| x_PartnerCreditRate | String | Source: Microsoft 1.0-preview(v1)+. | +| x_PricingBlockSize | Real | Source: Microsoft 1.0-preview(v1)+. | +| x_PricingSubcategory | String | Source: Microsoft 1.0-preview(v1)+. | +| x_PricingUnitDescription | String | Source: Microsoft 1.0-preview(v1)+. | +| x_Project | String | Source: GCP Jan 2024. | +| x_PublisherCategory | String | Source: Microsoft 1.0-preview(v1)+. | +| x_PublisherId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ResellerId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ResellerName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ResourceGroupName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ResourceType | String | Source: Microsoft 1.0-preview(v1)+. | +| x_ServiceCode | String | Source: AWS 1.0. | +| x_ServiceId | String | Source: GCP Jan 2024. | +| x_ServiceModel | String | Source: Microsoft 1.2-preview+. | +| x_ServicePeriodEnd | Date/time | Source: Microsoft 1.0-preview(v1)+. | +| x_ServicePeriodStart | Date/time | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuCoreCount | String | Source: Hubs add-on. | +| x_SkuDescription | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuDetails | Dynamic | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuInstanceType | String | Source: Hubs add-on. | +| x_SkuIsCreditEligible | Bool | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuLicenseQuantity | String | Source: Hubs add-on. | +| x_SkuLicenseStatus | String | Source: Hubs add-on. | +| x_SkuLicenseType | String | Source: Hubs add-on. | +| x_SkuLicenseUnit | String | Source: Hubs add-on. | +| x_SkuMeterCategory | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuMeterId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuMeterSubcategory | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuOfferId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuOperatingSystem | String | Source: Hubs add-on. | +| x_SkuOrderId | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuOrderName | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuPartNumber | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuPlanName | String | Source: Microsoft 1.2-preview+. | +| x_SkuRegion | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuServiceFamily | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuTerm | Int | Source: Microsoft 1.0-preview(v1)+. | +| x_SkuTier | String | Source: Microsoft 1.0-preview(v1)+. | +| x_SourceChanges | String | Source: Hubs add-on. | +| x_SourceName | String | Source: Hubs add-on. | +| x_SourceProvider | String | Source: Hubs add-on. | +| x_SourceType | String | Source: Hubs add-on. | +| x_SourceValues | Dynamic | Source: Hubs add-on. | +| x_SourceVersion | String | Source: Hubs add-on. | +| x_TotalDiscountPercent | String | Source: Hubs add-on. | +| x_TotalSavings | String | Source: Hubs add-on. | +| x_UsageType | String | Source: AWS 1.0. |
@@ -539,12 +539,12 @@ parse_resourceid('/subscriptions/###/resourceGroups/foo/providers/Microsoft.Comp ```json { - "ResourceId": "/subscriptions/###/resourceGroups/foo/providers/Microsoft.Compute/virtualMachines/bar", - "ResourceName": "bar", - "SubAccountId": "###", - "x_ResourceGroupName": "foo", - "x_ResourceProvider": "Microsoft.Compute", - "x_ResourceType": "microsoft.compute/virtualmachines" + "ResourceId": "/subscriptions/###/resourceGroups/foo/providers/Microsoft.Compute/virtualMachines/bar", + "ResourceName": "bar", + "SubAccountId": "###", + "x_ResourceGroupName": "foo", + "x_ResourceProvider": "Microsoft.Compute", + "x_ResourceType": "microsoft.compute/virtualmachines" } ``` @@ -555,8 +555,8 @@ parse_resourceid('/subscriptions/###/resourceGroups/foo/providers/Microsoft.Comp The **percent(table: (Count: long))** function in Data Explorer calculates the percentage of each record based on a required Count column. { - let total = todouble(toscalar(t | summarize sum(Count))); - percentOfTotal(t, total) +let total = todouble(toscalar(t | summarize sum(Count))); +percentOfTotal(t, total) }
@@ -581,12 +581,12 @@ Examples: ## plusminus() KQL function -The ****plusminus KQL function in Data Explorer a +/- sign based on the direction of the number. -plusminus(val: string) -{ - let neg = substring(val, 0, 1) == '-'; - iff(neg, val, strcat('+', val)) -} +The **plusminus()** KQL function in Data Explorer returns a +/- sign based on the direction of the number. + +Examples: + +- `plusminus(0.5)` = "+0.5" +- `plusminus(-0.5)` = "-0.5"
@@ -630,63 +630,63 @@ The **Prices_raw** table supports Microsoft Cost Management export schemas for E Columns in the **Prices** managed dataset include: -| Column | Type | Notes | -| ------------------------------------ | --------- | ------------------------------------------------- | -| BillingAccountId | String | | -| BillingAccountName | String | | -| BillingCurrency | String | | -| ChargeCategory | String | | -| CommitmentDiscountCategory | String | | -| CommitmentDiscountType | String | | -| CommitmentDiscountUnit | String | | -| ContractedUnitPrice | Real | | -| ListUnitPrice | Real | | -| PricingCategory | String | | -| PricingCurrency | String | New in FOCUS 1.2. Renamed from x_PricingCurrency. | -| PricingUnit | String | | -| SkuId | String | | -| SkuMeter | String | New in FOCUS 1.2. Renamed from x_SkuMeterName. | -| SkuPriceId | String | | -| SkuPriceIdv2 | String | Source: Hubs add-on. | -| x_BaseUnitPrice | Real | Source: Microsoft. | -| x_BillingAccountAgreement | String | Source: Hubs add-on. | -| x_BillingAccountId | String | Source: Microsoft, MCA only. | -| x_BillingProfileId | String | Source: Microsoft, MCA only. | -| x_CommitmentDiscountNormalizedRatio | Real | Source: Hubs add-on. | -| x_CommitmentDiscountSpendEligibility | String | Source: Hubs add-on. | -| x_CommitmentDiscountUsageEligibility | String | Source: Hubs add-on. | -| x_ContractedUnitPriceDiscount | Real | Source: Hubs add-on. | -| x_ContractedUnitPriceDiscountPercent | Real | Source: Hubs add-on. | -| x_EffectivePeriodEnd | Date/time | Source: Microsoft. | -| x_EffectivePeriodStart | Date/time | Source: Microsoft. | -| x_EffectiveUnitPrice | Real | Source: Microsoft. | -| x_EffectiveUnitPriceDiscount | Real | Source: Hubs add-on. | -| x_EffectiveUnitPriceDiscountPercent | Real | Source: Hubs add-on. | -| x_IngestionTime | Date/time | Source: Hubs add-on. | -| x_PricingBlockSize | Real | Source: Hubs add-on. | -| x_PricingSubcategory | String | Source: Hubs add-on. | -| x_PricingUnitDescription | String | Source: Microsoft. | -| x_SkuDescription | String | Source: Microsoft. | -| x_SkuId | String | Source: Microsoft. | -| x_SkuIncludedQuantity | Real | Source: Microsoft, EA only. | -| x_SkuMeterCategory | String | Source: Microsoft. | -| x_SkuMeterId | String | Source: Microsoft. | -| x_SkuMeterSubcategory | String | Source: Microsoft. | -| x_SkuMeterType | String | Source: Microsoft. | -| x_SkuPriceType | String | Source: Microsoft. | -| x_SkuProductId | String | Source: Microsoft. | -| x_SkuRegion | String | Source: Microsoft. | -| x_SkuServiceFamily | String | Source: Microsoft. | -| x_SkuOfferId | String | Source: Microsoft, EA only. | -| x_SkuPartNumber | String | Source: Microsoft, EA only. | -| x_SkuTerm | Int | Source: Microsoft. | -| x_SkuTier | Real | Source: Microsoft, MCA only. | -| x_SourceName | String | Source: Hubs add-on. | -| x_SourceProvider | String | Source: Hubs add-on. | -| x_SourceType | String | Source: Hubs add-on. | -| x_SourceVersion | String | Source: Hubs add-on. | -| x_TotalUnitPriceDiscount | Real | Source: Hubs add-on. | -| x_TotalUnitPriceDiscountPercent | Real | Source: Hubs add-on. | +| Column | Type | Notes | +| ------------------------------------ | --------- | ------------------------------------------------------------ | +| BillingAccountId | String | | +| BillingAccountName | String | | +| BillingCurrency | String | | +| ChargeCategory | String | | +| CommitmentDiscountCategory | String | | +| CommitmentDiscountType | String | | +| CommitmentDiscountUnit | String | | +| ContractedUnitPrice | Real | | +| ListUnitPrice | Real | | +| PricingCategory | String | | +| PricingCurrency | String | New in FOCUS 1.2. Renamed from x_PricingCurrency. | +| PricingUnit | String | | +| SkuId | String | | +| SkuMeter | String | New in FOCUS 1.2. Renamed from x_SkuMeterName. | +| SkuPriceId | String | | +| SkuPriceIdv2 | String | Source: Hubs add-on. | +| x_BaseUnitPrice | Real | Source: Microsoft. | +| x_BillingAccountAgreement | String | Source: Hubs add-on. | +| x_BillingAccountId | String | Source: Microsoft, MCA only. | +| x_BillingProfileId | String | Source: Microsoft, MCA only. | +| x_CommitmentDiscountNormalizedRatio | Real | Source: Hubs add-on. | +| x_CommitmentDiscountSpendEligibility | String | Reports savings plan pricing as of v15. Source: Hubs add-on. | +| x_CommitmentDiscountUsageEligibility | String | Reports reservation pricing as of v15. Source: Hubs add-on. | +| x_ContractedUnitPriceDiscount | Real | Source: Hubs add-on. | +| x_ContractedUnitPriceDiscountPercent | Real | Source: Hubs add-on. | +| x_EffectivePeriodEnd | Date/time | Source: Microsoft. | +| x_EffectivePeriodStart | Date/time | Source: Microsoft. | +| x_EffectiveUnitPrice | Real | Source: Microsoft. | +| x_EffectiveUnitPriceDiscount | Real | Source: Hubs add-on. | +| x_EffectiveUnitPriceDiscountPercent | Real | Source: Hubs add-on. | +| x_IngestionTime | Date/time | Source: Hubs add-on. | +| x_PricingBlockSize | Real | Source: Hubs add-on. | +| x_PricingSubcategory | String | Source: Hubs add-on. | +| x_PricingUnitDescription | String | Source: Microsoft. | +| x_SkuDescription | String | Source: Microsoft. | +| x_SkuId | String | Source: Microsoft. | +| x_SkuIncludedQuantity | Real | Source: Microsoft, EA only. | +| x_SkuMeterCategory | String | Source: Microsoft. | +| x_SkuMeterId | String | Source: Microsoft. | +| x_SkuMeterSubcategory | String | Source: Microsoft. | +| x_SkuMeterType | String | Source: Microsoft. | +| x_SkuPriceType | String | Source: Microsoft. | +| x_SkuProductId | String | Source: Microsoft. | +| x_SkuRegion | String | Source: Microsoft. | +| x_SkuServiceFamily | String | Source: Microsoft. | +| x_SkuOfferId | String | Source: Microsoft, EA only. | +| x_SkuPartNumber | String | Source: Microsoft, EA only. | +| x_SkuTerm | Int | Source: Microsoft. | +| x_SkuTier | Real | Source: Microsoft, MCA only. | +| x_SourceName | String | Source: Hubs add-on. | +| x_SourceProvider | String | Source: Hubs add-on. | +| x_SourceType | String | Source: Hubs add-on. | +| x_SourceVersion | String | Source: Hubs add-on. | +| x_TotalUnitPriceDiscount | Real | Source: Hubs add-on. | +| x_TotalUnitPriceDiscountPercent | Real | Source: Hubs add-on. |
diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index 9c3e1cabb..4d79e1aa6 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 09/07/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit @@ -183,6 +183,11 @@ Transforms: - `x_CommitmentDiscountPercent` - `x_TotalDiscountPercent` - Added `x_SourceValues` to track data changes during ingestion. +- v15+: + - Select the highest on-demand price when duplicate price rows collapse under a single reservation price lookup key, so commitment discount savings aren't understated. + - Only recompute `ContractedCost` when it differs beyond a null-safe tolerance, avoiding no-op rewrites that polluted the `x_SourceValues` audit trail. + - Enrich dimensions (`PricingUnits`, `Regions`, `ResourceTypes`, `Services`) with `lookup` instead of `join`. + - Deduplicate the `Services` mapping per resource type to prevent cost row fan-out. ### Price data transforms @@ -217,6 +222,8 @@ Transforms: - Renamed `x_SkuMeterName` to `SkuMeter`. - Implemented the following columns when not set by Cost Management: - `CommitmentDiscountUnit` +- v15+: + - Swap `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` to match the `CommitmentDiscountCategory` value assigned by the same transform. The spend column now reports savings plan pricing and the usage column reports reservation pricing. Prices ingested before upgrading keep the old values until they're reingested. ### Recommendation data transforms diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index eb7b112d6..07f16426d 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 09/07/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit @@ -162,6 +162,8 @@ Replace the use of deprecated columns and functions: | 0.7 | N/A | `daterange()` | `datestring(datetime, [datetime])` | | 0.7 | N/A | `monthsago()` | `startofmonth(datetime, [offset])` | +As of FinOps hubs v15, `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` were fixed to use the correct values. In previous releases, they were accurate, but swapped. `x_CommitmentDiscountSpendEligibility` now represents savings plan eligibility correctly and `x_CommitmentDiscountUsageEligibility` represents reservation eligibility. Before v15, the values were saved to the wrong columns. Prices ingested before upgrading keep the old values. You will need to re-ingest older months to fix these rows. + If using unversioned functions or updating from the `v1_0` schema version, review your code for any explicit use of the `decimal` data type and replace it with `real`. As of FinOps hubs v12 (schema version `v1_2`), all `decimal` data types changed to `real` to improve performance. To learn more about schema versions, see [About schema versions](data-model.md#schema-version). If updating queries to use a newer schema version, use the following table to understand the changes introduced in each schema version for each managed dataset. diff --git a/docs/README.md b/docs/README.md index 6852359a2..aad161ccf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,9 +18,9 @@ Automate and extend the Microsoft Cloud with starter kits, scripts, and advanced ---
-

What's new in August 2026v15

+

What's new in September 2026v15

- August adds a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI, private networking support for FinOps hubs, a new Instance size flexibility open dataset, several KQL performance and correctness fixes across hubs, workbooks, and alerts, and minor improvements in Power BI, the Optimization Engine, and the PowerShell module. + September adds a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI, private networking support for FinOps hubs, a new Instance size flexibility open dataset, several KQL performance and correctness fixes across hubs, workbooks, and alerts, and minor improvements in Power BI, the Optimization Engine, and the PowerShell module.

See all changes

diff --git a/docs/agent-plugin.md b/docs/agent-plugin.md index 3ab7a24f0..10f033c94 100644 --- a/docs/agent-plugin.md +++ b/docs/agent-plugin.md @@ -20,7 +20,7 @@ Bring AI-powered cloud financial management to Claude Code and GitHub Copilot CL The FinOps toolkit agent plugin pairs role-specific agents, ready-to-run commands, and a FinOps hubs query skill with a read-only Azure MCP server, so you can analyze cost data, review recommendations, and manage FinOps hubs without leaving your terminal.
-

What's new in August 2026v15

+

What's new in September 2026v15

The FinOps toolkit agent plugin is new this release: a shared plugin for Claude Code and GitHub Copilot CLI with 5 agents (CFO, FinOps practitioner, database query, hubs agent, and Azure capacity manager), 4 commands, a FinOps hubs KQL skill, and a read-only Azure MCP server.

diff --git a/docs/alerts.md b/docs/alerts.md index 2d33581e0..57d0cebfc 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -20,9 +20,9 @@ Accelerate your cost optimization efforts with scheduled notifications that cont Designed with flexibility in mind, FinOps alerts run on a fully configurable schedule, continuously scanning your selected subscriptions for idle resources.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, FinOps alerts made join kinds explicit in the idle application gateway and idle public IP queries so they no longer rely on the default join behavior. + In September, FinOps alerts made join kinds explicit in the idle application gateway and idle public IP queries so they no longer rely on the default join behavior.

See all changes

diff --git a/docs/guide.md b/docs/guide.md index 85bd3ef4b..8f711333a 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -16,9 +16,9 @@ Your guide to implementing FinOps in the Microsoft Cloud. FinOps is an expansive area with branches that extend out into many cloud management and governance activities. Cost especially is a part of many cloud and business initiatives, like security, resiliency, and adopting cloud-native solutions like AI. And to streamline cloud adoption in a way that prioritizes business value, you need the right guidance to help you make the right data-driven decisions. The Implementing FinOps guide is a collection of resources that help you facilitate your FinOps goals.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, the Implementing FinOps guide added a FinOps toolkit ecosystem page and aligned capability names and links to current FinOps Framework terminology. + In September, the Implementing FinOps guide added a FinOps toolkit ecosystem page and aligned capability names and links to current FinOps Framework terminology.

See all changes

diff --git a/docs/hubs.md b/docs/hubs.md index dae52d916..c8cd654c6 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -20,9 +20,9 @@ Open, extensible, and scalable cost governance for the enterprise. FinOps hubs are a reliable, trustworthy platform for cost analytics, insights, and optimization – virtual command centers for leaders throughout the organization to report on, monitor, and optimize cost based on their organizational needs.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, FinOps hubs added VNet and private network modes with optional NAT Gateway support, a guide to run FinOps hubs using a local Kusto emulator, and fixed several KQL join and cost recompute issues. + In September, FinOps hubs added VNet and private network modes with optional NAT Gateway support, a guide to run FinOps hubs using a local Kusto emulator, and fixed several KQL join, managed export permission, and cost recompute issues.

See all changes

diff --git a/docs/open-data.md b/docs/open-data.md index 97702ca4d..c6eca644b 100644 --- a/docs/open-data.md +++ b/docs/open-data.md @@ -20,9 +20,9 @@ Leverage open data to normalize and enhance your FinOps reporting. FinOps toolkit open data is used to transform Cost Management actual and amortized data into FOCUS. Use the same mappings to clean your FinOps datasets.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, open data added a new Instance size flexibility dataset sourced from the Azure Reservations Catalogs API, replacing the deprecated ISF ratio files, and fixed the commitment discount eligibility dataset refresh to be reproducible and complete. + In September, open data added a new Instance size flexibility dataset sourced from the Azure Reservations Catalogs API, replacing the deprecated ISF ratio files, and fixed the commitment discount eligibility dataset refresh to be reproducible and complete, including a fix for the spend and usage eligibility columns carrying each other's values.

See all changes

diff --git a/docs/optimization-engine.md b/docs/optimization-engine.md index ae9b90fa9..2d313d3aa 100644 --- a/docs/optimization-engine.md +++ b/docs/optimization-engine.md @@ -20,9 +20,9 @@ Optimize your Azure environment. The Azure Optimization Engine (AOE) is an extensible solution designed to generate optimization recommendations for your Azure environment. See it like a fully customizable Azure Advisor.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, Azure Optimization Engine migrated Log Analytics ingestion to a Data Collection Rule-based solution, switched to the new Instance size flexibility open dataset, and removed a deprecated Azure Classic administrators API call. + In September, Azure Optimization Engine added a comprehensive reference for runbooks, schedules, variables, and tables, migrated Log Analytics ingestion to a Data Collection Rule-based solution, switched to the new Instance size flexibility open dataset, and removed a deprecated Azure Classic administrators API call.

diff --git a/docs/power-bi.md b/docs/power-bi.md index 55f4a9e60..5e45826db 100644 --- a/docs/power-bi.md +++ b/docs/power-bi.md @@ -20,9 +20,9 @@ Accelerate your analytics efforts with simple, targeted reports. Summarize and b FinOps toolkit Power BI reports provide a great starting point for FinOps reporting. Customize and augment reports with your own data to facilitate organizational requirements.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, Power BI reports switched to the new Instance size flexibility open dataset, fixed storage report refresh errors from ISO 8601 duration values, and paginated Azure Resource Graph queries to avoid payload size errors. + In September, Power BI reports switched to the new Instance size flexibility open dataset, fixed storage report refresh errors from ISO 8601 duration values, and paginated Azure Resource Graph queries to avoid payload size errors.

See all changes

diff --git a/docs/powershell.md b/docs/powershell.md index 2db1bcd74..b3ad52d45 100644 --- a/docs/powershell.md +++ b/docs/powershell.md @@ -20,9 +20,9 @@ Automate and scale your FinOps efforts with PowerShell commands that streamline The FinOps toolkit PowerShell module helps you automate and scale common Cost Management and FinOps toolkit management operations and work with FinOps toolkit open data.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, the PowerShell module added Initialize-FinOpsHubLocal for setting up a local FinOps hub with one command, updated test compatibility with Pester 6, and fixed Get-FinOpsCostExport -RunHistory to return the complete run history. + In September, the PowerShell module added Initialize-FinOpsHubLocal for setting up a local FinOps hub with one command, updated test compatibility with Pester 6, and fixed Get-FinOpsCostExport -RunHistory and Start-FinOpsCostExport date handling for UTC offsets.

See all changes

diff --git a/docs/workbooks.md b/docs/workbooks.md index 3210c5d09..f313d9e97 100644 --- a/docs/workbooks.md +++ b/docs/workbooks.md @@ -20,9 +20,9 @@ Engineering hub to maximize cloud ROI through FinOps. FinOps workbooks are Azure workbooks that provide a series of tools to help engineers perform targeted FinOps tasks, modeled after the Well-Architected Framework guidance.
-

What's new in August 2026v15

+

What's new in September 2026v15

- In August, FinOps workbooks fixed savings plan and Azure Hybrid Benefit queries that were silently dropping or missing recommendations due to implicit join behavior. + In September, FinOps workbooks fixed savings plan and Azure Hybrid Benefit queries that were silently dropping or missing recommendations due to implicit join behavior.

See all changes

From 38a9bbe3519a6dc559f971934d98e86f1da78cea Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Mon, 7 Sep 2026 17:04:34 -0700 Subject: [PATCH 08/28] Fix PathNotFound error in msexports ETL ingestion on first run (#2247) Co-authored-by: Claude Sonnet 5 --- .../modules/Microsoft.CostManagement/Exports/app.bicep | 5 +++-- .../modules/Microsoft.FinOpsHubs/Analytics/app.bicep | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep b/src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep index b372acac7..015eef74a 100644 --- a/src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep +++ b/src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep @@ -1109,6 +1109,7 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = { } } fieldList: [ + 'exists' 'childItems' ] storeSettings: { @@ -1128,14 +1129,14 @@ resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' existing = { { activity: 'Get Existing Parquet Files' dependencyConditions: [ - 'Completed' + 'Succeeded' ] } ] userProperties: [] typeProperties: { items: { - value: '@if(contains(activity(\'Get Existing Parquet Files\').output, \'childItems\'), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' + value: '@if(and(activity(\'Get Existing Parquet Files\').output.exists, contains(activity(\'Get Existing Parquet Files\').output, \'childItems\')), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' type: 'Expression' } condition: { diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep index 84a29d95c..b87b230a1 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep @@ -1694,6 +1694,7 @@ resource pipeline_ExecuteIngestionETL 'Microsoft.DataFactory/factories/pipelines } } fieldList: [ + 'exists' 'childItems' ] storeSettings: { @@ -1720,7 +1721,7 @@ resource pipeline_ExecuteIngestionETL 'Microsoft.DataFactory/factories/pipelines userProperties: [] typeProperties: { items: { - value: '@if(contains(activity(\'Get Existing Parquet Files\').output, \'childItems\'), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' + value: '@if(and(activity(\'Get Existing Parquet Files\').output.exists, contains(activity(\'Get Existing Parquet Files\').output, \'childItems\')), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' type: 'Expression' } condition: { From 6e33ed6f52e6210c7b47e4c0cad7312db7ee9a93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Oct 2026 14:44:03 +0000 Subject: [PATCH 09/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index 159686331..eddd05bc2 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 08/14/2026 +ms.date: 10/12/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index 99e1cc6e4..370ffff25 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 08/14/2026 +ms.date: 10/12/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index 00311561c..6b2f77fa0 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 08/14/2026 +ms.date: 10/12/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index 16790868c..11fc82a58 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 09/07/2026 +ms.date: 10/12/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index 4d79e1aa6..215488382 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 09/07/2026 +ms.date: 10/12/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 07f16426d..31c3d5b7d 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 09/07/2026 +ms.date: 10/12/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From 42484fc4a3c445be58ae1028f9e3545472849a26 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 14 Oct 2026 01:12:37 -0700 Subject: [PATCH 10/28] docs: update v15 changelog and what's new blurbs for October Co-Authored-By: Claude Sonnet 5.5 --- docs-mslearn/toolkit/changelog.md | 7 ++++--- docs/README.md | 4 ++-- docs/agent-plugin.md | 2 +- docs/alerts.md | 4 ++-- docs/guide.md | 4 ++-- docs/hubs.md | 4 ++-- docs/open-data.md | 4 ++-- docs/optimization-engine.md | 4 ++-- docs/power-bi.md | 4 ++-- docs/powershell.md | 4 ++-- docs/workbooks.md | 4 ++-- 11 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index d94540d4d..d964dab05 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -31,7 +31,7 @@ The following section lists features and enhancements that are currently in deve ## v15 -_Released September 2026_ +_Released October 2026_ ### [Implementing FinOps guide](../implementing-finops-guide.md) v15 @@ -112,6 +112,7 @@ _Released September 2026_ - **Added** - Added [Initialize-FinOpsHubLocal](powershell/hubs/initialize-finopshublocal.md) to set up a local FinOps hub in a running Kusto emulator with one command. + - Added [Get-FinOpsCommitmentDiscountEligibility](powershell/data/get-finopscommitmentdiscounteligibility.md) and [Get-FinOpsInstanceSizeFlexibility](powershell/data/get-finopsinstancesizeflexibility.md) to generate the commitment discount eligibility and instance size flexibility open data files ([#2298](https://github.com/microsoft/finops-toolkit/pull/2298)). - **Changed** - Updated PowerShell test compatibility with Pester 6 ([#2204](https://github.com/microsoft/finops-toolkit/pull/2204)). - **Fixed** @@ -124,7 +125,7 @@ _Released September 2026_ - **Added** - Added a new [Instance size flexibility](open-data.md#instance-size-flexibility) dataset that maps each ARM SKU to its instance size flexibility group and ratio, sourced from the Azure Reservations Catalogs API. It replaces the deprecated ISF ratio files hosted on `ccmstorageprod.blob.core.windows.net` ([#2090](https://github.com/microsoft/finops-toolkit/issues/2090)). - **Fixed** - - Fixed the commitment discount eligibility dataset refresh so it is reproducible and complete; retired meters now age out and previously missed meters are included ([#2164](https://github.com/microsoft/finops-toolkit/pull/2164)). + - Fixed the [instance size flexibility](open-data.md#instance-size-flexibility) ratio scale and region coverage, and made regeneration produce the same output on any host. The dataset grew from 1,353 to 1,363 SKUs ([#2308](https://github.com/microsoft/finops-toolkit/pull/2308), [#2331](https://github.com/microsoft/finops-toolkit/pull/2331)). - Fixed the [commitment discount eligibility](open-data.md#commitment-discount-eligibility) dataset refresh so it's reproducible and complete; retired meters now age out and previously missed meters are included ([#2164](https://github.com/microsoft/finops-toolkit/pull/2164)). - Fixed the weekly commitment discount eligibility refresh timing out before it could publish, which left the dataset unchanged since it first shipped in v14. The refresh now walks each price type directly instead of sharding by service family, and verifies completeness by comparing two independent traversals before writing ([#2251](https://github.com/microsoft/finops-toolkit/pull/2251)). - Fixed `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` carrying each other's values. FOCUS classifies a reservation as a usage commitment (committed to a quantity of usage) and a savings plan as a spend commitment (committed to an amount of money), so the spend column now reports savings plan pricing and the usage column reports reservation pricing. The dataset shipped with the two reversed from v14 ([#2279](https://github.com/microsoft/finops-toolkit/issues/2279)). diff --git a/docs/README.md b/docs/README.md index 5c152e69e..229a28a1c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,9 +18,9 @@ Automate and extend the Microsoft Cloud with starter kits, scripts, and advanced ---
-

What's new in September 2026v15

+

What's new in October 2026v15

- September adds a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI, private networking support for FinOps hubs, a new Instance size flexibility open dataset, several KQL performance and correctness fixes across hubs, workbooks, and alerts, and minor improvements in Power BI, the Optimization Engine, and the PowerShell module. + October adds a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI, private networking support for FinOps hubs, a new Instance size flexibility open dataset, several KQL performance and correctness fixes across hubs, workbooks, and alerts, and minor improvements in Power BI, the Optimization Engine, and the PowerShell module.

See all changes

diff --git a/docs/agent-plugin.md b/docs/agent-plugin.md index 10f033c94..b596054a3 100644 --- a/docs/agent-plugin.md +++ b/docs/agent-plugin.md @@ -20,7 +20,7 @@ Bring AI-powered cloud financial management to Claude Code and GitHub Copilot CL The FinOps toolkit agent plugin pairs role-specific agents, ready-to-run commands, and a FinOps hubs query skill with a read-only Azure MCP server, so you can analyze cost data, review recommendations, and manage FinOps hubs without leaving your terminal.
-

What's new in September 2026v15

+

What's new in October 2026v15

The FinOps toolkit agent plugin is new this release: a shared plugin for Claude Code and GitHub Copilot CLI with 5 agents (CFO, FinOps practitioner, database query, hubs agent, and Azure capacity manager), 4 commands, a FinOps hubs KQL skill, and a read-only Azure MCP server.

diff --git a/docs/alerts.md b/docs/alerts.md index 57d0cebfc..6d4d77de2 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -20,9 +20,9 @@ Accelerate your cost optimization efforts with scheduled notifications that cont Designed with flexibility in mind, FinOps alerts run on a fully configurable schedule, continuously scanning your selected subscriptions for idle resources.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, FinOps alerts made join kinds explicit in the idle application gateway and idle public IP queries so they no longer rely on the default join behavior. + In October, FinOps alerts made join kinds explicit in the idle application gateway and idle public IP queries so they no longer rely on the default join behavior.

See all changes

diff --git a/docs/guide.md b/docs/guide.md index 8f711333a..d29e15b0c 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -16,9 +16,9 @@ Your guide to implementing FinOps in the Microsoft Cloud. FinOps is an expansive area with branches that extend out into many cloud management and governance activities. Cost especially is a part of many cloud and business initiatives, like security, resiliency, and adopting cloud-native solutions like AI. And to streamline cloud adoption in a way that prioritizes business value, you need the right guidance to help you make the right data-driven decisions. The Implementing FinOps guide is a collection of resources that help you facilitate your FinOps goals.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, the Implementing FinOps guide added a FinOps toolkit ecosystem page and aligned capability names and links to current FinOps Framework terminology. + In October, the Implementing FinOps guide added a FinOps toolkit ecosystem page and aligned capability names and links to current FinOps Framework terminology.

See all changes

diff --git a/docs/hubs.md b/docs/hubs.md index c8cd654c6..5bbb21df8 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -20,9 +20,9 @@ Open, extensible, and scalable cost governance for the enterprise. FinOps hubs are a reliable, trustworthy platform for cost analytics, insights, and optimization – virtual command centers for leaders throughout the organization to report on, monitor, and optimize cost based on their organizational needs.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, FinOps hubs added VNet and private network modes with optional NAT Gateway support, a guide to run FinOps hubs using a local Kusto emulator, and fixed several KQL join, managed export permission, and cost recompute issues. + In October, FinOps hubs added VNet and private network modes with optional NAT Gateway support, a guide to run FinOps hubs using a local Kusto emulator, and fixed several KQL join, managed export permission, and cost recompute issues.

See all changes

diff --git a/docs/open-data.md b/docs/open-data.md index 30cb2ab17..11ef6dbd8 100644 --- a/docs/open-data.md +++ b/docs/open-data.md @@ -20,9 +20,9 @@ Leverage open data to normalize and enhance your FinOps reporting. FinOps toolkit open data is used to transform Cost Management actual and amortized data into FOCUS. Use the same mappings to clean your FinOps datasets.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, open data added a new Instance size flexibility dataset sourced from the Azure Reservations Catalogs API, replacing the deprecated ISF ratio files, and fixed the commitment discount eligibility dataset refresh to be reproducible and complete, including a fix for the spend and usage eligibility columns carrying each other's values. + In October, open data added a new Instance size flexibility dataset sourced from the Azure Reservations Catalogs API, replacing the deprecated ISF ratio files, fixed the commitment discount eligibility dataset refresh and its spend and usage columns, and corrected instance size flexibility ratios and region coverage.

See all changes

diff --git a/docs/optimization-engine.md b/docs/optimization-engine.md index 2d313d3aa..38013b494 100644 --- a/docs/optimization-engine.md +++ b/docs/optimization-engine.md @@ -20,9 +20,9 @@ Optimize your Azure environment. The Azure Optimization Engine (AOE) is an extensible solution designed to generate optimization recommendations for your Azure environment. See it like a fully customizable Azure Advisor.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, Azure Optimization Engine added a comprehensive reference for runbooks, schedules, variables, and tables, migrated Log Analytics ingestion to a Data Collection Rule-based solution, switched to the new Instance size flexibility open dataset, and removed a deprecated Azure Classic administrators API call. + In October, Azure Optimization Engine added a comprehensive reference for runbooks, schedules, variables, and tables, migrated Log Analytics ingestion to a Data Collection Rule-based solution, switched to the new Instance size flexibility open dataset, and removed a deprecated Azure Classic administrators API call.

diff --git a/docs/power-bi.md b/docs/power-bi.md index 5e45826db..14aac976b 100644 --- a/docs/power-bi.md +++ b/docs/power-bi.md @@ -20,9 +20,9 @@ Accelerate your analytics efforts with simple, targeted reports. Summarize and b FinOps toolkit Power BI reports provide a great starting point for FinOps reporting. Customize and augment reports with your own data to facilitate organizational requirements.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, Power BI reports switched to the new Instance size flexibility open dataset, fixed storage report refresh errors from ISO 8601 duration values, and paginated Azure Resource Graph queries to avoid payload size errors. + In October, Power BI reports switched to the new Instance size flexibility open dataset, fixed storage report refresh errors from ISO 8601 duration values, and paginated Azure Resource Graph queries to avoid payload size errors.

See all changes

diff --git a/docs/powershell.md b/docs/powershell.md index b3ad52d45..3d755e93e 100644 --- a/docs/powershell.md +++ b/docs/powershell.md @@ -20,9 +20,9 @@ Automate and scale your FinOps efforts with PowerShell commands that streamline The FinOps toolkit PowerShell module helps you automate and scale common Cost Management and FinOps toolkit management operations and work with FinOps toolkit open data.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, the PowerShell module added Initialize-FinOpsHubLocal for setting up a local FinOps hub with one command, updated test compatibility with Pester 6, and fixed Get-FinOpsCostExport -RunHistory and Start-FinOpsCostExport date handling for UTC offsets. + In October, the PowerShell module added Initialize-FinOpsHubLocal for setting up a local FinOps hub with one command, plus commands to generate the commitment discount eligibility and instance size flexibility open data, updated test compatibility with Pester 6, and fixed Get-FinOpsCostExport -RunHistory and Start-FinOpsCostExport date handling for UTC offsets.

See all changes

diff --git a/docs/workbooks.md b/docs/workbooks.md index f313d9e97..58e795a87 100644 --- a/docs/workbooks.md +++ b/docs/workbooks.md @@ -20,9 +20,9 @@ Engineering hub to maximize cloud ROI through FinOps. FinOps workbooks are Azure workbooks that provide a series of tools to help engineers perform targeted FinOps tasks, modeled after the Well-Architected Framework guidance.
-

What's new in September 2026v15

+

What's new in October 2026v15

- In September, FinOps workbooks fixed savings plan and Azure Hybrid Benefit queries that were silently dropping or missing recommendations due to implicit join behavior. + In October, FinOps workbooks fixed savings plan and Azure Hybrid Benefit queries that were silently dropping or missing recommendations due to implicit join behavior.

See all changes

From aeb0b114cd13e907e19b4dffe8f3bd8269625ff9 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 14 Oct 2026 01:12:37 -0700 Subject: [PATCH 11/28] chore: keep marketplace.json versions three-part during version updates Add an opt-in -AsSemVer switch to Get-Version and use it for marketplace.json so Update-Version stops rewriting 15.0.0 as 15.0. Co-Authored-By: Claude Sonnet 5.5 --- src/scripts/Get-Version.ps1 | 14 ++++++++++++++ src/scripts/Update-Version.ps1 | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/scripts/Get-Version.ps1 b/src/scripts/Get-Version.ps1 index 90ad37c7f..452f681fc 100644 --- a/src/scripts/Get-Version.ps1 +++ b/src/scripts/Get-Version.ps1 @@ -16,6 +16,9 @@ .PARAMETER AsTag Optional. Returns the version as a git tag (e.g., "v13" instead of "13.0"). Default = false. + .PARAMETER AsSemVer + Optional. Returns a full three-part semantic version (e.g., "13.0.0" instead of "13.0") for files that require it, like marketplace.json. Default = false. + .PARAMETER AsDotNetVersion Optional. Indicates that the returned version should be in the format "x.x.x.x". Otherwise, semantic versioning is used. Default = false. @@ -33,6 +36,9 @@ param( [switch] $AsTag, + [switch] + $AsSemVer, + [switch] $AsDotNetVersion ) @@ -48,6 +54,14 @@ if ($AsTag) return 'v' + (($ver -replace '-.*$', '') -replace '\.0$', '') } +if ($AsSemVer) +{ + # Pad major.minor to major.minor.patch and keep any prerelease label + $base = ($ver -replace '-.*$', '') -split '\.' + while ($base.Count -lt 3) { $base += '0' } + return ($base[0..2] -join '.') + ($ver -replace '^[^-]*', '') +} + if ($AsDotNetVersion -and $ver.Contains('-')) { $arr = (($ver -replace '-[^\.0-9]+', '') -split '\.') + @(0, 0) diff --git a/src/scripts/Update-Version.ps1 b/src/scripts/Update-Version.ps1 index ee814b78e..03ddb091b 100644 --- a/src/scripts/Update-Version.ps1 +++ b/src/scripts/Update-Version.ps1 @@ -141,6 +141,7 @@ if ($update -or $Version) # Update version in marketplace.json plugin entries Write-Verbose "Updating marketplace.json files..." + $semVer = & "$PSScriptRoot/Get-Version" -AsSemVer Get-RepositoryVersionFiles "marketplace.json" ` | ForEach-Object { $json = Get-Content $_ -Raw | ConvertFrom-Json @@ -149,13 +150,13 @@ if ($update -or $Version) Write-Verbose "- $($_.FullName.Replace($repoRoot + [IO.Path]::DirectorySeparatorChar, ''))" if ($json.PSObject.Properties['metadata'] -and $json.metadata.PSObject.Properties['version']) { - $json.metadata.version = $ver + $json.metadata.version = $semVer } foreach ($plugin in $json.plugins) { if ($plugin.PSObject.Properties['version']) { - $plugin.version = $ver + $plugin.version = $semVer } } $json | ConvertTo-Json -Depth 10 | Out-File $_ -Encoding utf8 -NoNewline From 4a3c5a6d191f52006b24f86b4cbe4940793f92ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Oct 2026 08:14:38 +0000 Subject: [PATCH 12/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index eddd05bc2..0bbf4be5f 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index 370ffff25..befb94deb 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index 6b2f77fa0..04325eb7b 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index 11fc82a58..c16ab287e 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index 215488382..f941c32ee 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 31c3d5b7d..1c7dfa099 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 10/12/2026 +ms.date: 10/14/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From 5a9b60d8d58666405587d056ebca3f190a5b7a26 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Thu, 15 Oct 2026 10:27:06 -0700 Subject: [PATCH 13/28] fix(power-bi): repoint rate optimization data quality link Refs #2319 Co-Authored-By: Claude Opus 5.5 --- docs-mslearn/toolkit/changelog.md | 3 ++- src/power-bi/kql/RateOptimization.Report/report.json | 2 +- src/power-bi/storage/RateOptimization.Report/report.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 1bece4fe5..a2af06875 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -81,6 +81,7 @@ _Released October 2026_ - Paginated Azure Resource Graph queries by subscription to mitigate [payload size limit](help/errors.md#response-payload-size-is-and-has-exceeded-the-limit) errors in the [Governance](power-bi/governance.md) and [Workload optimization](power-bi/workload-optimization.md) reports ([#1768](https://github.com/microsoft/finops-toolkit/issues/1768)). - The `AdvisorRecommendations` and `AdvisorReservationRecommendations` tables now return complete results per batch instead of silently dropping rows beyond the payload limit. - A batch that still exceeds the limit surfaces an error rather than truncating; see [Reduce the batch size](help/errors.md#option-1-reduce-the-batch-size). + - Fixed the rate optimization report data quality link, which landed on a Bing search page ([#2319](https://github.com/microsoft/finops-toolkit/issues/2319)). ### [FinOps workbooks](workbooks/finops-workbooks-overview.md) v15 diff --git a/src/power-bi/kql/RateOptimization.Report/report.json b/src/power-bi/kql/RateOptimization.Report/report.json index f5ca03c6b..15c8cb87a 100644 --- a/src/power-bi/kql/RateOptimization.Report/report.json +++ b/src/power-bi/kql/RateOptimization.Report/report.json @@ -4479,7 +4479,7 @@ "z": 86000.0 }, { - "config": "{\"name\":\"84837d48720ae6705601\",\"layouts\":[{\"id\":0,\"position\":{\"x\":1232.002186090996,\"y\":-0.8809946714031502,\"z\":19000,\"width\":48,\"height\":48,\"tabOrder\":19000}}],\"singleVisual\":{\"visualType\":\"actionButton\",\"drillFilterOtherVisuals\":true,\"objects\":{\"icon\":[{\"properties\":{\"shapeType\":{\"expr\":{\"Literal\":{\"Value\":\"'information'\"}}},\"placement\":{\"expr\":{\"Literal\":{\"Value\":\"'right'\"}}},\"lineWeight\":{\"expr\":{\"Literal\":{\"Value\":\"2L\"}}},\"lineColor\":{\"solid\":{\"color\":{\"expr\":{\"ThemeDataColor\":{\"ColorId\":0,\"Percent\":0}}}}}},\"selector\":{\"id\":\"default\"}},{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}}}}]},\"vcObjects\":{\"background\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"false\"}}}}}],\"visualLink\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}},\"type\":{\"expr\":{\"Literal\":{\"Value\":\"'WebUrl'\"}}},\"webUrl\":{\"expr\":{\"Literal\":{\"Value\":\"'https://aka.ms/ftk/rateoptimization#dq'\"}}},\"tooltip\":{\"expr\":{\"Literal\":{\"Value\":\"'About this page'\"}}}}}]}}}", + "config": "{\"name\":\"84837d48720ae6705601\",\"layouts\":[{\"id\":0,\"position\":{\"x\":1232.002186090996,\"y\":-0.8809946714031502,\"z\":19000,\"width\":48,\"height\":48,\"tabOrder\":19000}}],\"singleVisual\":{\"visualType\":\"actionButton\",\"drillFilterOtherVisuals\":true,\"objects\":{\"icon\":[{\"properties\":{\"shapeType\":{\"expr\":{\"Literal\":{\"Value\":\"'information'\"}}},\"placement\":{\"expr\":{\"Literal\":{\"Value\":\"'right'\"}}},\"lineWeight\":{\"expr\":{\"Literal\":{\"Value\":\"2L\"}}},\"lineColor\":{\"solid\":{\"color\":{\"expr\":{\"ThemeDataColor\":{\"ColorId\":0,\"Percent\":0}}}}}},\"selector\":{\"id\":\"default\"}},{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}}}}]},\"vcObjects\":{\"background\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"false\"}}}}}],\"visualLink\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}},\"type\":{\"expr\":{\"Literal\":{\"Value\":\"'WebUrl'\"}}},\"webUrl\":{\"expr\":{\"Literal\":{\"Value\":\"'https://aka.ms/ftk/pbi/RateOptimization#data-quality'\"}}},\"tooltip\":{\"expr\":{\"Literal\":{\"Value\":\"'About this page'\"}}}}}]}}}", "filters": "[]", "height": 48.0, "width": 48.0, diff --git a/src/power-bi/storage/RateOptimization.Report/report.json b/src/power-bi/storage/RateOptimization.Report/report.json index ab1d4a9e0..ebfaee070 100644 --- a/src/power-bi/storage/RateOptimization.Report/report.json +++ b/src/power-bi/storage/RateOptimization.Report/report.json @@ -5957,7 +5957,7 @@ "z": 44000.00 }, { - "config": "{\"name\":\"db83ba666b4a89133e11\",\"layouts\":[{\"id\":0,\"position\":{\"x\":1232,\"y\":-0.8809946714031502,\"z\":19000,\"width\":48,\"height\":48,\"tabOrder\":19000}}],\"singleVisual\":{\"visualType\":\"actionButton\",\"drillFilterOtherVisuals\":true,\"objects\":{\"icon\":[{\"properties\":{\"shapeType\":{\"expr\":{\"Literal\":{\"Value\":\"'information'\"}}},\"placement\":{\"expr\":{\"Literal\":{\"Value\":\"'right'\"}}},\"lineWeight\":{\"expr\":{\"Literal\":{\"Value\":\"2L\"}}},\"lineColor\":{\"solid\":{\"color\":{\"expr\":{\"ThemeDataColor\":{\"ColorId\":0,\"Percent\":0}}}}}},\"selector\":{\"id\":\"default\"}},{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}}}}]},\"vcObjects\":{\"background\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"false\"}}}}}],\"visualLink\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}},\"type\":{\"expr\":{\"Literal\":{\"Value\":\"'WebUrl'\"}}},\"webUrl\":{\"expr\":{\"Literal\":{\"Value\":\"'https://aka.ms/ftk/rateoptimization#dq'\"}}},\"tooltip\":{\"expr\":{\"Literal\":{\"Value\":\"'About this page'\"}}}}}]}}}", + "config": "{\"name\":\"db83ba666b4a89133e11\",\"layouts\":[{\"id\":0,\"position\":{\"x\":1232,\"y\":-0.8809946714031502,\"z\":19000,\"width\":48,\"height\":48,\"tabOrder\":19000}}],\"singleVisual\":{\"visualType\":\"actionButton\",\"drillFilterOtherVisuals\":true,\"objects\":{\"icon\":[{\"properties\":{\"shapeType\":{\"expr\":{\"Literal\":{\"Value\":\"'information'\"}}},\"placement\":{\"expr\":{\"Literal\":{\"Value\":\"'right'\"}}},\"lineWeight\":{\"expr\":{\"Literal\":{\"Value\":\"2L\"}}},\"lineColor\":{\"solid\":{\"color\":{\"expr\":{\"ThemeDataColor\":{\"ColorId\":0,\"Percent\":0}}}}}},\"selector\":{\"id\":\"default\"}},{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}}}}]},\"vcObjects\":{\"background\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"false\"}}}}}],\"visualLink\":[{\"properties\":{\"show\":{\"expr\":{\"Literal\":{\"Value\":\"true\"}}},\"type\":{\"expr\":{\"Literal\":{\"Value\":\"'WebUrl'\"}}},\"webUrl\":{\"expr\":{\"Literal\":{\"Value\":\"'https://aka.ms/ftk/pbi/RateOptimization#data-quality'\"}}},\"tooltip\":{\"expr\":{\"Literal\":{\"Value\":\"'About this page'\"}}}}}]}}}", "filters": "[]", "height": 48.00, "width": 48.00, From 5e2fcd6c33d625726407224bdeec36ab92bfc3d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Oct 2026 22:13:08 +0000 Subject: [PATCH 14/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index 0bbf4be5f..b538cfec1 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index befb94deb..edaf62c8e 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index 04325eb7b..e6a1f2d18 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index c16ab287e..ae8798625 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index f941c32ee..f94d52ec3 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 1c7dfa099..459a8d427 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 10/14/2026 +ms.date: 10/15/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From edff1427850055a34ca122b13b1989130acf416a Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Thu, 15 Oct 2026 18:26:09 -0700 Subject: [PATCH 15/28] chore: trigger CI after ms.date bot commit Co-Authored-By: Claude Sonnet 5.5 From 38a071e43a69405f15e02ab9c8d64f6116eeb9dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Oct 2026 01:26:52 +0000 Subject: [PATCH 16/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/changelog.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index b538cfec1..058a16977 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index b174ed6fa..20d4a761d 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index edaf62c8e..05bbcede4 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index e6a1f2d18..79a5456f7 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index ae8798625..247df368f 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index f94d52ec3..07a841469 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 459a8d427..7ecfb8161 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 10/15/2026 +ms.date: 10/16/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From 204d0db468a3dd708d25e5265d7b5c14b53ef0be Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 18 Oct 2026 02:10:29 -0700 Subject: [PATCH 17/28] fix(hubs): use one Total savings definition and derived Cost summary on dashboard - Cost summary derives Contracted/List from clamped savings so it reconciles with Savings summary - Total savings = Negotiated + Commitment in pies, trends, resource tiles - Savings by month grid uses the derived ESR basis (excludes commitment purchases) Co-Authored-By: RolandKrummenacher Co-Authored-By: Claude Sonnet 5.5 --- docs-mslearn/toolkit/changelog.md | 2 +- src/templates/finops-hub/dashboard.json | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 3df45dcc4..5b1c182a6 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -95,7 +95,7 @@ _Released June 2026_ - **Changed** - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)). - - Changed the Data Explorer dashboard's savings-breakdown, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)). + - Changed the Data Explorer dashboard's Cost summary, savings-breakdown, Savings by month, savings pie and trend, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums. Total savings is now the sum of the clamped Negotiated and Commitment savings everywhere it is shown ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)). - **Fixed** - Fixed Data Factory ingestion memory pressure during emptiness filtering. - Replaced `isnotempty(strcat(x_SkuMeterId, x_SkuOfferId))` with separate `isnotempty()` checks in FinOps hub ingestion scripts to avoid temporary string allocation. diff --git a/src/templates/finops-hub/dashboard.json b/src/templates/finops-hub/dashboard.json index 119d422cc..31d5ac597 100644 --- a/src/templates/finops-hub/dashboard.json +++ b/src/templates/finops-hub/dashboard.json @@ -3011,13 +3011,13 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_TotalSavings)\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", "id": "2d7b6447-2769-40b8-958a-f252dab68b1e", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sum(x_TotalSavings) by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", + "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings) by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", "id": "f062533b-8c94-412f-bf83-0eb5cd06063d", "usedVariables": ["CostsByDay", "CostsPlus", "numberOfDays"] }, @@ -3065,7 +3065,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize \n ListCost = round(sum(ListCost), 2),\n ContractedCost = round(sum(ContractedCost), 2),\n EffectiveCost = round(sum(EffectiveCost), 2)\n by\n Period\n | project Period, json = todynamic(strcat('[{ \"Label\":\"List\", \"Value\":', ListCost, ' }, { \"Label\":\"Contracted\", \"Value\":', ContractedCost, ' }, { \"Label\":\"Effective\", \"Value\":', EffectiveCost, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata", + "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n Period\n //\n // Derive Contracted and List cost from the same clamped, row-level savings basis used by\n // the Savings summary tile beside this one, so List - Contracted = Negotiated savings and\n // Contracted - Effective = Commitment savings reconcile across both tiles. EffectiveCost is\n // the only unclamped value, so it anchors the chain.\n | extend ContractedCost = round(EffectiveCost + CommitmentDiscountSavings, 2)\n | extend ListCost = round(EffectiveCost + CommitmentDiscountSavings + NegotiatedDiscountSavings, 2)\n | extend EffectiveCost = round(EffectiveCost, 2)\n | project Period, json = todynamic(strcat('[{ \"Label\":\"List\", \"Value\":', ListCost, ' }, { \"Label\":\"Contracted\", \"Value\":', ContractedCost, ' }, { \"Label\":\"Effective\", \"Value\":', EffectiveCost, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata", "id": "5ff29428-de83-4a2c-8f86-d8beebe68750", "usedVariables": ["CostsByDay", "CostsByMonth"] }, @@ -3077,7 +3077,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where ChargeCategory == 'Usage'\n| where isnotempty(CommitmentDiscountId)\n//\n| extend x_CommitmentDiscountUtilizationPotential = case(\n ProviderName == 'Microsoft', EffectiveCost,\n CommitmentDiscountCategory == 'Usage', ConsumedQuantity,\n CommitmentDiscountCategory == 'Spend', EffectiveCost,\n real(0)\n)\n| extend x_CommitmentDiscountUtilizationAmount = iff(CommitmentDiscountStatus == 'Used', x_CommitmentDiscountUtilizationPotential, real(0))\n//\n| summarize\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType),\n x_SkuTerm = take_any(x_SkuTerm),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_TotalSavings),\n x_CommitmentDiscountUtilizationAmount = sum(x_CommitmentDiscountUtilizationAmount),\n x_CommitmentDiscountUtilizationPotential = sum(x_CommitmentDiscountUtilizationPotential)\n by\n CommitmentDiscountId\n| order by EffectiveCost desc\n| project\n CommitmentDiscountName,\n CommitmentDiscountType,\n Term = case(isempty(x_SkuTerm) or x_SkuTerm <= 0, '', x_SkuTerm < 12, strcat(x_SkuTerm, ' month', iff(x_SkuTerm != 1, 's', '')), strcat(x_SkuTerm / 12, ' year', iff(x_SkuTerm != 12, 's', ''))),\n Utilization = round(x_CommitmentDiscountUtilizationAmount / x_CommitmentDiscountUtilizationPotential * 100, 1),\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2)\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where ChargeCategory == 'Usage'\n| where isnotempty(CommitmentDiscountId)\n//\n| extend x_CommitmentDiscountUtilizationPotential = case(\n ProviderName == 'Microsoft', EffectiveCost,\n CommitmentDiscountCategory == 'Usage', ConsumedQuantity,\n CommitmentDiscountCategory == 'Spend', EffectiveCost,\n real(0)\n)\n| extend x_CommitmentDiscountUtilizationAmount = iff(CommitmentDiscountStatus == 'Used', x_CommitmentDiscountUtilizationPotential, real(0))\n//\n| summarize\n CommitmentDiscountName = take_any(CommitmentDiscountName),\n CommitmentDiscountType = take_any(CommitmentDiscountType),\n x_SkuTerm = take_any(x_SkuTerm),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings),\n x_CommitmentDiscountUtilizationAmount = sum(x_CommitmentDiscountUtilizationAmount),\n x_CommitmentDiscountUtilizationPotential = sum(x_CommitmentDiscountUtilizationPotential)\n by\n CommitmentDiscountId\n| order by EffectiveCost desc\n| project\n CommitmentDiscountName,\n CommitmentDiscountType,\n Term = case(isempty(x_SkuTerm) or x_SkuTerm <= 0, '', x_SkuTerm < 12, strcat(x_SkuTerm, ' month', iff(x_SkuTerm != 1, 's', '')), strcat(x_SkuTerm / 12, ' year', iff(x_SkuTerm != 12, 's', ''))),\n Utilization = round(x_CommitmentDiscountUtilizationAmount / x_CommitmentDiscountUtilizationPotential * 100, 1),\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2)\n", "id": "d7f31381-ba44-46a1-ad3e-dc6b8826706d", "usedVariables": ["CostsByDay"] }, @@ -3113,25 +3113,25 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_TotalSavings)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5bbb5369-ac95-45fc-853c-a6a2ce6a9e7b", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_TotalSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "93f0eb9f-fa4d-4c54-96c2-8c1d3387d13a", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_TotalSavings)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5c442903-65b0-4b53-9e7c-3ea9c1af7be5", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_TotalSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "9a9ee4b0-a37d-475f-bf1d-f51573243491", "usedVariables": ["CostsByDay"] }, @@ -3257,7 +3257,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n ['List cost'] = round(sumif(ListCost, x_AmortizationClass != 'Principal'), 2),\n ['Effective cost'] = round(sum(EffectiveCost), 2),\n Savings = round(sum(x_TotalSavings), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| extend ESR = percentstring(Savings/ ['List cost'])\n| order by Month desc", + "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n EffectiveCost = sum(EffectiveCost),\n NegotiatedSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n CommitmentSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n//\n// Same derivation as the Effective Savings Rate tiles: Savings is the sum of the clamped\n// components and List cost is Effective cost + Savings, so List - Effective = Savings and\n// the ESR matches the ESR tiles. Commitment purchases are excluded from savings.\n| extend Savings = NegotiatedSavings + CommitmentSavings\n| extend ListCost = EffectiveCost + Savings\n| project\n Account,\n Month,\n ['List cost'] = round(ListCost, 2),\n ['Effective cost'] = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n ESR = percentstring(Savings / ListCost)\n| order by Month desc", "id": "3b3f0a58-2d84-4e3e-bebc-3e747a7d5ede", "usedVariables": ["CostsByMonth"] }, From 0c226999fbc856a21537af92264e6b86528220d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Oct 2026 09:14:00 +0000 Subject: [PATCH 18/28] chore: Update ms.date in docs-mslearn files --- docs-mslearn/toolkit/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 941346bf1..4ecbecb88 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/16/2026 +ms.date: 10/18/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit From 5f7643711c618e16a17ad57243ee88e4e14d243f Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 18 Oct 2026 12:14:26 -0700 Subject: [PATCH 19/28] fix(hubs): exclude commitment purchases from dashboard savings; one purchase filter; NaN guard - Summary queries: sumif(x_*Savings, x_AmortizationClass != 'Principal') on the derived basis (combined form of #2352) - Rate optimization: use x_AmortizationClass != 'Principal' everywhere; drop local re-derivations - ESR: iff(ListCost == 0, real(null), ...) to avoid NaN - Fix stale stacked-chart comment; move changelog entry to Unreleased v16 and fix tile names Co-Authored-By: Claude Opus 5.5 --- docs-mslearn/toolkit/changelog.md | 7 ++--- src/templates/finops-hub/dashboard.json | 36 ++++++++++++------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 4ecbecb88..2599238d5 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -18,14 +18,16 @@ ms.reviewer: brettwil This article summarizes the features and enhancements in each release of the FinOps toolkit. - +### [FinOps hubs](hubs/finops-hubs-overview.md) v16 + +- **Changed** + - Changed the Data Explorer dashboard's Cost summary, Savings summary, Savings breakdown by month, savings pie and trend, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums. Total savings is now the sum of the clamped Negotiated and Commitment savings everywhere it is shown, commitment discount purchases are excluded from savings on every Summary and Rate optimization tile using the same `x_AmortizationClass != 'Principal'` filter, and the Effective Savings Rate shows blank instead of NaN when List cost is 0 ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)).
@@ -57,7 +59,6 @@ _Released October 2026_ - **Changed** - Documented how the hub schema's columns nest in the hub database guide - the organization chain and the two parallel service chains - and brought the `Costs()` column reference back in line with the deployed schema by adding 27 missing columns, correcting three columns still listed under the names hubs v12 renamed away from, and fixing one column typed as `long` instead of `int` ([#2325](https://github.com/microsoft/finops-toolkit/issues/2325), [#2329](https://github.com/microsoft/finops-toolkit/issues/2329)). - Added a callout to the `config_RunBackfillJob` backfill option clarifying that it isn't supported on Microsoft Customer Agreement (MCA) billing accounts or billing profiles ([#2113](https://github.com/microsoft/finops-toolkit/issues/2113)). - - Changed the Data Explorer dashboard's Cost summary, savings-breakdown, Savings by month, savings pie and trend, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums. Total savings is now the sum of the clamped Negotiated and Commitment savings everywhere it is shown ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)). - Clarified that the FinOps toolkit exclusively manages the FinOps hub virtual network and documented customer-managed private endpoints as the preferred private-access topology, with virtual network peering as a secondary option ([#2156](https://github.com/microsoft/finops-toolkit/issues/2156)). - Replaced redundant `tolower()` comparisons in hub KQL with case-insensitive operators (`has`, `=~`, `!~`) so the engine can use the term index instead of scanning every row ([#2213](https://github.com/microsoft/finops-toolkit/issues/2213)). - Replaced whole-term `contains` matches with `has` across hub KQL and the query catalog (resource ID paths, licensing phrases, SKU description terms) and added a per-row operator-equivalence regression harness with unit test coverage ([#2220](https://github.com/microsoft/finops-toolkit/pull/2220)). diff --git a/src/templates/finops-hub/dashboard.json b/src/templates/finops-hub/dashboard.json index bf27f1263..29b5a82fe 100644 --- a/src/templates/finops-hub/dashboard.json +++ b/src/templates/finops-hub/dashboard.json @@ -2873,25 +2873,25 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\n//\n// Stacked area: EffectiveCost is the unclamped base layer; CommitmentDiscountSavings and\n// NegotiatedDiscountSavings are clamped, row-level sums layered on top. The stack top\n// (Effective + Commitment + Negotiated) is therefore a derived 'cost without discounts'\n// figure built on the same clamped basis as the savings layers -- it reconciles with this\n// chart's own layers by construction, but will not always equal the raw sum(ListCost) KPI\n// shown elsewhere on the page. See the savings-breakdown tiles for the same derivation.\n//\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\n//\n// Stacked area: EffectiveCost is the unclamped base layer; CommitmentDiscountSavings and\n// NegotiatedDiscountSavings are clamped, row-level sums layered on top. The stack top\n// (Effective + Commitment + Negotiated) is therefore a derived 'cost without discounts'\n// figure built on the same clamped basis as the savings layers -- it reconciles with this\n// chart's own layers by construction and matches the Cost summary tile, which uses the same\n// derived basis.\n//\nCostsPlus\n| where startofmonth(ChargePeriodStart) >= startofmonth(now(), -1)\n| summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'),\n NegotiatedDiscountSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal')\n by\n ChargePeriodStart,\n Month = strcat(format_datetime(ChargePeriodStart, 'MM '), monthname[monthofyear(ChargePeriodStart)])\n| order by ChargePeriodStart asc\n| extend EffectiveCostRunningTotal = row_cumsum(EffectiveCost, prev(Month) != Month)\n| extend CommitmentDiscountSavingsRunningTotal = row_cumsum(CommitmentDiscountSavings, prev(Month) != Month)\n| extend NegotiatedDiscountSavingsRunningTotal = row_cumsum(NegotiatedDiscountSavings, prev(Month) != Month)\n| project ChargePeriodStart, CommitmentDiscountSavingsRunningTotal, NegotiatedDiscountSavingsRunningTotal, EffectiveCostRunningTotal, Month\n| render areachart ", "id": "dcc1f533-e5f9-4855-9e4c-21e21fcdf943", "usedVariables": ["CostsPlus"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = materialize(\n CostsByMonth\n | where ChargePeriodStart >= startofmonth(now(), -1)\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend Month = monthname[monthofyear(ChargePeriodStart)]\n | project json = todynamic(strcat('[{ \"Type\":\"Billed cost\", \"Count\":', BilledCost, ' }, { \"Type\":\"Effective cost\", \"Count\":', EffectiveCost, ' }, { \"Type\":\"Commitment savings\", \"Count\":', CommitmentDiscountSavings, ' }, { \"Type\":\"Negotiated savings\", \"Count\":', NegotiatedDiscountSavings, ' }]')), Month, IsThisMonth = ChargePeriodStart >= startofmonth(now())\n | mv-expand json\n | project Label = strcat(json.Type, ' (', Month, ')'), Count = tolong(json.Count), IsThisMonth\n);\ndata", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = materialize(\n CostsByMonth\n | where ChargePeriodStart >= startofmonth(now(), -1)\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'),\n NegotiatedDiscountSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal')\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend Month = monthname[monthofyear(ChargePeriodStart)]\n | project json = todynamic(strcat('[{ \"Type\":\"Billed cost\", \"Count\":', BilledCost, ' }, { \"Type\":\"Effective cost\", \"Count\":', EffectiveCost, ' }, { \"Type\":\"Commitment savings\", \"Count\":', CommitmentDiscountSavings, ' }, { \"Type\":\"Negotiated savings\", \"Count\":', NegotiatedDiscountSavings, ' }]')), Month, IsThisMonth = ChargePeriodStart >= startofmonth(now())\n | mv-expand json\n | project Label = strcat(json.Type, ' (', Month, ')'), Count = tolong(json.Count), IsThisMonth\n);\ndata", "id": "e8b343dc-7430-4487-8d44-ef48ac454f2d", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sum(x_CommitmentDiscountSavings), 2)),\n NegotiatedDiscountSavings = todouble(round(sum(x_NegotiatedDiscountSavings), 2))\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'), 2)),\n NegotiatedDiscountSavings = todouble(round(sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'), 2))\n by\n ChargePeriodStart\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n", "id": "c077a6d4-719f-42fe-b39b-36f77dc68976", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sum(x_CommitmentDiscountSavings), 2)),\n NegotiatedDiscountSavings = todouble(round(sum(x_NegotiatedDiscountSavings), 2))\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart)\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n//, BilledCost, NegotiatedDiscountSavings, CommitmentDiscountSavings)\n//| project ChargePeriod, BilledCost = round(BilledCost, 2), EffectiveCost = round(EffectiveCost, 2), NegotiatedDiscountSavings, CommitmentDiscountSavings\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = todouble(round(sum(BilledCost), 2)),\n EffectiveCost = todouble(round(sum(EffectiveCost), 2)),\n CommitmentDiscountSavings = todouble(round(sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'), 2)),\n NegotiatedDiscountSavings = todouble(round(sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'), 2))\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart)\n | order by ChargePeriodStart asc\n | extend ChargePeriod = strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)])\n);\ndata | extend Data = 'Billed cost' | evaluate pivot(ChargePeriod, sum(BilledCost), Data)\n| union (data | extend Data = 'Effective cost' | evaluate pivot(ChargePeriod, sum(EffectiveCost), Data))\n| union (data | extend Data = 'Negotiated savings' | evaluate pivot(ChargePeriod, sum(NegotiatedDiscountSavings), Data))\n| union (data | extend Data = 'Commitment savings' | evaluate pivot(ChargePeriod, sum(CommitmentDiscountSavings), Data))\n//, BilledCost, NegotiatedDiscountSavings, CommitmentDiscountSavings)\n//| project ChargePeriod, BilledCost = round(BilledCost, 2), EffectiveCost = round(EffectiveCost, 2), NegotiatedDiscountSavings, CommitmentDiscountSavings\n", "id": "3d947a7c-edca-46b5-a862-de77a725c85f", "usedVariables": ["CostsByMonth"] }, @@ -2933,7 +2933,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart,\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(CommitmentDiscountSavings, 2))\n | extend NegotiatedDiscountSavings = todouble(round(NegotiatedDiscountSavings, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nlet data = (\n CostsByMonth\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'),\n NegotiatedDiscountSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n SubAccountName = take_any(SubAccountName)\n by\n ChargePeriodStart,\n SubAccountId\n | as per\n | union (\n per\n | summarize \n BilledCost = sum(BilledCost),\n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(NegotiatedDiscountSavings),\n SubAccountName = take_any(SubAccountName)\n by\n SubAccountId\n )\n | order by ChargePeriodStart asc\n | extend BilledCost = todouble(round(BilledCost, 2))\n | extend EffectiveCost = todouble(round(EffectiveCost, 2))\n | extend CommitmentDiscountSavings = todouble(round(CommitmentDiscountSavings, 2))\n | extend NegotiatedDiscountSavings = todouble(round(NegotiatedDiscountSavings, 2))\n | extend ChargePeriod = iff(isempty(ChargePeriodStart), strcat('Total'), strcat(format_datetime(ChargePeriodStart, 'yyyy-MM - '), monthname[monthofyear(ChargePeriodStart)]))\n);\ndata | evaluate pivot(ChargePeriod, sum(EffectiveCost), SubAccountName)\n| order by Total desc", "id": "90502e9a-2d0d-4ae4-8d9d-cc21f9b72d5d", "usedVariables": ["CostsByMonth"] }, @@ -3011,13 +3011,13 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", + "text": "let monthname = dynamic(['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']);\nCostsByDay\n| where isnotempty(ResourceType)\n| summarize \n Count = dcount(ResourceId),\n EffectiveCost = sum(EffectiveCost),\n Savings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal') + sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n ResourceType\n| order by Count desc\n| project \n Type = ResourceType,\n Count,\n Cost = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n [\"Cost / Resource\"] = round(EffectiveCost / Count, 2)\n", "id": "2d7b6447-2769-40b8-958a-f252dab68b1e", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings) by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", + "text": "let costs = CostsByDay\n| where isnotempty(ResourceType);\ncosts | summarize Value = tostring(dcount(ResourceId)) by Label = \"Resources\", Order = 10\n| union (costs | summarize Value = tostring(dcount(ResourceType)) by Label = \"Resource types\", Order = 11)\n| union (costs | summarize Count = dcount(ResourceId) by Value = ResourceType | order by Count desc | limit 1 | extend Label = \"Most used\", Order = 21)\n| union (costs | summarize sum(EffectiveCost) by Value = ResourceType | order by sum_EffectiveCost desc | limit 1 | extend Label = \"Most cost\", Order = 22)\n| union (costs | summarize AllTypes = dcount(ResourceType), CommittedTypes = dcountif(ResourceType, isnotempty(CommitmentDiscountType)) by Label = \"Covered by commitment discounts\", Order = 31 | extend Value = strcat(round(1.0 * CommittedTypes / AllTypes * 100, 1), '%'))\n| union (costs | summarize Savings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal') + sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal') by Value = ResourceType | order by Savings desc | limit 1 | extend Label = \"Most savings\", Order = 32)\n| union (costs | summarize CostPerResource = sum(EffectiveCost) / dcount(ResourceId) by Value = ResourceType | order by CostPerResource desc | limit 1 | extend Label = \"Most expensive (cost / resource)\", Order = 41)\n| union (costs | where ResourceType !in (CostsPlus | where ChargePeriodStart < ago(numberOfDays * 1d) - 1d | distinct ResourceType) | summarize Count = dcount(ResourceId) by ResourceType | order by Count desc | as d | count | extend Label = \"New in last n days\", Value = case(Count == 0, '(none)', Count == 1, toscalar(d | project ResourceType), strcat(toscalar(d | limit 1 | project ResourceType), ' and ', (Count - 1), ' more')), Order = 42)\n| order by Order asc\n", "id": "f062533b-8c94-412f-bf83-0eb5cd06063d", "usedVariables": ["CostsByDay", "CostsPlus", "numberOfDays"] }, @@ -3065,13 +3065,13 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n by\n Period\n //\n // Derive Contracted and List cost from the same clamped, row-level savings basis used by\n // the Savings summary tile beside this one, so List - Contracted = Negotiated savings and\n // Contracted - Effective = Commitment savings reconcile across both tiles. EffectiveCost is\n // the only unclamped value, so it anchors the chain.\n | extend ContractedCost = round(EffectiveCost + CommitmentDiscountSavings, 2)\n | extend ListCost = round(EffectiveCost + CommitmentDiscountSavings + NegotiatedDiscountSavings, 2)\n | extend EffectiveCost = round(EffectiveCost, 2)\n | project Period, json = todynamic(strcat('[{ \"Label\":\"List\", \"Value\":', ListCost, ' }, { \"Label\":\"Contracted\", \"Value\":', ContractedCost, ' }, { \"Label\":\"Effective\", \"Value\":', EffectiveCost, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata", + "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'),\n NegotiatedDiscountSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Period\n //\n // Derive Contracted and List cost from the same clamped, row-level savings basis used by\n // the Savings summary tile beside this one, so List - Contracted = Negotiated savings and\n // Contracted - Effective = Commitment savings reconcile across both tiles. EffectiveCost is\n // the only unclamped value, so it anchors the chain.\n | extend ContractedCost = round(EffectiveCost + CommitmentDiscountSavings, 2)\n | extend ListCost = round(EffectiveCost + CommitmentDiscountSavings + NegotiatedDiscountSavings, 2)\n | extend EffectiveCost = round(EffectiveCost, 2)\n | project Period, json = todynamic(strcat('[{ \"Label\":\"List\", \"Value\":', ListCost, ' }, { \"Label\":\"Contracted\", \"Value\":', ContractedCost, ' }, { \"Label\":\"Effective\", \"Value\":', EffectiveCost, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata", "id": "5ff29428-de83-4a2c-8f86-d8beebe68750", "usedVariables": ["CostsByDay", "CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n NegotiatedDiscountSavings = round(sum(x_NegotiatedDiscountSavings), 2),\n CommitmentDiscountSavings = round(sum(x_CommitmentDiscountSavings), 2)\n by\n Period\n //\n // Total savings is derived as the sum of the two clamped components (not the\n // independently row-clamped x_TotalSavings) so the Total bar always reconciles\n // with Negotiated + Commitment shown alongside it.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', TotalSavings, ' }, { \"Label\":\"Negotiated\", \"Value\":', NegotiatedDiscountSavings, ' }, { \"Label\":\"Commitment\", \"Value\":', CommitmentDiscountSavings, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", + "text": "let data = materialize(\n CostsByMonth | extend Period = 'Last n months'\n | union (CostsByDay | extend Period = 'Last n days')\n | summarize\n NegotiatedDiscountSavings = round(sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'), 2),\n CommitmentDiscountSavings = round(sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal'), 2)\n by\n Period\n //\n // Total savings is derived as the sum of the two clamped components (not the\n // independently row-clamped x_TotalSavings) so the Total bar always reconciles\n // with Negotiated + Commitment shown alongside it.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project Period, json = todynamic(strcat('[{ \"Label\":\"Total\", \"Value\":', TotalSavings, ' }, { \"Label\":\"Negotiated\", \"Value\":', NegotiatedDiscountSavings, ' }, { \"Label\":\"Commitment\", \"Value\":', CommitmentDiscountSavings, ' }]'))\n | mv-expand json\n | project Label = tostring(json.Label), Value = tolong(json.Value), Period\n);\ndata\n", "id": "f5f240a8-a818-4f27-bdfb-e96fcfe433bd", "usedVariables": ["CostsByDay", "CostsByMonth"] }, @@ -3089,7 +3089,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive displayed cost operands from the same clamped, row-level savings basis as the\n // results so the on-screen equations reconcile (List - Contracted = Negotiated savings,\n // Contracted - Effective = Commitment savings). EffectiveCost is the only unclamped value;\n // Contracted and List are built up from it so both sides of each equation match exactly.\n | extend ContractedCost = EffectiveCost + CommitmentDiscountSavings\n | extend ListCost = ContractedCost + NegotiatedDiscountSavings\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive displayed cost operands from the same clamped, row-level savings basis as the\n // results so the on-screen equations reconcile (List - Contracted = Negotiated savings,\n // Contracted - Effective = Commitment savings). EffectiveCost is the only unclamped value;\n // Contracted and List are built up from it so both sides of each equation match exactly.\n | extend ContractedCost = EffectiveCost + CommitmentDiscountSavings\n | extend ListCost = ContractedCost + NegotiatedDiscountSavings\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":13, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"PartialSavings\", \"label\":\"Negotiated savings\", \"value\":\"', numberstring(round(NegotiatedDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":21, \"type\":\"Contracted\", \"label\":\"After negotiated discounts\", \"value\":\"', numberstring(round(ContractedCost, 2)), '\" },',\n '{ \"order\":22, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":23, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":24, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":25, \"type\":\"PartialSavings\", \"label\":\"Commitment savings\", \"value\":\"', numberstring(round(CommitmentDiscountSavings, 2)), '\" },',\n //\n '{ \"order\":31, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":32, \"type\":\"\", \"label\":\"\", \"value\":\"➖\" },',\n '{ \"order\":33, \"type\":\"Effective\", \"label\":\"After commitment discounts\", \"value\":\"', numberstring(round(EffectiveCost, 2)), '\" },',\n '{ \"order\":34, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":35, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "f1ef29df-7a1d-4dd0-8619-0c0164707b31", "usedVariables": ["CostsByDay"] }, @@ -3113,25 +3113,25 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where x_AmortizationClass != 'Principal'\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofmonth(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5bbb5369-ac95-45fc-853c-a6a2ce6a9e7b", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByMonth\n//\n// Filter out commitment discount purchases\n| where x_AmortizationClass != 'Principal'\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "93f0eb9f-fa4d-4c54-96c2-8c1d3387d13a", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where x_AmortizationClass != 'Principal'\n//\n| summarize \n Savings = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)\n by\n ChargePeriodStart = startofday(ChargePeriodStart),\n CommitmentDiscountType\n| project\n ChargePeriodStart,\n Savings = round(Savings, 2),\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "5c442903-65b0-4b53-9e7c-3ea9c1af7be5", "usedVariables": ["CostsByDay"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where ChargeCategory == 'Usage' or isempty(CommitmentDiscountId)\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", + "text": "CostsByDay\n//\n// Filter out commitment discount purchases\n| where x_AmortizationClass != 'Principal'\n//\n| summarize \n Savings = round(sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings), 2)\n by\n Type = case(\n isnotempty(CommitmentDiscountType), CommitmentDiscountType,\n 'Negotiated'\n )", "id": "9a9ee4b0-a37d-475f-bf1d-f51573243491", "usedVariables": ["CostsByDay"] }, @@ -3251,19 +3251,19 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e1bc2d51-44af-4dd9-8b0d-a71088b551f5", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n EffectiveCost = sum(EffectiveCost),\n NegotiatedSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n CommitmentSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n//\n// Same derivation as the Effective Savings Rate tiles: Savings is the sum of the clamped\n// components and List cost is Effective cost + Savings, so List - Effective = Savings and\n// the ESR matches the ESR tiles. Commitment purchases are excluded from savings.\n| extend Savings = NegotiatedSavings + CommitmentSavings\n| extend ListCost = EffectiveCost + Savings\n| project\n Account,\n Month,\n ['List cost'] = round(ListCost, 2),\n ['Effective cost'] = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n ESR = percentstring(Savings / ListCost)\n| order by Month desc", + "text": "CostsByMonth\n| summarize\n EffectiveCost = sum(EffectiveCost),\n NegotiatedSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n CommitmentSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n//\n// Same derivation as the Effective Savings Rate tiles: Savings is the sum of the clamped\n// components and List cost is Effective cost + Savings, so List - Effective = Savings and\n// the ESR matches the ESR tiles. Commitment purchases are excluded from savings.\n| extend Savings = NegotiatedSavings + CommitmentSavings\n| extend ListCost = EffectiveCost + Savings\n| project\n Account,\n Month,\n ['List cost'] = round(ListCost, 2),\n ['Effective cost'] = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n ESR = percentstring(iff(ListCost == 0, real(null), Savings / ListCost))\n| order by Month desc", "id": "3b3f0a58-2d84-4e3e-bebc-3e747a7d5ede", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = TotalSavings / ListCost\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "cb34f22b-9370-460c-9658-3e73d220bbc7", "usedVariables": ["CostsByDay"] }, @@ -3275,7 +3275,7 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| extend x_AmortizationClass = case(\n ChargeCategory == 'Purchase' and isnotempty(CommitmentDiscountCategory), 'Principal',\n isnotempty(CommitmentDiscountCategory), 'Amortized Charge',\n ''\n)\n| summarize\n ['On-demand'] = round(sumif(EffectiveCost, PricingCategory == 'Standard'), 2),\n Spot = round(sumif(EffectiveCost, PricingCategory == 'Dynamic'), 2),\n Reservation = round(sumif(EffectiveCost, CommitmentDiscountType == 'Reservation'), 2),\n ['Savings plan'] = round(sumif(EffectiveCost, CommitmentDiscountType == 'Savings Plan'), 2),\n ['Other'] = round(sumif(EffectiveCost, PricingCategory !in ('Standard', 'Dynamic') and isempty(CommitmentDiscountType)), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| order by Month desc", + "text": "CostsByMonth\n| summarize\n ['On-demand'] = round(sumif(EffectiveCost, PricingCategory == 'Standard'), 2),\n Spot = round(sumif(EffectiveCost, PricingCategory == 'Dynamic'), 2),\n Reservation = round(sumif(EffectiveCost, CommitmentDiscountType == 'Reservation'), 2),\n ['Savings plan'] = round(sumif(EffectiveCost, CommitmentDiscountType == 'Savings Plan'), 2),\n ['Other'] = round(sumif(EffectiveCost, PricingCategory !in ('Standard', 'Dynamic') and isempty(CommitmentDiscountType)), 2)\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n| order by Month desc", "id": "0341b3e4-eccc-4924-9555-9835b128c543", "usedVariables": ["CostsByMonth"] }, From 3821cd48155ab1c996f7dd3e1c450b28bfc16ae1 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 18 Oct 2026 20:00:30 -0700 Subject: [PATCH 20/28] Return blank instead of % when list cost is zero in dashboard savings rate Address PR #2248 feedback. Co-Authored-By: RolandKrummenacher Co-Authored-By: Claude Sonnet 5.5 --- src/templates/finops-hub/dashboard.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/templates/finops-hub/dashboard.json b/src/templates/finops-hub/dashboard.json index 29b5a82fe..97798ee91 100644 --- a/src/templates/finops-hub/dashboard.json +++ b/src/templates/finops-hub/dashboard.json @@ -3251,19 +3251,19 @@ }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByMonth\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', iff(isnull(EffectiveSavingsRate), '', percentstring(EffectiveSavingsRate)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "e1bc2d51-44af-4dd9-8b0d-a71088b551f5", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "CostsByMonth\n| summarize\n EffectiveCost = sum(EffectiveCost),\n NegotiatedSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n CommitmentSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n//\n// Same derivation as the Effective Savings Rate tiles: Savings is the sum of the clamped\n// components and List cost is Effective cost + Savings, so List - Effective = Savings and\n// the ESR matches the ESR tiles. Commitment purchases are excluded from savings.\n| extend Savings = NegotiatedSavings + CommitmentSavings\n| extend ListCost = EffectiveCost + Savings\n| project\n Account,\n Month,\n ['List cost'] = round(ListCost, 2),\n ['Effective cost'] = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n ESR = percentstring(iff(ListCost == 0, real(null), Savings / ListCost))\n| order by Month desc", + "text": "CostsByMonth\n| summarize\n EffectiveCost = sum(EffectiveCost),\n NegotiatedSavings = sumif(x_NegotiatedDiscountSavings, x_AmortizationClass != 'Principal'),\n CommitmentSavings = sumif(x_CommitmentDiscountSavings, x_AmortizationClass != 'Principal')\n by\n Account = x_BillingProfileId,\n Month = substring(startofmonth(ChargePeriodStart), 0, 7)\n//\n// Same derivation as the Effective Savings Rate tiles: Savings is the sum of the clamped\n// components and List cost is Effective cost + Savings, so List - Effective = Savings and\n// the ESR matches the ESR tiles. Commitment purchases are excluded from savings.\n| extend Savings = NegotiatedSavings + CommitmentSavings\n| extend ListCost = EffectiveCost + Savings\n| project\n Account,\n Month,\n ['List cost'] = round(ListCost, 2),\n ['Effective cost'] = round(EffectiveCost, 2),\n Savings = round(Savings, 2),\n ESR = iff(ListCost == 0, '', percentstring(Savings / ListCost))\n| order by Month desc", "id": "3b3f0a58-2d84-4e3e-bebc-3e747a7d5ede", "usedVariables": ["CostsByMonth"] }, { "dataSource": { "kind": "inline", "dataSourceId": "23540be2-ffc9-4b61-8c4c-05e493e682a6" }, - "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', percentstring(EffectiveSavingsRate), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", + "text": "let data = materialize(\n CostsByDay\n //\n // Don't double-count commitment discount purchases\n | where x_AmortizationClass != 'Principal'\n //\n | summarize \n EffectiveCost = sum(EffectiveCost),\n CommitmentDiscountSavings = sum(x_CommitmentDiscountSavings),\n NegotiatedDiscountSavings = sum(x_NegotiatedDiscountSavings)\n //\n // Derive List cost and Total savings from the same clamped, row-level basis used across\n // the other savings tiles so Total savings / List cost = Effective savings rate reconciles\n // on screen. EffectiveCost is the only unclamped value; Total savings is the sum of the two\n // clamped components, and List cost is built up from Effective + Total savings.\n | extend TotalSavings = NegotiatedDiscountSavings + CommitmentDiscountSavings\n | extend ListCost = EffectiveCost + TotalSavings\n | extend EffectiveSavingsRate = iff(ListCost == 0, real(null), TotalSavings / ListCost)\n | project json = todynamic(strcat('[',\n '{ \"order\":11, \"type\":\"TotalSavings\", \"label\":\"Total savings\", \"value\":\"', numberstring(round(TotalSavings, 2)), '\" },',\n '{ \"order\":12, \"type\":\"\", \"label\":\"\", \"value\":\"➗\" },',\n '{ \"order\":13, \"type\":\"List\", \"label\":\"Cost without discounts\", \"value\":\"', numberstring(round(ListCost, 2)), '\" },',\n '{ \"order\":14, \"type\":\"\", \"label\":\"\", \"value\":\"🟰\" },',\n '{ \"order\":15, \"type\":\"EffectiveSavingsRate\", \"label\":\"Effective savings rate\", \"value\":\"', iff(isnull(EffectiveSavingsRate), '', percentstring(EffectiveSavingsRate)), '\" }',\n ']'))\n | mv-expand json\n | order by toint(json.order) asc\n | project Label = tostring(json.label), Value = tostring(json.value), Type = tostring(json.type)\n);\ndata", "id": "cb34f22b-9370-460c-9658-3e73d220bbc7", "usedVariables": ["CostsByDay"] }, From 8659e04927b1b3d09443ab549716e4a521cac25f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Oct 2026 03:01:19 +0000 Subject: [PATCH 21/28] chore: Update ms.date in docs-mslearn files --- docs-mslearn/toolkit/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 2599238d5..13493ca3f 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/18/2026 +ms.date: 10/19/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit From f1cd3fb5a1c27a2098673273419e1b5161357254 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 18 Oct 2026 20:01:42 -0700 Subject: [PATCH 22/28] Use semver for plugin.json and add #2247 changelog entry Address PR #2341 feedback. Co-Authored-By: RolandKrummenacher Co-Authored-By: Claude Sonnet 5.5 --- docs-mslearn/toolkit/changelog.md | 1 + src/scripts/Update-Version.ps1 | 6 +++--- src/templates/agent-plugin/.claude-plugin/plugin.json | 2 +- src/templates/agent-plugin/plugin.json | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 20d4a761d..9f35edf9f 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -64,6 +64,7 @@ _Released October 2026_ - **Fixed** - Fixed FinOps hubs labeling FOCUS 1.0 cost data as FOCUS 1.2 in `x_SourceVersion`. The cost transform backfilled `PricingCurrency` from `x_PricingCurrency` before detecting the FOCUS version, so every 1.0 row looked like 1.2 and skipped the 1.0-specific fixes (for example, the savings plan purchase `ChargeFrequency` correction). Version detection now uses the value delivered by the export ([#2333](https://github.com/microsoft/finops-toolkit/pull/2333)). - Fixed FinOps hub dashboard "Governance, policy, and risk" and "FinOps tools and services" links that landed on a 404 page by linking directly to the Microsoft Learn pages ([#2319](https://github.com/microsoft/finops-toolkit/issues/2319)). + - Fixed the `PathNotFound` error in the `msexports_ETL_ingestion` pipeline's "Get Existing Parquet Files" step on the first ingestion for a scope, dataset, and month, by checking that the destination folder exists before listing its files ([#2247](https://github.com/microsoft/finops-toolkit/pull/2247)). - Fixed ADF schedule trigger deployment failures (`InvalidWorkflowTriggerRecurrence`) in regions not covered by the region-to-time-zone map (for example, Sweden Central), where the trigger's `startTime` was missing the required `Z` suffix for the `UTC` fallback time zone ([#2157](https://github.com/microsoft/finops-toolkit/issues/2157), [#2291](https://github.com/microsoft/finops-toolkit/pull/2291)). - Fixed Data Factory ingestion memory pressure during emptiness filtering. - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings ([#2176](https://github.com/microsoft/finops-toolkit/issues/2176)). diff --git a/src/scripts/Update-Version.ps1 b/src/scripts/Update-Version.ps1 index 03ddb091b..732b8601c 100644 --- a/src/scripts/Update-Version.ps1 +++ b/src/scripts/Update-Version.ps1 @@ -126,22 +126,22 @@ if ($update -or $Version) } } - # Update version in plugin.json files + # Update version in plugin.json files (three-part semantic version) Write-Verbose "Updating plugin.json files..." + $semVer = & "$PSScriptRoot/Get-Version" -AsSemVer Get-RepositoryVersionFiles "plugin.json" ` | ForEach-Object { $json = Get-Content $_ -Raw | ConvertFrom-Json if ($json.PSObject.Properties['name'] -and $json.PSObject.Properties['version']) { Write-Verbose "- $($_.FullName.Replace($repoRoot + [IO.Path]::DirectorySeparatorChar, ''))" - $json.version = $ver + $json.version = $semVer $json | ConvertTo-Json -Depth 10 | Out-File $_ -Encoding utf8 -NoNewline } } # Update version in marketplace.json plugin entries Write-Verbose "Updating marketplace.json files..." - $semVer = & "$PSScriptRoot/Get-Version" -AsSemVer Get-RepositoryVersionFiles "marketplace.json" ` | ForEach-Object { $json = Get-Content $_ -Raw | ConvertFrom-Json diff --git a/src/templates/agent-plugin/.claude-plugin/plugin.json b/src/templates/agent-plugin/.claude-plugin/plugin.json index 09d2a04d0..ce771a3b3 100644 --- a/src/templates/agent-plugin/.claude-plugin/plugin.json +++ b/src/templates/agent-plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "microsoft-finops-toolkit", - "version": "15.0", + "version": "15.0.0", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "author": { "name": "FinOps Toolkit Team", diff --git a/src/templates/agent-plugin/plugin.json b/src/templates/agent-plugin/plugin.json index 946292328..8c2a13717 100644 --- a/src/templates/agent-plugin/plugin.json +++ b/src/templates/agent-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "microsoft-finops-toolkit", - "version": "15.0", + "version": "15.0.0", "description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.", "author": { "name": "FinOps Toolkit Team", From 38e6957bfd87a24b0a815b33b1d0842c279be193 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Oct 2026 03:02:21 +0000 Subject: [PATCH 23/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/changelog.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index 058a16977..a44efda73 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 9f35edf9f..6681b228c 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index 05bbcede4..e3d84d798 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index 79a5456f7..fa3feb151 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index 247df368f..7ccfa6b74 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index 07a841469..dc8d85315 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 7ecfb8161..534cd0b29 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 10/16/2026 +ms.date: 10/19/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From 417790ef4a22b34624c0e91feceb26d72b3ed722 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 18 Oct 2026 21:28:58 -0700 Subject: [PATCH 24/28] Address review: drop percent() code block, simplify eligibility note Co-Authored-By: Claude Opus 5.5 --- docs-mslearn/toolkit/hubs/data-model.md | 5 ----- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index 7ccfa6b74..0e6fa07c0 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -554,11 +554,6 @@ parse_resourceid('/subscriptions/###/resourceGroups/foo/providers/Microsoft.Comp The **percent(table: (Count: long))** function in Data Explorer calculates the percentage of each record based on a required Count column. -{ -let total = todouble(toscalar(t | summarize sum(Count))); -percentOfTotal(t, total) -} -
## percentOfTotal() KQL function diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index dc8d85315..ae8210078 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -223,7 +223,7 @@ Transforms: - Implemented the following columns when not set by Cost Management: - `CommitmentDiscountUnit` - v15+: - - Swap `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` to match the `CommitmentDiscountCategory` value assigned by the same transform. The spend column now reports savings plan pricing and the usage column reports reservation pricing. Prices ingested before upgrading keep the old values until they're reingested. + - Fix `x_CommitmentDiscountSpendEligibility` and `x_CommitmentDiscountUsageEligibility` values. Prices ingested before upgrading keep the old values until they're re-ingested. ### Recommendation data transforms From a172db5ead73a33c2e890205b9e9148c40af0ea7 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Thu, 22 Oct 2026 19:23:59 -0700 Subject: [PATCH 25/28] docs: fill v15 changelog gaps (stacked on v15-prep) (#2365) Co-authored-by: Claude Sonnet 5.5 --- docs-mslearn/toolkit/changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 6681b228c..200853b28 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/19/2026 +ms.date: 10/21/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -67,6 +67,9 @@ _Released October 2026_ - Fixed the `PathNotFound` error in the `msexports_ETL_ingestion` pipeline's "Get Existing Parquet Files" step on the first ingestion for a scope, dataset, and month, by checking that the destination folder exists before listing its files ([#2247](https://github.com/microsoft/finops-toolkit/pull/2247)). - Fixed ADF schedule trigger deployment failures (`InvalidWorkflowTriggerRecurrence`) in regions not covered by the region-to-time-zone map (for example, Sweden Central), where the trigger's `startTime` was missing the required `Z` suffix for the `UTC` fallback time zone ([#2157](https://github.com/microsoft/finops-toolkit/issues/2157), [#2291](https://github.com/microsoft/finops-toolkit/pull/2291)). - Fixed Data Factory ingestion memory pressure during emptiness filtering. + - Fixed the `PathNotFound` error in the `msexports_ETL_ingestion` and analytics ingestion pipelines on the first ingestion for a scope, dataset, and month, most often hit by reservation recommendation exports ([#2088](https://github.com/microsoft/finops-toolkit/issues/2088), [#2247](https://github.com/microsoft/finops-toolkit/pull/2247)). + - Fixed managed exports schedule trigger validation failing for Japan by mapping the region to `Tokyo Standard Time` instead of `Japan Standard Time` ([#2236](https://github.com/microsoft/finops-toolkit/pull/2236)). + - Fixed commitment purchases inflating savings and effective savings rate (ESR) in the FinOps hub dashboard. Summary and Rate optimization tiles now exclude purchase rows consistently ([#2315](https://github.com/microsoft/finops-toolkit/issues/2315), [#2352](https://github.com/microsoft/finops-toolkit/pull/2352)). - Hardened the reservation price backfill to select the highest on-demand price (`max()` instead of `min()`) when duplicate price rows collapse under a single reservation price lookup key, preventing understated commitment discount savings ([#2176](https://github.com/microsoft/finops-toolkit/issues/2176)). - Fixed the `ContractedCost` recompute guard to compare with a null-safe tolerance instead of exact float equality, eliminating millions of no-op rewrites that polluted the `x_SourceValues` audit trail while preserving the null-cost backfill and no longer overwriting an existing cost when the unit price is missing ([#2216](https://github.com/microsoft/finops-toolkit/issues/2216)). - Fixed the SQL VMs without Azure Hybrid Benefit recommendation query to join on the SQL VM `virtualMachineResourceId` instead of a case-sensitive VM name match that skipped VMs with uppercase names and dropped duplicate names, and made all Azure Resource Graph join kinds explicit so no query relies on the `innerunique` default ([#2225](https://github.com/microsoft/finops-toolkit/pull/2225)). @@ -86,6 +89,7 @@ _Released October 2026_ - The `AdvisorRecommendations` and `AdvisorReservationRecommendations` tables now return complete results per batch instead of silently dropping rows beyond the payload limit. - A batch that still exceeds the limit surfaces an error rather than truncating; see [Reduce the batch size](help/errors.md#option-1-reduce-the-batch-size). - Fixed the rate optimization report data quality link, which landed on a Bing search page ([#2319](https://github.com/microsoft/finops-toolkit/issues/2319)). + - Fixed the duplicate `Region` column in the `AdvisorReservationRecommendations` table that broke Advisor reservation recommendations in the storage and KQL datasets ([#2347](https://github.com/microsoft/finops-toolkit/issues/2347), [#2351](https://github.com/microsoft/finops-toolkit/pull/2351)). ### [FinOps workbooks](workbooks/finops-workbooks-overview.md) v15 From 4f7562ac1aaaa51779701854dd70b1c013071c53 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 05:02:23 +0000 Subject: [PATCH 26/28] chore: Update ms.date in docs-mslearn files --- .../agent-plugin/finops-toolkit-agent-plugin-overview.md | 2 +- docs-mslearn/toolkit/changelog.md | 2 +- docs-mslearn/toolkit/finops-toolkit-overview.md | 2 +- docs-mslearn/toolkit/help/errors.md | 2 +- docs-mslearn/toolkit/hubs/data-model.md | 2 +- docs-mslearn/toolkit/hubs/data-processing.md | 2 +- docs-mslearn/toolkit/hubs/upgrade.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md index a44efda73..6d14f0531 100644 --- a/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md +++ b/docs-mslearn/toolkit/agent-plugin/finops-toolkit-agent-plugin-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit agent plugin overview description: The FinOps toolkit agent plugin brings AI-powered cloud financial management to Claude Code and GitHub Copilot CLI. author: MSBrett ms.author: brettwil -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index 200853b28..1a29fdad5 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/21/2026 +ms.date: 10/23/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/finops-toolkit-overview.md b/docs-mslearn/toolkit/finops-toolkit-overview.md index e3d84d798..cd21bb1cb 100644 --- a/docs-mslearn/toolkit/finops-toolkit-overview.md +++ b/docs-mslearn/toolkit/finops-toolkit-overview.md @@ -3,7 +3,7 @@ title: FinOps toolkit overview description: Learn how the FinOps toolkit helps you automate and extend the Microsoft Cloud with starter kits, scripts, and advanced solutions to improve FinOps practices. author: flanakin ms.author: micflan -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/help/errors.md b/docs-mslearn/toolkit/help/errors.md index fa3feb151..eb5090b48 100644 --- a/docs-mslearn/toolkit/help/errors.md +++ b/docs-mslearn/toolkit/help/errors.md @@ -3,7 +3,7 @@ title: Troubleshoot common FinOps toolkit errors description: This article describes common FinOps toolkit errors and provides solutions to help you resolve issues you might encounter. author: flanakin ms.author: micflan -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: troubleshooting ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-model.md b/docs-mslearn/toolkit/hubs/data-model.md index 0e6fa07c0..74ea760a3 100644 --- a/docs-mslearn/toolkit/hubs/data-model.md +++ b/docs-mslearn/toolkit/hubs/data-model.md @@ -3,7 +3,7 @@ title: FinOps hubs data model description: Learn about the tables and functions available in FinOps hubs to build your own queries, reports, and dashboards. author: flanakin ms.author: micflan -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/data-processing.md b/docs-mslearn/toolkit/hubs/data-processing.md index ae8210078..d09a9dd2c 100644 --- a/docs-mslearn/toolkit/hubs/data-processing.md +++ b/docs-mslearn/toolkit/hubs/data-processing.md @@ -3,7 +3,7 @@ title: FinOps hubs data processing description: Learn how FinOps hubs process data, including scope setup, data normalization, and optimization, to enhance cost management and analysis. author: flanakin ms.author: micflan -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-toolkit diff --git a/docs-mslearn/toolkit/hubs/upgrade.md b/docs-mslearn/toolkit/hubs/upgrade.md index 534cd0b29..45bd2c64b 100644 --- a/docs-mslearn/toolkit/hubs/upgrade.md +++ b/docs-mslearn/toolkit/hubs/upgrade.md @@ -3,7 +3,7 @@ title: Upgrade your FinOps hubs description: Learn how to upgrade your existing FinOps hub instance to the latest version, including necessary steps and considerations. author: flanakin ms.author: micflan -ms.date: 10/19/2026 +ms.date: 10/23/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit From 563db68cf1c35f72135efbaec693fa57f45c994c Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 28 Oct 2026 22:38:43 -0700 Subject: [PATCH 27/28] test(hubs): Add dashboard savings regression tests and restore changelog entry Co-Authored-By: Claude Sonnet 5.5 --- docs-mslearn/toolkit/changelog.md | 7 ++- .../Tests/Unit/HubsDashboardSavings.Tests.ps1 | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/powershell/Tests/Unit/HubsDashboardSavings.Tests.ps1 diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index d1f201360..c043e70ca 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -18,14 +18,17 @@ ms.reviewer: brettwil This article summarizes the features and enhancements in each release of the FinOps toolkit. - +### [FinOps hubs](hubs/finops-hubs-overview.md) v16 + +- **Changed** + - Changed the Data Explorer dashboard's Cost summary, Savings summary, Savings breakdown by month, savings pie and trend, stacked cost + savings, and Effective Savings Rate tiles to derive their displayed List cost and Contracted cost operands from the same clamped, row-level savings basis as the results shown alongside them, so the on-screen equations (for example, "List − Contracted = Negotiated savings") always reconcile. This changes the Effective Savings Rate definition on the affected tiles from `(sum(ListCost) − sum(EffectiveCost)) / sum(ListCost)` to a derived List cost built from Effective cost plus the clamped Negotiated and Commitment savings sums. Total savings is now the sum of the clamped Negotiated and Commitment savings everywhere it is shown, commitment discount purchases are excluded from savings on every Summary and Rate optimization tile using the same `x_AmortizationClass != 'Principal'` filter, and the Effective Savings Rate shows blank instead of NaN when List cost is 0 ([#2093](https://github.com/microsoft/finops-toolkit/issues/2093)). +
diff --git a/src/powershell/Tests/Unit/HubsDashboardSavings.Tests.ps1 b/src/powershell/Tests/Unit/HubsDashboardSavings.Tests.ps1 new file mode 100644 index 000000000..4d6237cb4 --- /dev/null +++ b/src/powershell/Tests/Unit/HubsDashboardSavings.Tests.ps1 @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + Regression coverage for Data Explorer dashboard savings formulas (#2093 / PR #2248): + + 1. Savings are never computed by subtracting raw aggregates (sum(ListCost) - sum(EffectiveCost)); tiles use the + row-level clamped x_*Savings columns so every displayed operand and result share one basis. + 2. Commitment purchases are excluded via x_AmortizationClass != 'Principal'; the older + ChargeCategory == 'Usage' or isempty(CommitmentDiscountId) variant is gone. + 3. Every Effective Savings Rate guards against a zero List cost (blank instead of NaN). +#> + +Describe 'HubsDashboardSavings' { + + BeforeDiscovery { + $repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path + $dashboard = Get-Content (Join-Path $repoRoot 'src/templates/finops-hub/dashboard.json') -Raw | ConvertFrom-Json + $queries = @($dashboard.queries | ForEach-Object { @{ Id = $_.id.Substring(0, 8); Text = [string]$_.text } }) + # 3886b5cd pivots Effective cost only; its savings columns are never displayed. + $savingsQueries = @($queries | Where-Object { $_.Id -ne '3886b5cd' -and $_.Text -match '(?i)sum(if)?\(\s*x_(Negotiated|Commitment|Total)\w*Savings' }) + } + + It 'Excludes commitment purchases from all Summary page savings queries' { + $summaryQueries = @($queries | Where-Object { $_.Text -match "(?i)sumif\(\s*x_(Negotiated|Commitment)DiscountSavings\s*,\s*x_AmortizationClass\s*!=\s*'Principal'\s*\)" }) + $summaryQueries.Count | Should -BeGreaterOrEqual 9 + } + + Context 'Query ' -ForEach $queries { + It 'Does not subtract raw aggregates to compute savings' { + $Text | Should -Not -Match '(?i)sum\(\s*(List|Contracted)Cost\s*\)\s*-\s*sum\(' + } + + It 'Does not use the legacy commitment purchase filter' { + $Text | Should -Not -Match "(?i)ChargeCategory\s*==\s*'Usage'\s+or\s+isempty\(CommitmentDiscountId\)" + } + } + + Context 'Savings query ' -ForEach $savingsQueries { + It 'Excludes commitment purchases or is limited to usage rows' { + $Text | Should -Match "(?i)x_AmortizationClass\s*!=\s*'Principal'|ChargeCategory\s*==\s*'Usage'" + } + + It 'Guards the savings rate against zero list cost' { + if ($Text -match '(?i)EffectiveSavingsRate\s*=') { + $Text | Should -Match '(?i)ListCost\s*==\s*0' + } + } + } +} From d9f7f2f8a1271eb990b608a5e270da7ffefb07bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 29 Oct 2026 05:39:22 +0000 Subject: [PATCH 28/28] chore: Update ms.date in docs-mslearn files --- docs-mslearn/toolkit/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index c043e70ca..59761f8fe 100644 --- a/docs-mslearn/toolkit/changelog.md +++ b/docs-mslearn/toolkit/changelog.md @@ -3,7 +3,7 @@ title: FinOps toolkit changelog description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more. author: MSBrett ms.author: brettwil -ms.date: 10/28/2026 +ms.date: 10/29/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit