Investigating Event Logs in Practice with Get-WinEvent — Filtering Speed Decides How Long the Investigation Takes

· · PowerShell, Windows, Event Log, Failure Investigation, Operational Improvement, Information Systems, Auditing, Troubleshooting

“It looks like the server rebooted on its own late on Friday night.” “The business application crashes a few times a month, but only on one particular machine.” — Investigations like these almost always start with the Windows event log. But scroll through several hundred thousand entries in the Event Viewer GUI and the whole morning is gone.

With PowerShell’s Get-WinEvent, the same work takes tens of seconds. There is a condition, though: you must filter in the right place. Write Get-WinEvent | Where-Object { ... } and you end up loading every event before throwing it away — which can even be slower than the GUI.

This article covers how to use Get-WinEvent’s filtering correctly, recipes for the investigations that come up most often in the field (unexpected reboots, logons, application crashes, service stoppages), and collecting from multiple machines.

1. The Bottom Line First

  • Use Get-WinEvent, not Get-EventLog. The latter is Windows PowerShell-only and classic-logs-only, and is unavailable in PowerShell 7.1
  • Filter with -FilterHashtable. Because the filtering happens on the event log side, it is faster by orders of magnitude than narrowing down afterwards with Where-Object.2
  • The keys for -FilterHashtable are fixed. They are LogName, ProviderName, ID, Level, StartTime, EndTime, Keywords, Path, UserID and so on.2
  • For complex conditions or filtering on event data, use XPath (-FilterXPath). You can copy the XPath out of a Custom View in the Event Viewer.1
  • Level is a number. 1 = Critical, 2 = Error, 3 = Warning, 4 = Information, 5 = Verbose.3
  • Reading the Security log requires special privileges. Running as administrator is the quick route, but giving investigators read-only access via the Event Log Readers group or a channel ACL is more in line with least privilege.14
  • Look at the event data, not the message string. Fetching the structured data with ToXml() gives you a script that does not depend on the OS language settings.1
  • You can analyse .evtx files too. Specify -Path and you can examine logs captured on site from your own machine.1
  • For permanent aggregation, use Windows Event Forwarding (WEF). For a one-off investigation, parallel execution via Invoke-Command is plenty.5

2. Two Kinds of Log, and Choosing the Cmdlet

Windows event logs fall broadly into two families.

Kind Examples Cmdlets that can read them
Classic logs System / Application / Security Get-EventLog (5.1 only) and Get-WinEvent
Applications and Services Logs Microsoft-Windows-TaskScheduler/Operational and others Get-WinEvent only

It is the latter that holds the information useful for investigation. Task Scheduler execution history, PowerShell script block logging, Windows Update installation history — most of the logs that decide an investigation are on this side. So standardising on Get-WinEvent for scripts you write from now on is the right answer.1

Start by checking which logs exist.

# List the logs (most entries first). A log with RecordCount 0 is not recording
Get-WinEvent -ListLog * -ErrorAction SilentlyContinue |
    Where-Object RecordCount -gt 0 |
    Sort-Object RecordCount -Descending |
    Select-Object LogName, RecordCount, MaximumSizeInBytes, IsEnabled -First 20

# Find the logs for a specific product
Get-WinEvent -ListLog *TaskScheduler* | Format-Table LogName, IsEnabled, RecordCount
Get-WinEvent -ListProvider *PowerShell* | Select-Object Name

3. With Filtering, “Where” Is Everything

Compare three ways of getting the same result.

# [Worst] Read everything, then discard it on the PowerShell side
Get-WinEvent -LogName System | Where-Object { $_.Id -eq 41 }

# [Recommended] Filter on the event log side (FilterHashtable)
Get-WinEvent -FilterHashtable @{ LogName = 'System'; ID = 41 }

# [Complex conditions] Filter with XPath
Get-WinEvent -LogName System -FilterXPath "*[System[EventID=41]]"

The first turns all several hundred thousand events into objects before discarding them, so most of the wait is wasted. The second and third filter on the event log API side, so only what you need comes back.2

