Private/Aggregation.ps1
|
#Requires -Version 5.1 function Read-SMBeatSampleFiles { param( [Parameter(Mandatory = $true)] [string]$DataRoot, [datetime]$StartUtc, [datetime]$EndUtc ) $dir = Get-SMBeatSampleDirectory -DataRoot $DataRoot $records = New-Object System.Collections.Generic.List[object] if (-not (Test-Path -LiteralPath $dir)) { return @() } $startDay = $StartUtc.Date.AddDays(-1) $endDay = $EndUtc.Date.AddDays(1) Get-ChildItem -LiteralPath $dir -Filter '*.jsonl' -ErrorAction SilentlyContinue | Sort-Object Name | ForEach-Object { $dayPart = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) $fileDay = [datetime]::MinValue if (-not [datetime]::TryParseExact($dayPart, 'yyyy-MM-dd', $script:SMBeatInvariant, [System.Globalization.DateTimeStyles]::AssumeUniversal, [ref]$fileDay)) { return } if ($fileDay -lt $startDay -or $fileDay -gt $endDay) { return } $lines = [System.IO.File]::ReadAllLines($_.FullName, $script:SMBeatUtf8NoBom) foreach ($line in $lines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $obj = ConvertFrom-SMBeatJson -Json $line $ts = ConvertFrom-SMBeatUtcString -Value $obj.ts if ($ts -lt $StartUtc -or $ts -ge $EndUtc) { continue } $records.Add($obj) | Out-Null } } return $records.ToArray() } function Get-SMBeatExpectedSampleCount { param( [datetime]$StartUtc, [datetime]$EndUtc, [int]$IntervalSec ) if ($IntervalSec -le 0) { $IntervalSec = 60 } $seconds = ($EndUtc - $StartUtc).TotalSeconds if ($seconds -le 0) { return 1 } [math]::Max(1, [int][math]::Floor($seconds / $IntervalSec)) } function Add-SMBeatAccumulator { param( [hashtable]$Map, [string]$Key, [int64]$Read, [int64]$Write, [int64]$Sent, [int64]$Received ) if (-not $Map.ContainsKey($Key)) { $Map[$Key] = @{ Read = [int64]0 Write = [int64]0 Sent = [int64]0 Received = [int64]0 } } $Map[$Key].Read += $Read $Map[$Key].Write += $Write $Map[$Key].Sent += $Sent $Map[$Key].Received += $Received } function ConvertTo-SMBeatNamedTotals { param( [hashtable]$Map ) $list = New-Object System.Collections.Generic.List[object] foreach ($key in $Map.Keys) { $v = $Map[$key] $list.Add([PSCustomObject]@{ Name = $key ReadBytes = [int64]$v.Read WriteBytes = [int64]$v.Write SentBytes = [int64]$v.Sent ReceivedBytes = [int64]$v.Received ReadGiB = (ConvertTo-SMBeatGiB -Bytes $v.Read) WriteGiB = (ConvertTo-SMBeatGiB -Bytes $v.Write) SentGiB = (ConvertTo-SMBeatGiB -Bytes $v.Sent) ReceivedGiB = (ConvertTo-SMBeatGiB -Bytes $v.Received) }) | Out-Null } $list | Sort-Object -Property @{ Expression = { $outB = [int64]0 $inB = [int64]0 if ($_.SentBytes) { $outB = [int64]$_.SentBytes } if ($_.ReceivedBytes) { $inB = [int64]$_.ReceivedBytes } if ($outB -gt $inB) { $outB } else { $inB } } } -Descending } function New-SMBeatEmptyTotals { [PSCustomObject]@{ ReadBytes = [int64]0 WriteBytes = [int64]0 SentBytes = [int64]0 ReceivedBytes = [int64]0 NicBytesIn = [int64]0 NicBytesOut = [int64]0 ReadGiB = 0 WriteGiB = 0 SentGiB = 0 ReceivedGiB = 0 NicInGiB = 0 NicOutGiB = 0 RetrievedBytes = [int64]0 RetrievedGiB = 0 WrittenBytes = [int64]0 WrittenGiB = 0 } } function ConvertTo-SMBeatTotalsObject { param( [int64]$Read, [int64]$Write, [int64]$Sent, [int64]$Received, [int64]$NicIn, [int64]$NicOut, [int64]$Retrieved, [int64]$Written ) if (-not $PSBoundParameters.ContainsKey('Retrieved')) { $Retrieved = $NicOut } if (-not $PSBoundParameters.ContainsKey('Written')) { $Written = $Received } [PSCustomObject]@{ ReadBytes = $Read WriteBytes = $Write SentBytes = $Sent ReceivedBytes = $Received NicBytesIn = $NicIn NicBytesOut = $NicOut ReadGiB = (ConvertTo-SMBeatGiB -Bytes $Read) WriteGiB = (ConvertTo-SMBeatGiB -Bytes $Write) SentGiB = (ConvertTo-SMBeatGiB -Bytes $Sent) ReceivedGiB = (ConvertTo-SMBeatGiB -Bytes $Received) NicInGiB = (ConvertTo-SMBeatGiB -Bytes $NicIn) NicOutGiB = (ConvertTo-SMBeatGiB -Bytes $NicOut) RetrievedBytes = $Retrieved RetrievedGiB = (ConvertTo-SMBeatGiB -Bytes $Retrieved) WrittenBytes = $Written WrittenGiB = (ConvertTo-SMBeatGiB -Bytes $Written) } } function Get-SMBeatPeakStats { param( [object[]]$Hourly, [int64]$RetrievedBytes, [int64]$WrittenBytes, [datetime]$StartUtc, [datetime]$EndUtc ) $peakStart = $null $peakBytes = [int64]0 $peakWriteStart = $null $peakWriteBytes = [int64]0 foreach ($row in @($Hourly)) { if ($null -eq $row) { continue } $b = [int64]0 if ($row.PSObject.Properties['RetrievedBytes'] -and $row.RetrievedBytes) { $b = [int64]$row.RetrievedBytes } if ($b -gt $peakBytes) { $peakBytes = $b $peakStart = $row.PeriodStart } $w = [int64]0 if ($row.PSObject.Properties['WrittenBytes'] -and $row.WrittenBytes) { $w = [int64]$row.WrittenBytes } elseif ($row.PSObject.Properties['ReceivedBytes'] -and $row.ReceivedBytes) { $w = [int64]$row.ReceivedBytes } if ($w -gt $peakWriteBytes) { $peakWriteBytes = $w $peakWriteStart = $row.PeriodStart } } $hours = ($EndUtc - $StartUtc).TotalHours if ($hours -le 0) { $hours = 1 } [PSCustomObject]@{ PeakStart = $peakStart PeakRetrievedBytes = $peakBytes PeakRetrievedGiB = (ConvertTo-SMBeatGiB -Bytes $peakBytes) AverageRetrievedGiBPerHour = (ConvertTo-SMBeatGiB -Bytes ([int64][math]::Round($RetrievedBytes / $hours))) PeakWrittenStart = $peakWriteStart PeakWrittenBytes = $peakWriteBytes PeakWrittenGiB = (ConvertTo-SMBeatGiB -Bytes $peakWriteBytes) AverageWrittenGiBPerHour = (ConvertTo-SMBeatGiB -Bytes ([int64][math]::Round($WrittenBytes / $hours))) } } function Get-SMBeatSeries { param( [object[]]$Records, [datetime]$StartUtc, [datetime]$EndUtc, [TimeZoneInfo]$TimeZone, [string]$Granularity ) $buckets = @{} foreach ($rec in $Records) { $ts = ConvertFrom-SMBeatUtcString -Value $rec.ts $local = Convert-SMBeatUtcToTimeZone -Utc $ts -TimeZone $TimeZone $start = Get-SMBeatBucketStart -LocalTime $local -Granularity $Granularity $key = $start.ToString('o', $script:SMBeatInvariant) if (-not $buckets.ContainsKey($key)) { $buckets[$key] = @{ Start = $start Read = [int64]0 Write = [int64]0 Sent = [int64]0 Received = [int64]0 ServerRead = [int64]0 ServerWrite = [int64]0 ServerSent = [int64]0 ServerRecv = [int64]0 HasShare = $false NicIn = [int64]0 NicOut = [int64]0 Tcp445Sent = [int64]0 Tcp445Recv = [int64]0 HasTcp445 = $false } } $kind = [string]$rec.kind if ($kind -eq 'server') { $buckets[$key].ServerRead += [int64]$rec.readBytesDelta $buckets[$key].ServerWrite += [int64]$rec.writeBytesDelta $buckets[$key].ServerSent += [int64]$rec.sentBytesDelta $buckets[$key].ServerRecv += [int64]$rec.receivedBytesDelta } elseif ($kind -eq 'share') { $buckets[$key].Read += [int64]$rec.readBytesDelta $buckets[$key].Write += [int64]$rec.writeBytesDelta $buckets[$key].Sent += [int64]$rec.sentBytesDelta $buckets[$key].Received += [int64]$rec.receivedBytesDelta $buckets[$key].HasShare = $true } elseif ($kind -eq 'tcp445') { $name = [string]$rec.name if ($name -eq '_listen445') { $buckets[$key].Tcp445Sent += [int64]$rec.sentBytesDelta $buckets[$key].Tcp445Recv += [int64]$rec.receivedBytesDelta $buckets[$key].HasTcp445 = $true } } elseif ($kind -eq 'nic') { $buckets[$key].NicIn += [int64]$rec.receivedBytesDelta $buckets[$key].NicOut += [int64]$rec.sentBytesDelta } } $result = New-Object System.Collections.Generic.List[object] foreach ($key in ($buckets.Keys | Sort-Object)) { $b = $buckets[$key] $end = Get-SMBeatBucketEnd -BucketStart $b.Start -Granularity $Granularity $useShare = [bool]$b.HasShare $readB = $b.ServerRead $writeB = $b.ServerWrite $sentB = $b.ServerSent $recvB = $b.ServerRecv if ($useShare) { $readB = $b.Read $writeB = $b.Write $sentB = $b.Sent $recvB = $b.Received } $retrievedB = $b.NicOut $writtenB = $recvB if ([bool]$b.HasTcp445) { $retrievedB = $b.Tcp445Sent $writtenB = $b.Tcp445Recv if ([bool]$b.HasShare -and $b.Received -gt $writtenB -and $b.Received -gt (10 * [math]::Max($writtenB, [int64]1))) { $writtenB = $b.Received } } $result.Add([PSCustomObject]@{ Granularity = $Granularity PeriodStart = $b.Start PeriodEnd = $end ReadBytes = $readB WriteBytes = $writeB SentBytes = $sentB ReceivedBytes = $recvB NicBytesIn = $b.NicIn NicBytesOut = $b.NicOut RetrievedBytes = $retrievedB WrittenBytes = $writtenB ReadGiB = (ConvertTo-SMBeatGiB -Bytes $readB) WriteGiB = (ConvertTo-SMBeatGiB -Bytes $writeB) SentGiB = (ConvertTo-SMBeatGiB -Bytes $sentB) ReceivedGiB = (ConvertTo-SMBeatGiB -Bytes $recvB) NicInGiB = (ConvertTo-SMBeatGiB -Bytes $b.NicIn) NicOutGiB = (ConvertTo-SMBeatGiB -Bytes $b.NicOut) RetrievedGiB = (ConvertTo-SMBeatGiB -Bytes $retrievedB) WrittenGiB = (ConvertTo-SMBeatGiB -Bytes $writtenB) }) | Out-Null } return $result.ToArray() } function New-SMBeatReportFromSamples { param( [object[]]$Records, [datetime]$StartUtc, [datetime]$EndUtc, [TimeZoneInfo]$TimeZone, [string[]]$Granularity, [int]$IntervalSec, $PreviousTotals ) if ($null -eq $Records) { $Records = @() } $warnings = New-Object System.Collections.Generic.List[string] $serverRecords = @($Records | Where-Object { $_.kind -eq 'tcp445' -and $_.name -eq '_listen445' }) if ($serverRecords.Count -eq 0) { $serverRecords = @($Records | Where-Object { $_.kind -eq 'server' }) } $expected = Get-SMBeatExpectedSampleCount -StartUtc $StartUtc -EndUtc $EndUtc -IntervalSec $IntervalSec $actual = $serverRecords.Count $coverage = 0 if ($expected -gt 0) { $coverage = [math]::Min(100, [math]::Round(100.0 * $actual / $expected, 1)) } if ($coverage -lt 90) { $warnings.Add(("Coverage {0}% - collector gaps are possible (expected ~{1} server samples, found {2})." -f $coverage, $expected, $actual)) | Out-Null } $serverRead = [int64]0 $serverWrite = [int64]0 $serverSent = [int64]0 $serverRecv = [int64]0 $shareRead = [int64]0 $shareWrite = [int64]0 $shareSent = [int64]0 $shareRecv = [int64]0 $nicIn = [int64]0 $nicOut = [int64]0 $tcp445Sent = [int64]0 $tcp445Recv = [int64]0 $hasTcp445 = $false $byServerFromServer = @{} $byServerFromShares = @{} $byServerFromTcp = @{} $byShare = @{} $byClient = @{} $byClientTcp = @{} $byClientEtw = @{} $byUser = @{} foreach ($rec in $Records) { $r = [int64]0 $w = [int64]0 $s = [int64]0 $v = [int64]0 if ($rec.readBytesDelta) { $r = [int64]$rec.readBytesDelta } if ($rec.writeBytesDelta) { $w = [int64]$rec.writeBytesDelta } if ($rec.sentBytesDelta) { $s = [int64]$rec.sentBytesDelta } if ($rec.receivedBytesDelta) { $v = [int64]$rec.receivedBytesDelta } $kind = [string]$rec.kind $serverName = [string]$rec.server if ($kind -eq 'server') { $serverRead += $r $serverWrite += $w $serverSent += $s $serverRecv += $v Add-SMBeatAccumulator -Map $byServerFromServer -Key $serverName -Read $r -Write $w -Sent $s -Received $v } elseif ($kind -eq 'share') { $shareRead += $r $shareWrite += $w $shareSent += $s $shareRecv += $v Add-SMBeatAccumulator -Map $byShare -Key $rec.name -Read $r -Write $w -Sent $s -Received $v Add-SMBeatAccumulator -Map $byServerFromShares -Key $serverName -Read $r -Write $w -Sent $s -Received $v } elseif ($kind -eq 'session') { $clientKey = $rec.client if ([string]::IsNullOrWhiteSpace([string]$clientKey)) { $clientKey = [string]$rec.name } $userKey = $rec.user if ([string]::IsNullOrWhiteSpace([string]$userKey)) { $userKey = '(unknown)' } if (-not [string]::IsNullOrWhiteSpace([string]$clientKey)) { Add-SMBeatAccumulator -Map $byClient -Key ([string]$clientKey) -Read $r -Write $w -Sent $s -Received $v } Add-SMBeatAccumulator -Map $byUser -Key ([string]$userKey) -Read $r -Write $w -Sent $s -Received $v } elseif ($kind -eq 'client') { $clientKey = $rec.client if ([string]::IsNullOrWhiteSpace([string]$clientKey)) { $clientKey = [string]$rec.name } if (-not [string]::IsNullOrWhiteSpace([string]$clientKey)) { Add-SMBeatAccumulator -Map $byClientEtw -Key ([string]$clientKey) -Read $r -Write $w -Sent $s -Received $v } } elseif ($kind -eq 'user') { $userKey = $rec.user if ([string]::IsNullOrWhiteSpace([string]$userKey)) { $userKey = [string]$rec.name } if ([string]::IsNullOrWhiteSpace([string]$userKey)) { $userKey = '(unknown)' } Add-SMBeatAccumulator -Map $byUser -Key ([string]$userKey) -Read $r -Write $w -Sent $s -Received $v } elseif ($kind -eq 'tcp445') { $hasTcp445 = $true $name = [string]$rec.name if ($name -eq '_listen445') { $tcp445Sent += $s $tcp445Recv += $v Add-SMBeatAccumulator -Map $byServerFromTcp -Key $serverName -Read 0 -Write 0 -Sent $s -Received $v } else { $clientKey = $rec.client if ([string]::IsNullOrWhiteSpace([string]$clientKey)) { $clientKey = $name } Add-SMBeatAccumulator -Map $byClientTcp -Key ([string]$clientKey) -Read 0 -Write 0 -Sent $s -Received $v } } elseif ($kind -eq 'nic') { $nicIn += $v $nicOut += $s } } $useShareTotals = ($shareSent + $shareRecv) -gt ($serverSent + $serverRecv) $read = $serverRead $write = $serverWrite $sent = $serverSent $recv = $serverRecv $byServer = $byServerFromServer if ($useShareTotals) { $read = $shareRead $write = $shareWrite $sent = $shareSent $recv = $shareRecv $byServer = $byServerFromShares } if ($hasTcp445) { $byServer = $byServerFromTcp if ($byClientTcp.Count -gt 0) { $byClient = $byClientTcp } } if ($byClientEtw.Count -gt 0) { $byClient = $byClientEtw } $retrieved = $nicOut $written = $recv if ($hasTcp445) { $retrieved = $tcp445Sent $written = $tcp445Recv } elseif ($nicOut -gt 104857600 -and $nicOut -gt (10 * [math]::Max($sent, [int64]1))) { $warnings.Add('NIC Out is much larger than SMB Sent. On some hosts SMB share counters miss large reads; retrieved volume falls back to NIC Out.') | Out-Null } if ($hasTcp445 -and $retrieved -gt 10485760) { $shareFloor = [int64]([math]::Floor($retrieved * 0.1)) if ($shareSent -lt $shareFloor) { $warnings.Add('Share bytes are much smaller than TCP 445 retrieved. Sessions open before the collector started map to (unknown) until reconnect; ETW buffer loss is also possible.') | Out-Null } } $etwWritten = $shareRecv foreach ($uk in @($byUser.Keys)) { $uRecv = [int64]$byUser[$uk].Received if ($uRecv -gt $etwWritten) { $etwWritten = $uRecv } } if ($hasTcp445 -and $etwWritten -gt 1048576 -and $etwWritten -gt (10 * [math]::Max($written, [int64]1))) { $written = $etwWritten $warnings.Add('TCP 445 inbound is much smaller than ETW written. Short-lived writes are counted per user and share; ESTATA often misses them.') | Out-Null } $hourly = @() $daily = @() $weekly = @() if ($Granularity -contains 'Hour') { $hourly = @(Get-SMBeatSeries -Records $Records -StartUtc $StartUtc -EndUtc $EndUtc -TimeZone $TimeZone -Granularity Hour) } if ($Granularity -contains 'Day') { $daily = @(Get-SMBeatSeries -Records $Records -StartUtc $StartUtc -EndUtc $EndUtc -TimeZone $TimeZone -Granularity Day) } if ($Granularity -contains 'Week') { $weekly = @(Get-SMBeatSeries -Records $Records -StartUtc $StartUtc -EndUtc $EndUtc -TimeZone $TimeZone -Granularity Week) } $hourlyForPeak = $hourly if ($hourlyForPeak.Count -eq 0) { $hourlyForPeak = @(Get-SMBeatSeries -Records $Records -StartUtc $StartUtc -EndUtc $EndUtc -TimeZone $TimeZone -Granularity Hour) } $peak = Get-SMBeatPeakStats -Hourly $hourlyForPeak -RetrievedBytes $retrieved -WrittenBytes $written -StartUtc $StartUtc -EndUtc $EndUtc [PSCustomObject]@{ PSTypeName = 'Merlin.SMBeat.Report' StartUtc = $StartUtc EndUtc = $EndUtc TimeZone = $TimeZone.Id CoveragePct = $coverage IntervalSec = $IntervalSec Warnings = $warnings.ToArray() Totals = (ConvertTo-SMBeatTotalsObject -Read $read -Write $write -Sent $sent -Received $recv -NicIn $nicIn -NicOut $nicOut -Retrieved $retrieved -Written $written) ByServer = @(ConvertTo-SMBeatNamedTotals -Map $byServer) ByShare = @(ConvertTo-SMBeatNamedTotals -Map $byShare) ByClient = @(ConvertTo-SMBeatNamedTotals -Map $byClient) ByUser = @(ConvertTo-SMBeatNamedTotals -Map $byUser) Hourly = $hourly Daily = $daily Weekly = $weekly PeakStart = $peak.PeakStart PeakRetrievedGiB = $peak.PeakRetrievedGiB AverageRetrievedGiBPerHour = $peak.AverageRetrievedGiBPerHour PeakWrittenStart = $peak.PeakWrittenStart PeakWrittenGiB = $peak.PeakWrittenGiB AverageWrittenGiBPerHour = $peak.AverageWrittenGiBPerHour PreviousTotals = $PreviousTotals SampleCount = $Records.Count ModuleVersion = (Get-SMBeatModuleVersion) GeneratedUtc = (Get-SMBeatUtcNow) } } function New-SMBeatReportFromDataRoot { param( [string]$DataRoot, [datetime]$WindowStart, [datetime]$WindowEnd, [datetime]$PrevStart, [datetime]$PrevEnd, [TimeZoneInfo]$TimeZone, [string[]]$Granularity ) $config = Read-SMBeatConfig -DataRoot $DataRoot $interval = [int]$config.IntervalSec if ($interval -le 0) { $interval = 60 } $records = @(Read-SMBeatSampleFiles -DataRoot $DataRoot -StartUtc $WindowStart -EndUtc $WindowEnd) $prevRecords = @(Read-SMBeatSampleFiles -DataRoot $DataRoot -StartUtc $PrevStart -EndUtc $PrevEnd) $prevTotals = $null if ($prevRecords.Count -gt 0) { $prevReport = New-SMBeatReportFromSamples -Records $prevRecords -StartUtc $PrevStart -EndUtc $PrevEnd -TimeZone $TimeZone -Granularity @() -IntervalSec $interval $prevTotals = $prevReport.Totals } New-SMBeatReportFromSamples -Records $records -StartUtc $WindowStart -EndUtc $WindowEnd -TimeZone $TimeZone -Granularity $Granularity -IntervalSec $interval -PreviousTotals $prevTotals } function Resolve-SMBeatReportWindow { param( [datetime]$Start, [datetime]$End, [int]$LastHours, [int]$LastDays, [int]$LastWeeks ) $endUtc = Get-SMBeatUtcNow if ($PSBoundParameters.ContainsKey('End') -and $End) { $endUtc = $End.ToUniversalTime() } $startUtc = $null if ($PSBoundParameters.ContainsKey('Start') -and $Start) { $startUtc = $Start.ToUniversalTime() } elseif ($LastHours -gt 0) { $startUtc = $endUtc.AddHours(-1 * $LastHours) } elseif ($LastDays -gt 0) { $startUtc = $endUtc.AddDays(-1 * $LastDays) } elseif ($LastWeeks -gt 0) { $startUtc = $endUtc.AddDays(-7 * $LastWeeks) } else { $startUtc = $endUtc.AddDays(-7) } if ($startUtc -ge $endUtc) { throw 'Start must be earlier than End.' } [PSCustomObject]@{ StartUtc = $startUtc EndUtc = $endUtc } } function Merge-SMBeatNamedTotalLists { param( [object[]]$Lists ) $map = @{} foreach ($list in $Lists) { if ($null -eq $list) { continue } foreach ($row in @($list)) { Add-SMBeatAccumulator -Map $map -Key ([string]$row.Name) -Read ([int64]$row.ReadBytes) -Write ([int64]$row.WriteBytes) -Sent ([int64]$row.SentBytes) -Received ([int64]$row.ReceivedBytes) } } @(ConvertTo-SMBeatNamedTotals -Map $map) } function Merge-SMBeatSeriesLists { param( [object[]]$Lists, [string]$Granularity ) $map = @{} foreach ($list in $Lists) { if ($null -eq $list) { continue } foreach ($row in @($list)) { $key = $row.PeriodStart.ToString('o') if (-not $map.ContainsKey($key)) { $map[$key] = @{ Start = $row.PeriodStart End = $row.PeriodEnd Read = [int64]0 Write = [int64]0 Sent = [int64]0 Received = [int64]0 NicIn = [int64]0 NicOut = [int64]0 Retrieved = [int64]0 Written = [int64]0 } } $map[$key].Read += [int64]$row.ReadBytes $map[$key].Write += [int64]$row.WriteBytes if ($row.SentBytes) { $map[$key].Sent += [int64]$row.SentBytes } if ($row.ReceivedBytes) { $map[$key].Received += [int64]$row.ReceivedBytes } if ($row.NicBytesIn) { $map[$key].NicIn += [int64]$row.NicBytesIn } if ($row.NicBytesOut) { $map[$key].NicOut += [int64]$row.NicBytesOut } if ($row.RetrievedBytes) { $map[$key].Retrieved += [int64]$row.RetrievedBytes } elseif ($row.NicBytesOut) { $map[$key].Retrieved += [int64]$row.NicBytesOut } if ($row.PSObject.Properties['WrittenBytes'] -and $row.WrittenBytes) { $map[$key].Written += [int64]$row.WrittenBytes } elseif ($row.ReceivedBytes) { $map[$key].Written += [int64]$row.ReceivedBytes } } } $result = New-Object System.Collections.Generic.List[object] foreach ($key in ($map.Keys | Sort-Object)) { $b = $map[$key] $result.Add([PSCustomObject]@{ Granularity = $Granularity PeriodStart = $b.Start PeriodEnd = $b.End ReadBytes = $b.Read WriteBytes = $b.Write SentBytes = $b.Sent ReceivedBytes = $b.Received NicBytesIn = $b.NicIn NicBytesOut = $b.NicOut RetrievedBytes = $b.Retrieved WrittenBytes = $b.Written ReadGiB = (ConvertTo-SMBeatGiB -Bytes $b.Read) WriteGiB = (ConvertTo-SMBeatGiB -Bytes $b.Write) SentGiB = (ConvertTo-SMBeatGiB -Bytes $b.Sent) ReceivedGiB = (ConvertTo-SMBeatGiB -Bytes $b.Received) NicInGiB = (ConvertTo-SMBeatGiB -Bytes $b.NicIn) NicOutGiB = (ConvertTo-SMBeatGiB -Bytes $b.NicOut) RetrievedGiB = (ConvertTo-SMBeatGiB -Bytes $b.Retrieved) WrittenGiB = (ConvertTo-SMBeatGiB -Bytes $b.Written) }) | Out-Null } $result.ToArray() } function Merge-SMBeatReports { param( [Parameter(Mandatory = $true)] [object[]]$Reports, $PreviousTotals ) if ($null -eq $Reports -or $Reports.Count -eq 0) { return $null } if ($Reports.Count -eq 1) { $one = $Reports[0] if ($PreviousTotals) { $one.PreviousTotals = $PreviousTotals } return $one } $first = $Reports[0] $read = [int64]0 $write = [int64]0 $sent = [int64]0 $recv = [int64]0 $nicIn = [int64]0 $nicOut = [int64]0 $retrieved = [int64]0 $written = [int64]0 $samples = 0 $covSum = 0.0 $warnings = New-Object System.Collections.Generic.List[string] $serverLists = New-Object System.Collections.Generic.List[object] $shareLists = New-Object System.Collections.Generic.List[object] $clientLists = New-Object System.Collections.Generic.List[object] $userLists = New-Object System.Collections.Generic.List[object] $hourlyLists = New-Object System.Collections.Generic.List[object] $dailyLists = New-Object System.Collections.Generic.List[object] $weeklyLists = New-Object System.Collections.Generic.List[object] foreach ($r in $Reports) { $read += [int64]$r.Totals.ReadBytes $write += [int64]$r.Totals.WriteBytes $sent += [int64]$r.Totals.SentBytes $recv += [int64]$r.Totals.ReceivedBytes $nicIn += [int64]$r.Totals.NicBytesIn $nicOut += [int64]$r.Totals.NicBytesOut if ($r.Totals.RetrievedBytes) { $retrieved += [int64]$r.Totals.RetrievedBytes } else { $retrieved += [int64]$r.Totals.NicBytesOut } if ($r.Totals.PSObject.Properties['WrittenBytes'] -and $r.Totals.WrittenBytes) { $written += [int64]$r.Totals.WrittenBytes } else { $written += [int64]$r.Totals.ReceivedBytes } $samples += [int]$r.SampleCount $covSum += [double]$r.CoveragePct foreach ($w in @($r.Warnings)) { if ($w) { $warnings.Add([string]$w) | Out-Null } } $serverLists.Add(@($r.ByServer)) | Out-Null $shareLists.Add(@($r.ByShare)) | Out-Null $clientLists.Add(@($r.ByClient)) | Out-Null $userLists.Add(@($r.ByUser)) | Out-Null $hourlyLists.Add(@($r.Hourly)) | Out-Null $dailyLists.Add(@($r.Daily)) | Out-Null $weeklyLists.Add(@($r.Weekly)) | Out-Null } $hourlyMerged = @(Merge-SMBeatSeriesLists -Lists $hourlyLists.ToArray() -Granularity Hour) $peak = Get-SMBeatPeakStats -Hourly $hourlyMerged -RetrievedBytes $retrieved -WrittenBytes $written -StartUtc $first.StartUtc -EndUtc $first.EndUtc [PSCustomObject]@{ PSTypeName = 'Merlin.SMBeat.Report' StartUtc = $first.StartUtc EndUtc = $first.EndUtc TimeZone = $first.TimeZone CoveragePct = [math]::Round($covSum / $Reports.Count, 1) IntervalSec = $first.IntervalSec Warnings = $warnings.ToArray() Totals = (ConvertTo-SMBeatTotalsObject -Read $read -Write $write -Sent $sent -Received $recv -NicIn $nicIn -NicOut $nicOut -Retrieved $retrieved -Written $written) ByServer = (Merge-SMBeatNamedTotalLists -Lists $serverLists.ToArray()) ByShare = (Merge-SMBeatNamedTotalLists -Lists $shareLists.ToArray()) ByClient = (Merge-SMBeatNamedTotalLists -Lists $clientLists.ToArray()) ByUser = (Merge-SMBeatNamedTotalLists -Lists $userLists.ToArray()) Hourly = $hourlyMerged Daily = @(Merge-SMBeatSeriesLists -Lists $dailyLists.ToArray() -Granularity Day) Weekly = @(Merge-SMBeatSeriesLists -Lists $weeklyLists.ToArray() -Granularity Week) PeakStart = $peak.PeakStart PeakRetrievedGiB = $peak.PeakRetrievedGiB AverageRetrievedGiBPerHour = $peak.AverageRetrievedGiBPerHour PeakWrittenStart = $peak.PeakWrittenStart PeakWrittenGiB = $peak.PeakWrittenGiB AverageWrittenGiBPerHour = $peak.AverageWrittenGiBPerHour PreviousTotals = $PreviousTotals SampleCount = $samples ModuleVersion = (Get-SMBeatModuleVersion) GeneratedUtc = (Get-SMBeatUtcNow) } } |