diff --git a/docs-mslearn/best-practices/compute.md b/docs-mslearn/best-practices/compute.md index 403d54a31..a54c02a97 100644 --- a/docs-mslearn/best-practices/compute.md +++ b/docs-mslearn/best-practices/compute.md @@ -3,7 +3,7 @@ title: FinOps best practices for compute description: This article provides FinOps best practices for compute services, including cost optimization, efficiency improvements, and insights into Azure resources. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 08/03/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-learning-resources @@ -180,7 +180,7 @@ Costs // Join with prices to filter out ineligible SKUs | extend tmp_MeterKey = strcat(substring(ChargePeriodStart, 0, 7), x_SkuMeterId) | project tmp_MeterKey, EffectiveCost, PricingCategory, CommitmentDiscountCategory, ResourceName, x_ResourceGroupName, SubAccountName, BillingCurrency -| join kind=leftouter ( +| lookup kind=leftouter ( Prices | where x_SkuMeterCategory startswith 'Virtual Machines' | summarize sp = countif(x_SkuPriceType == 'SavingsPlan'), ri = countif(x_SkuPriceType == 'ReservedInstance') @@ -216,7 +216,7 @@ Costs // Join with prices to filter out ineligible SKUs | extend tmp_MeterKey = strcat(substring(ChargePeriodStart, 0, 7), x_SkuMeterId) | project tmp_MeterKey, EffectiveCost, PricingCategory, CommitmentDiscountCategory, ResourceName, x_ResourceGroupName, SubAccountName, BillingCurrency -| join kind=leftouter ( +| lookup kind=leftouter ( Prices | where x_SkuMeterCategory startswith 'Virtual Machines' | summarize sp = countif(x_SkuPriceType == 'SavingsPlan'), ri = countif(x_SkuPriceType == 'ReservedInstance') @@ -366,7 +366,7 @@ resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring(properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | project SubscriptionName = name, subscriptionId -| join ( +| join kind=inner ( resources | where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets' @@ -422,13 +422,14 @@ resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring(properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | project SubscriptionName = name, subscriptionId -| join ( +| join kind=inner ( resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' | project ResourceId = id, ResourceName = name, + VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), LicenseType = tostring(properties.['sqlServerLicenseType']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU = tostring(properties.['sqlImageSku']), @@ -436,14 +437,13 @@ resourcecontainers ResourceGroupName = resourceGroup, subscriptionId ) on subscriptionId -| join ( +| join kind=inner ( resources | where type =~ 'Microsoft.Compute/virtualMachines' | project - ResourceName = tolower(name), - VMSize = tostring(properties.hardwareProfile.vmSize), - subscriptionId -) on ResourceName + VMResourceId = tolower(id), + VMSize = tostring(properties.hardwareProfile.vmSize) +) on VMResourceId | where SQLSKU != 'Developer' and SQLSKU != 'Express' | project ResourceId, diff --git a/docs-mslearn/best-practices/networking.md b/docs-mslearn/best-practices/networking.md index dc5796416..c34639dda 100644 --- a/docs-mslearn/best-practices/networking.md +++ b/docs-mslearn/best-practices/networking.md @@ -3,7 +3,7 @@ title: FinOps best practices for Networking description: This article outlines proven FinOps practices for networking services, focusing on cost optimization, efficiency improvements, and resource insights. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 08/03/2026 ms.topic: concept-article ms.service: finops ms.subservice: finops-learning-resources @@ -134,7 +134,7 @@ resources backendPools = properties.backendAddressPools, resourceGroup = strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup) | project id, name, SKUName, SKUTier, SKUCapacity, resourceGroup, subscriptionId -| join ( +| join kind=inner ( resources | where type =~ 'Microsoft.Network/applicationGateways' | mvexpand backendPools = properties.backendAddressPools @@ -578,8 +578,8 @@ resources | mv-expand IPconfig | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id) | project PublicIpId - | join ( - resource + | join kind=inner ( + resources | where type =~ 'Microsoft.Network/publicIPAddresses' | extend PublicIpId = id, diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md index c7c27e96c..43e183a7a 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: 07/30/2026 +ms.date: 08/03/2026 ms.topic: reference ms.service: finops ms.subservice: finops-toolkit @@ -34,6 +34,19 @@ The following section lists features and enhancements that are currently in deve - 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 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)). + +### [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)). + +### [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) diff --git a/docs-mslearn/toolkit/workbooks/customize-workbooks.md b/docs-mslearn/toolkit/workbooks/customize-workbooks.md index 8d4a0a8d6..1f67cc6d3 100644 --- a/docs-mslearn/toolkit/workbooks/customize-workbooks.md +++ b/docs-mslearn/toolkit/workbooks/customize-workbooks.md @@ -3,7 +3,7 @@ title: Use and customize FinOps workbooks description: Learn how to install and customize FinOps workbooks to achieve FinOps goals, including cost recommendations, idle resource identification, and more. author: flanakin ms.author: micflan -ms.date: 04/01/2026 +ms.date: 08/03/2026 ms.topic: how-to ms.service: finops ms.subservice: finops-toolkit @@ -122,7 +122,7 @@ In the last section, the query joins the `ResourceContainerstable` with the `res ```kusto ResourceContainers | "Insert first code section go here" -| join ( +| join kind=inner ( resources "Insert second code section here" ) on subscriptionId | order by type asc @@ -137,7 +137,7 @@ Here's the full code example that you use to insert into the workbook. ```kusto ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has "MSDNDevTest_2014-09-01" | extend SubscriptionName=name -| join ( +| join kind=inner ( resources | where resourceGroup in ({ResourceGroup}) | where type == 'microsoft.azurestackhci/clusters' diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 index dcb3aae36..a94466517 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 @@ -162,7 +162,7 @@ $baseQuery = @" | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Maximum' | summarize P99DTUPercentage = percentile(todouble(MetricValue_s), dtuPercentPercentile) by ResourceId | where P99DTUPercentage < DTUPercentageThreshold - | join ( + | join kind=inner ( $sqlDbsTableName | where TimeGenerated > ago(1d) | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s @@ -311,7 +311,7 @@ $baseQuery = @" | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' | summarize AvgDTUPercentage = avg(todouble(MetricValue_s)) by ResourceId | where AvgDTUPercentage > DTUPercentageThreshold - | join ( + | join kind=inner ( $sqlDbsTableName | where TimeGenerated > ago(1d) | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s diff --git a/src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1 b/src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1 new file mode 100644 index 000000000..6e9be0c18 --- /dev/null +++ b/src/powershell/Tests/Lint/KqlJoinKinds.Tests.ps1 @@ -0,0 +1,114 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + Lint rule: every KQL join must state an explicit kind (PR #2225). + + A bare `| join (...)` defaults to kind=innerunique, which deduplicates the left side + on the join key and silently drops rows. This has caused real data loss (savings plan + recommendations collapsing to one row per subscription, SQL VMs with duplicate names + disappearing). In Azure Resource Graph the same default applies and `lookup` is not + available, so an explicit kind is the only way to state intent. + + The rule scans every surface that carries KQL: hub database scripts, the query catalog, + ARG recommendation queries, the ADX dashboard, the finops-alerts logic app, workbooks, + optimization engine runbooks and views, and the published docs examples. + + Known pre-existing bare joins are baselined per file below. The baseline is a ratchet: + - Fixing a bare join REQUIRES lowering the count here (the test fails on stale entries). + - Adding a new bare join is never allowed; write `join kind=...` explicitly. + + ARG-only surfaces (workbooks, recommendation queries, the alerts logic app) are additionally + checked for operators Azure Resource Graph rejects: the lookup operator and the semi/anti + join flavors. Verified live against ARG (2026-08): supported kinds are inner, innerunique, + leftouter, rightouter, fullouter; lookup, leftsemi, leftanti, rightsemi, rightanti, and + `in`/`!in` with a subquery are all rejected with InvalidQuery. Exclusion joins in ARG must + therefore use the leftouter + `where isempty()` emulation (with a key-unique + right side) — the one place that pattern is acceptable. +#> + +Describe 'KqlJoinKinds' { + + BeforeDiscovery { + $repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path + + $scanTargets = @( + @{ Path = 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts'; Filter = '*.kql'; Recurse = $false } + @{ Path = 'src/queries/catalog'; Filter = '*.kql'; Recurse = $false } + @{ Path = 'src/powershell/Tests/assets'; Filter = '*.kql'; Recurse = $false } + @{ Path = 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries'; Filter = '*.json'; Recurse = $false } + @{ Path = 'src/templates/finops-hub'; Filter = 'dashboard.json'; Recurse = $false } + @{ Path = 'src/templates/finops-alerts/modules'; Filter = 'logicApp.bicep'; Recurse = $false } + @{ Path = 'src/workbooks'; Filter = '*.workbook'; Recurse = $true } + @{ Path = 'src/workbooks'; Filter = 'workbook.json'; Recurse = $true } + @{ Path = 'src/optimization-engine/runbooks'; Filter = '*.ps1'; Recurse = $true } + @{ Path = 'src/optimization-engine/views'; Filter = '*.json'; Recurse = $true } + @{ Path = 'docs-mslearn'; Filter = '*.md'; Recurse = $true } + ) + + $scanFiles = @($scanTargets | ForEach-Object { + $full = Join-Path $repoRoot $_.Path + Get-ChildItem -Path $full -Filter $_.Filter -Recurse:$_.Recurse -File -ErrorAction SilentlyContinue + } | Sort-Object FullName -Unique | ForEach-Object { + @{ Name = $_.Name; FullName = $_.FullName; RelPath = $_.FullName.Substring($repoRoot.Length + 1).Replace('\', '/') } + }) + + # Surfaces whose KQL runs on Azure Resource Graph. Workbook files may also contain the + # occasional Log Analytics query (queryType 0); if one legitimately needs lookup or a + # semi/anti join, add a per-file allowlist analogous to the bare-join baseline. + $argFiles = @($scanFiles | Where-Object { + $_.RelPath -like 'src/workbooks/*' -or + $_.RelPath -like 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/*' -or + $_.RelPath -eq 'src/templates/finops-alerts/modules/logicApp.bicep' + }) + } + + BeforeAll { + $repoRoot = (Resolve-Path "$PSScriptRoot/../../../..").Path + $scanFileCount = @( + (Join-Path $repoRoot 'src/workbooks'), + (Join-Path $repoRoot 'src/optimization-engine'), + (Join-Path $repoRoot 'src/queries/catalog'), + (Join-Path $repoRoot 'src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts') + ) | ForEach-Object { Get-ChildItem -Path $_ -Recurse -Include '*.kql', '*.workbook', 'workbook.json', '*.ps1', '*.json' -File -ErrorAction SilentlyContinue } | Measure-Object | Select-Object -ExpandProperty Count + + # Matches `| join` not followed by `kind=` before the right-table parenthesis. + # Catches `| join (`, `| join(`, and `| join hint.x=y (`; ignores `| join kind=...` and `lookup`. + $bareJoinPattern = [regex]'\|\s*join\b(?![^(\r\n]*\bkind\s*=)' + + # Pre-existing bare joins, counted per repo-relative path. Ratchet only: lower on fix, never raise. + # All remaining entries are benign today (left side unique on the join key) but rely on the + # innerunique default implicitly. Convert to an explicit kind when touching these queries. + $baseline = @{ + 'src/workbooks/optimization/AHB/AHB.workbook' = 24 + 'src/workbooks/optimization/Compute/AHB.workbook' = 20 + 'src/workbooks/optimization/Networking/Networking.workbook' = 3 + 'src/workbooks/governance/workbook.json' = 1 + } + } + + It 'Should scan at least one file per surface' { + $scanFileCount | Should -BeGreaterThan 100 + } + + It 'Should not add bare joins (no explicit kind): ' -ForEach $scanFiles { + $content = Get-Content -Path $FullName -Raw + $bareJoins = @($bareJoinPattern.Matches($content)) + $allowed = if ($baseline.ContainsKey($RelPath)) { $baseline[$RelPath] } else { 0 } + + $bareJoins.Count | Should -BeLessOrEqual $allowed -Because ('a bare "| join" defaults to kind=innerunique, which deduplicates the left side on the join key and silently drops rows (see PR #2225). State the kind explicitly: kind=inner for lookups/filters, kind=leftouter for enrichment, kind=leftanti for exclusion. In ADX/Log Analytics, prefer the lookup operator for small dimension tables.') + + if ($bareJoins.Count -le $allowed) + { + # Ratchet: if bare joins were removed, the baseline must be lowered so they cannot return. + $bareJoins.Count | Should -Be $allowed -Because ("the bare-join count in this file dropped below the baseline ($allowed); lower the baseline entry for '$RelPath' in KqlJoinKinds.Tests.ps1 to $($bareJoins.Count) (or remove it if 0) so the fix is locked in.") + } + } + + It 'Should not use operators ARG rejects (lookup, semi/anti joins): ' -ForEach $argFiles { + $content = Get-Content -Path $FullName -Raw + $rejected = @([regex]::Matches($content, '\|\s*lookup\b|join\s+kind\s*=\s*(leftanti|leftsemi|rightanti|rightsemi|anti|semi|leftantisemi|rightantisemi)\b')) + + @($rejected | ForEach-Object { $_.Value }) -join '; ' | Should -BeNullOrEmpty -Because ('Azure Resource Graph rejects the lookup operator and all semi/anti join flavors with InvalidQuery (verified live; supported kinds are inner, innerunique, leftouter, rightouter, fullouter). For exclusions in ARG, use join kind=leftouter + where isempty() with a key-unique right side. If this file contains a Log Analytics query that legitimately needs the operator, add a per-file allowlist to this test.') + } +} diff --git a/src/queries/catalog/macc-consumption-vs-commitment.kql b/src/queries/catalog/macc-consumption-vs-commitment.kql index e61df18a0..b776ad943 100644 --- a/src/queries/catalog/macc-consumption-vs-commitment.kql +++ b/src/queries/catalog/macc-consumption-vs-commitment.kql @@ -47,11 +47,11 @@ let accountCommitment = // switch to max() or scan for the most-recent MACC event per period. | summarize AccountCommitment = todouble(sum(x_MonetaryCommitment)) by BillingCurrency, BillingAccountId, ReportingMonth; profileConsumption -| join kind=leftouter ( +| lookup kind=leftouter ( profileConsumption | summarize AccountConsumption = sum(ProfileConsumption) by BillingCurrency, BillingAccountId, ReportingMonth ) on BillingCurrency, BillingAccountId, ReportingMonth -| join kind=leftouter accountCommitment on BillingCurrency, BillingAccountId, ReportingMonth +| lookup kind=leftouter (accountCommitment) on BillingCurrency, BillingAccountId, ReportingMonth | extend AccountCommitment = todouble(coalesce(AccountCommitment, 0.0)) | extend ProfileShare = iff(AccountConsumption == 0.0, 0.0, ProfileConsumption / AccountConsumption) | extend ConsumptionAmount = ProfileConsumption diff --git a/src/queries/catalog/storage-tier-distribution.kql b/src/queries/catalog/storage-tier-distribution.kql index 5ec2dc7ff..6c96f9000 100644 --- a/src/queries/catalog/storage-tier-distribution.kql +++ b/src/queries/catalog/storage-tier-distribution.kql @@ -48,7 +48,7 @@ bucketedCosts EffectiveCost = todouble(sum(EffectiveCost)), GBMonths = todouble(sum(GBMonths)) by BillingCurrency, TierBucket -| join kind=inner totalsByCurrency on BillingCurrency +| lookup kind=inner (totalsByCurrency) on BillingCurrency | project BillingCurrency, TierBucket, diff --git a/src/queries/catalog/tagging-policy-compliance.kql b/src/queries/catalog/tagging-policy-compliance.kql index 1156932d2..1c9a65ed3 100644 --- a/src/queries/catalog/tagging-policy-compliance.kql +++ b/src/queries/catalog/tagging-policy-compliance.kql @@ -47,7 +47,7 @@ let tag_compliance = materialize( | project tmp_TagsString, tmp_IsCompliant ); base -| join kind=leftouter tag_compliance on tmp_TagsString +| lookup kind=leftouter (tag_compliance) on tmp_TagsString | summarize CompliantEffectiveCost = todouble(sumif(EffectiveCost, tmp_IsCompliant)), TotalEffectiveCost = todouble(sum(EffectiveCost)) diff --git a/src/queries/finops-hub-database-guide.md b/src/queries/finops-hub-database-guide.md index 1ea575922..163df6fae 100644 --- a/src/queries/finops-hub-database-guide.md +++ b/src/queries/finops-hub-database-guide.md @@ -126,17 +126,16 @@ This example demonstrates how to report costs using the full financial hierarchy ```kusto let numberOfMonths = 1; // Set to desired reporting period +let GrandTotal = toscalar( + Costs() + | where ChargePeriodStart >= monthsago(numberOfMonths) + | summarize sum(EffectiveCost) +); Costs() | where ChargePeriodStart >= monthsago(numberOfMonths) | extend Team = tostring(Tags['team']), Product = tostring(Tags['product']), Application = tostring(Tags['application']) | summarize TotalCost = sum(EffectiveCost) by x_BillingProfileName, x_InvoiceSectionName, Team, Product, Application -| join kind=leftouter ( - Costs() - | where ChargePeriodStart >= monthsago(numberOfMonths) - | summarize GrandTotal = sum(EffectiveCost) -) -on 1 == 1 | extend PercentOfTotal = 100.0 * TotalCost / GrandTotal | project x_BillingProfileName, x_InvoiceSectionName, Team, Product, Application, TotalCost, PercentOfTotal | order by TotalCost desc diff --git a/src/templates/agent-skills/azure-cost-management/references/azure-orphaned-resources.md b/src/templates/agent-skills/azure-cost-management/references/azure-orphaned-resources.md index 100aeafde..4e75f73f3 100644 --- a/src/templates/agent-skills/azure-cost-management/references/azure-orphaned-resources.md +++ b/src/templates/agent-skills/azure-cost-management/references/azure-orphaned-resources.md @@ -105,15 +105,16 @@ az graph query -q " resources | where type == 'microsoft.compute/snapshots' | where todatetime(properties.timeCreated) < ago(30d) -| extend sourceDisk = tostring(properties.creationData.sourceResourceId) +| extend sourceDisk = tolower(tostring(properties.creationData.sourceResourceId)) | where not(sourceDisk has '/snapshots/') -| join kind=leftanti ( +| join kind=leftouter ( resources | where type == 'microsoft.compute/disks' - | project id -) on \$left.sourceDisk == \$right.id + | project diskId = tolower(id) +) on \$left.sourceDisk == \$right.diskId +| where isempty(diskId) | project name, resourceGroup, subscriptionId, location, - sizeGb = properties.diskSizeGB, + sizeGb = toint(properties.diskSizeGB), created = properties.timeCreated, sourceDisk | order by sizeGb desc diff --git a/src/templates/agent-skills/finops-toolkit/references/cost-spike-investigation.md b/src/templates/agent-skills/finops-toolkit/references/cost-spike-investigation.md index 733ce043c..b200adc8a 100644 --- a/src/templates/agent-skills/finops-toolkit/references/cost-spike-investigation.md +++ b/src/templates/agent-skills/finops-toolkit/references/cost-spike-investigation.md @@ -87,6 +87,8 @@ let baseline = | summarize BaselineCost = sum(EffectiveCost) by ServiceName; spike | join kind=fullouter baseline on ServiceName +| extend ServiceName = coalesce(ServiceName, ServiceName1) +| project-away ServiceName1 | extend SpikeCost = coalesce(SpikeCost, 0.0), BaselineCost = coalesce(BaselineCost, 0.0) | extend CostIncrease = SpikeCost - BaselineCost | order by CostIncrease desc diff --git a/src/templates/agent-skills/finops-toolkit/references/service-cost-deep-dive.md b/src/templates/agent-skills/finops-toolkit/references/service-cost-deep-dive.md index b4edc2c24..8bc915e4c 100644 --- a/src/templates/agent-skills/finops-toolkit/references/service-cost-deep-dive.md +++ b/src/templates/agent-skills/finops-toolkit/references/service-cost-deep-dive.md @@ -207,6 +207,10 @@ let prior = | summarize PriorCost = sum(EffectiveCost) by RegionName, x_ResourceGroupName, ResourceType; recent | join kind=fullouter prior on RegionName, x_ResourceGroupName, ResourceType +| extend RegionName = coalesce(RegionName, RegionName1), + x_ResourceGroupName = coalesce(x_ResourceGroupName, x_ResourceGroupName1), + ResourceType = coalesce(ResourceType, ResourceType1) +| project-away RegionName1, x_ResourceGroupName1, ResourceType1 | extend RecentCost = coalesce(RecentCost, 0.0), PriorCost = coalesce(PriorCost, 0.0), Delta = RecentCost - PriorCost diff --git a/src/templates/claude-plugin/agents/ftk-database-query.md b/src/templates/claude-plugin/agents/ftk-database-query.md index 42e20002b..9a1a1b18e 100644 --- a/src/templates/claude-plugin/agents/ftk-database-query.md +++ b/src/templates/claude-plugin/agents/ftk-database-query.md @@ -109,10 +109,9 @@ Costs() **Percent-of-total:** ```kusto +let GrandTotal = toscalar(Costs() | summarize sum(EffectiveCost)); Costs() -| as allCosts -| summarize GrandTotal = sum(EffectiveCost) -| join kind=inner (allCosts | summarize Cost = sum(EffectiveCost) by ServiceName) on 1 == 1 +| summarize Cost = sum(EffectiveCost) by ServiceName | extend Pct = 100.0 * Cost / GrandTotal ``` diff --git a/src/templates/finops-alerts/modules/logicApp.bicep b/src/templates/finops-alerts/modules/logicApp.bicep index 1d4692384..055385e74 100644 --- a/src/templates/finops-alerts/modules/logicApp.bicep +++ b/src/templates/finops-alerts/modules/logicApp.bicep @@ -344,7 +344,7 @@ resource finopsAlerts 'Microsoft.Logic/workflows@2019-05-01' = { uri: '${environment().resourceManager}//providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01' method: 'POST' body: { - query: '@{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/applicationGateways\'| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier=tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools| join (resources | where type =~ \'Microsoft.Network/applicationGateways\'| mvexpand backendPools = properties.backendAddressPools| extend backendIPCount =array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName=backendPools.properties.backendAddressPools.name | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id) on id| project-away id1| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))| order by id asc | join kind=leftouter ( resourcecontainers | where type == \'microsoft.resources/subscriptions\' | project subscriptionId, subscriptionName = name) on subscriptionId' + query: '@{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/applicationGateways\'| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier=tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools| join kind=inner (resources | where type =~ \'Microsoft.Network/applicationGateways\'| mvexpand backendPools = properties.backendAddressPools| extend backendIPCount =array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName=backendPools.properties.backendAddressPools.name | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id) on id| project-away id1| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))| order by id asc | join kind=leftouter ( resourcecontainers | where type == \'microsoft.resources/subscriptions\' | project subscriptionId, subscriptionName = name) on subscriptionId' scope: 'Tenant' } authentication: { @@ -382,7 +382,7 @@ resource finopsAlerts 'Microsoft.Logic/workflows@2019-05-01' = { uri: '${environment().resourceManager}//providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01' method: 'POST' body: { - query: '@{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/publicIPAddresses\' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, tenantId | union ( @{variables(\'resourcesTable\')} | where type =~ \'microsoft.network/networkinterfaces\' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) | extend IPconfig = properties.ipConfigurations | mv-expand IPconfig | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id) | project PublicIpId | join ( @{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/publicIPAddresses\' | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location ) on PublicIpId | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, tenantId) | join kind=leftouter ( resourcecontainers | where type == \'microsoft.resources/subscriptions\' | project subscriptionId, subscriptionName = name) on subscriptionId' + query: '@{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/publicIPAddresses\' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, tenantId | union ( @{variables(\'resourcesTable\')} | where type =~ \'microsoft.network/networkinterfaces\' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) | extend IPconfig = properties.ipConfigurations | mv-expand IPconfig | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id) | project PublicIpId | join kind=inner ( @{variables(\'resourcesTable\')} | where type =~ \'Microsoft.Network/publicIPAddresses\' | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location ) on PublicIpId | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, tenantId) | join kind=leftouter ( resourcecontainers | where type == \'microsoft.resources/subscriptions\' | project subscriptionId, subscriptionName = name) on subscriptionId' scope: 'Tenant' } authentication: { 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 af5adb4c5..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 @@ -24,7 +24,7 @@ CommitmentDiscountUsage_v1_2() x_CommitmentDiscountCommittedAmount = toreal(x_CommitmentDiscountCommittedAmount), x_CommitmentDiscountNormalizedRatio = toreal(x_CommitmentDiscountNormalizedRatio) // Add new columns - | lookup kind=leftouter (Services | distinct x_ResourceType, ServiceSubcategory) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceSubcategory) by x_ResourceType) on x_ResourceType | extend CommitmentDiscountQuantity = ConsumedQuantity * x_CommitmentDiscountNormalizedRatio | extend CommitmentDiscountUnit = case( x_CommitmentDiscountNormalizedRatio == 1, 'Hours', diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_0.kql b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_0.kql index 82b2cb0b7..50401ebb7 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_0.kql +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/scripts/IngestionSetup_v1_0.kql @@ -840,12 +840,12 @@ ActualCosts_transform_v1_0() | project-rename PricingQuantity = Quantity, x_PricingUnitDescription = UnitOfMeasure - | join kind=leftouter (PricingUnits) on x_PricingUnitDescription - | join kind=leftouter (Regions) on ResourceLocation - | join kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType + | lookup kind=leftouter (PricingUnits) on x_PricingUnitDescription + | lookup kind=leftouter (Regions) on ResourceLocation + | lookup kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType // TODO: Add the following in 1.2: ServiceSubcategory, PublisherName, x_PublisherCategory, x_Environment, x_ServiceModel - | join kind=leftouter (Services | where isnotempty(x_ResourceType) | project x_ResourceType, ServiceName, ServiceCategory) on x_ResourceType - | join kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory) by ConsumedService = x_ConsumedService) on ConsumedService + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory) by x_ResourceType) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory) by ConsumedService = x_ConsumedService) on ConsumedService | extend CommitmentDiscountCategory = iff(isnotempty(ReservationId), 'Usage', '') // TODO: CommitmentDiscountCategory needs to handle savings plans | project AvailabilityZone = AvailabilityZone, @@ -1062,12 +1062,12 @@ AmortizedCosts_transform_v1_0() | project-rename PricingQuantity = Quantity, x_PricingUnitDescription = UnitOfMeasure - | join kind=leftouter (PricingUnits) on x_PricingUnitDescription - | join kind=leftouter (Regions) on ResourceLocation - | join kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType + | lookup kind=leftouter (PricingUnits) on x_PricingUnitDescription + | lookup kind=leftouter (Regions) on ResourceLocation + | lookup kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType // TODO: Add the following in 1.2: ServiceSubcategory, PublisherName, x_PublisherCategory, x_Environment, x_ServiceModel - | join kind=leftouter (Services | where isnotempty(x_ResourceType) | project x_ResourceType, ServiceName, ServiceCategory) on x_ResourceType - | join kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory) by ConsumedService = x_ConsumedService) on ConsumedService + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory) by x_ResourceType) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory) by ConsumedService = x_ConsumedService) on ConsumedService | extend CommitmentDiscountCategory = iff(isnotempty(ReservationId), 'Usage', '') // TODO: CommitmentDiscountCategory needs to handle savings plans | project AvailabilityZone = AvailabilityZone, @@ -1282,7 +1282,7 @@ CommitmentDiscountUsage_transform_v1_0() | extend x_ResourceGroupName = tostring(tmp_ResourceDetails.x_ResourceGroupName) | extend x_ResourceType = tostring(tmp_ResourceDetails.x_ResourceType) | lookup kind=leftouter (ResourceTypes | distinct x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType - | lookup kind=leftouter (Services | distinct x_ResourceType, ServiceName, ServiceCategory, x_ServiceModel) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(x_ServiceModel) by x_ResourceType) on x_ResourceType // // Sort columns and apply final transforms | project 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 a71f1aef7..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 @@ -1120,12 +1120,12 @@ ActualCosts_transform_v1_2() | project-rename PricingQuantity = Quantity, x_PricingUnitDescription = UnitOfMeasure - | join kind=leftouter (PricingUnits) on x_PricingUnitDescription - | join kind=leftouter (Regions) on ResourceLocation - | join kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType + | lookup kind=leftouter (PricingUnits) on x_PricingUnitDescription + | lookup kind=leftouter (Regions) on ResourceLocation + | lookup kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType // TODO: Add the following in 1.2: PublisherName, x_PublisherCategory, x_Environment - | join kind=leftouter (Services | where isnotempty(x_ResourceType) | project x_ResourceType, ServiceName, ServiceCategory, ServiceSubcategory, x_ServiceModel) on x_ResourceType - | join kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by ConsumedService = x_ConsumedService) on ConsumedService + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by x_ResourceType) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by ConsumedService = x_ConsumedService) on ConsumedService | extend CommitmentDiscountCategory = iff(isnotempty(ReservationId), 'Usage', '') // TODO: CommitmentDiscountCategory needs to handle savings plans | project AvailabilityZone = AvailabilityZone, @@ -1341,12 +1341,12 @@ AmortizedCosts_transform_v1_2() | project-rename PricingQuantity = Quantity, x_PricingUnitDescription = UnitOfMeasure - | join kind=leftouter (PricingUnits) on x_PricingUnitDescription - | join kind=leftouter (Regions) on ResourceLocation - | join kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType + | lookup kind=leftouter (PricingUnits) on x_PricingUnitDescription + | lookup kind=leftouter (Regions) on ResourceLocation + | lookup kind=leftouter (ResourceTypes | project x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType // TODO: Add the following in 1.2: PublisherName, x_PublisherCategory, x_Environment - | join kind=leftouter (Services | where isnotempty(x_ResourceType) | project x_ResourceType, ServiceName, ServiceCategory, ServiceSubcategory, x_ServiceModel) on x_ResourceType - | join kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by ConsumedService = x_ConsumedService) on ConsumedService + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by x_ResourceType) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ConsumedService) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by ConsumedService = x_ConsumedService) on ConsumedService | extend CommitmentDiscountCategory = iff(isnotempty(ReservationId), 'Usage', '') // TODO: CommitmentDiscountCategory needs to handle savings plans | project AvailabilityZone = AvailabilityZone, @@ -1554,7 +1554,7 @@ CommitmentDiscountUsage_transform_v1_2() | extend x_ResourceGroupName = tostring(tmp_ResourceDetails.x_ResourceGroupName) | extend x_ResourceType = tostring(tmp_ResourceDetails.x_ResourceType) | lookup kind=leftouter (ResourceTypes | distinct x_ResourceType, ResourceType = SingularDisplayName) on x_ResourceType - | lookup kind=leftouter (Services | distinct x_ResourceType, ServiceName, ServiceCategory, ServiceSubcategory, x_ServiceModel) on x_ResourceType + | lookup kind=leftouter (Services | where isnotempty(x_ResourceType) | summarize take_any(ServiceName), take_any(ServiceCategory), take_any(ServiceSubcategory), take_any(x_ServiceModel) by x_ResourceType) on x_ResourceType // // Sort columns and apply final transforms | project diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessAppGateways.json b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessAppGateways.json index 31c5d2b6e..0e5c0bde8 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessAppGateways.json +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessAppGateways.json @@ -1,7 +1,7 @@ { "dataset": "Recommendations", "provider": "Microsoft", - "query": "resources | where type =~ 'Microsoft.Network/applicationGateways' | extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools| project id, name, SKUName, SKUTier, SKUCapacity,resourceGroup,subscriptionId, AppGWName=name, type, Location=location| join ( resources | where type =~ 'Microsoft.Network/applicationGateways' | mvexpand backendPools = properties.backendAddressPools | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName = backendPools.properties.backendAddressPools.name | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id) on id| project-away id1| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))| project x_RecommendationId=strcat(tolower(id),'-idle'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Application Gateway without any backend pool', ResourceId = tolower(id), ResourceName=tolower(AppGWName), x_RecommendationDetails= tostring(bag_pack('backendIPCount', backendIPCount, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review and remove this resource if not needed', 'x_RecommendationTypeId', '4f69df93-5972-44e0-97cf-4343c2bcf4b8', 'x_ResourceType', type, 'x_RecommendationMaturityLevel', 'Preview')), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", + "query": "resources | where type =~ 'Microsoft.Network/applicationGateways' | extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools| project id, name, SKUName, SKUTier, SKUCapacity,resourceGroup,subscriptionId, AppGWName=name, type, Location=location| join kind=inner ( resources | where type =~ 'Microsoft.Network/applicationGateways' | mvexpand backendPools = properties.backendAddressPools | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) | extend backendPoolName = backendPools.properties.backendAddressPools.name | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id) on id| project-away id1| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))| project x_RecommendationId=strcat(tolower(id),'-idle'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Application Gateway without any backend pool', ResourceId = tolower(id), ResourceName=tolower(AppGWName), x_RecommendationDetails= tostring(bag_pack('backendIPCount', backendIPCount, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review and remove this resource if not needed', 'x_RecommendationTypeId', '4f69df93-5972-44e0-97cf-4343c2bcf4b8', 'x_ResourceType', type, 'x_RecommendationMaturityLevel', 'Preview')), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", "queryEngine": "ResourceGraph", "scope": "Tenant", "source": "FinOps hubs", diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json index 1e4691147..5e90e91f2 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json @@ -1,7 +1,7 @@ { "dataset": "Recommendations", "provider": "Microsoft", - "query": "resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | extend SubscriptionName=name | join ( resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' | extend SQLID=id, VMName = name, resourceGroup, Location = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])) on subscriptionId | join ( resources | where type =~ 'Microsoft.Compute/virtualmachines' | extend ActualCores = toint(extract('.[A-Z]([0-9]+)', 1, tostring(properties.hardwareProfile.vmSize))) | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize),ActualCores, subscriptionId, vmType=type, vmResourceGroup=resourceGroup) on VMName| order by id asc | where SQLSKU != 'Developer' and SQLSKU != 'Express'| extend CheckAHBSQLVM= case( type == 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType']) != 'AHUB', 'AHB-disabled', 'AHB-enabled'), 'Not Windows')| project SQLID,VMName,resourceGroup, Location, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName,type,CheckAHBSQLVM, subscriptionId,ActualCores, vmType, vmResourceGroup| project x_RecommendationId=strcat(tolower(SQLID),'-CheckAHBSQLVM'), x_ResourceGroupName=tolower(vmResourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='SQL virtual machine is not leveraging Azure Hybrid Benefit', ResourceId = tolower(SQLID), ResourceName=tolower(VMName), x_RecommendationDetails= tostring(bag_pack('VMSize', VMSize, 'CheckAHBSQLVM', CheckAHBSQLVM, 'ActualCores', ActualCores, 'SQLVersion', SQLVersion, 'SQLSKU', SQLSKU, 'SQLAgentType', SQLAgentType, 'LicenseType', LicenseType, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review the SQL licensing option', 'x_RecommendationTypeId', '01decd62-f91b-4434-abe5-9a09e13e018f', 'x_ResourceType', vmType)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", + "query": "resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | extend SubscriptionName=name | join kind=inner ( resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' | extend SQLID=id, VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), VMName = name, resourceGroup, Location = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])) on subscriptionId | join kind=inner ( resources | where type =~ 'Microsoft.Compute/virtualmachines' | extend ActualCores = toint(extract('.[A-Z]([0-9]+)', 1, tostring(properties.hardwareProfile.vmSize))) | project VMResourceId = tolower(id), VMSize = tostring(properties.hardwareProfile.vmSize),ActualCores, subscriptionId, vmType=type, vmResourceGroup=resourceGroup) on VMResourceId| order by id asc | where SQLSKU != 'Developer' and SQLSKU != 'Express'| extend CheckAHBSQLVM= case( type == 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType']) != 'AHUB', 'AHB-disabled', 'AHB-enabled'), 'Not Windows')| project SQLID,VMName,resourceGroup, Location, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName,type,CheckAHBSQLVM, subscriptionId,ActualCores, vmType, vmResourceGroup| project x_RecommendationId=strcat(tolower(SQLID),'-CheckAHBSQLVM'), x_ResourceGroupName=tolower(vmResourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='SQL virtual machine is not leveraging Azure Hybrid Benefit', ResourceId = tolower(SQLID), ResourceName=tolower(VMName), x_RecommendationDetails= tostring(bag_pack('VMSize', VMSize, 'CheckAHBSQLVM', CheckAHBSQLVM, 'ActualCores', ActualCores, 'SQLVersion', SQLVersion, 'SQLSKU', SQLSKU, 'SQLAgentType', SQLAgentType, 'LicenseType', LicenseType, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review the SQL licensing option', 'x_RecommendationTypeId', '01decd62-f91b-4434-abe5-9a09e13e018f', 'x_ResourceType', vmType)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", "queryEngine": "ResourceGraph", "scope": "Tenant", "source": "FinOps hubs", diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedPublicIPs.json b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedPublicIPs.json index 67cc597b4..013707b61 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedPublicIPs.json +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedPublicIPs.json @@ -1,7 +1,7 @@ { "dataset": "Recommendations", "provider": "Microsoft", - "query": "resources | where type =~ 'Microsoft.Network/publicIPAddresses' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location | project PublicIpId, IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, type, name | union ( resources | where type =~ 'microsoft.network/networkinterfaces' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) | extend IPconfig = properties.ipConfigurations | mv-expand IPconfig | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id) | project PublicIpId, name | join ( resources | where type =~ 'Microsoft.Network/publicIPAddresses'| extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location, name, id ) on PublicIpId | extend PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod,name, subscriptionId )| project x_RecommendationId=strcat(tolower(PublicIpId),'-idle'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Unattached (orphaned) public IP', ResourceId = tolower(PublicIpId), ResourceName=tolower(name), x_RecommendationDetails= tostring(bag_pack('SKUName', SKUName, 'AllocationMethod', AllocationMethod, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review and remove this resource if not needed', 'x_RecommendationTypeId', '3ecbf770-9404-4504-a450-cc198e8b2a7d', 'x_ResourceType', type)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", + "query": "resources | where type =~ 'Microsoft.Network/publicIPAddresses' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location | project PublicIpId, IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId, type, name | union ( resources | where type =~ 'microsoft.network/networkinterfaces' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) | extend IPconfig = properties.ipConfigurations | mv-expand IPconfig | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id) | project PublicIpId, name | join kind=inner ( resources | where type =~ 'Microsoft.Network/publicIPAddresses'| extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location, name, id ) on PublicIpId | extend PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod,name, subscriptionId )| project x_RecommendationId=strcat(tolower(PublicIpId),'-idle'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Unattached (orphaned) public IP', ResourceId = tolower(PublicIpId), ResourceName=tolower(name), x_RecommendationDetails= tostring(bag_pack('SKUName', SKUName, 'AllocationMethod', AllocationMethod, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review and remove this resource if not needed', 'x_RecommendationTypeId', '3ecbf770-9404-4504-a450-cc198e8b2a7d', 'x_ResourceType', type)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", "queryEngine": "ResourceGraph", "scope": "Tenant", "source": "FinOps hubs", diff --git a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json index 603771ebb..b9a26b05a 100644 --- a/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json +++ b/src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json @@ -1,7 +1,7 @@ { "dataset": "Recommendations", "provider": "Microsoft", - "query": "resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | extend SubscriptionName=name | join ( resources | where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets' | where (tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' and tostring(properties.virtualMachineProfile.storageProfile.imageReference.publisher) !in~ ('microsoftwindowsdesktop','microsoftvisualstudio') and tostring(properties.virtualMachineProfile.licenseType) !startswith 'Windows') or (tostring(properties.storageProfile.osDisk.osType) == 'Windows' and tostring(properties.storageProfile.imageReference.publisher) !in~ ('microsoftwindowsdesktop','microsoftvisualstudio') and tostring(properties.licenseType) !startswith 'Windows') | extend WindowsId=id, VMSku=tostring(properties.hardwareProfile.vmSize), vmResourceGroup=resourceGroup, vmType=type, Location=location,LicenseType = tostring(properties.['licenseType']) | extend ActualCores = toint(extract('.[A-Z]([0-9]+)', 1, tostring(properties.hardwareProfile.vmSize))) | extend CheckAHBWindows = case( type == 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets', iif((properties.['licenseType']) !has 'Windows' and (properties.virtualMachineProfile.['licenseType']) !has 'Windows' , 'AHB-disabled', 'AHB-enabled'), 'Not Windows' )) on subscriptionId | project x_RecommendationId=strcat(tolower(WindowsId),'-CheckAHBWindows'), x_ResourceGroupName=tolower(vmResourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Windows virtual machine is not leveraging Azure Hybrid Benefit', ResourceId = tolower(WindowsId), ResourceName=tolower(split(WindowsId,'/')[-1]), x_RecommendationDetails= tostring(bag_pack('VMSku', VMSku, 'CheckAHBWindows', CheckAHBWindows, 'ActualCores', ActualCores, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review the virtual machine licensing option', 'x_RecommendationTypeId', 'f326c065-b9f7-4a0e-a0f1-5a1c060bc25d', 'x_ResourceType', vmType)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", + "query": "resourcecontainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has 'MSDNDevTest_2014-09-01' | extend SubscriptionName=name | join kind=inner ( resources | where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets' | where (tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' and tostring(properties.virtualMachineProfile.storageProfile.imageReference.publisher) !in~ ('microsoftwindowsdesktop','microsoftvisualstudio') and tostring(properties.virtualMachineProfile.licenseType) !startswith 'Windows') or (tostring(properties.storageProfile.osDisk.osType) == 'Windows' and tostring(properties.storageProfile.imageReference.publisher) !in~ ('microsoftwindowsdesktop','microsoftvisualstudio') and tostring(properties.licenseType) !startswith 'Windows') | extend WindowsId=id, VMSku=tostring(properties.hardwareProfile.vmSize), vmResourceGroup=resourceGroup, vmType=type, Location=location,LicenseType = tostring(properties.['licenseType']) | extend ActualCores = toint(extract('.[A-Z]([0-9]+)', 1, tostring(properties.hardwareProfile.vmSize))) | extend CheckAHBWindows = case( type == 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets', iif((properties.['licenseType']) !has 'Windows' and (properties.virtualMachineProfile.['licenseType']) !has 'Windows' , 'AHB-disabled', 'AHB-enabled'), 'Not Windows' )) on subscriptionId | project x_RecommendationId=strcat(tolower(WindowsId),'-CheckAHBWindows'), x_ResourceGroupName=tolower(vmResourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Windows virtual machine is not leveraging Azure Hybrid Benefit', ResourceId = tolower(WindowsId), ResourceName=tolower(split(WindowsId,'/')[-1]), x_RecommendationDetails= tostring(bag_pack('VMSku', VMSku, 'CheckAHBWindows', CheckAHBWindows, 'ActualCores', ActualCores, 'Location', Location, 'x_RecommendationProvider', 'FinOps hubs', 'x_RecommendationSolution', 'Review the virtual machine licensing option', 'x_RecommendationTypeId', 'f326c065-b9f7-4a0e-a0f1-5a1c060bc25d', 'x_ResourceType', vmType)), x_RecommendationDate = now() | join kind=leftouter ( resourcecontainers | where type == 'microsoft.resources/subscriptions' | project SubAccountName=name, SubAccountId=subscriptionId ) on SubAccountId | project-away SubAccountId1", "queryEngine": "ResourceGraph", "scope": "Tenant", "source": "FinOps hubs", diff --git a/src/workbooks/optimization/AHB/AHB.workbook b/src/workbooks/optimization/AHB/AHB.workbook index 9b3751fcd..70a52ec38 100644 --- a/src/workbooks/optimization/AHB/AHB.workbook +++ b/src/workbooks/optimization/AHB/AHB.workbook @@ -1277,7 +1277,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "resourcechanges\r\n| where properties.changeType == \"Update\" and properties.targetResourceType == \"microsoft.compute/virtualmachines\"\r\n| mv-expand changes = properties.changes\r\n| mv-expand LicenseChanges=changes.['properties.licenseType']\r\n| extend WindowsId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| where isnotnull(LicenseChanges)\r\n| where tostring(LicenseChanges.newValue) has \"Windows\"\r\n| project VMID=properties.targetResourceId, NewLicense=tostring(LicenseChanges.newValue), DateofChange=todatetime(properties.changeAttributes.timestamp)\r\n", + "query": "resourcechanges\r\n| where properties.changeType == \"Update\" and properties.targetResourceType == \"microsoft.compute/virtualmachines\"\r\n| mv-expand changes = properties.changes\r\n| mv-expand LicenseChanges=changes.['properties.licenseType']\r\n| extend WindowsId=tolower(tostring(properties.targetResourceId))\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=tolower(id)\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| where isnotnull(LicenseChanges)\r\n| where tostring(LicenseChanges.newValue) has \"Windows\"\r\n| project VMID=properties.targetResourceId, NewLicense=tostring(LicenseChanges.newValue), DateofChange=todatetime(properties.changeAttributes.timestamp)\r\n", "size": 0, "title": "VM Latest Change Last 7 days", "noDataMessage": "AHB was not enabled in the last 7 days.", @@ -2143,7 +2143,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join kind=inner (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join kind=inner (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMResourceId = tolower(id), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMResourceId\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Disabled", "noDataMessage": "All of your VMs have AHB enabled.", @@ -2165,7 +2165,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join kind=inner (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join kind=inner (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMResourceId = tolower(id), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMResourceId\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Enabled", "noDataMessage": "None of your VMs have AHB enabled.", diff --git a/src/workbooks/optimization/Compute/AHB.workbook b/src/workbooks/optimization/Compute/AHB.workbook index 969e68022..2c020a3ff 100644 --- a/src/workbooks/optimization/Compute/AHB.workbook +++ b/src/workbooks/optimization/Compute/AHB.workbook @@ -249,7 +249,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "resourcechanges\r\n| where properties.changeType == \"Update\" and properties.targetResourceType == \"microsoft.compute/virtualmachines\"\r\n| mv-expand changes = properties.changes\r\n| mv-expand LicenseChanges=changes.['properties.licenseType']\r\n| extend WindowsId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| where isnotnull(LicenseChanges)\r\n| where tostring(LicenseChanges.newValue) has \"Windows\"\r\n| project VMID=properties.targetResourceId, NewLicense=tostring(LicenseChanges.newValue), DateofChange=todatetime(properties.changeAttributes.timestamp)\r\n", + "query": "resourcechanges\r\n| where properties.changeType == \"Update\" and properties.targetResourceType == \"microsoft.compute/virtualmachines\"\r\n| mv-expand changes = properties.changes\r\n| mv-expand LicenseChanges=changes.['properties.licenseType']\r\n| extend WindowsId=tolower(tostring(properties.targetResourceId))\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=tolower(id)\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| where isnotnull(LicenseChanges)\r\n| where tostring(LicenseChanges.newValue) has \"Windows\"\r\n| project VMID=properties.targetResourceId, NewLicense=tostring(LicenseChanges.newValue), DateofChange=todatetime(properties.changeAttributes.timestamp)\r\n", "size": 0, "title": "VM Latest Change Last 7 days", "noDataMessage": "AHB was not enabled in the last 7 days.", @@ -1120,7 +1120,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join kind=inner (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join kind=inner (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMResourceId = tolower(id), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMResourceId\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Disabled", "noDataMessage": "All of your VMs have AHB enabled.", @@ -1142,7 +1142,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join kind=inner (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMResourceId = tolower(tostring(properties.virtualMachineResourceId)), VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join kind=inner (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMResourceId = tolower(id), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMResourceId\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind=inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Enabled", "noDataMessage": "None of your VMs have AHB enabled.", diff --git a/src/workbooks/optimization/SavingsPlan/SavingsPlan.workbook b/src/workbooks/optimization/SavingsPlan/SavingsPlan.workbook index 4a2ed55b7..bc165a347 100644 --- a/src/workbooks/optimization/SavingsPlan/SavingsPlan.workbook +++ b/src/workbooks/optimization/SavingsPlan/SavingsPlan.workbook @@ -105,7 +105,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend recommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| summarize Subscriptions=dcount(resources), \r\n bin (sum(savings), 0.01) by subscription ,commitment ,lookbackPeriod,scope,term ,currency\r\n| order by sum_savings desc\r\n| join (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend recommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| summarize Subscriptions=dcount(resources), \r\n bin (sum(savings), 0.01) by subscription ,commitment ,lookbackPeriod,scope,term ,currency\r\n| order by sum_savings desc\r\n| join kind=inner (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", "size": 0, "title": "Savings plan Summary", "noDataMessage": "No savings plan recommendations found!", @@ -144,7 +144,7 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nregion = tostring(properties.extendedProperties.region),\r\nsubscription = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| project Recommendation,savings,commitment,currency,lookbackPeriod,term,subscription\r\n| order by savings desc\r\n| join (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project id,name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nregion = tostring(properties.extendedProperties.region),\r\nsubscription = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| project Recommendation,savings,commitment,currency,lookbackPeriod,term,subscription\r\n| order by savings desc\r\n| join kind=inner (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project id,name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", "size": 0, "title": "Savings plan details", "noDataMessage": "No savings plan recommendations found!",