The keys you can use with -FilterHashtable are fixed.2

Key What it specifies Example
LogName The log name 'System', 'Microsoft-Windows-TaskScheduler/Operational'
ProviderName The source of the event 'Application Error', 'Service Control Manager'
ID The event ID (arrays allowed) 41, @(1000, 1001)
Level Severity (numeric) 2 (Error), @(1,2)
StartTime / EndTime The time range (Get-Date).AddDays(-7)
Keywords Keywords (audit success/failure and so on) 9007199254740992 (audit success)
Path An .evtx file 'D:\collect\srv01_System.evtx'
UserID A user SID 'S-1-5-21-...'

The numeric values for Level are as follows.3

Value Meaning
1 Critical
2 Error
3 Warning
4 Information
5 Verbose

In a practical form, it looks like this.

# Summarise error and critical events from the last 7 days by source (the first move in sizing up a situation)
$filter = @{
    LogName   = 'System', 'Application'
    Level     = 1, 2
    StartTime = (Get-Date).AddDays(-7)
}
try {
    Get-WinEvent -FilterHashtable $filter -ErrorAction Stop |
        Group-Object ProviderName, Id |
        Sort-Object Count -Descending |
        Select-Object Count, Name -First 15
}
catch {
    # Silently swallow only "no matching events". This is locale-independent
    # because we test FullyQualifiedErrorId (the message string will not match
    # in a Japanese-language environment)
    if ($_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*') { throw }
}

When there are no matching events at all, Get-WinEvent raises an error. Do not casually slap -ErrorAction SilentlyContinue on it here. “No matches”, “you do not have permission to read that log” and “the target is unreachable” all collapse into the same empty result. During an investigation, that is the most troublesome way for things to break.

The right answer is to catch it with -ErrorAction Stop as above and swallow it only when FullyQualifiedErrorId is NoMatchingEventsFound. Testing on the message string does not work, because it will not match in a Japanese-language environment (for the thinking behind error handling, see “PowerShell Error Handling and Retry Design”).

4. Investigation Recipes You Will Use Constantly

(1) Unexpected Reboots and Shutdowns

# 41:   rebooted without a clean shutdown (Kernel-Power)
# 6008: unexpected shutdown (EventLog)
# 1074: shutdown requested by a process/user (who, or what, brought it down)
# 6005/6006: Event Log service started/stopped (= markers for boot/shutdown)
Get-WinEvent -FilterHashtable @{
    LogName   = 'System'
    ID        = 41, 1074, 6005, 6006, 6008
    StartTime = (Get-Date).AddDays(-30)
} | Select-Object TimeCreated, Id, ProviderName,
      @{ n = 'Message'; e = { ($_.Message -split "`r?`n")[0] } } |
    Sort-Object TimeCreated -Descending | Format-Table -AutoSize

1074 is the important event, because it tells you who requested the reboot. If WindowsUpdate or a particular process name shows up there, the cause is all but confirmed. If you see only 41 and 6008 lined up with no 1074, suspect an abnormal stop from power loss or a hang.

(2) Tracking Logons and Logoffs (Security Log)

# 4624: logon succeeded / 4625: logon failed / 4634: logoff
# Requires permission to read the Security log (run as administrator, or add the
# executing account to the Event Log Readers group in advance)
Get-WinEvent -FilterHashtable @{
    LogName   = 'Security'
    ID        = 4624, 4625, 4634
    StartTime = (Get-Date).AddDays(-1)
} | ForEach-Object {
    $xml = [xml]$_.ToXml()
    $d   = @{}
    foreach ($n in $xml.Event.EventData.Data) { $d[$n.Name] = $n.'#text' }
    [pscustomobject]@{
        Time      = $_.TimeCreated
        Kind      = switch ($_.Id) { 4624 { 'Logon succeeded' } 4625 { 'Logon failed' } 4634 { 'Logoff' } }
        User      = $d['TargetUserName']
        LogonKind = $d['LogonType']    # 2=interactive 3=network 10=RDP
        Source    = $d['IpAddress']
    }
} | Where-Object User -notlike '*$' | Format-Table -AutoSize

This is the archetypal case for using event data. Carving up Message with a regular expression depends on the OS language settings, whereas the EventData from ToXml() can be referenced by name, so the same script runs in a Japanese-language environment and an English-language one alike.6

Bear in mind, too, that no event is recorded at all unless auditing is enabled. “Nothing shows up” may mean “nothing was recorded” rather than “nothing happened”.

(3) Application Crashes

# 1000: Application Error (an application crash)
# 1026: .NET Runtime (termination from a managed exception)
# 1001: Windows Error Reporting (fault bucket information)
Get-WinEvent -FilterHashtable @{
    LogName   = 'Application'
    ID        = 1000, 1001, 1026
    StartTime = (Get-Date).AddDays(-14)
} | Select-Object TimeCreated, Id, ProviderName, Message |
    Sort-Object TimeCreated -Descending | Format-List

1000 carries the name of the faulting module and the offset, and 1026 carries the .NET exception stack. Getting your bearings here before moving on to dump analysis is the efficient route (see “An Introduction to Collecting Windows Crash Dumps” and “Reading Crash Dumps with WinDbg + SOS”).

(4) Services Stopping and Restarting

# 7034: a service terminated unexpectedly / 7031: recovery action after termination / 7045: a new service was installed
Get-WinEvent -FilterHashtable @{
    LogName     = 'System'
    ProviderName = 'Service Control Manager'
    ID          = 7031, 7034, 7045
    StartTime   = (Get-Date).AddDays(-30)
} | Select-Object TimeCreated, Id, Message | Format-List

7045 (a new service installed) is also useful for detecting the installation of unintended software. For operational design of Windows services, see “How to Build and Operate Windows Services”.

(5) Task Scheduler Execution History

# [Bad] Fetch everything, then filter on the display message (language-dependent)
Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-TaskScheduler/Operational'
    StartTime = (Get-Date).AddDays(-3)
} -ErrorAction SilentlyContinue |
    Where-Object { $_.Message -like '*NightlyAggregation*' }

