Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Collecting Windows Update Logs with an SCCM Package

On Windows 10 and 11 there's no Windows Update logging to switch on. The trick is collecting it, and this script does that in one zip per machine.

AT A GLANCEExport-WindowsUpdateLog.ps1
What it does
Decodes the Windows Update ETW traces with Get-WindowsUpdateLog, adds the ConfigMgr client's update logs (and optionally CBS.log and the raw .etl files), and saves everything as one zip named after the computer.
Requires
  • Windows PowerShell 5.1 (Get-WindowsUpdateLog ships with Windows 10/11 and Server 2016+)
  • No extra modules
Permissions
Local administrator or SYSTEM. If you save to a share, the computer account needs write access to it.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run against a fake log folder with Get-WindowsUpdateLog mocked, in PowerShell 7.4

The old version of this post set a Trace\Level registry value to turn on "verbose" Windows Update logging. That was a Windows 7-era trick for the old text-based WindowsUpdate.log. On Windows 10 and 11 it doesn't do anything useful, because Windows Update doesn't write a text log anymore. It writes ETW traces to C:\Windows\Logs\WindowsUpdate, all the time, whether you asked or not.

So the logging is already on. The annoying part is getting it off the machine in a form a human can read. Get-WindowsUpdateLog decodes the traces into a proper WindowsUpdate.log, but it only does that on demand, on the device itself. And if ConfigMgr is doing your patching, half the story is in the client's own logs anyway.

This script grabs both halves, zips them up with the computer name and a timestamp, and drops the zip somewhere you can get at it. Deploy it to the three machines that won't patch, go get a coffee, and come back to a folder of zips.

Export-WindowsUpdateLog.ps1Download
<#
.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
}

Parameters

ParameterTypeDefaultWhat it's for
-DestinationPathstring$env:SystemRoot\Temp\UpdateLogsFolder where the zip is saved. Use a UNC path to collect from lots of machines into one place.
-SkipConfigMgrLogsswitch—Leave out WUAHandler.log, UpdatesDeployment.log, UpdatesHandler.log, UpdatesStore.log and ScanAgent.log.
-IncludeCbsLogswitch—Add CBS.log, which is where most install failures (as opposed to scan failures) actually explain themselves.
-IncludeEtlswitch—Add the raw .etl trace files too, for when someone else wants to decode them their own way.

Run it

Collect on this machine and keep the zip locally.

.\Export-WindowsUpdateLog.ps1 -Verbose

The full set, dropped on a collection share.

.\Export-WindowsUpdateLog.ps1 -DestinationPath \\sccm01\UpdateLogs$ -IncludeCbsLog

As a ConfigMgr program command line.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Export-WindowsUpdateLog.ps1 -DestinationPath \\sccm01\UpdateLogs$

Just the Windows side, on a machine without the ConfigMgr client.

.\Export-WindowsUpdateLog.ps1 -SkipConfigMgrLogs

What you'll see

Example outputvalues are illustrative
WARNING: Couldn't copy CBS.log: The process cannot access the file because it is being used by another process.

ComputerName : PC-0142
ZipPath      : \\sccm01\UpdateLogs$\PC-0142-WULogs-20260929-101544.zip
Files        : 6
SizeMB       : 4.87
Warnings     : 1

How it works

  1. Make a scratch folder. Everything is staged in a temp folder named after the computer and timestamp, so two runs never step on each other.
  2. Decode the traces. If Get-WindowsUpdateLog exists, it writes a readable WindowsUpdate.log into the scratch folder. On anything older, it copies the plain-text log from the Windows folder instead.
  3. Add the ConfigMgr logs. If C:\Windows\CCM\Logs is there, it copies the update-related logs, including rolled-over copies like WUAHandler-20260901-101010.log. WUAHandler.log tells you whether the scan worked. UpdatesDeployment.log tells you whether the client even thinks it should install something.
  4. Keep going on errors. A missing folder or a locked file gets written to collection-notes.txt inside the zip and shown as a warning, instead of stopping the run.
  5. Zip and clean up. Compress-Archive builds the zip at the destination, the scratch folder is deleted, and you get an object back with the path, file count and size.

For a handful of machines, ConfigMgr's Run Scripts is the fastest way to fire this off. For a whole collection, make it a package with a program so it runs whether or not anyone is signed in, and point everything at one share.

Take it further

  • Read the right log first. Scan problems live in WindowsUpdate.log and WUAHandler.log. Install problems usually live in CBS.log. Starting in the right place saves a lot of scrolling.
  • Add the event logs. wevtutil epl System and the Microsoft-Windows-WindowsUpdateClient/Operational channel export cleanly and are small.
  • Tidy the share. A scheduled task that deletes zips older than 30 days keeps the collection folder from turning into an archive nobody asked for.

Things that'll trip you up

  • The share needs the computer account's permission. Run as SYSTEM, the script writes to the share as DOMAIN\PC-0142$. Give Domain Computers write access to a drop folder (and no read, if you'd rather machines couldn't browse each other's logs).
  • Older builds want symbols. On early Windows 10 releases and Server 2016, Get-WindowsUpdateLog downloads symbols from Microsoft to decode the traces. No internet, no readable log. Current builds don't need them.
  • CBS.log gets big. It can run to hundreds of megabytes on a machine that's been failing for a while. That's why it's opt-in. Multiply by a collection of 500 before you point this at a share.
  • It's a snapshot. The ETW traces roll over. If the failure happened a week ago on a busy machine, the evidence may already be gone, so collect soon after the problem shows up.