Private/AWS/New-SilkTCOAWSVMMetrics.ps1

function New-SilkTCOAWSVMMetrics {
    param(
        [Parameter()]
        [int] $days = 1,
        [Parameter()]
        [int] $offsetDays = 1,
        [Parameter(Mandatory)]
        [array] $vmlist
    )

    $StartDate = (Get-Date).ToUniversalTime().AddDays(-($days + $offsetDays))
    $EndDate = (Get-Date).ToUniversalTime().AddDays(-$offsetDays)

    # CloudTrail is no longer required - uptime calculation now uses CloudWatch metrics
    $requiredModules = @('AWS.Tools.EC2', 'AWS.Tools.CloudWatch')
    foreach ($module in $requiredModules) {
        if (-not (Get-Module -ListAvailable -Name $module)) {
            throw "Required module '$module' is not installed. Install it with: Install-Module $module"
        }
        if (-not (Get-Module -Name $module)) {
            Import-Module $module -ErrorAction Stop
        }
    }

    $thelist = @()
    $periodSeconds = ($EndDate - $StartDate).TotalSeconds

    # EBS volume type to disk class mapping
    $diskClassMap = @{
        'gp2'      = 'SSD'
        'gp3'      = 'SSD'
        'io1'      = 'SSD'
        'io2'      = 'SSD'
        'st1'      = 'HDD'
        'sc1'      = 'HDD'
        'standard' = 'HDD'
    }

    foreach ($i in $vmlist) {
        $instanceId = $i.InstanceId
        $nameTag = ($i.Tags | Where-Object { $_.Key -eq 'Name' }).Value
        $vmName = if ($nameTag) { $nameTag } else { $instanceId }

        Write-Verbose "-> Gathering info for Instance - $vmName ($instanceId)" -Verbose

        $uptime = Get-AWSEC2Uptime -InstanceId $instanceId -vmList $vmlist -days $days -offsetDays $offsetDays -WarningAction SilentlyContinue

        # Get the region from placement
        $region = $i.Placement.AvailabilityZone -replace '[a-z]$', ''
        $zone = $i.Placement.AvailabilityZone

        # Get resource group equivalent from tags
        $rgTag = ($i.Tags | Where-Object { $_.Key -eq 'ResourceGroup' -or $_.Key -eq 'Project' -or $_.Key -eq 'Environment' }).Value
        $resourceGroup = if ($rgTag) { $rgTag } else { 'N/A' }

        # Try to get available memory from CloudWatch Agent metrics
        $vmstatavg = $null
        try {
            $memDimension = New-Object Amazon.CloudWatch.Model.Dimension
            $memDimension.Name = 'InstanceId'
            $memDimension.Value = $instanceId

            $memMetric = Get-CWMetricStatistic -Namespace 'CWAgent' -MetricName 'mem_available_percent' `
                -Dimension $memDimension -StartTime $StartDate -EndTime $EndDate `
                -Period ([int]$periodSeconds) -Statistic 'Average' -ErrorAction SilentlyContinue

            if ($memMetric.Datapoints -and $memMetric.Datapoints.Count -gt 0) {
                # mem_available_percent gives us a percentage; convert using instance memory
                # For now store the percentage - we'd need instance type specs for absolute GB
                $vmstatavg = $memMetric.Datapoints[0].Average
            }
        } catch {
            Write-Verbose "-> CloudWatch Agent memory metrics not available for $vmName" -Verbose
        }

        # Get attached EBS volumes
        $volumes = Get-EC2Volume -Filter @{
            Name   = 'attachment.instance-id'
            Values = @($instanceId)
        }

        if ($volumes) {
            foreach ($vol in $volumes) {
                $volumeId = $vol.VolumeId
                $volumeType = $vol.VolumeType.Value
                $diskClass = if ($diskClassMap.ContainsKey($volumeType)) { $diskClassMap[$volumeType] } else { $volumeType }

                Write-Verbose "---> Gathering metrics for volume $volumeId" -Verbose

                $dimension = New-Object Amazon.CloudWatch.Model.Dimension
                $dimension.Name = 'VolumeId'
                $dimension.Value = $volumeId

                $cwParams = @{
                    Namespace  = 'AWS/EBS'
                    Dimension  = $dimension
                    StartTime  = $StartDate
                    EndTime    = $EndDate
                    Period     = [int]$periodSeconds
                    Statistic  = 'Average'
                }

                # Collect EBS metrics
                $readBytes = $null
                $writeBytes = $null
                $readOps = $null
                $writeOps = $null

                try {
                    $readBytesMetric = Get-CWMetricStatistic @cwParams -MetricName 'VolumeReadBytes' -ErrorAction SilentlyContinue
                    if ($readBytesMetric.Datapoints) {
                        $readBytes = $readBytesMetric.Datapoints[0].Average / $periodSeconds
                    }
                } catch { }

                try {
                    $writeBytesMetric = Get-CWMetricStatistic @cwParams -MetricName 'VolumeWriteBytes' -ErrorAction SilentlyContinue
                    if ($writeBytesMetric.Datapoints) {
                        $writeBytes = $writeBytesMetric.Datapoints[0].Average / $periodSeconds
                    }
                } catch { }

                try {
                    $readOpsMetric = Get-CWMetricStatistic @cwParams -MetricName 'VolumeReadOps' -ErrorAction SilentlyContinue
                    if ($readOpsMetric.Datapoints) {
                        $readOps = $readOpsMetric.Datapoints[0].Average / $periodSeconds
                    }
                } catch { }

                try {
                    $writeOpsMetric = Get-CWMetricStatistic @cwParams -MetricName 'VolumeWriteOps' -ErrorAction SilentlyContinue
                    if ($writeOpsMetric.Datapoints) {
                        $writeOps = $writeOpsMetric.Datapoints[0].Average / $periodSeconds
                    }
                } catch { }

                # Calculate provisioned/baseline throughput (MB/s) based on volume type
                $diskThroughputMBps = switch ($volumeType) {
                    'gp3' {
                        # gp3 has configurable throughput (125-1000 MB/s)
                        if ($vol.Throughput) { $vol.Throughput } else { 125 }  # Default is 125 MB/s
                    }
                    'gp2' {
                        # gp2: Baseline of 3 IOPS/GB, burst to 3000 IOPS
                        # Throughput: 128-250 MB/s depending on volume size
                        # Under 170 GB: 128 MB/s, 170-334 GB scales to 250 MB/s, above 334 GB: 250 MB/s
                        if ($vol.Size -lt 170) { 128 }
                        elseif ($vol.Size -lt 334) { [Math]::Round(128 + (($vol.Size - 170) * 0.744), 2) }
                        else { 250 }
                    }
                    'io1' {
                        # io1: Throughput is 256 KB per provisioned IOPS (up to 1000 MB/s for <= 32 GiB volumes)
                        # Max throughput: 1000 MB/s (for volumes <= 32,000 IOPS)
                        $calculatedThroughput = [Math]::Round(($vol.Iops * 256) / 1024, 2)
                        [Math]::Min($calculatedThroughput, 1000)
                    }
                    'io2' {
                        # io2: Similar to io1 but supports up to 4000 MB/s
                        if ($vol.Throughput) { 
                            $vol.Throughput 
                        } else {
                            # Calculate based on IOPS: 256 KB per IOPS
                            $calculatedThroughput = [Math]::Round(($vol.Iops * 256) / 1024, 2)
                            [Math]::Min($calculatedThroughput, 4000)
                        }
                    }
                    'st1' {
                        # st1 (Throughput Optimized HDD): 40 MB/s per TB baseline, 500 MB/s max
                        # Burst: 250 MB/s per TB, 500 MB/s max
                        $baselineThroughput = ($vol.Size / 1024) * 40
                        [Math]::Min([Math]::Round($baselineThroughput, 2), 500)
                    }
                    'sc1' {
                        # sc1 (Cold HDD): 12 MB/s per TB baseline, 250 MB/s max
                        $baselineThroughput = ($vol.Size / 1024) * 12
                        [Math]::Min([Math]::Round($baselineThroughput, 2), 250)
                    }
                    'standard' {
                        # Magnetic (standard): ~40-90 MB/s
                        40
                    }
                    default { $null }
                }

                $o = New-Object psobject
                $o | Add-Member -MemberType NoteProperty -Name "VM name" -Value $vmName
                $o | Add-Member -MemberType NoteProperty -Name "VM Zone" -Value $zone
                $o | Add-Member -MemberType NoteProperty -Name "VM size" -Value $i.InstanceType.Value
                $o | Add-Member -MemberType NoteProperty -Name 'AvailableMemoryBytesGB' -Value $(if ($vmstatavg) { [Math]::Round($vmstatavg, 2) } else { 'N/A' })
                $o | Add-Member -MemberType NoteProperty -Name "Disk Name" -Value $volumeId
                $o | Add-Member -MemberType NoteProperty -Name "DiskSKU" -Value $volumeType
                $o | Add-Member -MemberType NoteProperty -Name "DiskSizeGB" -Value $vol.Size
                $o | Add-Member -MemberType NoteProperty -Name "Disk Tier" -Value $volumeType
                $o | Add-Member -MemberType NoteProperty -Name "Disk Class" -Value $diskClass
                $o | Add-Member -MemberType NoteProperty -Name "Disk IOPS" -Value $vol.Iops
                $o | Add-Member -MemberType NoteProperty -Name "Disk MBps" -Value $diskThroughputMBps
                $o | Add-Member -MemberType NoteProperty -Name "ResourceGroup" -Value $resourceGroup
                $o | Add-Member -MemberType NoteProperty -Name "Region" -Value $region
                $o | Add-Member -MemberType NoteProperty -Name "UptimePercentage" -Value $uptime.UptimePercentage
                $o | Add-Member -MemberType NoteProperty -Name "Days" -Value $days
                $o | Add-Member -MemberType NoteProperty -Name "CompositeDiskReadBytes/sec-avg" -Value $readBytes
                $o | Add-Member -MemberType NoteProperty -Name "CompositeDiskWriteBytes/sec-avg" -Value $writeBytes
                $o | Add-Member -MemberType NoteProperty -Name "CompositeDiskReadOperations/Sec-avg" -Value $readOps
                $o | Add-Member -MemberType NoteProperty -Name "CompositeDiskWriteOperations/Sec-avg" -Value $writeOps
                $o | Add-Member -MemberType NoteProperty -Name "DiskPaidBurstIOPS-avg" -Value 'N/A'
                $o | Add-Member -MemberType NoteProperty -Name "InstanceId" -Value $instanceId

                $thelist += $o
            }
        } else {
            $o = New-Object psobject
            $o | Add-Member -MemberType NoteProperty -Name "VM name" -Value $vmName
            $o | Add-Member -MemberType NoteProperty -Name "VM Zone" -Value $zone
            $o | Add-Member -MemberType NoteProperty -Name "VM size" -Value $i.InstanceType.Value
            $o | Add-Member -MemberType NoteProperty -Name 'AvailableMemoryBytesGB' -Value $(if ($vmstatavg) { [Math]::Round($vmstatavg, 2) } else { 'N/A' })
            $o | Add-Member -MemberType NoteProperty -Name "Disk Name" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "DiskSKU" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "DiskSizeGB" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "Disk Tier" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "Disk Class" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "Disk IOPS" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "Disk MBps" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "ResourceGroup" -Value $resourceGroup
            $o | Add-Member -MemberType NoteProperty -Name "Region" -Value $region
            $o | Add-Member -MemberType NoteProperty -Name "UptimePercentage" -Value $uptime.UptimePercentage
            $o | Add-Member -MemberType NoteProperty -Name "Days" -Value 'N/A'
            $o | Add-Member -MemberType NoteProperty -Name "InstanceId" -Value $instanceId

            $thelist += $o
        }
    }

    return $thelist
}