Public/Invoke-VMMetricsCollection.ps1
|
function Invoke-VMMetricsCollection { <# .SYNOPSIS Discovers VMs in a subscription, pulls their platform metrics, and exports VMPerformance-compatible rows for FinOps rightsizing. .DESCRIPTION End-to-end orchestrator: 1. Sets the subscription context (if -SubscriptionId given). 2. Discovers VMs via Get-AzVM (optionally filtered by resource group / name). 3. For each VM pulls 'Percentage CPU' + 'Available Memory Bytes' and aggregates to monthly Min/Median/P95/P99/Max rows. 4. Writes one CSV (+ manifest) and returns a run report. Platform metrics retain only ~93 days; -WindowDays above that is capped with a warning. For longer history, run this on a schedule so rows accumulate, or stand up a diagnostic-setting export to Log Analytics (see README). .PARAMETER SubscriptionId Subscription to operate in. If omitted, the current Az context is used. .PARAMETER ResourceGroupName Optional resource-group filter. .PARAMETER VMName Optional VM-name filter (one or more exact names). .PARAMETER WindowDays Look-back window in days, default 90 (capped at 93 -- platform retention). .PARAMETER TimeGrain Aggregation grain, default 00:05:00 (PT5M). .PARAMETER OutputPath Directory to write the CSV + manifest into. Default: current directory. .PARAMETER Format 'CSV' (default) or 'Parquet'. .PARAMETER StorageAccountName Optional. When set, the CSV (+ manifest) is uploaded to this storage account under {Customer}/{date}/ after the local write. Requires -Customer. .PARAMETER ContainerName Blob container for the upload (default 'vmperformance'); created if missing. .PARAMETER Customer Real customer name; first blob path segment. Required when -StorageAccountName is set. .PARAMETER StorageAccountKey Account key for the upload. If omitted, -StorageAccountResourceGroup is used to fetch it. .PARAMETER StorageAccountResourceGroup Resource group of the storage account, used to fetch the key when none is supplied. .OUTPUTS [pscustomobject] run report: OutputFile, BlobUri, VMsProcessed, VMsSkipped, RowCount, Skipped (per-VM reasons), Window. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [string] $SubscriptionId, [string] $ResourceGroupName, [string[]] $VMName, [ValidateRange(1, 93)] [int] $WindowDays = 90, [timespan] $TimeGrain = ([timespan]'00:05:00'), [string] $OutputPath = (Get-Location).Path, [ValidateSet('CSV', 'Parquet')] [string] $Format = 'CSV', # --- Optional blob upload (account-key auth) --- [string] $StorageAccountName, [string] $ContainerName = 'vmperformance', [string] $Customer, [string] $StorageAccountKey, [string] $StorageAccountResourceGroup ) if ($StorageAccountName -and -not $Customer) { throw "-Customer is required when -StorageAccountName is set (it is the first blob path segment)." } if ($SubscriptionId) { Write-Verbose "Setting subscription context to $SubscriptionId" Set-AzContext -Subscription $SubscriptionId -ErrorAction Stop | Out-Null } $ctx = Get-AzContext -ErrorAction Stop if (-not $ctx) { throw "No Azure context. Run Connect-AzAccount first." } $endTime = (Get-Date).ToUniversalTime() $startTime = $endTime.AddDays(-$WindowDays) # Discover VMs. $vmParams = @{ Status = $false } if ($ResourceGroupName) { $vmParams['ResourceGroupName'] = $ResourceGroupName } $vms = Get-AzVM @vmParams -ErrorAction Stop if ($VMName) { $vms = $vms | Where-Object { $_.Name -in $VMName } } if (-not $vms) { Write-Warning "No VMs found for the given filters." return [pscustomobject]@{ OutputFile = $null; BlobUri = $null; VMsProcessed = 0; VMsSkipped = 0; RowCount = 0 Skipped = @(); Window = @{ Start = $startTime; End = $endTime; Days = $WindowDays } } } Write-Verbose "Discovered $($vms.Count) VM(s)." $allRows = [System.Collections.Generic.List[object]]::new() $skipped = [System.Collections.Generic.List[object]]::new() $processed = 0 foreach ($vm in $vms) { try { $rows = Get-AzVMUtilization -VM $vm -StartTime $startTime -EndTime $endTime -TimeGrain $TimeGrain if ($rows -and $rows.Count -gt 0) { foreach ($r in $rows) { $allRows.Add($r) } $processed++ } else { $skipped.Add([pscustomobject]@{ VM = $vm.Name; Reason = 'No metric data in window' }) } } catch { Write-Warning "Failed for $($vm.Name): $($_.Exception.Message)" $skipped.Add([pscustomobject]@{ VM = $vm.Name; Reason = $_.Exception.Message }) } } $stamp = $endTime.ToString('yyyy-MM-dd') $fileName = "VMPerformance-$stamp.$($Format.ToLowerInvariant())" $dataPath = Join-Path $OutputPath $fileName $manifest = @{ subscriptionId = $ctx.Subscription.Id windowStartUtc = $startTime.ToString('o') windowEndUtc = $endTime.ToString('o') windowDays = $WindowDays timeGrain = $TimeGrain.ToString() vmsProcessed = $processed vmsSkipped = $skipped.Count } if ($Customer) { $manifest['customer'] = $Customer } $written = $null if ($PSCmdlet.ShouldProcess($dataPath, "Export $($allRows.Count) rows")) { $written = Export-VMPerformanceData -Row $allRows.ToArray() -Path $dataPath -Format $Format -Manifest $manifest } # Optional upload to blob storage. $blobUri = $null if ($StorageAccountName -and $written) { $uploaded = Publish-VMPerformanceData -Path $written -Customer $Customer ` -StorageAccountName $StorageAccountName -ContainerName $ContainerName ` -StorageAccountKey $StorageAccountKey -StorageAccountResourceGroup $StorageAccountResourceGroup ` -Date $endTime.ToString('yyyy-MM-dd') $blobUri = $uploaded.BlobUri } [pscustomobject]@{ OutputFile = $written BlobUri = $blobUri VMsProcessed = $processed VMsSkipped = $skipped.Count RowCount = $allRows.Count Skipped = $skipped.ToArray() Window = @{ Start = $startTime; End = $endTime; Days = $WindowDays } } } |