Investigating Event Logs in Practice with Get-WinEvent — Filtering Speed Decides How Long the Investigation Takes
· Go Komura · 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, notGet-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 withWhere-Object.2 - The keys for
-FilterHashtableare fixed. They areLogName,ProviderName,ID,Level,StartTime,EndTime,Keywords,Path,UserIDand 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 Levelis 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
.evtxfiles too. Specify-Pathand 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-Commandis 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-EventLogis unavailable in PowerShell 7, and the logs it can handle are limited. - Filter with
-FilterHashtableor-FilterXPath. Narrowing down afterwards withWhere-Objectmakes 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.evtxfiles 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.
Related Articles
- When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation
- An Introduction to Windows Event Log and ETW — Putting Your Business App’s Logs on the OS’s Standard Mechanisms
- An Introduction to Collecting Windows Crash Dumps - WER/ProcDump/WinDbg
- An Introduction to PowerShell Remoting (WinRM) — Managing Multiple Windows Machines at Once
- How to Build and Operate Windows Services
- Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting
Related Consulting Areas
KomuraSoft LLC handles failure investigation in Windows environments, root-cause analysis of intermittent reboots and application crashes, and building log collection and monitoring arrangements.
- Bug Investigation & Root-Cause Analysis
- Technical Consulting & Design Review
- Maintenance & Modernization of Existing Windows Software
- Contact Us
References
-
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
-
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
-
Microsoft Learn, Event Levels. On the standard values for event severity levels (1 = Critical, 2 = Error, 3 = Warning, 4 = Informational, 5 = Verbose). ↩ ↩2
-
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. ↩
-
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
-
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. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Hardening PowerShell — Logging, AMSI, Language Modes, and JEA
A practical guide to using PowerShell safely instead of banning it. Covers enabling script block logging and transcription, AMSI and disa...
Will NTLM Deprecation Stop Your Business Apps? — How to Collect Audit Logs, and the Order in Which to Kill Dependencies
A practical procedure for finding out where your Windows environment and business applications depend on NTLM ahead of its retirement: au...
Automating PC Provisioning With winget + PowerShell — Making the Runbook Executable
How to make new-hire PC setup reproducible. Covers installing applications with winget and export/import, declarative configuration with ...
Distributing and Updating PowerShell Modules In-House — PSResourceGet and an Internal Repository
How to move on from copying and reusing ps1 files in a shared folder. Covers how to write a module manifest, versioning, building an inte...
Integrating With REST APIs From PowerShell — Invoke-RestMethod in Practice
A practical guide to calling in-house and SaaS REST APIs from PowerShell: passing authentication headers, avoiding mojibake in non-ASCII ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
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.
Public links