<# .SYNOPSIS Collects Windows Update logs from a device into one zip file. .DESCRIPTION On Windows 10/11 and Server 2016+, Windows Update writes ETW traces instead of a text log. This script runs Get-WindowsUpdateLog to turn them into a readable WindowsUpdate.log, adds the ConfigMgr client's update logs if the client is installed, optionally adds CBS.log and the raw .etl files, and zips the lot as COMPUTERNAME-WULogs-timestamp.zip. One missing log doesn't stop the rest from being collected. .PARAMETER DestinationPath Folder (local or UNC) where the zip is saved. .PARAMETER SkipConfigMgrLogs Don't collect WUAHandler.log, UpdatesDeployment.log and friends. .PARAMETER IncludeCbsLog Also collect CBS.log, where servicing stack and install failures often show up. .PARAMETER IncludeEtl Also collect the raw .etl trace files, for when someone else wants to decode them. .EXAMPLE .\Export-WindowsUpdateLog.ps1 .EXAMPLE .\Export-WindowsUpdateLog.ps1 -DestinationPath \\sccm01\UpdateLogs$ -IncludeCbsLog #> [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$DestinationPath = (Join-Path $env:SystemRoot 'Temp\UpdateLogs'), [switch]$SkipConfigMgrLogs, [switch]$IncludeCbsLog, [switch]$IncludeEtl ) $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $name = "$env:COMPUTERNAME-WULogs-$stamp" $work = Join-Path ([System.IO.Path]::GetTempPath()) $name $notes = [System.Collections.Generic.List[string]]::new() $null = New-Item -Path $work -ItemType Directory -Force function Copy-LogSet { param([string]$Source, [string[]]$Filter, [string]$Label) if (-not (Test-Path -Path $Source)) { $notes.Add("$Label folder not found"); return } $target = Join-Path $work $Label $null = New-Item -Path $target -ItemType Directory -Force foreach ($pattern in $Filter) { foreach ($file in @(Get-ChildItem -Path $Source -Filter $pattern -File -ErrorAction SilentlyContinue)) { try { Copy-Item -Path $file.FullName -Destination $target -ErrorAction Stop } catch { $notes.Add("Couldn't copy $($file.Name): $($_.Exception.Message)") } } } } # 1. The main event: decode the ETW traces into WindowsUpdate.log. if (Get-Command -Name Get-WindowsUpdateLog -ErrorAction SilentlyContinue) { try { Write-Verbose 'Decoding Windows Update ETW traces. This can take a minute or two.' Get-WindowsUpdateLog -LogPath (Join-Path $work 'WindowsUpdate.log') -ErrorAction Stop | Out-Null } catch { $notes.Add("Get-WindowsUpdateLog failed: $($_.Exception.Message)") } } else { # Pre-Windows 10 machines still write a plain text log. Copy-LogSet -Source $env:SystemRoot -Filter 'WindowsUpdate.log' -Label 'Legacy' } # 2. The ConfigMgr side of the story, if there is one. if (-not $SkipConfigMgrLogs) { $ccmLogs = Join-Path $env:SystemRoot 'CCM\Logs' if (Test-Path -Path $ccmLogs) { Copy-LogSet -Source $ccmLogs -Label 'ConfigMgr' -Filter 'WUAHandler*.log', 'UpdatesDeployment*.log', 'UpdatesHandler*.log', 'UpdatesStore*.log', 'ScanAgent*.log' } else { Write-Verbose 'No ConfigMgr client logs on this device.' } } # 3. Optional extras. if ($IncludeCbsLog) { Copy-LogSet -Source (Join-Path $env:SystemRoot 'Logs\CBS') -Filter 'CBS.log' -Label 'CBS' } if ($IncludeEtl) { Copy-LogSet -Source (Join-Path $env:SystemRoot 'Logs\WindowsUpdate') -Filter '*.etl' -Label 'ETL' } if ($notes.Count) { Set-Content -Path (Join-Path $work 'collection-notes.txt') -Value $notes } # 4. Zip it up and clean up after ourselves. try { if (-not (Test-Path -Path $DestinationPath)) { $null = New-Item -Path $DestinationPath -ItemType Directory -Force -ErrorAction Stop } $zip = Join-Path $DestinationPath "$name.zip" Compress-Archive -Path (Join-Path $work '*') -DestinationPath $zip -ErrorAction Stop $fileCount = @(Get-ChildItem -Path $work -File -Recurse).Count } catch { Write-Error "Couldn't write the archive to ${DestinationPath}: $($_.Exception.Message)" Remove-Item -Path $work -Recurse -Force -ErrorAction SilentlyContinue exit 1 } Remove-Item -Path $work -Recurse -Force -ErrorAction SilentlyContinue foreach ($note in $notes) { Write-Warning $note } [pscustomobject]@{ ComputerName = $env:COMPUTERNAME ZipPath = $zip Files = $fileCount SizeMB = [math]::Round((Get-Item -Path $zip).Length / 1MB, 2) Warnings = $notes.Count }