bin/Public/Get-sqmDiskSpaceReport.ps1

<#
.SYNOPSIS
    Ermittelt freien Speicherplatz aller SQL-relevanten Volumes und
    schaetzt den Zeitpunkt bis zur Erschoepfung basierend auf Wachstumsdaten.
 
.DESCRIPTION
    Fragt sys.dm_os_volume_stats fuer alle Datenbankdateien ab und ermittelt:
    - Freier Speicherplatz pro Volume
    - Gesamtgroesse der Datenbankdateien auf dem Volume
    - AutoGrowth-Volumen der letzten -HistoryDays Tage (aus Default Trace)
    - Geschaetzte Tage bis Erschoepfung basierend auf Wachstumsrate
    - Warnung wenn freier Speicher unter -WarnThresholdPct faellt
 
    Die Ergebnisse werden als TXT-Bericht und CSV-Datei im angegebenen Verzeichnis gespeichert.
    Zusaetzlich gibt die Funktion ein Objekt mit den Detaildaten und den Dateipfaden zurueck.
 
.PARAMETER SqlInstance
    SQL Server-Instanz(en). Pipeline-faehig. Standard: aktueller Computername.
 
.PARAMETER SqlCredential
    Optionales PSCredential fuer die Verbindung.
 
.PARAMETER WarnThresholdPct
    Warnung wenn freier Speicher unter diesem Prozentwert. Standard: 20.
 
.PARAMETER CriticalThresholdPct
    Kritisch wenn freier Speicher unter diesem Prozentwert. Standard: 10.
 
.PARAMETER HistoryDays
    Zeitraum fuer Wachstumsberechnung in Tagen. Standard: 30.
 
.PARAMETER OutputPath
    Ausgabeverzeichnis fuer die Berichtsdateien. Standard: C:\System\WinSrvLog\MSSQL
 
.PARAMETER ContinueOnError
    Bei Fehler auf einer Instanz fortfahren (ansonsten wird der Fehler ausgeloest).
 
.PARAMETER EnableException
    Ausnahmen sofort ausloesen (ueberschreibt ContinueOnError).
 
.PARAMETER Confirm
    Fordert vor dem Schreiben der Dateien eine Bestaetigung an.
 
.PARAMETER WhatIf
    Zeigt, welche Dateien erstellt wuerden, ohne sie tatsaechlich zu schreiben.
 
.EXAMPLE
    Get-sqmDiskSpaceReport
 
.EXAMPLE
    Get-sqmDiskSpaceReport -SqlInstance "SQL01" -WarnThresholdPct 15 -OutputPath "D:\Reports"
 
.NOTES
    Autor: MSSQLTools
    Voraussetzungen: dbatools, Invoke-sqmLogging
    Standard-Ausgabepfad: C:\System\WinSrvLog\MSSQL
    Die Wachstumsberechnung basiert auf dem Default Trace (falls aktiviert).
    Ist der Trace deaktiviert, wird kein Wachstumswert ermittelt.
#>

function Get-sqmDiskSpaceReport
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [string[]]$SqlInstance = @($env:COMPUTERNAME),
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 99)]
        [int]$WarnThresholdPct = 20,
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 99)]
        [int]$CriticalThresholdPct = 10,
        [Parameter(Mandatory = $false)]
        [int]$HistoryDays = 30,
        [Parameter(Mandatory = $false)]
        [string]$OutputPath = 'C:\System\WinSrvLog\MSSQL',
        [Parameter(Mandatory = $false)]
        [switch]$ContinueOnError,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )
    
    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $allInstanceResults = [System.Collections.Generic.List[PSCustomObject]]::new()
        
        if (-not (Get-Module -ListAvailable -Name dbatools))
        {
            $errMsg = "dbatools-Modul nicht gefunden."
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
            throw $errMsg
        }
        
        Invoke-sqmLogging -Message "Starte $functionName mit OutputPath: $OutputPath" -FunctionName $functionName -Level "INFO"
    }
    
    process
    {
        foreach ($instance in $SqlInstance)
        {
            $connParams = @{ SqlInstance = $instance }
            if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential }
            
            $detailRows = [System.Collections.Generic.List[PSCustomObject]]::new()
            
            try
            {
                Invoke-sqmLogging -Message "[$instance] Lade Volume-Statistiken ..." -FunctionName $functionName -Level "INFO"
                
                # 1. Volumedaten aus sys.dm_os_volume_stats
                $volumeQuery = @"
SELECT DISTINCT
    vs.volume_mount_point AS MountPoint,
    vs.logical_volume_name AS VolumeName,
    vs.total_bytes / 1073741824.0 AS TotalGB,
    vs.available_bytes / 1073741824.0 AS FreeGB,
    (vs.total_bytes - vs.available_bytes) / 1073741824.0 AS UsedGB,
    CAST(vs.available_bytes * 100.0 / NULLIF(vs.total_bytes,0) AS DECIMAL(5,1)) AS FreePct
FROM sys.master_files mf
CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
ORDER BY vs.volume_mount_point;
"@

                $volRows = Invoke-DbaQuery @connParams -Query $volumeQuery -EnableException:$EnableException
                
                if (-not $volRows)
                {
                    Invoke-sqmLogging -Message "[$instance] Keine Volumedaten gefunden (keine Datenbankdateien?)." -FunctionName $functionName -Level "WARNING"
                    $allInstanceResults.Add([PSCustomObject]@{
                            SqlInstance = $instance
                            Status        = 'Warning'
                            Message        = 'Keine Volumedaten (keine Datenbankdateien oder keine Berechtigung fuer dm_os_volume_stats)'
                            DetailRows  = @()
                            TxtFile        = $null
                            CsvFile        = $null
                        })
                    continue
                }
                
                # 2. AutoGrowth-Wachstum aus Default Trace (optional)
                $growthLookup = @{ }
                try
                {
                    $growthQuery = @"
DECLARE @tracefile NVARCHAR(500);
SELECT @tracefile = REVERSE(SUBSTRING(REVERSE(path),CHARINDEX('\',REVERSE(path)),500)) + N'log.trc'
FROM sys.traces WHERE is_default = 1;
 
IF @tracefile IS NOT NULL
BEGIN
    SELECT
        LEFT(FileName, LEN(FileName) - CHARINDEX('\', REVERSE(FileName))) AS FolderPath,
        SUM(CAST(IntegerData AS BIGINT) * 8 * 1024) AS TotalGrowthBytes
    FROM sys.fn_trace_gettable(@tracefile, DEFAULT)
    WHERE EventClass IN (92,93)
      AND StartTime >= DATEADD(DAY, -$HistoryDays, GETDATE())
    GROUP BY LEFT(FileName, LEN(FileName) - CHARINDEX('\', REVERSE(FileName)));
END
"@

                    $growthRows = Invoke-DbaQuery @connParams -Query $growthQuery -EnableException:$false -ErrorAction SilentlyContinue
                    foreach ($r in $growthRows)
                    {
                        $drive = if ($r.FolderPath -match '^([A-Za-z]:)') { $Matches[1].ToUpper() + '\' }
                        else { $r.FolderPath }
                        $growthLookup[$drive] = [math]::Round($r.TotalGrowthBytes / 1073741824.0, 2)
                    }
                    if ($growthRows.Count -eq 0)
                    {
                        Invoke-sqmLogging -Message "[$instance] Keine AutoGrowth-Daten im Default Trace gefunden (Trace evtl. deaktiviert)." -FunctionName $functionName -Level "VERBOSE"
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "[$instance] Fehler beim Lesen des Default Trace: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
                }
                
                # 3. Detailzeilen aufbereiten
                foreach ($vol in $volRows)
                {
                    $mount = $vol.MountPoint
                    $totalGB = [math]::Round($vol.TotalGB, 1)
                    $freeGB = [math]::Round($vol.FreeGB, 1)
                    $usedGB = [math]::Round($vol.UsedGB, 1)
                    $freePct = [math]::Round($vol.FreePct, 1)
                    
                    $growthGB = $growthLookup[$mount]
                    $growthPerDay = if ($growthGB -and $HistoryDays -gt 0) { [math]::Round($growthGB / $HistoryDays, 3) }
                    else { 0 }
                    $daysUntilFull = if ($growthPerDay -gt 0) { [math]::Round($freeGB / $growthPerDay, 0) }
                    else { $null }
                    
                    $status = if ($freePct -le $CriticalThresholdPct) { 'Critical' }
                    elseif ($freePct -le $WarnThresholdPct) { 'Warning' }
                    elseif ($daysUntilFull -and $daysUntilFull -le 30) { 'Warning' }
                    else { 'OK' }
                    
                    $detailRows.Add([PSCustomObject]@{
                            SqlInstance = $instance
                            MountPoint  = $mount
                            VolumeName  = $vol.VolumeName
                            TotalGB        = $totalGB
                            UsedGB        = $usedGB
                            FreeGB        = $freeGB
                            FreePct        = $freePct
                            GrowthLastPeriodGB = if ($growthGB) { $growthGB } else { $null }
                            GrowthPerDayGB = if ($growthPerDay -gt 0) { $growthPerDay } else { $null }
                            DaysUntilFull = $daysUntilFull
                            HistoryDays = $HistoryDays
                            Status        = $status
                            Message        = switch ($status)
                            {
                                'Critical' { "Kritisch: nur $freePct% frei ($freeGB GB)!" }
                                'Warning'  {
                                    if ($daysUntilFull -le 30 -and $daysUntilFull)
                                    {
                                        "Warnung: voll in ca. $daysUntilFull Tagen."
                                    }
                                    else { "Warnung: nur $freePct% frei ($freeGB GB)." }
                                }
                                default    { "OK: $freePct% frei ($freeGB GB)." }
                            }
                        })
                }
                
                # 4. Berichtsdateien schreiben
                $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                $datestamp = Get-Date -Format 'yyyy-MM-dd'
                $safeInst = $instance -replace '[\\/:*?"<>|]', '_'
                $txtFile = Join-Path $OutputPath "DiskSpaceReport_${safeInst}_${datestamp}.txt"
                $csvFile = Join-Path $OutputPath "DiskSpaceReport_${safeInst}_${datestamp}.csv"
                
                if ($PSCmdlet.ShouldProcess($instance, "Erstelle Disk-Space-Bericht in $OutputPath"))
                {
                    # Verzeichnis anlegen
                    if (-not (Test-Path $OutputPath))
                    {
                        New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop | Out-Null
                        Invoke-sqmLogging -Message "Verzeichnis $OutputPath wurde erstellt." -FunctionName $functionName -Level "INFO"
                    }
                    
                    # TXT-Bericht
                    $cntCrit = ($detailRows | Where-Object Status -eq 'Critical').Count
                    $cntWarn = ($detailRows | Where-Object Status -eq 'Warning').Count
                    $lines = [System.Collections.Generic.List[string]]::new()
                    $lines.Add("# ================================================================")
                    $lines.Add("# MSSQLTools - Disk Space Report")
                    $lines.Add("# Instanz : $instance")
                    $lines.Add("# Erstellt : $timestamp")
                    $lines.Add("# Warn: <${WarnThresholdPct}% | Critical: <${CriticalThresholdPct}% | Wachstum: letzte $HistoryDays Tage")
                    $lines.Add("# Critical: $cntCrit | Warning: $cntWarn")
                    $lines.Add("# ================================================================")
                    $lines.Add("")
                    $lines.Add(("{0,-6} {1,-20} {2,-8} {3,-8} {4,-8} {5,-7} {6,-10} {7,-8} {8}" -f
                            'Status', 'Laufwerk', 'TotalGB', 'UsedGB', 'FreeGB', 'Free%', 'GrowthGB', 'DaysFull', 'Info'))
                    $lines.Add(("-" * 105))
                    foreach ($e in ($detailRows | Sort-Object Status, MountPoint))
                    {
                        $growthDisplay = if ($e.GrowthLastPeriodGB) { $e.GrowthLastPeriodGB }
                        else { 'n/a' }
                        $daysDisplay = if ($e.DaysUntilFull) { $e.DaysUntilFull }
                        else { '?' }
                        $lines.Add(("{0,-6} {1,-20} {2,-8} {3,-8} {4,-8} {5,-7} {6,-10} {7,-8} {8}" -f
                                $e.Status, $e.MountPoint, $e.TotalGB, $e.UsedGB, $e.FreeGB,
                                "$($e.FreePct)%", $growthDisplay, $daysDisplay, $e.Message))
                    }
                    $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force
                    
                    # CSV-Datei
                    $detailRows | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force
                    
                    Invoke-sqmLogging -Message "[$instance] Disk-Space-Bericht erstellt: $txtFile" -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    Invoke-sqmLogging -Message "[$instance] WhatIf: Berichtsdateien wuerden erstellt werden." -FunctionName $functionName -Level "VERBOSE"
                    $txtFile = $null
                    $csvFile = $null
                }
                
                # Ergebnisobjekt fuer diese Instanz
                $result = [PSCustomObject]@{
                    SqlInstance                         = $instance
                    Timestamp                         = $timestamp
                    DetailRows                         = $detailRows
                    TxtFile                             = $txtFile
                    CsvFile                             = $csvFile
                    Status                             = if ($cntCrit -gt 0) { 'Critical' } elseif ($cntWarn -gt 0) { 'Warning' } else { 'OK' }
                }
                $allInstanceResults.Add($result)
                
                if ($cntCrit -gt 0)
                {
                    Invoke-sqmLogging -Message "[$instance] $cntCrit Critical Disk-Space-Issue(s) - Bericht: $txtFile" -FunctionName $functionName -Level "WARNING"
                }
            }
            catch
            {
                $errMsg = "Fehler auf '$instance': $($_.Exception.Message)"
                Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                $allInstanceResults.Add([PSCustomObject]@{
                        SqlInstance = $instance
                        Status        = 'Error'
                        Message        = $errMsg
                        DetailRows  = $null
                        TxtFile        = $null
                        CsvFile        = $null
                    })
                if ($EnableException) { throw }
                if (-not $ContinueOnError) { throw $_ }
            }
        }
    }
    
    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allInstanceResults.Count) Instanzen verarbeitet." -FunctionName $functionName -Level "INFO"
        return $allInstanceResults
    }
}