SMBpulse.psm1
|
#requires -version 5.1 <# .SYNOPSIS SMBpulse - Messung und Auswertung des SMB-Durchsatzes auf Windows-Fileservern. .DESCRIPTION SMBpulse ist ein PowerShell-Modul und enthaelt vier Cmdlets: Start-SmbPulse Erstellt und startet eine Langzeitmessung als Data Collector Set "SMB_Pulse" (Standard: 7 Tage, 15-Sekunden-Intervall, stuendliche BLG-Segmente). Watch-SmbPulse Zeigt den aktuellen SMB-Durchsatz direkt aus den Performance Countern an - mit farbcodierter Auslastungsanzeige fuer einen 1-Gbit/s-Link. Get-SmbPulseReport Wertet alle bereits abgeschlossenen BLG-Segmente aus und exportiert die Zusammenfassung sowie die groessten Einzelspitzen als CSV. Uninstall-SmbPulse Stoppt und entfernt Collector und Messdaten wieder. Voraussetzungen: - Windows PowerShell 5.1 - Start-SmbPulse und Uninstall-SmbPulse erfordern eine Administratorsitzung Fruehere Versionen des Monitoring-Kits (Publisher-Name "SMB-Throughput-Monitor") mit dem Aelierungen Collector "SMB_Throughput_Monitor", Log "SMB_Throughput-Monitor" oder Collector "SMB_7Tage"/"SMB-Messung" werden automatisch erkannt und beim Start des neuen Collectors aufgeraeumt bzw. vom Uninstaller mitentfernt. .NOTES Autor: Manuel Berfelde #> $ErrorActionPreference = 'Stop' $Script:CollectorName = 'SMB_Pulse' $Script:LegacyCollectors = @( 'SMB_Throughput_Monitor', 'SMB_7Tage' ) $Script:LogDirs = @( 'C:\SMBpulse', 'C:\PerfLogs\SMB-Throughput-Monitor', 'C:\PerfLogs\SMB-Messung' ) $Script:OutputDir = $Script:LogDirs[0] $Script:SummaryCsv = Join-Path $Script:OutputDir 'SMBpulse-Report.csv' $Script:PeaksCsv = Join-Path $Script:OutputDir 'SMBpulse-Peaks.csv' $Script:SessionIndexTask = 'SMBpulse-SessionIndex' $Script:SessionIndexScript = Join-Path $Script:OutputDir 'SMBpulse-SessionIndex.ps1' $Script:SessionIndexCsv = Join-Path $Script:OutputDir 'SMBpulse-Sessions.csv' $Script:CollectorConfigPath = Join-Path $Script:OutputDir 'SMBpulse-Collector.json' $Script:ModuleVersion = [version]'0.0.0' try { if ($MyInvocation.MyCommand.Module) { $Script:ModuleVersion = $MyInvocation.MyCommand.Module.Version } else { $psd1 = Join-Path $PSScriptRoot 'SMBpulse.psd1' if (Test-Path -LiteralPath $psd1) { $psdData = Import-PowerShellDataFile -Path $psd1 if ($psdData -and $psdData.ModuleVersion) { $Script:ModuleVersion = [version]$psdData.ModuleVersion } } } } catch { $Script:ModuleVersion = [version]'0.0.0' } # ---------------------------------------------------------------------------- # Interne Hilfsfunktionen # ---------------------------------------------------------------------------- function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Assert-Administrator { if (-not (Test-Administrator)) { throw 'Dieses Cmdlet muss in einer PowerShell als Administrator ausgefuehrt werden.' } } function Get-SmbCounterSet { param([Object]$AllSets) if (-not $AllSets) { $AllSets = @(Get-Counter -ListSet *) } $set = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Serverfreigaben' -or $_.CounterSetName -eq 'SMB Server Shares' -or $_.CounterSetName -match '^SMB.*Server.*(Share|Freig)' } | Select-Object -First 1 if (-not $set) { throw 'Der Performance-Counter-Satz fuer SMB-Serverfreigaben wurde nicht gefunden.' } return $set } function Get-SmbCounterKind { param([Parameter(Mandatory = $true)][string]$Path) if ($Path -match '\\(Datenbytes/Sek\.|Data Bytes/sec)$') { return 'Gesamt' } if ($Path -match '\\(Gelesene Bytes/s|Read Bytes/sec)$') { return 'Lesen' } if ($Path -match '\\(Geschriebene Bytes/Sek\.|Write Bytes/sec)$') { return 'Schreiben' } return $null } function Write-MonitorBanner { param( [Parameter(Mandatory = $true)][string]$Title, [string]$Subtitle, [string]$Color = 'Cyan' ) $width = 62 $top = '+' + ('=' * ($width - 2)) + '+' $empty = '|' + (' ' * ($width - 2)) + '|' $pad = [Math]::Max(0, [int](($width - 2 - $Title.Length) / 2)) $line = '|' + (' ' * $pad) + $Title + (' ' * ($width - 2 - $pad - $Title.Length)) + '|' $versionText = 'SMBpulse Version ' + $Script:ModuleVersion $vpad = [Math]::Max(0, [int](($width - 2 - $versionText.Length) / 2)) $vline = '|' + (' ' * $vpad) + $versionText + (' ' * ($width - 2 - $vpad - $versionText.Length)) + '|' Write-Host '' Write-Host $top -ForegroundColor $Color Write-Host $empty -ForegroundColor $Color Write-Host $line -ForegroundColor $Color Write-Host $empty -ForegroundColor $Color Write-Host $vline -ForegroundColor Gray if ($Subtitle) { Write-Host $empty -ForegroundColor $Color $pad = [Math]::Max(0, [int](($width - 2 - $Subtitle.Length) / 2)) $line = '|' + (' ' * $pad) + $Subtitle + (' ' * ($width - 2 - $pad - $Subtitle.Length)) + '|' Write-Host $line -ForegroundColor Gray } Write-Host $empty -ForegroundColor $Color Write-Host $top -ForegroundColor $Color Write-Host '' } function Write-SectionTitle { param([Parameter(Mandatory = $true)][string]$Title) Write-Host '' Write-Host ('--- ' + $Title + ' ---') -ForegroundColor Cyan } function Write-InfoBox { param( [string[]]$Lines, [string]$Title = 'Ueberblick', [string]$Color = 'Cyan' ) if (-not $Lines) { return } $width = 76 $top = '+' + ('=' * ($width - 2)) + '+' $empty = '|' + (' ' * ($width - 2)) + '|' Write-Host '' Write-Host $top -ForegroundColor $Color $pad = [Math]::Max(0, [int](($width - 2 - $Title.Length) / 2)) $line = '|' + (' ' * $pad) + $Title + (' ' * ($width - 2 - $pad - $Title.Length)) + '|' Write-Host $line -ForegroundColor $Color Write-Host $empty -ForegroundColor $Color foreach ($line in $Lines) { $text = $line if ($text -match ':') { $index = $text.IndexOf(':') $label = $text.Substring(0, $index).Trim() $value = $text.Substring($index + 1).Trim() $text = (' {0,-28}: {1}' -f $label, $value) } else { $text = ' ' + $text } if ($text.Length -gt ($width - 3)) { $text = $text.Substring(0, $width - 3) } $pad = $width - 2 - $text.Length Write-Host ('|' + $text + (' ' * $pad) + '|') } Write-Host $empty -ForegroundColor $Color Write-Host $top -ForegroundColor $Color Write-Host '' } function Write-Ok { param([string]$Text) Write-Host $Text -ForegroundColor Green } function Write-Warn { param([string]$Text) Write-Host $Text -ForegroundColor Yellow } function Write-Err { param([string]$Text) Write-Host $Text -ForegroundColor Red } function Write-Muted { param([string]$Text) Write-Host $Text -ForegroundColor DarkGray } function Convert-ToLogmanDuration { param([int]$TotalSeconds) $hours = [Math]::Floor($TotalSeconds / 3600) $minutes = [Math]::Floor(($TotalSeconds % 3600) / 60) $seconds = $TotalSeconds % 60 return ('{0:00}:{1:00}:{2:00}' -f $hours, $minutes, $seconds) } function Invoke-Logman { param( [Parameter(Mandatory = $true)][string[]]$ArgumentList, [switch]$Quiet ) $logman = Join-Path $env:SystemRoot 'System32\logman.exe' if (Test-Path (Join-Path $env:SystemRoot 'Sysnative\logman.exe')) { $logman = Join-Path $env:SystemRoot 'Sysnative\logman.exe' } # logman startet gelegentlich nicht, wenn das aktuelle Arbeitsverzeichnis # fuer den Child-Prozess ungueltig ist ("Der Verzeichnisname ist ungueltig"). # Deshalb immer aus einem garantiert gueltigen Verzeichnis heraus starten. Push-Location $env:SystemRoot try { if ($Quiet) { & $logman @ArgumentList 2>$null | Out-Null } else { $lines = @(& $logman @ArgumentList) foreach ($line in $lines) { Write-Host $line } } return $LASTEXITCODE } finally { Pop-Location } } function Stop-AndDeleteCollector { param([string]$Name) if ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -ne 0) { return } $null = Invoke-Logman -ArgumentList @('stop', $Name) -Quiet Start-Sleep -Milliseconds 500 $null = Invoke-Logman -ArgumentList @('delete', $Name) -Quiet # logman/PLA gibt die aktive BLG-Datei ggf. leicht verzoegert frei. for ($i = 0; $i -lt 20; $i++) { if ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -ne 0) { break } Start-Sleep -Milliseconds 500 } Write-Ok "Vorhandenen Datensammler '$Name' entfernt." } function Find-SmbTotalCounter { param( [string[]]$Patterns, [string[]]$Paths ) foreach ($pattern in $Patterns) { $match = $Paths | Where-Object { $_ -match '\(_Total\)\\' -and $_ -match $pattern } | Select-Object -First 1 if ($match) { return $match } } return $null } function Get-SmbServerShareName { param( [Parameter(Mandatory)][string]$Instance ) $parts = @($Instance -split '\\' | Where-Object { $_ -ne '' }) if ($parts.Count -lt 1) { return $null } return $parts[$parts.Count - 1] } function Test-AdminShareName { param( [Parameter(Mandatory)][string]$Name ) return ($Name -match '\$$') } function Get-SmbPrinterShareNames { $names = @() try { $names = @( Get-SmbShare -ErrorAction Stop | Where-Object { $_.Description -match 'LocalsplOnly' } | Select-Object -ExpandProperty Name ) } catch { } return $names } function Get-SmbSessionMap { $map = @{} try { foreach ($session in @(Get-SmbSession -ErrorAction Stop)) { $map[[string]$session.SessionId] = @{ Client = [string]$session.ClientComputerName User = [string]$session.ClientUserName } } } catch { } return $map } function Invoke-SmbSessionSnapshot { param( [string]$Path = $Script:SessionIndexCsv ) $dir = Split-Path -Parent $Path if ($dir) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $rows = @(Get-SmbSession -ErrorAction SilentlyContinue | Select-Object -Property SessionId, ClientComputerName, ClientUserName) if ($rows.Count -eq 0) { return } if (-not (Test-Path -LiteralPath $Path)) { '"Timestamp","SessionId","ClientComputerName","ClientUserName"' | Out-File -LiteralPath $Path -Encoding UTF8 } $stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $lines = foreach ($row in $rows) { $client = ([string]$row.ClientComputerName).Replace('"', '""') $user = ([string]$row.ClientUserName).Replace('"', '""') '"{0}","{1}","{2}","{3}"' -f $stamp, $row.SessionId, $client, $user } $lines | Out-File -LiteralPath $Path -Encoding UTF8 -Append } function New-SmbSessionIndexScript { $content = @' # SMBpulse Session-Index (wird von Start-SmbPulse erzeugt) $ErrorActionPreference = 'SilentlyContinue' $csv = 'C:\SMBpulse\SMBpulse-Sessions.csv' $dir = Split-Path -Parent $csv New-Item -ItemType Directory -Path $dir -Force | Out-Null if (-not (Test-Path -LiteralPath $csv)) { '"Timestamp","SessionId","ClientComputerName","ClientUserName"' | Out-File -LiteralPath $csv -Encoding UTF8 } $rows = @(Get-SmbSession | Select-Object -Property SessionId, ClientComputerName, ClientUserName) if ($rows.Count -gt 0) { $stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $lines = foreach ($row in $rows) { $client = ([string]$row.ClientComputerName).Replace('"', '""') $user = ([string]$row.ClientUserName).Replace('"', '""') '"{0}","{1}","{2}","{3}"' -f $stamp, $row.SessionId, $client, $user } $lines | Out-File -LiteralPath $csv -Encoding UTF8 -Append } '@ $script = Join-Path $Script:OutputDir 'SMBpulse-SessionIndex.ps1' $dir = Split-Path -Parent $script if ($dir) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } [System.IO.File]::WriteAllText($script, $content, [System.Text.Encoding]::ASCII) } function Invoke-Schtasks { param( [Parameter(Mandatory = $true)] [string[]]$Arguments ) $schtasks = Join-Path $env:SystemRoot 'System32\schtasks.exe' $oldEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & $schtasks @Arguments 2>&1 | Out-Null return $LASTEXITCODE } finally { $ErrorActionPreference = $oldEap } } function Remove-SmbSessionIndexTask { param( [string]$Name = $Script:SessionIndexTask ) $exit = Invoke-Schtasks -Arguments @('/query', '/tn', $Name) if ($exit -ne 0) { return } Invoke-Schtasks -Arguments @('/end', '/tn', $Name) | Out-Null Invoke-Schtasks -Arguments @('/delete', '/tn', $Name, '/f') | Out-Null } function New-SmbSessionIndexTask { param( [string]$Name = $Script:SessionIndexTask, [string]$ScriptPath = $Script:SessionIndexScript, [int]$Minutes = 5 ) Remove-SmbSessionIndexTask -Name $Name $inner = 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "' + $ScriptPath + '"' $tr = '"' + $inner.Replace('"', '\"') + '"' $exit = Invoke-Schtasks -Arguments @('/create', '/tn', $Name, '/tr', $tr, '/sc', 'minute', '/mo', ('{0}' -f $Minutes), '/ru', 'SYSTEM', '/f') if ($exit -ne 0) { throw "Der geplante Task '$Name' konnte nicht erstellt werden (Exitcode $exit)." } Invoke-Schtasks -Arguments @('/run', '/tn', $Name) | Out-Null } # ---------------------------------------------------------------------------- # Start-SmbPulse # ---------------------------------------------------------------------------- function Start-SmbPulse { <# .SYNOPSIS Erstellt und startet eine Langzeitmessung des SMB- und Netzwerkdurchsatzes. .DESCRIPTION Erstellt den Data Collector Set "SMB_Pulse" ueber logman. Storage: - 7 Tage Laufzeit - 15 Sekunden Abtastintervall - stuendliche BLG-Segmente Die stuendlichen Segmente erlauben bereits waehrend der Messwoche eine Auswertung abgeschlossener BLG-Dateien. Ergaenzend wird ein geplanter Task "SMBpulse-SessionIndex" angelegt, der alle 5 Minuten einen Schnappschuss der SMB-Serversitzungen in eine CSV schreibt. Dadurch koennen die gemessenen Sitzungs-IDs im Report den Clients und Benutzern zugeordnet werden. Eventuell vorhandene Collector frueherer Versionen ("SMB_Throughput_Monitor", "SMB_7Tage") werden gestoppt und entfernt. Alte Messdaten bleiben erhalten. .PARAMETER Days Laufzeit der Messung in Tagen (1-30, Standard 7). .PARAMETER SampleSeconds Abstand zwischen zwei Messwerten in Sekunden (1-3600, Standard 15). .PARAMETER SegmentMinutes Groesse eines BLG-Zeitsegments in Minuten (5-1440, Standard 60). .EXAMPLE Start-SmbPulse .EXAMPLE Start-SmbPulse -Days 14 -SampleSeconds 10 -SegmentMinutes 30 #> [CmdletBinding()] param( [ValidateRange(1,30)] [int]$Days = 7, [ValidateRange(1,3600)] [int]$SampleSeconds = 15, [ValidateRange(5,1440)] [int]$SegmentMinutes = 60 ) Assert-Administrator $CollectorName = $Script:CollectorName $OutputDir = $Script:OutputDir Write-MonitorBanner -Title 'SMBpulse' -Subtitle 'Langzeitmessung von SMB- und Netzwerkdurchsatz' # Alte bzw. bereits vorhandene Collector sauber entfernen. Write-SectionTitle 'Bestehende Collector entfernen' foreach ($name in @($Script:LegacyCollectors + $CollectorName)) { Stop-AndDeleteCollector -Name $name } New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null Write-SectionTitle 'Performance Counter ermitteln' $AllSets = Get-Counter -ListSet * $SmbSet = Get-SmbCounterSet -AllSets $AllSets $NetSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'Netzwerkschnittstelle' -or $_.CounterSetName -eq 'Network Interface' } | Select-Object -First 1 if (-not $NetSet) { throw 'Der Performance-Counter-Satz fuer Netzwerkschnittstellen wurde nicht gefunden.' } $SmbPaths = @($SmbSet.PathsWithInstances) $NetPaths = @($NetSet.PathsWithInstances) $SmbData = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @( '\\Datenbytes/Sek\.$', '\\Data Bytes/sec$' ) $SmbRead = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @( '\\Gelesene Bytes/s$', '\\Read Bytes/sec$' ) $SmbWrite = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @( '\\Geschriebene Bytes/Sek\.$', '\\Write Bytes/sec$' ) $SmbCounters = @($SmbData, $SmbRead, $SmbWrite) | Where-Object { $_ } if ($SmbCounters.Count -lt 3) { Write-Host '' Write-Warn 'Gefundene SMB-Counter:' $SmbPaths | Where-Object { $_ -match '\(_Total\)\\' } | ForEach-Object { Write-Host " $_" } throw 'Nicht alle benoetigten SMB-Durchsatzzaehler wurden gefunden.' } $NetCounters = @( $NetPaths | Where-Object { $_ -match '\\(Gesamtanzahl Bytes/s|Bytes Total/sec|Bytes gesendet/s|Bytes Sent/sec|Empfangene Bytes/s|Bytes Received/sec)$' } ) if ($NetCounters.Count -eq 0) { throw 'Keine Netzwerk-Byte/s-Counter wurden gefunden.' } $SessionSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Serversitzungen' -or $_.CounterSetName -eq 'SMB Server Sessions' } | Select-Object -First 1 $ShareSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Serverfreigaben' -or $_.CounterSetName -eq 'SMB Server Shares' } | Select-Object -First 1 $SessionPaths = @(Get-WildcardCounterPaths -CounterSet $SessionSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') $SharePaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') if ($SessionPaths.Count -eq 0) { Write-Warn 'Der Counter-Satz "SMB Server Sessions" wurde nicht gefunden - Client-/Benutzer-Details werden nicht aufgezeichnet.' } if ($SharePaths.Count -eq 0) { Write-Warn 'Der Counter-Satz "SMB Server Shares" wurde nicht gefunden - Freigaben-Details werden nicht aufgezeichnet.' } if ($SharePaths.Count -gt 0 -and $SmbData) { $SmbCounters = @($SmbCounters | Where-Object { $_ -ne $SmbData }) } $Counters = @($SmbCounters | Sort-Object -Unique) + @($NetCounters | Sort-Object -Unique) + @($SessionPaths | Sort-Object -Unique) + @($SharePaths | Sort-Object -Unique) $smbCounterCount = @($Counters | Where-Object { $_ -match 'Serverfreigaben|Server Shares' }).Count $netCounterCount = @($NetCounters).Count $sessionTxt = if ($SessionPaths.Count -gt 0) { 'ja' } else { 'nein' } $shareTxt = if ($SharePaths.Count -gt 0) { 'ja' } else { 'nein' } Write-Ok (" Gefunden: {0} SMB-Zaehler, {1} Netz-Zaehler | Sessions: {2} | Freigaben: {3}" -f $smbCounterCount, $netCounterCount, $sessionTxt, $shareTxt) Write-SectionTitle 'Aufzuzeichnende Counter' $Counters | ForEach-Object { Write-Host " $_" } $SampleInterval = Convert-ToLogmanDuration -TotalSeconds $SampleSeconds $RunDuration = Convert-ToLogmanDuration -TotalSeconds ($Days * 24 * 3600) $SegmentDuration = Convert-ToLogmanDuration -TotalSeconds ($SegmentMinutes * 60) $Arguments = @( 'create', 'counter', $CollectorName, '-f', 'bin', '-o', "$OutputDir\SMB", '-si', $SampleInterval, '-rf', $RunDuration, '-cnf', $SegmentDuration, '-v', 'mmddhhmm', '-c' ) $Arguments += $Counters Write-SectionTitle 'Datensammler erstellen' $createExit = Invoke-Logman -ArgumentList $Arguments if ($createExit -ne 0) { throw "logman create ist mit Exitcode $createExit fehlgeschlagen." } Write-SectionTitle 'Messung starten' $startExit = Invoke-Logman -ArgumentList @('start', $CollectorName) if ($startExit -ne 0) { throw "logman start ist mit Exitcode $startExit fehlgeschlagen." } Write-Ok 'Messung laeuft.' $collectorConfig = @{ Version = [string]$Script:ModuleVersion Created = (Get-Date).ToString('o') } $collectorConfig | ConvertTo-Json | Set-Content -Path $Script:CollectorConfigPath -Encoding UTF8 Write-SectionTitle 'Session-Index einrichten' New-SmbSessionIndexScript Invoke-SmbSessionSnapshot -Path $Script:SessionIndexCsv New-SmbSessionIndexTask Write-Ok "Session-Index-Task '$($Script:SessionIndexTask)' aktiv (alle 5 Minuten)." Write-InfoBox -Title 'Ueberblick' -Lines @( "Collector: $CollectorName", "Dauer: $Days Tag(e)", "Intervall: $SampleSeconds Sekunden", "BLG-Segment: $SegmentMinutes Minuten", "Ausgabe: $OutputDir", "Session-Index: $($Script:SessionIndexCsv)" ) Write-Muted 'Status pruefen:' Write-Muted " logman query $CollectorName" Write-Muted '' Write-Muted 'Live-Monitor:' Write-Muted ' Watch-SmbPulse' Write-Muted '' Write-Muted 'Auswertung:' Write-Muted ' Get-SmbPulseReport' Write-Host '' } # ---------------------------------------------------------------------------- # Watch-SmbPulse # ---------------------------------------------------------------------------- function Get-SmbReferenceMbps { param( [string]$Mode = 'Auto' ) $speeds = @{} $norm = 1000.0 if ($Mode -ne 'Auto') { return [PSCustomObject]@{ Norm = [double]$Mode Speeds = $speeds } } try { $upAdapters = @(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.Status -eq 'Up' }) foreach ($adapter in $upAdapters) { $mbit = 0.0 if ($adapter.LinkSpeed -match '(\d+(?:\.\d+)?)\s*Gbps') { $mbit = [double]$Matches[1] * 1000 } elseif ($adapter.LinkSpeed -match '(\d+(?:\.\d+)?)\s*Mbps') { $mbit = [double]$Matches[1] } foreach ($key in @($adapter.Name, $adapter.InterfaceDescription, $adapter.InterfaceAlias, $adapter.ifAlias)) { if ($key) { $speeds[$key] = $mbit } } } $candidates = @($speeds.Values | Where-Object { $_ -gt 0 }) if ($candidates.Count -gt 0) { $norm = [double](($candidates | Measure-Object -Maximum).Maximum) } } catch { Write-Warn 'Get-NetAdapter nicht verfuegbar - Massstab faellt auf 1 Gbit/s zurueck.' } return [PSCustomObject]@{ Norm = $norm Speeds = $speeds } } $Script:CollectionCheckTime = $null $Script:CollectionCheckLine = '' $Script:CollectorConfigCheckTime = $null $Script:CollectorConfigLine = '' function Get-SmbCollectorConfigLine { $now = Get-Date if (-not $Script:CollectorConfigCheckTime -or ($now - $Script:CollectorConfigCheckTime).TotalSeconds -ge 10) { $Script:CollectorConfigCheckTime = $now $Script:CollectorConfigLine = 'Collector-Konfig: unbekannt' try { if (Test-Path -LiteralPath $Script:CollectorConfigPath) { $cfg = Get-Content -LiteralPath $Script:CollectorConfigPath -Raw | ConvertFrom-Json if ($cfg -and $cfg.Version) { $detail = '' if ($cfg.Created) { try { if ($cfg.Created -is [datetime]) { $detail = (" (erstellt {0})" -f $cfg.Created.ToString('dd.MM.yyyy HH:mm')) } else { $detail = (" (erstellt {0})" -f ([DateTime]::Parse([string]$cfg.Created, [Globalization.CultureInfo]::InvariantCulture)).ToString('dd.MM.yyyy HH:mm')) } } catch { $detail = '' } } $Script:CollectorConfigLine = ("Collector-Konfig: SMBpulse {0}{1}" -f [string]$cfg.Version, $detail) } } } catch { $Script:CollectorConfigLine = 'Collector-Konfig: unbekannt' } } return $Script:CollectorConfigLine } function Get-SmbCollectionStatusLine { $now = Get-Date if (-not $Script:CollectionCheckTime -or ($now - $Script:CollectionCheckTime).TotalSeconds -ge 5) { $Script:CollectionCheckTime = $now $last = @( Get-ChildItem -LiteralPath $Script:OutputDir -Filter '*.blg' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 ) if ($last.Count -eq 0) { $Script:CollectionCheckLine = ("Sammlung: keine Messdaten in {0} - Start-SmbPulse" -f $Script:OutputDir) } else { $staleSeconds = ($now - $last[0].LastWriteTime).TotalSeconds if ($staleSeconds -le 60) { $Script:CollectionCheckLine = ("Sammlung: aktiv ({0}, letzte Probe {1:HH:mm:ss})" -f $Script:CollectorName, $last[0].LastWriteTime) } else { $Script:CollectionCheckLine = ("Sammlung: letzte Probe {0:HH:mm:ss} (vor {1:N0} s) - laeuft Start-SmbPulse?" -f $last[0].LastWriteTime, $staleSeconds) } } } return $Script:CollectionCheckLine } function Watch-SmbPulse { <# .SYNOPSIS Live-Anzeige des aktuellen SMB- und Netzwerk-Durchsatzes samt Status der Langzeit-Erfassung. .DESCRIPTION Zeigt den aktuellen Durchsatz direkt aus den Windows Performance Countern und ob der Langzeit-Collector aktuell Messdaten aufzeichnet (letzte Probe der BLG-Datei in C:\SMBpulse). Es wird keine BLG-Datei benoetigt und der laufende Langzeit-Collector wird nicht beeinflusst. Angezeigt werden: - SMB Gesamt / Lesen / Schreiben (optional alle Freigaben) - Netzwerk-Gesamtwert je NIC samt Auslastung relativ zur tatsaechlichen Link-Geschwindigkeit (via Get-NetAdapter) - Status der Langzeit-Erfassung (letzte gemessene Probe) - Collector-Konfiguration (mit welcher SMBpulse-Version der Collector zuletzt erstellt wurde; fehlt der Marker, erscheint "unbekannt") Jede Zeile zeigt die Auslastung in Prozent relativ zur jeweiligen Link-Geschwindigkeit (gruen unter 70 %, gelb 70-90 %, rot ueber 90 %). Die Analyse der Top-Clients, Top-Benutzer und Top-Freigaben liefert Get-SmbPulseReport (Konsolenausgabe, CSV und HTML-Report). .PARAMETER IntervalSeconds Aktualisierungsintervall in Sekunden (1-60, Standard 2). .PARAMETER PerShare Zeigt zusaetzlich die einzelnen SMB-Freigaben statt nur _Total. Verursacht eine zusaetzliche Performance-Counter-Abfrage. .PARAMETER ReferenceMbps Referenz-Linkgeschwindigkeit in Mbit/s fuer die Auslastungs-Skala. 'Auto' nutzt die schnellste aktive NIC (Standard). Bei virtuellen NICs (z.B. Hyper-V VirtIO mit 10 Gbit/s intern, nach aussen aber nur 1 Gbit/s) kann die Skala mit 100, 1000 oder 10000 manuell gesetzt werden. Wirkt auf Kopfzeile, SMB-Durchsatz und die Netzwerk-Sektion. .EXAMPLE Watch-SmbPulse .EXAMPLE Watch-SmbPulse -IntervalSeconds 1 -PerShare #> [CmdletBinding()] param( [ValidateRange(1,60)] [int]$IntervalSeconds = 2, [switch]$PerShare, [ValidateSet('Auto', '100', '1000', '10000')] [string]$ReferenceMbps = 'Auto' ) $AllSets = @(Get-Counter -ListSet *) $NetSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'Netzwerkschnittstelle' -or $_.CounterSetName -eq 'Network Interface' } | Select-Object -First 1 $NetCounterPaths = @(Get-WildcardCounterPaths -CounterSet $NetSet -CounterRegex '\\(Gesamtanzahl Bytes/s|Bytes Total/sec)$') $ShareSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Serverfreigaben' -or $_.CounterSetName -eq 'SMB Server Shares' } | Select-Object -First 1 $ShareCounterPaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') $ShareReadPaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Gelesene Bytes/s|Read Bytes/sec)$') $ShareWritePaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Geschriebene Bytes/Sek\.|Write Bytes/sec)$') $shareExcludes = @(Get-SmbPrinterShareNames) $ShareTotalPaths = @( @($ShareCounterPaths) + @($ShareReadPaths) + @($ShareWritePaths) | ForEach-Object { $_ -replace '\(\*\)', '(_Total)' } | Select-Object -Unique ) $allCounterPaths = @($NetCounterPaths + $ShareTotalPaths) if ($PerShare) { $allCounterPaths = @($allCounterPaths + $ShareCounterPaths + $ShareReadPaths + $ShareWritePaths) } $refDetect = Get-SmbReferenceMbps -Mode $ReferenceMbps $refLinkMbps = $refDetect.Norm $linkSpeeds = $refDetect.Speeds $forceNorm = ($ReferenceMbps -ne 'Auto') function Get-LiveRowText { param( [string]$Label, [string]$Kind, [double]$MBPS, [double]$MbitSec, [double]$RefMbps ) $percent = if ($RefMbps -gt 0) { ($MbitSec / $RefMbps) * 100 } else { 0 } return (' {0,-24}{1,-10}{2,10}{3,10} {4,6:N1} % ' -f $Label, $Kind, ('{0:N2}' -f $MBPS), ('{0:N2}' -f $MbitSec), $percent) } Write-MonitorBanner -Title 'SMBpulse Live Monitor' -Subtitle 'SMB- und Netz-Durchsatz in Echtzeit' Write-Host "Intervall: $IntervalSeconds Sekunde(n)" if ($forceNorm) { Write-Host ("Link-Norm: {0:N0} Mbit/s (manuell via -ReferenceMbps)" -f $refLinkMbps) } else { Write-Host ("Link-Norm: {0:N0} Mbit/s (via Get-NetAdapter)" -f $refLinkMbps) } Write-Host 'Beenden: STRG+C' Write-Host '' $useConsoleFast = $true $cursorVisible = $true try { $cursorVisible = [Console]::CursorVisible [Console]::CursorVisible = $false } catch { $useConsoleFast = $false } try { while ($true) { try { try { if ($useConsoleFast) { [Console]::Clear() } else { Clear-Host } } catch { $useConsoleFast = $false } try { $allSample = @((Get-Counter -Counter $allCounterPaths -ErrorAction Stop).CounterSamples) $rows = New-Object System.Collections.Generic.List[object] $netRows = @() $seen = @{} foreach ($counter in $allSample) { $path = [string]$counter.Path $value = [double]$counter.CookedValue if ($seen.ContainsKey($path)) { continue } $seen[$path] = $true if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) { continue } $instance = [string]$counter.InstanceName if ($path -match 'Netzwerkschnittstelle|Network Interface') { $linkMbps = $refLinkMbps if (-not $forceNorm) { foreach ($key in $linkSpeeds.Keys) { if ($instance -like ('*' + $key + '*')) { $linkMbps = $linkSpeeds[$key] break } } } $netRows += [PSCustomObject]@{ Nic = $instance MBPS = $value / 1MB MbitSec = ($value * 8) / 1000000 LinkMbps = [double]$linkMbps } } else { $kind = Get-SmbCounterKind -Path $path if (-not $kind) { continue } $share = ($instance -split '\\')[-1] if ($instance -eq '_Total') { $rows.Add([PSCustomObject]@{ Freigabe = 'Alle Freigaben' Typ = $kind MBPS = $value / 1MB MbitSec = ($value * 8) / 1000000 }) } elseif ($PerShare) { if (-not $share -or (Test-AdminShareName -Name $share) -or ($share -in $shareExcludes)) { continue } $rows.Add([PSCustomObject]@{ Freigabe = $share Typ = $kind MBPS = $value / 1MB MbitSec = ($value * 8) / 1000000 }) } else { continue } } } $sorted = @( $rows | Sort-Object Freigabe, @{ Expression = { switch ($_.Typ) { 'Gesamt' { 1 } 'Lesen' { 2 } 'Schreiben' { 3 } default { 9 } } } } ) } catch { $sorted = @() $netRows = @() } Write-Host (("SMBpulse Live Monitor v{0} {1}" -f $Script:ModuleVersion, (Get-Date -Format 'dd.MM.yyyy HH:mm:ss'))) -ForegroundColor Cyan $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('=' * 100) [void]$sb.AppendLine(("Intervall: {0} Sekunde(n) | Link-Norm: {1:N0} Mbit/s" -f $IntervalSeconds, $refLinkMbps)) [void]$sb.AppendLine('Beenden: STRG+C') [void]$sb.AppendLine('') [void]$sb.AppendLine((Get-SmbCollectionStatusLine)) [void]$sb.AppendLine((Get-SmbCollectorConfigLine)) [void]$sb.AppendLine('') [void]$sb.AppendLine('--- SMB-Durchsatz ---') if ($sorted.Count -eq 0) { [void]$sb.AppendLine(' (momentan keine verwertbaren Messwerte)') } else { foreach ($row in $sorted) { [void]$sb.AppendLine((Get-LiveRowText -Label $row.Freigabe -Kind $row.Typ -MBPS $row.MBPS -MbitSec $row.MbitSec -RefMbps $refLinkMbps)) } } [void]$sb.AppendLine('') [void]$sb.AppendLine('--- Netzwerk (je Schnittstelle) ---') if ($netRows.Count -eq 0) { [void]$sb.AppendLine(' (keine Netzwerk-Gesamtwerte verfuegbar)') } else { [void]$sb.AppendLine((' {0,-34}{1,-10}{2,10}{3,10} {4,7}' -f 'Schnittstelle', 'Link', 'MB/s', 'Mbit/s', 'Auslast.')) foreach ($row in $netRows) { $percent = if ($row.LinkMbps -gt 0) { ($row.MbitSec / $row.LinkMbps) * 100 } else { 0 } $nicName = [string]$row.Nic if ($nicName.Length -lt 34) { $nicName = $nicName.PadRight(34) } else { $nicName = $nicName + ' ' } [void]$sb.AppendLine((' {0}{1,-10}{2,10}{3,10} {4,6:N1} % ' -f $nicName, ('{0:N0}' -f $row.LinkMbps), ('{0:N2}' -f $row.MBPS), ('{0:N2}' -f $row.MbitSec), $percent)) } } [void]$sb.AppendLine('') $sz = [string][char]0x00DF [void]$sb.AppendLine(("(Auslastung in % relativ zur Link-Norm {0:N0} Mbit/s; Ma{1}stab wie Kopfzeile)" -f $refLinkMbps, $sz)) Write-Host $sb.ToString() Start-Sleep -Seconds $IntervalSeconds } catch { try { if ($useConsoleFast) { [Console]::Clear() } else { Clear-Host } } catch { } Write-Host 'SMBpulse Live Monitor' -ForegroundColor Cyan Write-Host ('=' * 100) -ForegroundColor Cyan Write-Warn "Fehler beim Lesen der Performance-Counter: $($_.Exception.Message)" Write-Host '' Start-Sleep -Seconds $IntervalSeconds } } } finally { try { [Console]::CursorVisible = $cursorVisible } catch { } Clear-Host } } # ---------------------------------------------------------------------------- # Interne Funktionen der Auswertung und des HTML-Reports # ---------------------------------------------------------------------------- function Read-SmbSessionIndex { param( [string]$Path ) $index = @{} if (-not (Test-Path -LiteralPath $Path)) { return $index } $rows = @(Import-Csv -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue | Where-Object { $_.SessionId }) if ($rows.Count -eq 0) { return $index } foreach ($g in @($rows | Group-Object SessionId)) { $sorted = @( $g.Group | Sort-Object { ConvertTo-SmbIndexTime -Value $_.Timestamp } ) $index[[string]$g.Name] = @{ Times = @($sorted | ForEach-Object { ConvertTo-SmbIndexTime -Value $_.Timestamp }) Clients = @($sorted | ForEach-Object { [string]$_.ClientComputerName }) Users = @($sorted | ForEach-Object { [string]$_.ClientUserName }) } } return $index } function ConvertTo-SmbIndexTime { param( [string]$Value ) $time = [datetime]::MinValue [void][datetime]::TryParseExact( $Value, 'yyyy-MM-dd HH:mm:ss', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$time ) return $time } function Find-SmbSessionSnapshot { param( [hashtable]$Index, [string]$SessionId, [datetime]$Timestamp ) $entry = $Index[$SessionId] if (-not $entry -or $entry.Times.Count -eq 0) { return $null } $times = $entry.Times $lo = 0 $hi = $times.Count - 1 $best = -1 while ($lo -le $hi) { $mid = [int](($lo + $hi) / 2) if ($times[$mid] -le $Timestamp) { $best = $mid $lo = $mid + 1 } else { $hi = $mid - 1 } } if ($best -lt 0) { return $null } return @{ Client = $entry.Clients[$best] User = $entry.Users[$best] } } function Get-WildcardCounterPaths { param( [Object]$CounterSet, [string]$CounterRegex ) if (-not $CounterSet) { return @() } return @( $CounterSet.PathsWithInstances | Where-Object { $_ -match $CounterRegex } | ForEach-Object { $_ -replace '\([^)]*\)', '(*)' } | Sort-Object -Unique ) } function Get-SampleIntervalSeconds { param( [object[]]$OrderedSamples ) $count = $OrderedSamples.Count if ($count -lt 2) { return 15.0 } $deltas = [System.Collections.Generic.List[double]]::new() for ($i = 1; $i -lt $count; $i++) { $delta = ($OrderedSamples[$i].Timestamp - $OrderedSamples[$i - 1].Timestamp).TotalSeconds if ($delta -gt 0) { $deltas.Add($delta) } } if ($deltas.Count -eq 0) { return 15.0 } $sorted = @($deltas | Sort-Object) if ($sorted.Count % 2 -eq 0) { $upper = [int][Math]::Floor($sorted.Count / 2) return ([double]$sorted[$upper - 1] + [double]$sorted[$upper]) / 2.0 } return [double]$sorted[[int][Math]::Floor($sorted.Count / 2)] } function Get-TimeBucketStart { param( [datetime]$Timestamp, [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity ) switch ($Granularity) { 'Hour' { return $Timestamp.Date.AddHours($Timestamp.Hour) } 'Day' { return $Timestamp.Date } 'Week' { $date = $Timestamp.Date $delta = ([DayOfWeek]::Monday - $date.DayOfWeek) if ($delta -gt 0) { $delta -= 7 } return $date.AddDays($delta) } } } function Get-TimeBucketLabel { param( [datetime]$Timestamp, [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity ) switch ($Granularity) { 'Hour' { return $Timestamp.ToString('dd.MM.yyyy HH:00') } 'Day' { return $Timestamp.ToString('dd.MM.yyyy') } 'Week' { $week = [Globalization.CultureInfo]::InvariantCulture.Calendar.GetWeekOfYear( $Timestamp, [Globalization.CalendarWeekRule]::FirstFourDayWeek, [DayOfWeek]::Monday ) return '{0:D4}-W{1:00}' -f $Timestamp.Year, $week } } } function Get-UsageRows { param( [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity, [object[]]$SmbSamples, [object[]]$NetSamples, [double]$SmbIntervalSeconds, [double]$NetIntervalSeconds ) $buckets = @{} foreach ($sample in $SmbSamples) { $key = Get-TimeBucketStart -Timestamp $sample.Timestamp -Granularity $Granularity if (-not $buckets.ContainsKey($key)) { $buckets[$key] = @{ SmbBytes = 0.0; NetBytes = 0.0 } } $buckets[$key].SmbBytes += $sample.BytesSec } foreach ($sample in $NetSamples) { $key = Get-TimeBucketStart -Timestamp $sample.Timestamp -Granularity $Granularity if (-not $buckets.ContainsKey($key)) { $buckets[$key] = @{ SmbBytes = 0.0; NetBytes = 0.0 } } $buckets[$key].NetBytes += $sample.BytesSec } $rows = foreach ($key in @($buckets.Keys | Sort-Object)) { $bucket = $buckets[$key] [PSCustomObject]@{ Zeitraum = Get-TimeBucketLabel -Timestamp $key -Granularity $Granularity 'SMB gesamt (GB)' = [Math]::Round(($bucket.SmbBytes * $SmbIntervalSeconds) / 1GB, 2) 'Netzwerk gesamt (GB)' = [Math]::Round(($bucket.NetBytes * $NetIntervalSeconds) / 1GB, 2) } } return @($rows) } function Get-SmbPulseUsage { param( [object[]]$Samples ) $smbSamples = @( $Samples | Where-Object { $_.Type -eq 'SMB' -and $_.Group -eq 'SMB Gesamt' } | Sort-Object Timestamp ) $netSamples = @( $Samples | Where-Object { $_.Type -eq 'Netzwerk' -and $_.Group -like 'Netzwerk Gesamt - *' } | Sort-Object Timestamp ) $smbInterval = Get-SampleIntervalSeconds -OrderedSamples $smbSamples $netInterval = Get-SampleIntervalSeconds -OrderedSamples $netSamples return @{ Hourly = Get-UsageRows -Granularity Hour -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval Daily = Get-UsageRows -Granularity Day -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval Weekly = Get-UsageRows -Granularity Week -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval } } function Write-UsageTable { param( [string]$Title, [object[]]$Rows ) Write-SectionTitle $Title if (-not $Rows -or $Rows.Count -eq 0) { Write-Warn ' keine Daten vorhanden' Write-Host '' return } Write-Host (' {0,-22} {1,18} {2,18}' -f 'Zeitraum', 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)') -ForegroundColor DarkGray foreach ($row in $Rows) { Write-Host (' {0,-22} {1,18:N2} {2,18:N2}' -f $row.Zeitraum, $row.'SMB gesamt (GB)', $row.'Netzwerk gesamt (GB)') } Write-Host '' } function Write-TopTable { param( [string]$Title, [object[]]$Rows, [string]$NameHeader ) Write-SectionTitle $Title if (-not $Rows -or $Rows.Count -eq 0) { Write-Warn " keine $NameHeader-Daten vorhanden (Collector ohne Client-Counter oder noch kein Segment abgeschlossen)" Write-Host '' return } Write-Host (' {0,-32}{1,16}{2,12}' -f $NameHeader, 'Datenmenge (GB)', 'Anteil (%)') -ForegroundColor DarkGray foreach ($row in $Rows) { Write-Host (' {0,-32}{1,16:N2}{2,11:N1} %' -f $row.Name, $row.'Datenmenge (GB)', $row.'Anteil (%)') } Write-Host '' } function ConvertTo-HtmlText { param([string]$Value) return [Net.WebUtility]::HtmlEncode([string]$Value) } function Format-Decimal { param( [double]$Value, [int]$Digits = 2 ) return $Value.ToString('N' + $Digits, [Globalization.CultureInfo]::InvariantCulture) } function Get-UsageTableHtml { param( [object[]]$Rows ) $max = 0.0 foreach ($row in $Rows) { $smb = [double]$row.'SMB gesamt (GB)' $net = [double]$row.'Netzwerk gesamt (GB)' if ($smb -gt $max) { $max = $smb } if ($net -gt $max) { $max = $net } } if ($max -le 0) { $max = 1 } $html = [Text.StringBuilder]::new() [void]$html.Append('<table><thead><tr><th>Zeitraum</th><th>SMB gesamt (GB)</th><th>Netzwerk gesamt (GB)</th></tr></thead><tbody>') foreach ($row in $Rows) { $smb = [double]$row.'SMB gesamt (GB)' $net = [double]$row.'Netzwerk gesamt (GB)' $smbPercent = [Math]::Round(($smb / $max) * 100, 1) $netPercent = [Math]::Round(($net / $max) * 100, 1) [void]$html.Append('<tr>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $row.Zeitraum) + '</td>') [void]$html.Append(('<td><div class="num" data-color="smb">{0} GB</div><div class="bar" data-color="smb"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $smb), $smbPercent)) [void]$html.Append(('<td><div class="num" data-color="net">{0} GB</div><div class="bar" data-color="net"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $net), $netPercent)) [void]$html.Append('</tr>') } [void]$html.Append('</tbody></table>') return $html.ToString() } function Get-PeaksTableHtml { param( [object[]]$Peaks ) $html = [Text.StringBuilder]::new() [void]$html.Append('<table><thead><tr><th>Nr.</th><th>Zeitpunkt</th><th>Messgruppe</th><th>Instanz</th><th>MB/s</th><th>Mbit/s</th></tr></thead><tbody>') $index = 1 foreach ($peak in $Peaks) { [void]$html.Append('<tr>') [void]$html.Append('<td>' + $index + '</td>') [void]$html.Append('<td>' + $peak.Timestamp.ToString('dd.MM.yyyy HH:mm') + '</td>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $peak.Group) + '</td>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $peak.Instance) + '</td>') [void]$html.Append('<td>' + (Format-Decimal $peak.MBsec) + '</td>') [void]$html.Append('<td>' + (Format-Decimal $peak.MbitSec) + '</td>') [void]$html.Append('</tr>') $index++ } [void]$html.Append('</tbody></table>') return $html.ToString() } function Get-TopTableHtml { param( [object[]]$Rows, [string]$NameHeader ) if (-not $Rows -or $Rows.Count -eq 0) { return '<div class="hint">keine Daten vorhanden</div>' } $max = [double]$Rows[0].'Datenmenge (GB)' if ($max -le 0) { $max = 1 } $html = [Text.StringBuilder]::new() [void]$html.Append(('<table><thead><tr><th>{0}</th><th>Datenmenge (GB)</th><th>Anteil (%)</th></tr></thead><tbody>' -f $NameHeader)) foreach ($row in $Rows) { $gb = [double]$row.'Datenmenge (GB)' $pct = [double]$row.'Anteil (%)' $width = [Math]::Round(($gb / $max) * 100, 1) [void]$html.Append('<tr>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $row.Name) + '</td>') [void]$html.Append(('<td><div class="num" data-color="smb">{0} GB</div><div class="bar" data-color="smb"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $gb), $width)) [void]$html.Append(('<td>{0}</td>' -f (Format-Decimal $pct))) [void]$html.Append('</tr>') } [void]$html.Append('</tbody></table>') return $html.ToString() } function New-SmbPulseHtmlReport { param( [Parameter(Mandatory)] [string]$OutputPath, [string]$LogoPath, [hashtable]$Summary, [object[]]$Daily, [object[]]$Weekly, [object[]]$Hourly, [object[]]$Peaks, [object[]]$TopClients, [object[]]$TopUsers, [object[]]$TopShares ) $css = @' body { margin: 0; padding: 0; font-family: 'Segoe UI', system-ui, -apple-system, Arial, sans-serif; background: #eef2f7; color: #1f2937; } .wrap { max-width: 980px; margin: 0 auto; padding: 24px 16px 48px; } .hero { background: linear-gradient(135deg, #4f46e5, #0ea5e9); color: #ffffff; border-radius: 16px; padding: 22px 26px; display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; box-shadow: 0 8px 24px rgba(79, 70, 229, 0.25); } .logo { height: 56px; width: auto; border-radius: 10px; background: #ffffff; padding: 6px; } .hero-text h1 { margin: 0; font-size: 24px; } .hero-text p { margin: 4px 0 0; opacity: 0.92; font-size: 14px; } .meta { text-align: right; font-size: 12px; opacity: 0.85; line-height: 1.5; } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin: 22px 0; } .card { background: #ffffff; border-radius: 14px; padding: 16px 18px; box-shadow: 0 2px 8px rgba(31, 41, 55, 0.06); } .card .kpi { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; } .card .val { font-size: 22px; font-weight: 700; margin-top: 6px; } .card .sub { font-size: 12px; color: #9ca3af; margin-top: 2px; } section.box { background: #ffffff; border-radius: 14px; padding: 18px 22px; margin: 18px 0; box-shadow: 0 2px 8px rgba(31, 41, 55, 0.06); } section.box h2 { margin: 0 0 12px; font-size: 18px; } .hint { font-size: 12px; color: #9ca3af; margin-top: 8px; } table { width: 100%; border-collapse: collapse; font-size: 14px; } th { text-align: left; color: #6b7280; font-weight: 600; padding: 8px 10px; border-bottom: 2px solid #e5e7eb; } td { padding: 8px 10px; border-bottom: 1px solid #f0f2f5; } tbody tr:nth-child(even) td { background: #fafbfc; } .num { font-weight: 600; } .num[data-color="smb"] { color: #2563eb; } .num[data-color="net"] { color: #10b981; } .bar { background: #eef2f7; border-radius: 999px; height: 8px; margin-top: 5px; overflow: hidden; } .bar span { display: block; height: 100%; border-radius: 999px; } .bar[data-color="smb"] span { background: #2563eb; } .bar[data-color="net"] span { background: #10b981; } details { margin-top: 4px; } summary { cursor: pointer; font-weight: 600; color: #2563eb; padding: 6px 0; } footer { text-align: center; color: #9ca3af; font-size: 12px; margin-top: 26px; } '@ $logoImage = '' if (Test-Path -LiteralPath $LogoPath -PathType Leaf) { $bytes = [IO.File]::ReadAllBytes($LogoPath) $logoImage = '<img src="data:image/png;base64,' + [Convert]::ToBase64String($bytes) + '" alt="SMBpulse" class="logo">' } $html = [Text.StringBuilder]::new() [void]$html.Append('<!DOCTYPE html>') [void]$html.Append('<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SMBpulse Report</title><style>') [void]$html.Append($css) [void]$html.Append('</style></head><body><div class="wrap">') [void]$html.Append('<header class="hero">') [void]$html.Append($logoImage) [void]$html.Append('<div class="hero-text"><h1>SMBpulse - Durchsatz-Report</h1><p>Langzeitmessung des SMB- und Netzwerkdurchsatzes</p></div>') [void]$html.Append(('<div class="meta">Erstellt am {0}<br>Zeitraum: {1} - {2}</div>' -f (ConvertTo-HtmlText -Value $Summary.Generated), (ConvertTo-HtmlText -Value $Summary.PeriodFrom), (ConvertTo-HtmlText -Value $Summary.PeriodTo))) [void]$html.Append('</header>') $cards = @( @{ Kpi = 'SMB gesamt'; Val = (Format-Decimal $Summary.SmbTotalGb) + ' GB'; Sub = $Summary.DurationText } @{ Kpi = 'Netzwerk gesamt'; Val = (Format-Decimal $Summary.NetTotalGb) + ' GB'; Sub = 'alle Interfaces' } @{ Kpi = 'Durchschnitt'; Val = (Format-Decimal $Summary.AverageMbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'P95'; Val = (Format-Decimal $Summary.P95Mbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'Maximum'; Val = (Format-Decimal $Summary.MaximumMbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'Messdauer'; Val = $Summary.DurationText; Sub = 'abgeschlossene Segmente' } ) [void]$html.Append('<div class="cards">') foreach ($card in $cards) { [void]$html.Append('<div class="card">') [void]$html.Append('<div class="kpi">' + (ConvertTo-HtmlText -Value $card.Kpi) + '</div>') [void]$html.Append('<div class="val">' + (ConvertTo-HtmlText -Value $card.Val) + '</div>') [void]$html.Append('<div class="sub">' + (ConvertTo-HtmlText -Value $card.Sub) + '</div>') [void]$html.Append('</div>') } [void]$html.Append('</div>') [void]$html.Append('<section class="box"><h2>Tagesuebersicht</h2>') [void]$html.Append((Get-UsageTableHtml -Rows $Daily)) [void]$html.Append('<div class="hint">Balken relativ zum groessten Tageswert.</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Wochenuebersicht</h2>') [void]$html.Append((Get-UsageTableHtml -Rows $Weekly)) [void]$html.Append('<div class="hint">Kalenderwochen nach ISO 8601 (Montag als Wochenbeginn).</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Stundenuebersicht</h2>') [void]$html.Append(('<details><summary>Stundenwerte anzeigen ({0} Stunden)</summary>' -f $Hourly.Count)) [void]$html.Append((Get-UsageTableHtml -Rows $Hourly)) [void]$html.Append('</details>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Spitzenwerte</h2>') [void]$html.Append((Get-PeaksTableHtml -Peaks $Peaks)) [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Top Clients</h2>') if ($TopClients -and $TopClients.Count -gt 0) { [void]$html.Append((Get-TopTableHtml -Rows $TopClients -NameHeader 'Client')) } else { [void]$html.Append('<div class="hint">Keine Daten fuer diesen Zeitraum (eventuell fehlen Sitzungs-/Freigaben-Zaehler im Collector - Start-SmbPulse neu ausfuehren).</div>') } [void]$html.Append('<div class="hint">Uebertragene Datenmenge je Client ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Top Benutzer</h2>') if ($TopUsers -and $TopUsers.Count -gt 0) { [void]$html.Append((Get-TopTableHtml -Rows $TopUsers -NameHeader 'Benutzer')) } else { [void]$html.Append('<div class="hint">Keine Daten fuer diesen Zeitraum (eventuell fehlen Sitzungs-/Freigaben-Zaehler im Collector - Start-SmbPulse neu ausfuehren).</div>') } [void]$html.Append('<div class="hint">Uebertragene Datenmenge je Benutzer (Domaene\Benutzer) ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Top Freigaben</h2>') if ($TopShares -and $TopShares.Count -gt 0) { [void]$html.Append((Get-TopTableHtml -Rows $TopShares -NameHeader 'Freigabe')) } else { [void]$html.Append('<div class="hint">Keine Daten fuer diesen Zeitraum (eventuell fehlen Sitzungs-/Freigaben-Zaehler im Collector - Start-SmbPulse neu ausfuehren).</div>') } [void]$html.Append('<div class="hint">Uebertragene Datenmenge je SMB-Freigabe ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') [void]$html.Append(('<footer>SMBpulse {0} - {1}</footer>' -f (ConvertTo-HtmlText -Value $Summary.Version), (ConvertTo-HtmlText -Value $Summary.Generated))) [void]$html.Append('</div></body></html>') $parent = Split-Path -Path $OutputPath -Parent if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } [IO.File]::WriteAllText($OutputPath, $html.ToString(), [Text.UTF8Encoding]::new($false)) } # ---------------------------------------------------------------------------- # Get-SmbPulseReport # ---------------------------------------------------------------------------- function Get-SmbPulseReport { <# .SYNOPSIS Wertet die BLG-Dateien von SMBpulse aus. .DESCRIPTION Liest alle bereits abgeschlossenen BLG-Segmente aus dem aktuellen sowie aus den vorherigen Messpfaden ein. Eine momentan noch aktive/unvollstaendige BLG-Datei wird automatisch uebersprungen. Ausgabe: - Konsolenuebersicht mit Durchschnitt, P95 und Maximum - Zeitaufschluesselung (stundenweise, tagesweise, wochenweise) - Top Clients / Top Benutzer / Top Freigaben (Gesamtzeitraum) - SMBpulse-Report.csv - SMBpulse-Peaks.csv mit den groessten Einzelspitzen - SMBpulse-Stunden.csv / SMBpulse-Tage.csv / SMBpulse-Wochen.csv - SMBpulse-Clients.csv / SMBpulse-Benutzer.csv / SMBpulse-Freigaben.csv - SMBpulse-Report.html (selbst-enthaltender HTML-Report) .PARAMETER TopPeaks Anzahl der groessten Einzelspitzen in der Peaks-CSV (1-500, Standard 50). .PARAMETER TopClients Anzahl der groessten Clients/Benutzer/Freigaben in den Top-Listen und CSVs (1-100, Standard 20). .EXAMPLE Get-SmbPulseReport .EXAMPLE Get-SmbPulseReport -TopPeaks 100 .EXAMPLE Get-SmbPulseReport -TopClients 10 #> [CmdletBinding()] param( [ValidateRange(1,500)] [int]$TopPeaks = 50, [ValidateRange(1,100)] [int]$TopClients = 20 ) $OutputDir = $Script:OutputDir $SummaryCsv = $Script:SummaryCsv $PeaksCsv = $Script:PeaksCsv $HourCsv = Join-Path $OutputDir 'SMBpulse-Stunden.csv' $DayCsv = Join-Path $OutputDir 'SMBpulse-Tage.csv' $WeekCsv = Join-Path $OutputDir 'SMBpulse-Wochen.csv' $ClientCsv = Join-Path $OutputDir 'SMBpulse-Clients.csv' $UserCsv = Join-Path $OutputDir 'SMBpulse-Benutzer.csv' $ShareCsv = Join-Path $OutputDir 'SMBpulse-Freigaben.csv' $HtmlPath = Join-Path $OutputDir 'SMBpulse-Report.html' $LogoPath = Join-Path $PSScriptRoot 'logo.png' function Get-Percentile { param( [double[]]$Values, [ValidateRange(0,100)] [double]$Percentile ) if (-not $Values -or $Values.Count -eq 0) { return 0 } $sorted = @($Values | Sort-Object) $index = [Math]::Ceiling(($Percentile / 100) * $sorted.Count) - 1 if ($index -lt 0) { $index = 0 } return [double]$sorted[$index] } function Get-CounterInfo { param( $Counter, [datetime]$Timestamp ) $path = [string]$Counter.Path $instance = [string]$Counter.InstanceName if ($path -match 'SMB-Serverfreigaben|SMB Server Shares') { $kind = Get-SmbCounterKind -Path $path if (-not $kind) { return $null } if ($kind -eq 'Gesamt') { if ($instance -eq '_Total') { return @{ Group = 'SMB Gesamt'; Type = 'SMB'; Instance = $instance } } $share = Get-SmbServerShareName -Instance $instance if (-not $share -or (Test-AdminShareName -Name $share) -or ($share -in $shareExcludes)) { return $null } return @{ Group = "Freigabe $share" Type = 'Share' Instance = $instance Share = $share } } $group = switch ($kind) { 'Lesen' { 'SMB Lesen' } 'Schreiben' { 'SMB Schreiben' } } return @{ Group = $group; Type = 'SMB'; Instance = $instance } } if ($path -match 'Netzwerkschnittstelle|Network Interface') { if ($path -match '\\(Gesamtanzahl Bytes/s|Bytes Total/sec)$') { return @{ Group = "Netzwerk Gesamt - $instance"; Type = 'Netzwerk'; Instance = $instance } } if ($path -match '\\(Empfangene Bytes/s|Bytes Received/sec)$') { return @{ Group = "Netzwerk Empfang - $instance"; Type = 'Netzwerk'; Instance = $instance } } if ($path -match '\\(Bytes gesendet/s|Bytes Sent/sec)$') { return @{ Group = "Netzwerk Senden - $instance"; Type = 'Netzwerk'; Instance = $instance } } } if ($path -match 'SMB-Serversitzungen|SMB Server Sessions') { if ($path -match '\\(Datenbytes/Sek\.|Data bytes/sec)$') { if (-not $instance -or $instance -eq '_Total') { return $null } $snapshot = Find-SmbSessionSnapshot -Index $sessionIndex -SessionId $instance -Timestamp $Timestamp $client = $instance $user = $null if ($snapshot) { if ($snapshot.Client) { $client = $snapshot.Client } $user = $snapshot.User } return @{ Group = "Client $client" Type = 'Client' Instance = $instance Client = $client User = $user } } } return $null } Write-MonitorBanner -Title 'SMBpulse Report' -Subtitle 'Auswertung der abgeschlossenen BLG-Segmente' New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null $files = @() foreach ($dir in $Script:LogDirs) { if (Test-Path $dir) { $files += Get-ChildItem -Path $dir -Filter '*.blg' -File -ErrorAction SilentlyContinue } } $files = @($files | Sort-Object FullName -Unique) if ($files.Count -eq 0) { Write-Warn 'Es wurden noch keine BLG-Dateien gefunden.' return } Write-Muted "Gefundene BLG-Dateien: $($files.Count)" Write-Host '' $samples = New-Object System.Collections.Generic.List[object] $readableFiles = 0 $skippedFiles = 0 $sessionIndex = Read-SmbSessionIndex -Path $Script:SessionIndexCsv $shareExcludes = @(Get-SmbPrinterShareNames) if ($sessionIndex.Count -eq 0) { Write-Warn 'Kein Sitzungs-Index gefunden - Clients werden nur als Sitzungs-ID angezeigt.' } foreach ($file in ($files | Sort-Object LastWriteTime)) { Write-Host -NoNewline "Pruefe $($file.FullName) ... " try { $data = @(Import-Counter -Path $file.FullName -ErrorAction Stop) $added = 0 foreach ($sampleSet in $data) { foreach ($counter in $sampleSet.CounterSamples) { $info = Get-CounterInfo -Counter $counter -Timestamp $sampleSet.Timestamp if (-not $info) { continue } $value = [double]$counter.CookedValue if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) { continue } $samples.Add([PSCustomObject]@{ Timestamp = $sampleSet.Timestamp Group = $info.Group Type = $info.Type Instance = $info.Instance Client = $info.Client User = $info.User Share = $info.Share BytesSec = $value MBsec = [Math]::Round($value / 1MB, 3) MbitSec = [Math]::Round(($value * 8) / 1000000, 3) Source = $file.Name }) $added++ } } if ($added -gt 0) { Write-Ok "OK ($added Samples)" $readableFiles++ } else { Write-Warn 'keine relevanten Daten' } } catch { Write-Warn 'noch aktiv/unlesbar - uebersprungen' $skippedFiles++ } } if ($samples.Count -eq 0) { Write-Host '' Write-Warn 'Noch keine abgeschlossene BLG-Datei mit auswertbaren Daten vorhanden.' Write-Warn 'Bei der Standardkonfiguration entsteht nach etwa einer Stunde das erste abgeschlossene Segment.' return } $results = foreach ($group in ($samples | Group-Object Group)) { $values = @($group.Group | ForEach-Object { [double]$_.BytesSec }) $avg = [double](($values | Measure-Object -Average).Average) $max = [double](($values | Measure-Object -Maximum).Maximum) $p95 = [double](Get-Percentile -Values $values -Percentile 95) $first = ($group.Group | Sort-Object Timestamp | Select-Object -First 1).Timestamp $last = ($group.Group | Sort-Object Timestamp | Select-Object -Last 1).Timestamp [PSCustomObject]@{ Counter = $group.Name Samples = $values.Count Von = $first Bis = $last 'Durchschnitt MB/s' = [Math]::Round($avg / 1MB, 2) 'P95 MB/s' = [Math]::Round($p95 / 1MB, 2) 'Maximum MB/s' = [Math]::Round($max / 1MB, 2) 'Durchschnitt Mbit/s' = [Math]::Round(($avg * 8) / 1000000, 2) 'P95 Mbit/s' = [Math]::Round(($p95 * 8) / 1000000, 2) 'Maximum Mbit/s' = [Math]::Round(($max * 8) / 1000000, 2) } } $results = @($results | Sort-Object Counter) Write-Host '' Write-Ok "Lesbare BLG-Dateien: $readableFiles" Write-Warn "Uebersprungene BLG-Dateien: $skippedFiles" Write-SectionTitle 'Zusammenfassung' foreach ($result in $results) { Write-Host '' Write-Host $result.Counter -ForegroundColor DarkCyan Write-Muted (" {0} Messwerte ({1} - {2})" -f $result.Samples, $result.Von.ToString('dd.MM.yyyy HH:mm'), $result.Bis.ToString('dd.MM.yyyy HH:mm')) Write-Host (" Durchschnitt: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'Durchschnitt MB/s', $result.'Durchschnitt Mbit/s') -ForegroundColor Green Write-Host (" P95: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'P95 MB/s', $result.'P95 Mbit/s') -ForegroundColor Yellow Write-Host (" Maximum: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'Maximum MB/s', $result.'Maximum Mbit/s') -ForegroundColor Red } $results | Export-Csv -Path $SummaryCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $peaks = @( $samples | Sort-Object BytesSec -Descending | Select-Object -First $TopPeaks Timestamp, Group, Instance, MBsec, MbitSec, Source ) $peaks | Export-Csv -Path $PeaksCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $usage = Get-SmbPulseUsage -Samples $samples $hourRows = $usage.Hourly $dayRows = $usage.Daily $weekRows = $usage.Weekly Write-UsageTable -Title 'Datenmenge nach Stunde' -Rows $hourRows Write-UsageTable -Title 'Datenmenge nach Tag' -Rows $dayRows Write-UsageTable -Title 'Datenmenge nach Woche' -Rows $weekRows $hourRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $HourCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $dayRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $DayCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $weekRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $WeekCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $clientSamples = @($samples | Where-Object { $_.Type -eq 'Client' } | Sort-Object Timestamp) $shareSamples = @($samples | Where-Object { $_.Type -eq 'Share' } | Sort-Object Timestamp) if ($clientSamples.Count -eq 0 -and $shareSamples.Count -eq 0) { Write-Host '' Write-Warn 'Keine Sitzungs-/Freigaben-Daten im Messzeitraum - Collector-Konfig pruefen (Start-SmbPulse neu ausfuehren, Watch zeigt die Collector-Version).' } $clientDt = Get-SampleIntervalSeconds -OrderedSamples $clientSamples $shareDt = Get-SampleIntervalSeconds -OrderedSamples $shareSamples $topClientRows = @( if ($clientSamples.Count -gt 0) { $clientGroups = @($clientSamples | Group-Object Client) $clientTotal = [double](($clientGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $clientGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $clientDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($clientTotal -gt 0) { [Math]::Round(($gb / $clientTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) $topUserRows = @( if ($clientSamples.Count -gt 0) { $userSamples = @($clientSamples | Where-Object { $_.User }) $userGroups = @($userSamples | Group-Object User) $userTotal = [double](($userGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $userGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $clientDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($userTotal -gt 0) { [Math]::Round(($gb / $userTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) $topShareRows = @( if ($shareSamples.Count -gt 0) { $shareGroups = @($shareSamples | Group-Object Share) $shareTotal = [double](($shareGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $shareGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $shareDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($shareTotal -gt 0) { [Math]::Round(($gb / $shareTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) Write-TopTable -Title 'Top Clients (nach Datenmenge)' -Rows $topClientRows -NameHeader 'Client' Write-TopTable -Title 'Top Benutzer (nach Datenmenge)' -Rows $topUserRows -NameHeader 'Benutzer' Write-TopTable -Title 'Top Freigaben (nach Datenmenge)' -Rows $topShareRows -NameHeader 'Freigabe' if ($topClientRows.Count -gt 0) { $topClientRows | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $ClientCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } if ($topUserRows.Count -gt 0) { $topUserRows | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $UserCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } if ($topShareRows.Count -gt 0) { $topShareRows | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $ShareCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } $smbResult = @($results | Where-Object { $_.Counter -eq 'SMB Gesamt' }) | Select-Object -First 1 $averageMbs = 0.0 $p95Mbs = 0.0 $maxMbs = 0.0 if ($smbResult) { $averageMbs = [double]$smbResult.'Durchschnitt MB/s' $p95Mbs = [double]$smbResult.'P95 MB/s' $maxMbs = [double]$smbResult.'Maximum MB/s' } $allSamples = @($samples | Sort-Object Timestamp) $periodFrom = $allSamples[0].Timestamp $periodTo = $allSamples[$allSamples.Count - 1].Timestamp $periodLength = $periodTo - $periodFrom if ($periodLength.TotalDays -ge 1) { $durationText = '{0} Tage {1} Std' -f [int][Math]::Floor($periodLength.TotalDays), $periodLength.Hours } else { $durationText = '{0} Std {1} Min' -f $periodLength.Hours, $periodLength.Minutes } $smbTotalGb = [Math]::Round([double](($hourRows | Measure-Object -Property 'SMB gesamt (GB)' -Sum).Sum), 2) $netTotalGb = [Math]::Round([double](($hourRows | Measure-Object -Property 'Netzwerk gesamt (GB)' -Sum).Sum), 2) $htmlSummary = @{ SmbTotalGb = $smbTotalGb NetTotalGb = $netTotalGb AverageMbs = $averageMbs P95Mbs = $p95Mbs MaximumMbs = $maxMbs DurationText = $durationText PeriodFrom = $periodFrom.ToString('dd.MM.yyyy HH:mm') PeriodTo = $periodTo.ToString('dd.MM.yyyy HH:mm') Generated = (Get-Date).ToString('dd.MM.yyyy HH:mm') Version = [string]$Script:ModuleVersion } New-SmbPulseHtmlReport -OutputPath $HtmlPath -LogoPath $LogoPath -Summary $htmlSummary -Daily $dayRows -Weekly $weekRows -Hourly $hourRows -Peaks $peaks -TopClients $topClientRows -TopUsers $topUserRows -TopShares $topShareRows Write-SectionTitle 'Ergebnisdateien' Write-Ok " $SummaryCsv" Write-Ok " $PeaksCsv" Write-Ok " $HourCsv" Write-Ok " $DayCsv" Write-Ok " $WeekCsv" if ($topClientRows.Count -gt 0) { Write-Ok " $ClientCsv" } if ($topUserRows.Count -gt 0) { Write-Ok " $UserCsv" } if ($topShareRows.Count -gt 0) { Write-Ok " $ShareCsv" } Write-Ok " $HtmlPath" Write-Host '' return $results } # ---------------------------------------------------------------------------- # Uninstall-SmbPulse # ---------------------------------------------------------------------------- function Uninstall-SmbPulse { <# .SYNOPSIS Entfernt alle systemseitigen Aenderungen von SMBpulse. .DESCRIPTION Stoppt und loescht: - SMB_Pulse - den alten Collector SMB_Throughput_Monitor - den alten Collector SMB_7Tage - den geplanten Task SMBpulse-SessionIndex (Sitzungs-Index) Standardmaessig werden ausserdem die von SMBpulse verwendeten Messdaten-Verzeichnisse geloescht. Vor dem Loeschen der Messdaten fragt das Cmdlet nach; mit -Force wird die Rueckfrage uebersprungen, mit -KeepLogs werden die Messdaten grundsaetzlich behalten. Windows Performance Logs and Alerts (PLA) kann die zuletzt beschriebene BLG-Datei nach "logman stop/delete" noch einige Sekunden geoeffnet halten. Das Cmdlet wartet deshalb gezielt auf die Dateifreigabe und wiederholt die Loeschung. Der Dienst "Performance Logs & Alerts" wird bewusst NICHT global beendet, damit andere Performance-Collector-Sets auf dem Server nicht gestoert werden. .PARAMETER KeepLogs Behaelt die Messdaten, stoppt und entfernt aber die Collector-Sets. .PARAMETER Force Ueberspringt die Rueckfrage vor dem Loeschen der Messdaten. .PARAMETER WaitSeconds Maximale Wartezeit auf die Freigabe geoeffneter BLG-/CSV-Dateien (5-120, Standard 30). .EXAMPLE Uninstall-SmbPulse .EXAMPLE Uninstall-SmbPulse -Force .EXAMPLE Uninstall-SmbPulse -KeepLogs -WaitSeconds 60 #> [CmdletBinding()] param( [switch]$KeepLogs, [switch]$Force, [ValidateRange(5,120)] [int]$WaitSeconds = 30 ) Assert-Administrator $Collectors = $Script:LegacyCollectors + @($Script:CollectorName) $LogDirs = $Script:LogDirs function Test-CollectorExists { param([string]$Name) return ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -eq 0) } function Stop-AndRemoveCollector { param([string]$Name) if (-not (Test-CollectorExists -Name $Name)) { Write-Muted "Datensammler '$Name' ist nicht vorhanden." return } Write-Host "Stoppe Datensammler '$Name' ..." $null = Invoke-Logman -ArgumentList @('stop', $Name) -Quiet # PLA arbeitet beim Schliessen der aktiven BLG teilweise asynchron. Start-Sleep -Seconds 1 Write-Host "Loesche Datensammler '$Name' ..." $null = Invoke-Logman -ArgumentList @('delete', $Name) -Quiet for ($i = 0; $i -lt $WaitSeconds; $i++) { if (-not (Test-CollectorExists -Name $Name)) { Write-Ok ' entfernt' return } Start-Sleep -Seconds 1 } throw "Der Datensammler '$Name' konnte innerhalb von $WaitSeconds Sekunden nicht vollstaendig entfernt werden." } function Remove-DirectoryWithRetry { param( [string]$Path, [int]$TimeoutSeconds ) if (-not (Test-Path -LiteralPath $Path)) { Write-Muted "$Path ist nicht vorhanden." return } Write-Host "Loesche $Path ..." $lastError = $null for ($i = 0; $i -lt $TimeoutSeconds; $i++) { try { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop if (-not (Test-Path -LiteralPath $Path)) { Write-Ok ' entfernt' return } } catch [System.IO.IOException] { $lastError = $_ } catch [System.UnauthorizedAccessException] { $lastError = $_ } if ($i -eq 0) { Write-Warn ' Datei noch durch Windows/PLA geoeffnet - warte auf Freigabe ...' } Start-Sleep -Seconds 1 } # Noch einmal anzeigen, welche Dateien ggf. uebrig sind. $remaining = @( Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName ) Write-Host '' Write-Err "Das Verzeichnis konnte nach $TimeoutSeconds Sekunden noch nicht vollstaendig geloescht werden." if ($remaining.Count -gt 0) { Write-Warn 'Noch vorhandene Dateien:' $remaining | ForEach-Object { Write-Host " $_" } } if ($lastError) { throw $lastError } throw "Loeschen von '$Path' fehlgeschlagen." } Write-MonitorBanner -Title 'SMBpulse' -Subtitle 'Deinstallation' $removeLogs = -not $KeepLogs $planLines = @( ('Datensammler: ' + ($Collectors -join ', ')), ('Session-Index-Task: ' + $Script:SessionIndexTask), ('Messpfad: ' + ($LogDirs -join ', ')) ) if ($KeepLogs) { $planLines += 'Messdaten: bleiben erhalten (-KeepLogs)' } Write-InfoBox -Title 'Durchgefuehrte Aenderungen' -Lines $planLines if ($removeLogs -and -not $Force) { Write-Warn 'Die Messdaten-Verzeichnisse werden vollstaendig geloescht.' $answer = Read-Host 'Fortfahren [J/N]' if ($answer -notmatch '^(j|ja|y|yes)$') { $removeLogs = $false Write-Warn 'Messdaten werden behalten.' } } Write-SectionTitle 'Datensammler entfernen' foreach ($collector in $Collectors) { Stop-AndRemoveCollector -Name $collector } Write-SectionTitle 'Session-Index entfernen' Remove-SmbSessionIndexTask -Name $Script:SessionIndexTask Write-Ok "Geplanten Task '$($Script:SessionIndexTask)' entfernt." if ($removeLogs) { Write-SectionTitle 'Messdaten loeschen' Write-Host 'Warte kurz auf die Freigabe eventuell noch geoeffneter BLG-Dateien ...' Start-Sleep -Seconds 2 foreach ($dir in $LogDirs) { Remove-DirectoryWithRetry -Path $dir -TimeoutSeconds $WaitSeconds } } else { Write-SectionTitle 'Messdaten behalten' Write-Warn 'Die Messdaten werden nicht geloescht.' } Write-Host '' Write-Ok 'Deinstallation abgeschlossen.' Write-Host '' } Export-ModuleMember -Function 'Start-SmbPulse', 'Watch-SmbPulse', 'Get-SmbPulseReport', 'Uninstall-SmbPulse' |