From 6788b607ba785ed01e64965d2c493e553536e8f8 Mon Sep 17 00:00:00 2001 From: Roland Krummenacher Date: Wed, 9 Sep 2026 17:57:06 +0200 Subject: [PATCH] fix(aoe): keep reservations visible when their SKU has no ISF ratio The reservation workbooks join the instance size flexibility open data with kind=inner. The Catalogs API that feeds it only returns SKUs that can still be purchased, so an active reservation on a retired size has no row to match and is dropped from the report entirely -- not shown at zero, absent. The customer keeps paying for a reservation the tool stops mentioning. Reservations run one or three years and outlive the sellability of their size, so this is reachable in normal use (#2300). All seven ISF joins in reservations-usage.json become leftouter, with the group and ratio falling back to the SKU itself: | extend ISFGroup = coalesce(ISFGroup, SKUName_s), Ratio = coalesce(Ratio, 1.0) Without that fallback a plain leftouter is worse than the current behaviour: Ratio and ISFGroup come back null, every unmatched reservation collapses into one null bucket, and the utilization column reads empty. Ratio 1 is correct for these rows. Util7Days_s comes from Azure and already accounts for flexibility, so the percentage is right; the ratio only converts between group units, which is a no-op for a single-SKU group. reservations-potential.json and benefits-simulation.json keep kind=inner. They model what to buy, where restricting to purchasable SKUs is correct. Note the workbook KQL is not executed by any test in this repo, so this change is reasoned rather than verified. Co-Authored-By: Claude Opus 5 --- .../views/workbooks/reservations-usage.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/optimization-engine/views/workbooks/reservations-usage.json b/src/optimization-engine/views/workbooks/reservations-usage.json index 51277b3ce..b35474706 100644 --- a/src/optimization-engine/views/workbooks/reservations-usage.json +++ b/src/optimization-engine/views/workbooks/reservations-usage.json @@ -536,7 +536,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \"Windows\"\r\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\r\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\r\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\r\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\r\n| extend WindowsMeterSubCategory = MeterSubCategory_s\r\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\r\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\r\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\r\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\r\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\r\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\r\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\r\n| distinct MeterID_g, OnDemandUnitPrice;\r\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\r\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\r\n| distinct MeterID_g, OnDemandUnitPrice\r\n| union (VMOnDemandPriceSheet)\r\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\r\nlet ReservationPricesheet = AzureOptimizationReservationsPriceV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| extend Term_s = iif(reservationTerm_s == '3 Years', 'P3Y', 'P1Y')\r\n| extend TermDivider = iif(reservationTerm_s == '3 Years', 3, 1)\r\n| extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\r\n| summarize arg_max(TimeGenerated, ReservationPrice) by Location_s=tolower(armRegionName_s), SkuName=tolower(armSkuName_s), Term_s;\r\nlet ReservationOnDemandMeters = AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend SkuName = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| distinct ReservationId_g, MeterId_g, SkuName;\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| where ReservationId_g in ({Reservation:value})\r\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\r\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\r\n| summarize UsedRIsDaily=round(sum(UsedRIs),2) by Date_s, ReservationId_g\r\n| summarize AvgRIsUsedDaily=round(avg(UsedRIsDaily),2) by ReservationId_g\r\n| join kind=rightouter (\r\n AzureOptimizationReservationsUsageV1_CL\r\n | where TimeGenerated > ago(1d)\r\n | where ReservationId_g in ({Reservation:value})\r\n | summarize arg_max(TimeGenerated, *) by ReservationId_g\r\n | where ProvisioningState_s in ('Succeeded','Expiring')\r\n | extend UsedQuantity = todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100\r\n | extend UsedQuantity30d = todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100\r\n | extend SKUName_s=tolower(SKUName_s)\r\n | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s, Location_s, UsedQuantity, UsedQuantity30d, TotalReservedQuantity_s, Term_s, AppliedScopeType_s\r\n) on ReservationId_g\r\n| project ReservationId_g=ReservationId_g1, ReservationName_s, TotalReservedQuantity_s, SKUName_s, Location_s, AvgRIsUsedDaily=iif(isempty(AvgRIsUsedDaily), 0.0, AvgRIsUsedDaily), UsedQuantity, UsedQuantity30d, Term_s, AppliedScopeType_s\r\n| join kind=inner ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\r\n| join kind=leftouter ( ReservationOnDemandMeters ) on ReservationId_g\r\n| summarize arg_max(MeterId_g, *) by ReservationId_g\r\n| join kind=leftouter ( ReservationPricesheet ) on SkuName and Location_s and Term_s\r\n| join kind=leftouter ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\r\n| extend DiscountPercent = (1 - ReservationPrice/OnDemandUnitPrice) * 100\r\n| extend AvgRIsUsedInSmallestRatio = Ratio * AvgRIsUsedDaily\r\n| summarize TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), AvgRIsUsedDaily=sum(AvgRIsUsedInSmallestRatio), UsedQuantity=sum(UsedQuantity*Ratio), UsedQuantity30d=sum(UsedQuantity30d*Ratio), AvgDiscountPercent=avg(DiscountPercent) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\r\n| extend Util7Days_s = UsedQuantity/TotalReservedQuantity_s*100, Util30Days_s = UsedQuantity30d/TotalReservedQuantity_s*100\r\n| extend AvgRIUsagePercentInSmallestRatio = round(AvgRIsUsedDaily / TotalReservedQuantity_s * 100, 1)\r\n| extend AvgDiscountPercent=iif(AvgDiscountPercent > 0.0, AvgDiscountPercent, 0.0)\r\n| extend SavingsMargin=round(todouble(Util7Days_s))-100.0+AvgDiscountPercent \r\n| project-away AvgRIUsagePercentInSmallestRatio, AvgRIsUsedDaily\r\n| project-reorder ISFGroup, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UsedQuantity, Util30Days_s, UsedQuantity30d\r\n| order by Util7Days_s asc", + "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \"Windows\"\r\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\r\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\r\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\r\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\r\n| extend WindowsMeterSubCategory = MeterSubCategory_s\r\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\r\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\r\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\r\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\r\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\r\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\r\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\r\n| distinct MeterID_g, OnDemandUnitPrice;\r\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\r\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\r\n| distinct MeterID_g, OnDemandUnitPrice\r\n| union (VMOnDemandPriceSheet)\r\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\r\nlet ReservationPricesheet = AzureOptimizationReservationsPriceV1_CL\r\n| where TimeGenerated > ago(14d)\r\n| extend Term_s = iif(reservationTerm_s == '3 Years', 'P3Y', 'P1Y')\r\n| extend TermDivider = iif(reservationTerm_s == '3 Years', 3, 1)\r\n| extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\r\n| summarize arg_max(TimeGenerated, ReservationPrice) by Location_s=tolower(armRegionName_s), SkuName=tolower(armSkuName_s), Term_s;\r\nlet ReservationOnDemandMeters = AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend SkuName = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| distinct ReservationId_g, MeterId_g, SkuName;\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| where ReservationId_g in ({Reservation:value})\r\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\r\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\r\n| summarize UsedRIsDaily=round(sum(UsedRIs),2) by Date_s, ReservationId_g\r\n| summarize AvgRIsUsedDaily=round(avg(UsedRIsDaily),2) by ReservationId_g\r\n| join kind=rightouter (\r\n AzureOptimizationReservationsUsageV1_CL\r\n | where TimeGenerated > ago(1d)\r\n | where ReservationId_g in ({Reservation:value})\r\n | summarize arg_max(TimeGenerated, *) by ReservationId_g\r\n | where ProvisioningState_s in ('Succeeded','Expiring')\r\n | extend UsedQuantity = todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100\r\n | extend UsedQuantity30d = todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100\r\n | extend SKUName_s=tolower(SKUName_s)\r\n | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s, Location_s, UsedQuantity, UsedQuantity30d, TotalReservedQuantity_s, Term_s, AppliedScopeType_s\r\n) on ReservationId_g\r\n| project ReservationId_g=ReservationId_g1, ReservationName_s, TotalReservedQuantity_s, SKUName_s, Location_s, AvgRIsUsedDaily=iif(isempty(AvgRIsUsedDaily), 0.0, AvgRIsUsedDaily), UsedQuantity, UsedQuantity30d, Term_s, AppliedScopeType_s\r\n| join kind=leftouter ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, SKUName_s), Ratio = coalesce(Ratio, 1.0)\r\n| join kind=leftouter ( ReservationOnDemandMeters ) on ReservationId_g\r\n| summarize arg_max(MeterId_g, *) by ReservationId_g\r\n| join kind=leftouter ( ReservationPricesheet ) on SkuName and Location_s and Term_s\r\n| join kind=leftouter ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\r\n| extend DiscountPercent = (1 - ReservationPrice/OnDemandUnitPrice) * 100\r\n| extend AvgRIsUsedInSmallestRatio = Ratio * AvgRIsUsedDaily\r\n| summarize TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), AvgRIsUsedDaily=sum(AvgRIsUsedInSmallestRatio), UsedQuantity=sum(UsedQuantity*Ratio), UsedQuantity30d=sum(UsedQuantity30d*Ratio), AvgDiscountPercent=avg(DiscountPercent) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\r\n| extend Util7Days_s = UsedQuantity/TotalReservedQuantity_s*100, Util30Days_s = UsedQuantity30d/TotalReservedQuantity_s*100\r\n| extend AvgRIUsagePercentInSmallestRatio = round(AvgRIsUsedDaily / TotalReservedQuantity_s * 100, 1)\r\n| extend AvgDiscountPercent=iif(AvgDiscountPercent > 0.0, AvgDiscountPercent, 0.0)\r\n| extend SavingsMargin=round(todouble(Util7Days_s))-100.0+AvgDiscountPercent \r\n| project-away AvgRIUsagePercentInSmallestRatio, AvgRIsUsedDaily\r\n| project-reorder ISFGroup, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UsedQuantity, Util30Days_s, UsedQuantity30d\r\n| order by Util7Days_s asc", "size": 0, "showAnalytics": true, "title": "Reservation Usage Details grouped by ISF group (click on a line for more details)", @@ -817,7 +817,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\r\n| extend ConsumedSize = iif(isnotempty(VMSize), VMSize, strcat(MeterSubCategory_s, ' ', MeterName_s))\r\n| extend UsedRIs = todouble(Quantity_s) / 24 * Ratio\r\n| summarize round(sum(UsedRIs),1) by todatetime(Date_s), ConsumedSize", + "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, VMSize), Ratio = coalesce(Ratio, 1.0)\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\r\n| extend ConsumedSize = iif(isnotempty(VMSize), VMSize, strcat(MeterSubCategory_s, ' ', MeterName_s))\r\n| extend UsedRIs = todouble(Quantity_s) / 24 * Ratio\r\n| summarize round(sum(UsedRIs),1) by todatetime(Date_s), ConsumedSize", "size": 0, "aggregation": 3, "showAnalytics": true, @@ -851,7 +851,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\r\n and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\r\n and ChargeType_s == 'Usage'\r\n and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\r\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by ResourceId, SubscriptionId\r\n| join kind=leftouter ( \r\n AzureOptimizationResourceContainersV1_CL \r\n | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\r\n | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\r\n) on SubscriptionId\r\n| project-away SubscriptionId*\r\n| project-reorder ResourceId, Subscription, UsedRIPercentage\r\n| order by ResourceId asc, UsedRIPercentage", + "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\r\n and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\r\n and ChargeType_s == 'Usage'\r\n and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, VMSize), Ratio = coalesce(Ratio, 1.0)\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, VMSize), Ratio = coalesce(Ratio, 1.0)\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\r\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by ResourceId, SubscriptionId\r\n| join kind=leftouter ( \r\n AzureOptimizationResourceContainersV1_CL \r\n | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\r\n | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\r\n) on SubscriptionId\r\n| project-away SubscriptionId*\r\n| project-reorder ResourceId, Subscription, UsedRIPercentage\r\n| order by ResourceId asc, UsedRIPercentage", "size": 1, "showAnalytics": true, "title": "Average Daily Reservation Usage by Resource (click on a line in the table above)", @@ -892,7 +892,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\r\n and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\r\n and ChargeType_s == 'Usage'\r\n and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend Tags_s = iif(Tags_s startswith \"{\", Tags_s, strcat(\"{\", Tags_s, \"}\"))\r\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\r\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\r\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by AggregatorTag, SubscriptionId\r\n| join kind=leftouter ( \r\n AzureOptimizationResourceContainersV1_CL \r\n | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\r\n | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\r\n) on SubscriptionId\r\n| project-away SubscriptionId*\r\n| project-reorder AggregatorTag, Subscription, UsedRIPercentage\r\n| order by AggregatorTag asc, UsedRIPercentage", + "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\r\n and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\r\n and ChargeType_s == 'Usage'\r\n and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, VMSize), Ratio = coalesce(Ratio, 1.0)\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\r\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\r\n| where ResourceLocation_s =~ '{selectedRegion}'\r\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, VMSize), Ratio = coalesce(Ratio, 1.0)\r\n| where ISFGroup == '{selectedISFGroup:value}'\r\n| extend Tags_s = iif(Tags_s startswith \"{\", Tags_s, strcat(\"{\", Tags_s, \"}\"))\r\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\r\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\r\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by AggregatorTag, SubscriptionId\r\n| join kind=leftouter ( \r\n AzureOptimizationResourceContainersV1_CL \r\n | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\r\n | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\r\n) on SubscriptionId\r\n| project-away SubscriptionId*\r\n| project-reorder AggregatorTag, Subscription, UsedRIPercentage\r\n| order by AggregatorTag asc, UsedRIPercentage", "size": 1, "showAnalytics": true, "title": "Average Daily Reservation Usage by Tag (click on a line in the table above)", @@ -1310,7 +1310,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\r\n| where ReservationId_g in ({Reservation:value})\r\n| summarize TotalUnusedCost = sum(todouble(CostInBillingCurrency_s)) by ReservationId_g\r\n| where round(TotalUnusedCost) > 0\r\n| join kind=inner (\r\n AzureOptimizationReservationsUsageV1_CL\r\n | where TimeGenerated > ago(1d)\r\n | where ProvisioningState_s in ('Succeeded','Expiring')\r\n | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\r\n | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\r\n | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s=tolower(SKUName_s), Location_s, TotalReservedQuantity_s, Term_s, AppliedScopeType_s, UnusedQuantity, UnusedQuantity30d\r\n) on ReservationId_g\r\n| project-away ReservationId_g1\r\n| join kind=inner ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\r\n| summarize TotalUnusedCost=sum(TotalUnusedCost), TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), UnusedQuantity=sum(UnusedQuantity*Ratio), UnusedQuantity30d=sum(UnusedQuantity30d*Ratio) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\r\n| extend Util7Days_s = (1-UnusedQuantity/TotalReservedQuantity_s)*100, Util30Days_s = (1-UnusedQuantity30d/TotalReservedQuantity_s)*100\r\n| project-reorder ISFGroup, TotalUnusedCost, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UnusedQuantity, Util30Days_s\r\n| order by TotalUnusedCost", + "query": "let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\"https://raw.githubusercontent.com/microsoft/finops-toolkit/dev/src/open-data/InstanceSizeFlexibility.csv\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\r\nAzureOptimizationConsumptionV1_CL\r\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\r\n| where ReservationId_g in ({Reservation:value})\r\n| summarize TotalUnusedCost = sum(todouble(CostInBillingCurrency_s)) by ReservationId_g\r\n| where round(TotalUnusedCost) > 0\r\n| join kind=inner (\r\n AzureOptimizationReservationsUsageV1_CL\r\n | where TimeGenerated > ago(1d)\r\n | where ProvisioningState_s in ('Succeeded','Expiring')\r\n | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\r\n | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\r\n | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s=tolower(SKUName_s), Location_s, TotalReservedQuantity_s, Term_s, AppliedScopeType_s, UnusedQuantity, UnusedQuantity30d\r\n) on ReservationId_g\r\n| project-away ReservationId_g1\r\n| join kind=leftouter ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\r\n| extend ISFGroup = coalesce(ISFGroup, SKUName_s), Ratio = coalesce(Ratio, 1.0)\r\n| summarize TotalUnusedCost=sum(TotalUnusedCost), TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), UnusedQuantity=sum(UnusedQuantity*Ratio), UnusedQuantity30d=sum(UnusedQuantity30d*Ratio) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\r\n| extend Util7Days_s = (1-UnusedQuantity/TotalReservedQuantity_s)*100, Util30Days_s = (1-UnusedQuantity30d/TotalReservedQuantity_s)*100\r\n| project-reorder ISFGroup, TotalUnusedCost, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UnusedQuantity, Util30Days_s\r\n| order by TotalUnusedCost", "size": 2, "showAnalytics": true, "title": "Unused Reservations Details (grouped by Instance Size Flexibility group)",