# [Good] Filter server-side on the task name (event data). Fast, and language-independent
$xpath = @"
*[System[TimeCreated[timediff(@SystemTime) <= 259200000]]]
and
*[EventData[Data[@Name='TaskName']='\NightlyAggregation']]
"@
Get-WinEvent -LogName 'Microsoft-Windows-TaskScheduler/Operational' `
             -FilterXPath $xpath -ErrorAction SilentlyContinue |
    Select-Object TimeCreated, Id, LevelDisplayName, Message | Format-List

timediff is an XPath function that specifies “elapsed time from now” in milliseconds, and 259200000 is three days. Give the task name as the full path it was registered under (\TaskName if it sits directly under the root). The display-oriented Message depends on the language settings, and filtering on it happens client-side, so filter on the event data as this article recommends.

This log is disabled by default in some cases. How to enable it, and how to isolate the cause when a task does not run, are covered in “When Task Scheduler Tasks Don’t Run or Exit with 0x1”.

5. Investigating Logs on Several or Other Machines

Approach 1: read them remotely, directly

Get-WinEvent -ComputerName 'srv01' -FilterHashtable @{ LogName='System'; ID=41 } -MaxEvents 10

Approach 2: query in parallel with Invoke-Command (use this when there are many machines)

$servers = 'srv01', 'srv02', 'srv03'
Invoke-Command -ComputerName $servers -ScriptBlock {
    Get-WinEvent -FilterHashtable @{
        LogName = 'System'; Level = 1, 2; StartTime = (Get-Date).AddDays(-1)
    } -ErrorAction SilentlyContinue |
        Select-Object TimeCreated, Id, ProviderName, LevelDisplayName
} | Sort-Object PSComputerName, TimeCreated

Invoke-Command runs against multiple computers in parallel (see “An Introduction to PowerShell Remoting (WinRM)” and “Parallel Processing in PowerShell”).

Approach 3: collect .evtx files and analyse them locally

You can read a file exported on site directly. It is faster than querying repeatedly across the network, and it also survives as evidence.

# On site: wevtutil epl System D:\collect\srv01_System.evtx
Get-WinEvent -Path 'D:\collect\srv01_System.evtx' -FilterXPath "*[System[(Level=1 or Level=2)]]" |
    Select-Object TimeCreated, Id, ProviderName, Message

Approach 4: Windows Event Forwarding (WEF) — this is the one for permanent aggregation. It is a standard feature that forwards events from each machine to a collector server, with no additional agent to install.5

6. Log Size and Retention

“I went to investigate and the log for that period had already been overwritten” is a very common failure. The default maximum size is on the small side, and in busy environments it wraps around within a few days.

# Check the current size settings and retention status
Get-WinEvent -ListLog 'System', 'Application', 'Security' |
    Select-Object LogName, IsEnabled, LogMode,
        @{ n='MaxMB'; e={ [math]::Round($_.MaximumSizeInBytes / 1MB, 1) } },
        RecordCount, OldestRecordNumber

# Change the maximum size (requires administrator rights. Example: System log to 256 MB)
wevtutil sl System /ms:268435456

On servers where investigation is expected, and on machines that are currently experiencing failures, increasing the log size first and then waiting for a reproduction is the standard move. For generational management and automatic archiving of logs, see “Applied PowerShell Scripting — Log Investigation, Archiving and Reporting” as well.

7. Practical Rules of Thumb (Decision Table)

Situation Choice Notes
Scripts you write from now on Get-WinEvent Get-EventLog is 5.1-only and classic-logs-only1
Filtering by log name, ID and time range -FilterHashtable Fastest, and the most readable2
Filtering on the contents of the event data -FilterXPath / -FilterXml You can copy it from a Custom View in the Event Viewer1
Too many entries, taking too long Narrow the time range, use -MaxEvents Do not do the filtering on the PowerShell side
Extracting a value from a message EventData from ToXml() Gives you a script independent of the language settings1
One-off investigation across several machines Invoke-Command It runs in parallel5
Permanent aggregation Windows Event Forwarding (WEF) A standard feature with no extra agent5
The older logs are gone Increase the log size Do this before you settle in to wait for a reproduction

8. Summary

  • Standardise on Get-WinEvent. Get-EventLog is unavailable in PowerShell 7, and the logs it can handle are limited.
  • Filter with -FilterHashtable or -FilterXPath. Narrowing down afterwards with Where-Object makes investigation time worse by an order of magnitude.
  • For reboot investigations, look at 1074 (who requested it) in addition to 41 and 6008. For application crashes, 1000, 1026 and 1001 are the starting points.
  • Using the event data from ToXml() rather than the message string gives you a script that does not depend on the language settings.
  • For one-off investigations across several machines use Invoke-Command; for permanent aggregation use Windows Event Forwarding; and when you need evidence, capture .evtx files and analyse them locally.
  • If the log for the period you want no longer exists, there is nothing you can do. Reviewing log sizes is the top priority as preparation for incident response.

Download the Sample Code

The code covered in this article is distributed as a package you can run as-is. It contains reboot history, logon history, task failures and multi-machine collection.

Download the sample code (zip)

Because the samples for this article depend on Windows and on a tenant, they have not been verified by execution. Syntax parsing and static analysis with PSScriptAnalyzer have been carried out against every file, but be sure to confirm the behaviour on your own test machine.

# Syntax parsing + static analysis (runs on non-Windows platforms too)
./Invoke-SampleTests.ps1

The configuration values (paths, server names, tenant IDs and so on) are examples. Do not run them against production as-is — adapt them to your own environment.

KomuraSoft LLC handles failure investigation in Windows environments, root-cause analysis of intermittent reboots and application crashes, and building log collection and monitoring arrangements.

References

  1. Microsoft Learn, Get-WinEvent. On being able to retrieve both classic logs and the event logs introduced from Windows Vista onwards; on listing with -ListLog / -ListProvider; on filtering with -FilterHashtable / -FilterXPath / -FilterXml; on reading archived logs (.evtx) with -Path; on remote retrieval with -ComputerName; on limiting the number of entries with -MaxEvents; on administrator privileges being required to read the Security log; and on being able to obtain each event’s XML representation via its ToXml() method.  2 3 4 5 6 7 8 9

  2. Microsoft Learn, Creating Get-WinEvent queries with FilterHashtable. On the keys that can be specified with -FilterHashtable (LogName, ProviderName, Path, Keywords, ID, Level, StartTime, EndTime, UserID, Data and so on); on it being more efficient than filtering with Where-Object because the filtering happens server-side; and on how to specify keyword and severity values.  2 3 4 5

  3. Microsoft Learn, Event Levels. On the standard values for event severity levels (1 = Critical, 2 = Error, 3 = Warning, 4 = Informational, 5 = Verbose).  2

  4. Microsoft Learn, Active Directory security groups — Event Log Readers. On members of the built-in Event Log Readers group being able to read the event logs of the local computer (without being granted administrator rights). For changing the access permissions on an individual channel, see also wevtutil and its sl /ca (channel access) option. 

  5. Microsoft Learn, Windows Event Forwarding. On being able to forward events from multiple Windows machines to a collector server without installing an additional agent, and on specifying what to collect via subscriptions. See also Invoke-Command for parallel execution against multiple computers.  2 3 4

  6. Microsoft Learn, 4624(S): An account was successfully logged on. On the items contained in the event data of a successful logon event (TargetUserName, LogonType, IpAddress and so on) and the meaning of the logon type values, and on whether records are produced depending on the audit policy configuration. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Should I use Get-EventLog or Get-WinEvent?
Get-WinEvent. Get-EventLog is available only on Windows PowerShell 5.1, and it can only handle the traditional (classic) logs such as System, Application and Security. The logs added from Windows Vista onwards under Applications and Services Logs — detailed logs such as Microsoft-Windows-TaskScheduler/Operational — can only be read with Get-WinEvent. Since Get-EventLog is not available at all in PowerShell 7, standardise on Get-WinEvent for any script you write from now on.
Get-WinEvent is so slow that I cannot get any investigation done.
It is very likely that you are filtering with Where-Object further down the pipeline. Written that way, every event in the log is first turned into an object and read into PowerShell, and only then are the unwanted ones discarded. With a log of several hundred thousand entries that is impractical. If you use -FilterHashtable or -FilterXPath, the filtering happens on the event log side and only the events you need come back, which is faster by orders of magnitude. Get into the habit of specifying the log name, time range and event ID in a FilterHashtable first.
I get access denied when I try to read the Security log.
Reading the Security log requires special privileges by default. If you are just checking on your own machine, running PowerShell as administrator is enough, but if you do not want to hand administrator rights to the person doing the investigation, you can add them to the built-in Event Log Readers group, or grant read-only access via the channel's access permissions (ACL). From a least-privilege perspective we recommend the latter. Note also that the audit records may simply not exist in the first place. Logon auditing and the like depend on the audit policy configuration, so when you find no events at all, check whether the policy itself is enabled.
I want to extract a specific value (a username or a process name) from an event message.
Using Properties or the event data is more reliable than carving up the Message property with a regular expression. Every event exposes a ToXml() method that returns its XML representation, which contains named items under EventData. The display message string changes with the OS language settings, but the structure of the event data does not, so you can write a script that works in both Japanese-language and English-language environments.
I want to investigate the event logs of several servers together.
If there are only a few machines, Get-WinEvent's -ComputerName parameter or remote execution via Invoke-Command is enough. Invoke-Command runs against the machines you specify in parallel, so it is practical up to a few dozen. If you want a permanent aggregation, consider Windows Event Forwarding (WEF) to collect events on a collector server. For a one-off investigation, exporting evtx files on each server and analysing them locally with Get-WinEvent -Path is also effective.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog