bin/Public/Invoke-sqmExtendedEvents.ps1
|
<# .SYNOPSIS Verwaltet Extended Events Sessions fuer die Performance-Analyse auf SQL Server. .DESCRIPTION Erstellt, startet, stoppt, liest und wertet Extended Events Sessions aus. Betriebsarten (Switches, kombinierbar): -Create Legt eine neue XEvent-Session anhand eines Templates an. -Start Startet eine vorhandene (oder neu erstellte) Session. -Stop Stoppt eine laufende Session. -Read Liest Ereignisse aus dem XEL-Ringpuffer oder einer Datei aus. -Diagnose Aggregiert Ereignisse und erkennt Muster (Top Waits, Blocking-Ketten, Slow Queries, Deadlocks). -Drop Entfernt eine Session vollstaendig (inkl. XEL-Dateien). Wenn kein Switch angegeben wird, werden -Read und -Diagnose ausgefuehrt. Verfuegbare Session-Templates: SlowQueries sql_statement_completed > Schwellwert (Standard: 1000 ms) Blocking blocked_process_report Waits wait_info mit konfigurierbarer Warteliste Deadlocks xml_deadlock_report AllInOne Kombiniert alle vier Templates in einer Session .PARAMETER SqlInstance SQL Server-Instanz. Standard: aktueller Computername. .PARAMETER SqlCredential PSCredential fuer die Verbindung. .PARAMETER SessionName Name der XEvent-Session. Standard: 'sqmPerformance'. .PARAMETER Template Session-Template beim Erstellen. Werte: SlowQueries, Blocking, Waits, Deadlocks, AllInOne. Standard: AllInOne. .PARAMETER SlowQueryThresholdMs Minimale Ausfuehrungsdauer in Millisekunden fuer SlowQueries-Capture. Standard: 1000. .PARAMETER WaitTypes Komma-getrennte Liste von Wait Types fuer das Waits-Template. Standard: LCK_M_X,LCK_M_S,LCK_M_U,PAGEIOLATCH_SH,PAGEIOLATCH_EX,CXPACKET,SOS_SCHEDULER_YIELD .PARAMETER TargetType Zieltyp fuer die Ereignisspeicherung: RingBuffer oder File. Standard: RingBuffer. .PARAMETER TargetFilePath Verzeichnis fuer XEL-Dateien (nur bei TargetType = File). Standard: aus Modulkonfiguration OutputPath + \XEvents. .PARAMETER MaxFileSizeMB Maximale Groesse einer XEL-Datei (MB). Standard: 100. .PARAMETER MaxRolloverFiles Anzahl der XEL-Rollover-Dateien. Standard: 5. .PARAMETER RingBufferMaxMB Maximale Groesse des RingBuffers (MB). Standard: 50. .PARAMETER MaxEventsRead Maximale Anzahl Ereignisse beim Lesen. Standard: 10000. .PARAMETER LookbackMinutes Zeitfenster fuer Diagnose-Aggregation in Minuten. Standard: 60. .PARAMETER TopN Anzahl der Top-Eintraege in Diagnose-Tabellen. Standard: 25. .PARAMETER OutputPath Verzeichnis fuer gespeicherte Berichte. Standard: aus Modulkonfiguration + \XEvents. .PARAMETER Create Session anlegen. .PARAMETER Start Session starten. .PARAMETER Stop Session stoppen. .PARAMETER Read Ereignisse lesen. .PARAMETER Diagnose Ereignisse aggregieren und Probleme erkennen. .PARAMETER Drop Session entfernen. .PARAMETER EnableException Ausnahmen sofort ausloesen. .EXAMPLE # AllInOne-Session erstellen und sofort starten Invoke-sqmExtendedEvents -SqlInstance SQL01 -Create -Start .EXAMPLE # Slow Queries > 2 Sekunden aufzeichnen, in Datei speichern Invoke-sqmExtendedEvents -SqlInstance SQL01 -Template SlowQueries -SlowQueryThresholdMs 2000 -TargetType File -Create -Start .EXAMPLE # Laufende Session auslesen und Bericht erstellen Invoke-sqmExtendedEvents -SqlInstance SQL01 -Read -Diagnose .EXAMPLE # Session stoppen und entfernen Invoke-sqmExtendedEvents -SqlInstance SQL01 -Stop -Drop .NOTES Erfordert: dbatools, Invoke-sqmLogging, Get-sqmDefaultOutputPath Benoetigt VIEW SERVER STATE auf der Instanz. XEL-Datei-Lesen erfordert direkten Zugriff auf den SQL Server-Pfad. #> function Invoke-sqmExtendedEvents { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [string]$SqlInstance, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $false)] [string]$SessionName = 'sqmPerformance', [Parameter(Mandatory = $false)] [ValidateSet('SlowQueries', 'Blocking', 'Waits', 'Deadlocks', 'AllInOne')] [string]$Template = 'AllInOne', [Parameter(Mandatory = $false)] [ValidateRange(100, 3600000)] [int]$SlowQueryThresholdMs = 1000, [Parameter(Mandatory = $false)] [string]$WaitTypes = 'LCK_M_X,LCK_M_S,LCK_M_U,PAGEIOLATCH_SH,PAGEIOLATCH_EX,CXPACKET,SOS_SCHEDULER_YIELD', [Parameter(Mandatory = $false)] [ValidateSet('RingBuffer', 'File')] [string]$TargetType = 'RingBuffer', [Parameter(Mandatory = $false)] [string]$TargetFilePath, [Parameter(Mandatory = $false)] [ValidateRange(10, 2048)] [int]$MaxFileSizeMB = 100, [Parameter(Mandatory = $false)] [ValidateRange(1, 100)] [int]$MaxRolloverFiles = 5, [Parameter(Mandatory = $false)] [ValidateRange(1, 500)] [int]$RingBufferMaxMB = 50, [Parameter(Mandatory = $false)] [ValidateRange(100, 1000000)] [int]$MaxEventsRead = 10000, [Parameter(Mandatory = $false)] [ValidateRange(1, 525600)] [int]$LookbackMinutes = 60, [Parameter(Mandatory = $false)] [ValidateRange(1, 500)] [int]$TopN = 25, [Parameter(Mandatory = $false)] [string]$OutputPath, # --- Aktions-Switches --- [Parameter(Mandatory = $false)] [switch]$Create, [Parameter(Mandatory = $false)] [switch]$Start, [Parameter(Mandatory = $false)] [switch]$Stop, [Parameter(Mandatory = $false)] [switch]$Read, [Parameter(Mandatory = $false)] [switch]$Diagnose, [Parameter(Mandatory = $false)] [switch]$Drop, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance)) { $SqlInstance = $env:COMPUTERNAME } if (-not $script:dbatoolsAvailable) { $errMsg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } # Wenn kein Aktions-Switch gesetzt: Read + Diagnose $noActionSwitch = -not ($Create -or $Start -or $Stop -or $Read -or $Diagnose -or $Drop) $runRead = $Read -or $noActionSwitch $runDiagnose = $Diagnose -or $noActionSwitch # OutputPath / TargetFilePath aufloesen if (-not $OutputPath) { $OutputPath = Join-Path (Get-sqmDefaultOutputPath) 'XEvents' } if (-not $TargetFilePath){ $TargetFilePath = Join-Path (Get-sqmDefaultOutputPath) 'XEvents' } $connParams = @{ SqlInstance = $SqlInstance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } $result = [PSCustomObject]@{ SqlInstance = $SqlInstance SessionName = $SessionName Template = $Template SessionStatus = $null CreateStatus = $null StartStatus = $null StopStatus = $null DropStatus = $null Events = @() EventCount = 0 Diagnose = $null ReportFile = $null } Invoke-sqmLogging -Message "Starte $functionName auf $SqlInstance - Session '$SessionName' (Template=$Template, TargetType=$TargetType)" -FunctionName $functionName -Level "INFO" } process { try { # ================================================================= # Hilfsfunktion: Wait-Type-Filter-SQL bauen # ================================================================= $waitFilter = ($WaitTypes -split ',' | ForEach-Object { "N'$($_.Trim())'" }) -join ',' # ================================================================= # CREATE # ================================================================= if ($Create -and $PSCmdlet.ShouldProcess($SqlInstance, "XEvent-Session '$SessionName' erstellen (Template: $Template)")) { Invoke-sqmLogging -Message "Erstelle XEvent-Session '$SessionName' (Template=$Template, Target=$TargetType)..." -FunctionName $functionName -Level "INFO" # --- Event-Definitionen je Template --- $eventBlock = switch ($Template) { 'SlowQueries' { $thresholdMicro = $SlowQueryThresholdMs * 1000 @" ADD EVENT sqlserver.sql_statement_completed ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.username, sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.request_id, sqlserver.session_id, sqlserver.plan_handle) WHERE (duration >= $thresholdMicro AND sqlserver.is_system = 0) ), ADD EVENT sqlserver.rpc_completed ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.username, sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.session_id) WHERE (duration >= $thresholdMicro AND sqlserver.is_system = 0) ) "@ } 'Blocking' { @" ADD EVENT sqlserver.blocked_process_report ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.session_id, sqlserver.username) ) "@ } 'Waits' { @" ADD EVENT sqlos.wait_info ( ACTION (sqlserver.sql_text, sqlserver.session_id, sqlserver.database_name, sqlserver.username) WHERE (wait_type IN ($waitFilter) AND duration > 0 AND opcode = 1) ) "@ } 'Deadlocks' { @" ADD EVENT sqlserver.xml_deadlock_report ( ACTION (sqlserver.database_name) ) "@ } 'AllInOne' { $thresholdMicro = $SlowQueryThresholdMs * 1000 @" ADD EVENT sqlserver.sql_statement_completed ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.username, sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.session_id, sqlserver.plan_handle) WHERE (duration >= $thresholdMicro AND sqlserver.is_system = 0) ), ADD EVENT sqlserver.rpc_completed ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.username, sqlserver.client_app_name, sqlserver.session_id) WHERE (duration >= $thresholdMicro AND sqlserver.is_system = 0) ), ADD EVENT sqlserver.blocked_process_report ( ACTION (sqlserver.sql_text, sqlserver.database_name, sqlserver.session_id, sqlserver.username) ), ADD EVENT sqlos.wait_info ( ACTION (sqlserver.sql_text, sqlserver.session_id, sqlserver.database_name) WHERE (wait_type IN ($waitFilter) AND duration > 0 AND opcode = 1) ), ADD EVENT sqlserver.xml_deadlock_report ( ACTION (sqlserver.database_name) ) "@ } } # --- Target-Definition --- $targetBlock = if ($TargetType -eq 'File') { $xelPattern = "$($TargetFilePath.TrimEnd('\'))\$SessionName" $maxBytes = $MaxFileSizeMB * 1024 * 1024 @" ADD TARGET package0.event_file ( SET filename = N'$xelPattern.xel', max_file_size = $MaxFileSizeMB, max_rollover_files = $MaxRolloverFiles ) "@ } else { $maxBytes = $RingBufferMaxMB * 1024 * 1024 @" ADD TARGET package0.ring_buffer ( SET max_memory = $maxBytes ) "@ } $createSql = @" IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = N'$SessionName') DROP EVENT SESSION [$SessionName] ON SERVER; CREATE EVENT SESSION [$SessionName] ON SERVER $eventBlock $targetBlock WITH ( MAX_MEMORY = 4096 KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY = 5 SECONDS, STARTUP_STATE = OFF ); "@ try { Invoke-DbaQuery @connParams -Database master -Query $createSql -ErrorAction Stop $result.CreateStatus = 'Success' Invoke-sqmLogging -Message "Session '$SessionName' erfolgreich erstellt." -FunctionName $functionName -Level "INFO" } catch { $result.CreateStatus = "Failed: $($_.Exception.Message)" Invoke-sqmLogging -Message "Session '$SessionName' erstellen fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } } } # ================================================================= # START # ================================================================= if ($Start -and $PSCmdlet.ShouldProcess($SqlInstance, "XEvent-Session '$SessionName' starten")) { try { $startSql = "ALTER EVENT SESSION [$SessionName] ON SERVER STATE = START;" Invoke-DbaQuery @connParams -Database master -Query $startSql -ErrorAction Stop $result.StartStatus = 'Running' Invoke-sqmLogging -Message "Session '$SessionName' gestartet." -FunctionName $functionName -Level "INFO" } catch { $result.StartStatus = "Failed: $($_.Exception.Message)" Invoke-sqmLogging -Message "Session '$SessionName' starten fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } } } # ================================================================= # STOP # ================================================================= if ($Stop -and $PSCmdlet.ShouldProcess($SqlInstance, "XEvent-Session '$SessionName' stoppen")) { try { $stopSql = "ALTER EVENT SESSION [$SessionName] ON SERVER STATE = STOP;" Invoke-DbaQuery @connParams -Database master -Query $stopSql -ErrorAction Stop $result.StopStatus = 'Stopped' Invoke-sqmLogging -Message "Session '$SessionName' gestoppt." -FunctionName $functionName -Level "INFO" } catch { $result.StopStatus = "Failed: $($_.Exception.Message)" Invoke-sqmLogging -Message "Session '$SessionName' stoppen fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } } } # ================================================================= # SESSION-STATUS abfragen (immer) # ================================================================= try { $statusSql = @" SELECT s.name AS SessionName, s.create_time AS CreateTime, CASE ds.create_time WHEN NULL THEN 'Stopped' ELSE 'Running' END AS SessionState, ds.create_time AS StartedAt, t.target_name AS TargetType, CAST(t.target_data AS XML) AS TargetDataXml, (SELECT SUM(CAST(f.column_value AS BIGINT)) FROM sys.dm_xe_session_object_columns f WHERE f.event_session_address = ds.address AND f.column_name = 'event_count') AS ApproxEventCount FROM sys.server_event_sessions s LEFT JOIN sys.dm_xe_sessions ds ON s.name = ds.name LEFT JOIN sys.dm_xe_session_targets t ON ds.address = t.event_session_address WHERE s.name = N'$SessionName' "@ $statusRows = Invoke-DbaQuery @connParams -Database master -Query $statusSql -ErrorAction Stop if ($statusRows) { $result.SessionStatus = $statusRows | Select-Object -First 1 SessionName, SessionState, StartedAt, TargetType, ApproxEventCount } else { $result.SessionStatus = [PSCustomObject]@{ SessionName = $SessionName; SessionState = 'NotFound' } } } catch { Invoke-sqmLogging -Message "Session-Status konnte nicht abgefragt werden: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING" } # ================================================================= # READ - Ereignisse aus RingBuffer oder XEL-Datei lesen # ================================================================= if ($runRead) { Invoke-sqmLogging -Message "Lese Ereignisse aus Session '$SessionName' (max $MaxEventsRead)..." -FunctionName $functionName -Level "INFO" $sessionState = if ($result.SessionStatus) { $result.SessionStatus.SessionState } else { $null } if ($sessionState -notin @('Running', 'Stopped') -and $sessionState -ne $null) { Invoke-sqmLogging -Message "Session '$SessionName' nicht gefunden - kein Read moeglich." -FunctionName $functionName -Level "WARNING" } else { try { # Ziel-Typ der laufenden Session ermitteln $targetTypeSql = @" SELECT t.target_name, t.target_data FROM sys.dm_xe_sessions s JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address WHERE s.name = N'$SessionName' "@ $targetInfo = Invoke-DbaQuery @connParams -Database master -Query $targetTypeSql -ErrorAction SilentlyContinue $parsedEvents = [System.Collections.Generic.List[PSCustomObject]]::new() if ($targetInfo -and $targetInfo.target_name -eq 'ring_buffer') { # RingBuffer-XML parsen $rbReadSql = @" SELECT CAST(t.target_data AS XML) AS RingBufferXml FROM sys.dm_xe_sessions s JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address WHERE s.name = N'$SessionName' AND t.target_name = 'ring_buffer' "@ $rbRow = Invoke-DbaQuery @connParams -Database master -Query $rbReadSql -ErrorAction Stop if ($rbRow -and $rbRow.RingBufferXml) { [xml]$rbXml = $rbRow.RingBufferXml $eventNodes = $rbXml.RingBufferTarget.event | Select-Object -First $MaxEventsRead foreach ($evNode in $eventNodes) { $ev = [PSCustomObject]@{ EventName = $evNode.name Timestamp = $evNode.timestamp DatabaseName = ($evNode.action | Where-Object { $_.name -eq 'database_name' }).value Username = ($evNode.action | Where-Object { $_.name -eq 'username' }).value SessionId = ($evNode.action | Where-Object { $_.name -eq 'session_id' }).value ClientApp = ($evNode.action | Where-Object { $_.name -eq 'client_app_name' }).value ClientHost = ($evNode.action | Where-Object { $_.name -eq 'client_hostname' }).value DurationMs = [math]::Round( [double](($evNode.data | Where-Object { $_.name -eq 'duration' }).value) / 1000.0, 2) WaitType = ($evNode.data | Where-Object { $_.name -eq 'wait_type' }).text SqlText = ($evNode.action | Where-Object { $_.name -eq 'sql_text' }).value DeadlockXml = ($evNode.data | Where-Object { $_.name -eq 'xml_report' }).value RowsAffected = ($evNode.data | Where-Object { $_.name -eq 'row_count' }).value PhysicalReads = ($evNode.data | Where-Object { $_.name -eq 'physical_reads' }).value LogicalReads = ($evNode.data | Where-Object { $_.name -eq 'logical_reads' }).value Writes = ($evNode.data | Where-Object { $_.name -eq 'writes' }).value CpuTime = ($evNode.data | Where-Object { $_.name -eq 'cpu_time' }).value } $parsedEvents.Add($ev) } } } elseif ($targetInfo -and $targetInfo.target_name -eq 'event_file') { # XEL-Datei-Pfad aus sys.server_event_sessions lesen $xelPathSql = @" SELECT f.value AS XelPath FROM sys.server_event_sessions s JOIN sys.server_event_session_targets t ON s.event_session_id = t.event_session_id CROSS APPLY ( SELECT c.value FROM sys.server_event_session_fields c WHERE c.event_session_id = s.event_session_id AND c.object_id = t.target_id AND c.name = 'filename' ) f WHERE s.name = N'$SessionName' "@ $xelPathRow = Invoke-DbaQuery @connParams -Database master -Query $xelPathSql -ErrorAction SilentlyContinue if ($xelPathRow -and $xelPathRow.XelPath) { $xelPattern = $xelPathRow.XelPath -replace '\.xel$', '*.xel' $readXelSql = @" SELECT TOP ($MaxEventsRead) object_name AS EventName, CAST(event_data AS XML) AS EventXml, file_name AS SourceFile, file_offset AS FileOffset FROM sys.fn_xe_file_target_read_file(N'$xelPattern', NULL, NULL, NULL) ORDER BY file_offset DESC "@ $xelRows = Invoke-DbaQuery @connParams -Database master -Query $readXelSql -ErrorAction Stop foreach ($xelRow in $xelRows) { try { [xml]$evXml = $xelRow.EventXml $evData = $evXml.event $getVal = { param($name) $node = $evData.data | Where-Object { $_.name -eq $name } if ($node) { $node.value } else { $null } } $getAct = { param($name) $node = $evData.action | Where-Object { $_.name -eq $name } if ($node) { $node.value } else { $null } } $dur = & $getVal 'duration' $parsedEvents.Add([PSCustomObject]@{ EventName = $xelRow.EventName Timestamp = $evData.timestamp DatabaseName = & $getAct 'database_name' Username = & $getAct 'username' SessionId = & $getAct 'session_id' ClientApp = & $getAct 'client_app_name' ClientHost = & $getAct 'client_hostname' DurationMs = if ($dur) { [math]::Round([double]$dur / 1000.0, 2) } else { $null } WaitType = ($evData.data | Where-Object { $_.name -eq 'wait_type' }).text SqlText = & $getAct 'sql_text' DeadlockXml = & $getVal 'xml_report' RowsAffected = & $getVal 'row_count' PhysicalReads = & $getVal 'physical_reads' LogicalReads = & $getVal 'logical_reads' Writes = & $getVal 'writes' CpuTime = & $getVal 'cpu_time' SourceFile = $xelRow.SourceFile }) } catch { } } } } $result.Events = $parsedEvents.ToArray() $result.EventCount = $parsedEvents.Count Invoke-sqmLogging -Message "$($parsedEvents.Count) Ereignisse gelesen." -FunctionName $functionName -Level "INFO" } catch { Invoke-sqmLogging -Message "Fehler beim Lesen der Ereignisse: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } } } } # ================================================================= # DIAGNOSE - Ereignisse aggregieren und Muster erkennen # ================================================================= if ($runDiagnose -and $result.Events.Count -gt 0) { Invoke-sqmLogging -Message "Starte Diagnose ($($result.Events.Count) Ereignisse, Fenster: ${LookbackMinutes} Min)..." -FunctionName $functionName -Level "INFO" $cutoff = (Get-Date).AddMinutes(-$LookbackMinutes) $window = $result.Events | Where-Object { $ts = $null if ($_.Timestamp -and [datetime]::TryParse($_.Timestamp, [ref]$ts)) { $ts -ge $cutoff } else { $true } } $diagnoseResult = [PSCustomObject]@{ TopSlowQueries = @() TopWaits = @() BlockingEvents = @() DeadlockEvents = @() IssueCount = 0 Issues = @() } $issues = [System.Collections.Generic.List[PSCustomObject]]::new() # D1: Top Slow Queries $slowEvts = $window | Where-Object { $_.EventName -in @('sql_statement_completed','rpc_completed') -and $_.DurationMs -gt 0 } if ($slowEvts) { $topSlow = $slowEvts | Group-Object SqlText | ForEach-Object { $durs = $_.Group | ForEach-Object { [double]$_.DurationMs } [PSCustomObject]@{ SqlText = ($_.Name -replace '\s+', ' ').Trim() | ForEach-Object { $_.Substring(0, [Math]::Min(300, $_.Length)) } ExecCount = $_.Count AvgDurationMs = [math]::Round(($durs | Measure-Object -Average).Average, 2) MaxDurationMs = [math]::Round(($durs | Measure-Object -Maximum).Maximum, 2) TotalDurationMs = [math]::Round(($durs | Measure-Object -Sum).Sum, 2) DatabaseName = ($_.Group | Select-Object -First 1).DatabaseName } } | Sort-Object AvgDurationMs -Descending | Select-Object -First $TopN $diagnoseResult.TopSlowQueries = @($topSlow) $verySlowCount = @($slowEvts | Where-Object { $_.DurationMs -ge ($SlowQueryThresholdMs * 10) }).Count if ($verySlowCount -gt 0) { $issues.Add([PSCustomObject]@{ Severity = 'Warning' Category = 'VerySlowQueries' Description = "$verySlowCount Queries mit Dauer >= $($SlowQueryThresholdMs * 10) ms im Auswertungsfenster" Detail = "Top: $($topSlow[0].SqlText.Substring(0, [Math]::Min(200, $topSlow[0].SqlText.Length))) [Avg: $($topSlow[0].AvgDurationMs) ms]" }) } } # D2: Top Wait Types $waitEvts = $window | Where-Object { $_.EventName -eq 'wait_info' -and $_.WaitType } if ($waitEvts) { $topWaits = $waitEvts | Group-Object WaitType | ForEach-Object { $durs = $_.Group | ForEach-Object { [double]$_.DurationMs } [PSCustomObject]@{ WaitType = $_.Name WaitCount = $_.Count TotalWaitMs = [math]::Round(($durs | Measure-Object -Sum).Sum, 2) AvgWaitMs = [math]::Round(($durs | Measure-Object -Average).Average, 2) MaxWaitMs = [math]::Round(($durs | Measure-Object -Maximum).Maximum, 2) } } | Sort-Object TotalWaitMs -Descending | Select-Object -First $TopN $diagnoseResult.TopWaits = @($topWaits) # Lock-dominanz erkennen $lockWaits = $topWaits | Where-Object { $_.WaitType -like 'LCK_*' } if ($lockWaits) { $lockTotal = ($lockWaits | Measure-Object TotalWaitMs -Sum).Sum if ($lockTotal -gt 5000) { $issues.Add([PSCustomObject]@{ Severity = if ($lockTotal -gt 60000) { 'Critical' } else { 'Warning' } Category = 'LockContention' Description = "Lock-Waits: $([math]::Round($lockTotal/1000,1)) Sek gesamt im Auswertungsfenster" Detail = ($lockWaits | ForEach-Object { "$($_.WaitType): $($_.WaitCount)x / $($_.TotalWaitMs) ms" }) -join '; ' }) } } # CXPACKET / Parallelismus $cxp = $topWaits | Where-Object { $_.WaitType -eq 'CXPACKET' } if ($cxp -and $cxp.WaitCount -gt 100) { $issues.Add([PSCustomObject]@{ Severity = 'Warning' Category = 'ParallelismPressure' Description = "CXPACKET: $($cxp.WaitCount) Waits / $([math]::Round($cxp.TotalWaitMs/1000,1)) Sek gesamt - moeglicher uebermassiger Parallelismus" Detail = "MAXDOP oder Cost Threshold for Parallelism pruefen." }) } } # D3: Blocking-Ereignisse $blockEvts = $window | Where-Object { $_.EventName -eq 'blocked_process_report' } if ($blockEvts) { $diagnoseResult.BlockingEvents = @($blockEvts | Select-Object Timestamp, DatabaseName, DurationMs, SqlText, Username | Sort-Object DurationMs -Descending | Select-Object -First $TopN) $issues.Add([PSCustomObject]@{ Severity = if ($blockEvts.Count -gt 10) { 'Critical' } else { 'Warning' } Category = 'Blocking' Description = "$($blockEvts.Count) Blocking-Ereignis(se) erfasst" Detail = "Maximale Blockierungsdauer: $([math]::Round(($blockEvts | Measure-Object DurationMs -Maximum).Maximum, 2)) ms" }) } # D4: Deadlocks $deadlockEvts = $window | Where-Object { $_.EventName -eq 'xml_deadlock_report' } if ($deadlockEvts) { $diagnoseResult.DeadlockEvents = @($deadlockEvts | Select-Object Timestamp, DatabaseName, DeadlockXml) $issues.Add([PSCustomObject]@{ Severity = 'Critical' Category = 'Deadlock' Description = "$($deadlockEvts.Count) Deadlock(s) erfasst" Detail = "Timestamps: $(($deadlockEvts | ForEach-Object { $_.Timestamp }) -join ', ')" }) } $diagnoseResult.Issues = $issues.ToArray() $diagnoseResult.IssueCount = $issues.Count $result.Diagnose = $diagnoseResult $critCount = @($issues | Where-Object { $_.Severity -eq 'Critical' }).Count $warnCount = @($issues | Where-Object { $_.Severity -eq 'Warning' }).Count Invoke-sqmLogging -Message "Diagnose abgeschlossen: $critCount Critical, $warnCount Warning." -FunctionName $functionName -Level "INFO" } # ================================================================= # BERICHT SPEICHERN # ================================================================= if ($result.EventCount -gt 0 -or ($result.Diagnose -and $result.Diagnose.IssueCount -gt 0)) { if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } $safeInst = $SqlInstance -replace '[\\/:<>|]', '_' $safeSess = $SessionName -replace '[\\/:<>|]', '_' $timestamp = Get-Date -Format 'yyyyMMdd_HHmsqm' $baseFile = Join-Path $OutputPath "XE_${safeInst}_${safeSess}_${timestamp}" # Rohdaten (Ereignisse) als CSV if ($result.EventCount -gt 0) { $csvFile = "${baseFile}_Events.csv" $result.Events | Select-Object EventName, Timestamp, DatabaseName, Username, SessionId, ClientApp, ClientHost, DurationMs, WaitType, LogicalReads, PhysicalReads, Writes, CpuTime, RowsAffected, @{ N = 'SqlText'; E = { if ($_.SqlText) { $_.SqlText.ToString().Substring(0, [Math]::Min(500, $_.SqlText.ToString().Length)) } } } | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8 -Force Invoke-sqmLogging -Message "Ereignisse gespeichert: $csvFile" -FunctionName $functionName -Level "INFO" } # Diagnose-Bericht als TXT if ($result.Diagnose) { $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("=" * 70) $lines.Add(" EXTENDED EVENTS DIAGNOSE - $SqlInstance | Session: $SessionName") $lines.Add(" Erstellt: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") $lines.Add(" Erfasste Ereignisse: $($result.EventCount) | Auswertungsfenster: ${LookbackMinutes} Min") $lines.Add("=" * 70) $lines.Add("") if ($result.Diagnose.Issues.Count -gt 0) { $lines.Add("ERKANNTE PROBLEME ($($result.Diagnose.IssueCount)):") $lines.Add("-" * 50) $grouped = $result.Diagnose.Issues | Group-Object Severity | Sort-Object { if ($_.Name -eq 'Critical') { 0 } else { 1 } } foreach ($grp in $grouped) { foreach ($issue in $grp.Group) { $lines.Add(" [$($grp.Name.ToUpper())] $($issue.Category): $($issue.Description)") if ($issue.Detail) { $lines.Add(" $($issue.Detail.Substring(0, [Math]::Min(200, $issue.Detail.Length)))") } } } $lines.Add("") } if ($result.Diagnose.TopSlowQueries.Count -gt 0) { $lines.Add("TOP $TopN SLOW QUERIES (nach Avg-Dauer):") $lines.Add("-" * 50) $rank = 1 foreach ($sq in $result.Diagnose.TopSlowQueries) { $txt = if ($sq.SqlText) { $sq.SqlText.Substring(0, [Math]::Min(120, $sq.SqlText.Length)) } else { '(n/a)' } $lines.Add(" $rank. Avg: $($sq.AvgDurationMs) ms | Max: $($sq.MaxDurationMs) ms | Count: $($sq.ExecCount) | DB: $($sq.DatabaseName)") $lines.Add(" $txt") $rank++ } $lines.Add("") } if ($result.Diagnose.TopWaits.Count -gt 0) { $lines.Add("TOP $TopN WAIT TYPES (nach Total-Wartezeit):") $lines.Add("-" * 50) foreach ($wt in $result.Diagnose.TopWaits) { $lines.Add(" $($wt.WaitType.PadRight(30)) Count: $($wt.WaitCount.ToString().PadLeft(6)) Total: $([math]::Round($wt.TotalWaitMs/1000,2)) Sek Avg: $($wt.AvgWaitMs) ms Max: $($wt.MaxWaitMs) ms") } $lines.Add("") } if ($result.Diagnose.BlockingEvents.Count -gt 0) { $lines.Add("BLOCKING-EREIGNISSE ($($result.Diagnose.BlockingEvents.Count)):") $lines.Add("-" * 50) foreach ($bl in $result.Diagnose.BlockingEvents | Select-Object -First 10) { $lines.Add(" [$($bl.Timestamp)] $($bl.DatabaseName) | $($bl.DurationMs) ms | $($bl.Username)") } $lines.Add("") } if ($result.Diagnose.DeadlockEvents.Count -gt 0) { $lines.Add("DEADLOCK-EREIGNISSE ($($result.Diagnose.DeadlockEvents.Count)):") $lines.Add("-" * 50) foreach ($dl in $result.Diagnose.DeadlockEvents) { $lines.Add(" [$($dl.Timestamp)] $($dl.DatabaseName)") if ($dl.DeadlockXml) { $dlFile = "${baseFile}_Deadlock_$($dl.Timestamp -replace '[:\s]', '_').xml" $dl.DeadlockXml | Out-File -FilePath $dlFile -Encoding UTF8 -Force $lines.Add(" Deadlock-XML gespeichert: $dlFile") } } $lines.Add("") } $txtFile = "${baseFile}_Report.txt" $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force $result.ReportFile = $txtFile Invoke-sqmLogging -Message "Diagnose-Bericht gespeichert: $txtFile" -FunctionName $functionName -Level "INFO" # Top-Slow-Queries als eigene CSV if ($result.Diagnose.TopSlowQueries.Count -gt 0) { $slowCsv = "${baseFile}_TopSlowQueries.csv" $result.Diagnose.TopSlowQueries | Export-Csv -Path $slowCsv -NoTypeInformation -Encoding UTF8 -Force } # Top-Waits als eigene CSV if ($result.Diagnose.TopWaits.Count -gt 0) { $waitCsv = "${baseFile}_TopWaits.csv" $result.Diagnose.TopWaits | Export-Csv -Path $waitCsv -NoTypeInformation -Encoding UTF8 -Force } } } # ================================================================= # DROP # ================================================================= if ($Drop -and $PSCmdlet.ShouldProcess($SqlInstance, "XEvent-Session '$SessionName' entfernen")) { try { $dropSql = @" IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = N'$SessionName') BEGIN ALTER EVENT SESSION [$SessionName] ON SERVER STATE = STOP; DROP EVENT SESSION [$SessionName] ON SERVER; END "@ Invoke-DbaQuery @connParams -Database master -Query $dropSql -ErrorAction Stop $result.DropStatus = 'Dropped' Invoke-sqmLogging -Message "Session '$SessionName' entfernt." -FunctionName $functionName -Level "INFO" } catch { $result.DropStatus = "Failed: $($_.Exception.Message)" Invoke-sqmLogging -Message "Session '$SessionName' entfernen fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } } } } catch { $errMsg = "Schwerwiegender Fehler in $functionName : $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } Write-Error $errMsg } return $result } end { Invoke-sqmLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level "INFO" } } |