Private/Write-MetricOutput.ps1
|
function Write-MetricOutput { <# .SYNOPSIS Writes VMPerformance rows to disk as CSV (default) or Parquet, plus a sidecar manifest describing the run and the memory proxy. .DESCRIPTION CSV is the default and is LightIngest-compatible (UTF-8, no type info). Parquet is only produced when the Parquet.Net assembly is loadable; otherwise the function warns and falls back to CSV. A '<basename>.manifest.json' is always written next to the data so downstream consumers can see the source, grain, window and -- importantly -- that '% Committed Bytes In Use' is a host-available-memory PROXY, not guest-committed. .PARAMETER Row The VMPerformance rows to write. .PARAMETER Path Output file path. Extension is normalised to match -Format. .PARAMETER Format 'CSV' (default) or 'Parquet'. .PARAMETER Manifest Hashtable of run metadata to embed in the sidecar manifest. .OUTPUTS [string] the path of the data file written. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Row, [Parameter(Mandatory)] [string] $Path, [ValidateSet('CSV', 'Parquet')] [string] $Format = 'CSV', [hashtable] $Manifest = @{} ) $dir = Split-Path -Parent $Path if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $base = [System.IO.Path]::Combine( (Split-Path -Parent $Path), [System.IO.Path]::GetFileNameWithoutExtension($Path)) $effectiveFormat = $Format $dataPath = "$base.csv" if ($Format -eq 'Parquet') { $parquetAvailable = $null -ne (Get-Module -ListAvailable -Name 'Parquet.Net' -ErrorAction SilentlyContinue) ` -or ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Parquet' }) if ($parquetAvailable) { $dataPath = "$base.parquet" } else { Write-Warning "Parquet.Net not available; falling back to CSV. Install Parquet.Net to enable parquet output." $effectiveFormat = 'CSV' $dataPath = "$base.csv" } } if ($PSCmdlet.ShouldProcess($dataPath, "Write $($Row.Count) rows ($effectiveFormat)")) { if ($effectiveFormat -eq 'CSV') { # Sort for deterministic, append-safe output. $sorted = $Row | Sort-Object vmName, Date, CounterName $sorted | Export-Csv -Path $dataPath -NoTypeInformation -Encoding UTF8 } else { # Parquet path: reserved for when Parquet.Net is wired in. Not reached today # because the availability check above downgrades to CSV. throw [System.NotImplementedException]::new('Parquet writer not yet implemented.') } # Sidecar manifest -- always written. $manifestPath = "$base.manifest.json" $full = [ordered]@{ generatedBy = 'FinOpsVMMetrics' dataFile = Split-Path -Leaf $dataPath format = $effectiveFormat rowCount = $Row.Count source = 'Azure Monitor platform metrics (host, no guest agent)' counters = [ordered]@{ '% Processor Time' = 'Percentage CPU (per-interval average)' 'Available MBytes' = 'Available Memory Bytes / 1MB (exact host metric)' '% Committed Bytes In Use' = 'PROXY: 100 - (Available Memory Bytes / SKU total RAM * 100). Host-available derived, NOT guest committed bytes.' } } foreach ($k in $Manifest.Keys) { $full[$k] = $Manifest[$k] } ($full | ConvertTo-Json -Depth 6) | Set-Content -Path $manifestPath -Encoding UTF8 } return $dataPath } |