[Hubs] Add new custom recommendations#2049
[Hubs] Add new custom recommendations#2049flanakin wants to merge 4 commits intofeatures/hubs-recsfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR expands FinOps hubs recommendations by adding new Azure Resource Graph (ARG) recommendation queries and exposing new deployment switches to control noisy recommendation sets, alongside broad documentation refreshes and some build/test script improvements across the repo.
Changes:
- Added multiple new custom ARG recommendation query JSON files and normalized several existing recommendation query metadata fields.
- Updated FinOps hubs Bicep parameters to make recommendations opt-in by default and add separate toggles for AHB and non-Spot AKS recommendations.
- Updated PowerShell build/test/publish scripts and refreshed a large set of docs (notably
ms.date, formatting, and best-practices content).
Reviewed changes
Copilot reviewed 199 out of 201 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/templates/finops-hub/modules/hub.bicep | Adds recommendation enablement parameters and passes new flags to recommendations module |
| src/templates/finops-hub/modules/fx/scripts/Init-DataFactory.ps1 | Changes trigger subscription handling logic and polling |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnprovisionedExpressRouteCircuits.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnmanagedDisks.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedNICs.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnattachedDisks.json | Normalizes query metadata/provider strings |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-UnassociatedDDoSPlans.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-StoppedVMs.json | Normalizes query metadata/provider strings |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-PremiumSnapshots.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-OrphanedNATGateways.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyStorageAccounts.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyPostgreSQLServers.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyMySQLServers.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-IdleVNetGateways.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-EmptySQLElasticPools.json | Normalizes query metadata/provider strings |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-EmptyNSGs.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-EmptyAppServicePlans.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-ClassicAppGateways.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BasicPublicIPs.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BasicLoadBalancers.json | New recommendation query |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-BackendlessLoadBalancers.json | Normalizes query metadata/provider strings |
| src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-AdvisorCost.json | Normalizes query metadata/provider strings |
| src/templates/finops-hub/main.bicep | Exposes new recommendation parameters at top-level template |
| src/scripts/Update-Version.ps1 | Uses Get-Version -AsTag to compute tag |
| src/scripts/Test-PowerShell.ps1 | Adds Workbooks/Actions test switches and selection logic |
| src/scripts/Publish-Toolkit.ps1 | Updates help text and makes Find-Repo return first match |
| src/scripts/Package-Toolkit.ps1 | Adjusts packaging messages and ZIP naming to use tag |
| src/scripts/New-Directory.ps1 | Improves comment-based help |
| src/scripts/Invoke-Task.ps1 | Expands comment-based help for versioning switches |
| src/scripts/Get-Version.ps1 | Adds -AsTag option and help text |
| src/scripts/Build-Workbook.ps1 | Reorders/help text cleanup for workbook parameter docs |
| src/scripts/Build-Toolkit.ps1 | Expands help text for version increment switches |
| src/scripts/Build-PowerBI.ps1 | Improves comment-based help parameter descriptions |
| src/scripts/Build-Bicep.ps1 | Adds CmdletBinding and updates help text |
| src/powershell/Tests/Unit/New-FinOpsCostExport.Tests.ps1 | Adjusts assertions to trim folder path slashes |
| src/powershell/Tests/Unit/Deploy-FinOpsHub.Tests.ps1 | Adjusts mocks/signatures and adds Initialize-FinOpsHubDeployment mocking |
| src/powershell/Tests/Integration/Hubs.Tests.ps1 | Uses helper function instead of global var for required RPs |
| src/powershell/Tests/Initialize-Tests.ps1 | Replaces global required RPs with a function |
| src/powershell/Public/Start-FinOpsCostExport.ps1 | Adds SupportsShouldProcess and refines verbose messaging/flow |
| src/powershell/Public/Remove-FinOpsHub.ps1 | Switches user output from Write-Host to Write-Information |
| src/powershell/Public/Remove-FinOpsCostExport.ps1 | File encoding/BOM update |
| src/powershell/Public/New-FinOpsCostExport.ps1 | Adds SupportsShouldProcess guarding to RP registration and export creation |
| src/powershell/Public/Initialize-FinOpsHubDeployment.ps1 | Fixes ShouldProcess handling and removes WhatIfPreference pass-through hack |
| src/powershell/Public/Get-FinOpsCostExport.ps1 | File encoding/BOM update |
| src/powershell/Private/Split-AzureResourceId.ps1 | Changes handling for null/empty IDs (now returns nothing) |
| src/powershell/Private/Save-FinOpsHubTemplate.ps1 | Whitespace cleanup |
| src/powershell/Private/Invoke-Rest.ps1 | Whitespace cleanup for token acquisition |
| src/power-bi/storage/Shared.Dataset/definition/tables/Costs.tmdl | Adds CapacityReservationId/Status columns |
| src/power-bi/queries/ftk_NormalizeSchema.pq | Adds capacity reservation normalization columns |
| docs/guide.md | Removes ebook tile from landing page |
| docs/README.md | Adds contributor entry |
| docs-mslearn/toolkit/workbooks/optimization.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/workbooks/governance.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/workbooks/finops-workbooks-overview.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/roadmap.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/toolkit/get-finopstoolkitversion.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/toolkit/finops-toolkit-commands.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/powershell-commands.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/remove-finopshubscope.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/remove-finopshub.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/register-finopshubproviders.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/initialize-finopshubdeployment.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/get-finopshub.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/finops-hubs-commands.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/hubs/deploy-finopshub.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/data/open-data-commands.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/data/get-finopsservice.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/data/get-finopsresourcetype.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/data/get-finopsregion.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/data/get-finopspricingunit.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/start-finopscostexport.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/remove-finopscostexport.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/new-finopscostexport.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/get-finopscostexport.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/cost-management-commands.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/powershell/cost/add-finopsserviceprincipal.md | Updates ms.date and formatting/prettier ignore blocks |
| docs-mslearn/toolkit/power-bi/workload-optimization.md | Updates ms.date, formatting, and required dataset versions table |
| docs-mslearn/toolkit/power-bi/template-app.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/power-bi/help-me-choose.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/power-bi/data-ingestion.md | Updates ms.date and adds download links block formatting |
| docs-mslearn/toolkit/power-bi/connector.md | Updates ms.date, formatting, and fixes spacing typo |
| docs-mslearn/toolkit/optimization-engine/troubleshooting.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/optimization-engine/suppress-recommendations.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/optimization-engine/reports.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/optimization-engine/overview.md | Updates ms.date and formatting (incl. IMPORTANT block) |
| docs-mslearn/toolkit/optimization-engine/faq.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/optimization-engine/customize.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/optimization-engine/configure-workspaces.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/hubs/upgrade.md | Updates ms.date, formatting, and download/deploy blocks |
| docs-mslearn/toolkit/hubs/template.md | Fixes URL formatting to angle-bracket style |
| docs-mslearn/toolkit/hubs/savings-calculations.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/hubs/configure-remote-hubs.md | Updates content (China cloud wording + list formatting) |
| docs-mslearn/toolkit/hubs/configure-dashboards.md | Updates ms.date and markdownlint directives |
| docs-mslearn/toolkit/hubs/configure-ai.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/hubs/compatibility.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/help/troubleshooting.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/help/terms.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/help/support.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/help/help-options.md | Updates ms.date and formatting |
| docs-mslearn/toolkit/help/deploy.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/help/data-dictionary.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/finops-toolkit-overview.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/bicep-registry/scheduled-actions.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/bicep-registry/modules.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/toolkit/alerts/finops-alerts-overview.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/overview.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/index.yml | Removes ebook highlighted card |
| docs-mslearn/implementing-finops-guide.md | Updates ms.date and removes ebook link |
| docs-mslearn/framework/understand/understand-cloud-usage-cost.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/understand/reporting.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/understand/ingestion.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/understand/anomalies.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/understand/allocation.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/unit-economics.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/quantify-business-value.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/planning.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/forecasting.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/budgeting.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/quantify/benchmarking.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/optimize/workloads.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/optimize/rates.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/optimize/optimize-cloud-usage-cost.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/tools-services.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/operations.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/onboarding.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/manage-finops.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/invoicing-chargeback.md | Updates ms.date and fixes spacing in a bullet |
| docs-mslearn/framework/manage/intersecting-disciplines.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/governance.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/education.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/manage/assessment.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/finops-framework.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/framework/capabilities.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/what-is-focus.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/validate.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/metadata.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/mapping.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/convert.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/focus/conformance-summary.md | Updates ms.date and adds prettier ignore blocks |
| docs-mslearn/best-practices/web.md | Adds recommendation documentation for empty App Service plans |
| docs-mslearn/best-practices/storage.md | Adds premium snapshot + legacy storage guidance and rewrites section |
| docs-mslearn/best-practices/library.md | Updates ms.date and formatting |
| docs-mslearn/best-practices/compute.md | Adds recommendation documentation for unmanaged disks |
| docs-mslearn/TOC.yml | Removes ebook entry and adds remote hubs + data lake connectivity entries |
| docs-mslearn/.markdownlint.json | Adds markdownlint configuration overrides |
| README.md | Adds contributor entry |
| .vscode/settings.json | PowerShell editor settings update (encoding/whitespace) |
| .github/workflows/update-mslearn-dates.yml | New workflow to auto-update ms.date on PRs |
| .github/policies/issues-02-duplicate.yml | Policy now closes duplicates and adjusts steps |
| .github/policies/issues-01-new.yml | Policy description updated and adds welcome reply |
| .github/policies/issues-00-conventions.yml | Renames label from Skill to Tool for workbooks |
| .github/copilot-instructions.md | Updates Copilot repo instructions text/formatting |
| .editorconfig | Sets UTF-8 BOM charset for PowerShell files |
| .build/BuildHelper/Start-PesterTest.ps1 | Ensures Pester run exits with non-zero on failures |
| .all-contributorsrc | Adds new contributor metadata |
Comments suppressed due to low confidence (1)
src/powershell/Private/Split-AzureResourceId.ps1:57
- Split-AzureResourceId no longer returns an AzureResourceIdInfo object when $Id is null/empty (it returns nothing). Unit tests in Split-AzureResourceId.Tests.ps1 expect an object with null/empty properties for these cases, and callers may also rely on a consistent return type. Consider returning an empty AzureResourceIdInfo instance when $Id is null/empty, instead of exiting without output.
src/templates/finops-hub/modules/fx/scripts/Init-DataFactory.ps1
Outdated
Show resolved
Hide resolved
5bb0823 to
172f34f
Compare
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.compute/snapshots' | where sku.name =~ 'Premium_LRS' | extend DiskSizeGB=tostring(properties.diskSizeGB), TimeCreated=tostring(properties.timeCreated), Location=location, SKUName=tostring(sku.name) | project id, name, resourceGroup, subscriptionId, Location, DiskSizeGB, TimeCreated, SKUName, type | project x_RecommendationId=strcat(tolower(id),'-premiumSnapshot'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Snapshot is using Premium SSD storage instead of Standard', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\\\"DiskSizeGB\\\": ', DiskSizeGB, ', \\\"SKUName\\\": \\\"', SKUName, '\\\", \\\"TimeCreated\\\": \\\"', TimeCreated, '\\\", \\\"Location\\\": \\\"', Location, '\\\", \\\"x_RecommendationProvider\\\": \\\"FinOps hubs\\\", \\\"x_RecommendationSolution\\\": \\\"Change snapshot storage type from Premium SSD to Standard HDD\\\", \\\"x_RecommendationTypeId\\\": \\\"e7f8a9b0-1c2d-3e4f-5a6b-7c8d9e0f1a2b\\\", \\\"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted all query files from strcat() to bag_pack() for x_RecommendationDetails.
| FinOps hubs include the following recommendations. Most are enabled by default. Optional recommendations may generate noise for organizations where they don't apply and can be enabled during deployment via the specified template parameter. | ||
|
|
There was a problem hiding this comment.
This PR introduces customer-facing recommendations and docs changes, but there’s no corresponding entry in docs-mslearn/toolkit/changelog.md. Repo guidance requires external-facing changes to be captured in the changelog for the next release (see docs-wiki/Branching-strategy.md and docs-wiki/Release-process.md). Please add a changelog entry for the FinOps hubs recommendations expansion.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Noted
Changelog is covered by an existing entry. No additional update needed.
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/publicipaddresses' | where sku.name =~ 'Basic' | extend Location=location, AllocationMethod=tostring(properties.publicIPAllocationMethod), IpAddress=tostring(properties.ipAddress) | project id, name, resourceGroup, subscriptionId, Location, AllocationMethod, IpAddress, type | project x_RecommendationId=strcat(tolower(id),'-basicPublicIP'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Public IP is using the retired Basic SKU', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\"AllocationMethod\": \"', AllocationMethod, '\", \"IpAddress\": \"', IpAddress, '\", \"Location\": \"', Location, '\", \"x_RecommendationProvider\": \"FinOps hubs\", \"x_RecommendationSolution\": \"Upgrade to Standard SKU public IP\", \"x_RecommendationTypeId\": \"f1a2b3c4-d5e6-7f8a-9b0c-1d2e3f4a5b6c\", \"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) (as used in existing recommendation queries) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/applicationgateways' | where tostring(properties.sku.tier) in~ ('Standard', 'WAF') | extend Location=location, SKUName=tostring(properties.sku.name), SKUTier=tostring(properties.sku.tier), SKUCapacity=tostring(properties.sku.capacity) | project id, name, resourceGroup, subscriptionId, Location, SKUName, SKUTier, SKUCapacity, type | project x_RecommendationId=strcat(tolower(id),'-classicAppGateway'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Application Gateway is using the retiring v1 SKU', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\"SKUName\": \"', SKUName, '\", \"SKUTier\": \"', SKUTier, '\", \"SKUCapacity\": \"', SKUCapacity, '\", \"Location\": \"', Location, '\", \"x_RecommendationProvider\": \"FinOps hubs\", \"x_RecommendationSolution\": \"Migrate to Application Gateway v2 (Standard_v2 or WAF_v2)\", \"x_RecommendationTypeId\": \"b3c4d5e6-f7a8-9b0c-1d2e-3f4a5b6c7d8e\", \"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) (as used in existing recommendation queries) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/networksecuritygroups' | where isnull(properties.networkInterfaces) and isnull(properties.subnets) | extend Location=location | project id, name, resourceGroup, subscriptionId, Location, type | project x_RecommendationId=strcat(tolower(id),'-emptyNSG'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Network security group is not associated with any network interface or subnet', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\"Location\": \"', Location, '\", \"x_RecommendationProvider\": \"FinOps hubs\", \"x_RecommendationSolution\": \"Delete the orphaned network security group or associate it with a subnet or NIC\", \"x_RecommendationTypeId\": \"b9c0d1e2-f3a4-5b6c-7d8e-9f0a1b2c3d4e\", \"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) (as used in existing recommendation queries) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/natgateways' | where isnull(properties.subnets) or array_length(properties.subnets) == 0 | extend Location=location, SKUName=tostring(sku.name) | project id, name, resourceGroup, subscriptionId, Location, SKUName, type | project x_RecommendationId=strcat(tolower(id),'-orphanedNATGateway'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='NAT gateway is not associated with any subnet', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\\\"SKUName\\\": \\\"', SKUName, '\\\", \\\"Location\\\": \\\"', Location, '\\\", \\\"x_RecommendationProvider\\\": \\\"FinOps hubs\\\", \\\"x_RecommendationSolution\\\": \\\"Delete the orphaned NAT gateway or associate it with a subnet\\\", \\\"x_RecommendationTypeId\\\": \\\"b4c5d6e7-8f9a-0b1c-2d3e-4f5a6b7c8d9e\\\", \\\"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| SKUTier = tostring(sku.tier), | ||
| Location = location, | ||
| resourceGroup = tostring(strcat('/subscriptions/', subscriptionId, '/resourceGroups/', resourceGroup)), | ||
| subnet = tostring(properties.subnet), |
There was a problem hiding this comment.
properties.subnet doesn’t match the NAT gateway shape used elsewhere in this PR (properties.subnets). As written, subnet = tostring(properties.subnet) will always be empty/null; either project properties.subnets (for example, count/ids) or remove this column from the sample.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Fixed properties.subnet → properties.subnets in the docs sample query.
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/loadbalancers' | where sku.name =~ 'Basic' | extend Location=location | project id, name, resourceGroup, subscriptionId, Location, type | project x_RecommendationId=strcat(tolower(id),'-basicLoadBalancer'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Load balancer is using the retired Basic SKU', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\"Location\": \"', Location, '\", \"x_RecommendationProvider\": \"FinOps hubs\", \"x_RecommendationSolution\": \"Upgrade to Standard SKU load balancer\", \"x_RecommendationTypeId\": \"a2b3c4d5-e6f7-8a9b-0c1d-2e3f4a5b6c7d\", \"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) (as used in existing recommendation queries) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.storage/storageaccounts' | where kind in~ ('Storage', 'BlobStorage') | extend Location=location, AccessTier=tostring(properties.accessTier), Kind=tostring(kind) | project id, name, resourceGroup, subscriptionId, Location, Kind, AccessTier, type | project x_RecommendationId=strcat(tolower(id),'-legacyStorageAccount'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='Storage account is using a legacy kind that is being retired', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\"Kind\": \"', Kind, '\", \"AccessTier\": \"', AccessTier, '\", \"Location\": \"', Location, '\", \"x_RecommendationProvider\": \"FinOps hubs\", \"x_RecommendationSolution\": \"Upgrade to General Purpose v2 (StorageV2) storage account\", \"x_RecommendationTypeId\": \"c4d5e6f7-a8b9-0c1d-2e3f-4a5b6c7d8e9f\", \"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
| { | ||
| "dataset": "Recommendations", | ||
| "provider": "Microsoft", | ||
| "query": "resources | where type =~ 'microsoft.network/expressroutecircuits' | where tostring(properties.serviceProviderProvisioningState) != 'Provisioned' | extend CircuitState=tostring(properties.circuitProvisioningState), ProviderState=tostring(properties.serviceProviderProvisioningState), ServiceProvider=tostring(properties.serviceProviderProperties.serviceProviderName), BandwidthMbps=tostring(properties.serviceProviderProperties.bandwidthInMbps), SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), SKUFamily=tostring(sku.family), Location=location | project id, name, resourceGroup, subscriptionId, Location, CircuitState, ProviderState, ServiceProvider, BandwidthMbps, SKUName, SKUTier, SKUFamily, type | project x_RecommendationId=strcat(tolower(id),'-unprovisionedExpressRoute'), x_ResourceGroupName=tolower(resourceGroup), SubAccountId=subscriptionId, x_RecommendationCategory='Cost', x_RecommendationDescription='ExpressRoute circuit is not in provisioned state', ResourceId = tolower(id), ResourceName=tolower(name), x_RecommendationDetails= strcat('{\\\"CircuitState\\\": \\\"', CircuitState, '\\\", \\\"ProviderState\\\": \\\"', ProviderState, '\\\", \\\"ServiceProvider\\\": \\\"', ServiceProvider, '\\\", \\\"BandwidthMbps\\\": \\\"', BandwidthMbps, '\\\", \\\"SKUName\\\": \\\"', SKUName, '\\\", \\\"SKUTier\\\": \\\"', SKUTier, '\\\", \\\"SKUFamily\\\": \\\"', SKUFamily, '\\\", \\\"Location\\\": \\\"', Location, '\\\", \\\"x_RecommendationProvider\\\": \\\"FinOps hubs\\\", \\\"x_RecommendationSolution\\\": \\\"Complete provisioning or delete the ExpressRoute circuit\\\", \\\"x_RecommendationTypeId\\\": \\\"d6e7f8a9-0b1c-2d3e-4f5a-6b7c8d9e0f1a\\\", \\\"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", |
There was a problem hiding this comment.
x_RecommendationDetails is being built as a JSON-looking string with escaped quotes via strcat(...). This will be ingested as a string (not a dynamic object) and the value isn’t valid JSON due to the embedded \" sequences, which breaks downstream queries that expect x_RecommendationDetails to be a dynamic bag. Prefer emitting a dynamic value using bag_pack(...) and avoid manual JSON string construction.
There was a problem hiding this comment.
🤖 [AI][Claude] ✅ Implemented
Converted to bag_pack().
|
test summary |
- Convert 15 recommendation query files from strcat() to bag_pack() for x_RecommendationDetails - Fix networking docs NAT gateway query: add array_length check and fix subnet to subnets 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: copilot-pull-request-reviewer <copilot-pull-request-reviewer@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
5d23e40 to
89be70e
Compare
|
🤖 [AI][Claude] PR Update Summary Addressed: 19 thread(s)
Key changes:
|
...Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-IdleVNetGateways.json
Outdated
Show resolved
Hide resolved
...crosoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyMySQLServers.json
Outdated
Show resolved
Hide resolved
...ft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyPostgreSQLServers.json
Outdated
Show resolved
Hide resolved
...soft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-LegacyStorageAccounts.json
Outdated
Show resolved
Hide resolved
...crosoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-NonSpotAKSClusters.json
Show resolved
Hide resolved
...Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-SQLVMsWithoutAHB.json
Show resolved
Hide resolved
...es/Microsoft.FinOpsHubs/Recommendations/queries/Recommendations-Microsoft-VMsWithoutAHB.json
Show resolved
Hide resolved
- Replace IdleVNetGateways query to lowercase gateway ID and simplify mv-expand - Replace LegacyStorageAccounts query to exclude Premium blob storage accounts - Update EmptyAppServicePlans query to cast numberOfSites to int and exclude Free tier - Remove obsolete LegacyMySQLServers and LegacyPostgreSQLServers recommendations and docs 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: helderpinto <helderpinto@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
|
🤖 [AI][Claude] PR Update Summary — Addressed 7 threads: 5 implemented and 3 questions (group property deferred). Key changes: replaced IdleVNetGateways and LegacyStorageAccounts queries per reviewer feedback. Removed obsolete MySQL/PostgreSQL recs. Updated EmptyAppServicePlans query. |
🛠️ Description
Added 15 new custom recommendation queries and updated 3 existing ones to expand FinOps hub optimization coverage across compute, databases, networking, storage, and web services.
New recommendations: Basic LBs, Basic Public IPs, Classic App Gateways, Empty App Service Plans, Empty NSGs, Idle VNet Gateways, Legacy MySQL/PostgreSQL/Storage, Orphaned NAT Gateways, Premium Snapshots, Unassociated DDoS Plans, Unattached NICs, Unmanaged Disks, Unprovisioned ExpressRoute Circuits
Updated recommendations: NonSpotAKSClusters, SQLVMsWithoutAHB, VMsWithoutAHB
Docs updates: Expanded best practices docs (compute, databases, networking, storage, web) with recommendations documentation. Updated configure-recommendations.md with grouped service categories.
📋 Checklist
🔬 How did you test this change?
🙋♀️ Do any of the following that apply?
📑 Did you update
docs/changelog.md?📖 Did you update documentation?