Home / Guides / Search Event Logs on multiple servers

Search Windows Event Logs on multiple servers at once

Something failed overnight and the clue is in an Event Log, but you do not know which server. Event Viewer shows one computer at a time. This guide covers what Event Viewer and Event Forwarding can do, a PowerShell script that searches the logs of a list of servers and puts the results in one sorted table and a CSV file, what the remote read needs, the errors you will meet, and where the script stops.

Short answer.

Loop over your servers with Get-WinEvent -ComputerName and a -FilterHashtable that narrows the logs, levels and start time on the server side, then check the message text, collect everything into one list, sort it by time and export it to CSV. Each server needs your account in its Event Log Readers group and its Remote Event Log Management firewall rules turned on.

This finds what is in the logs right now. It does not keep anything, so an event that has rolled out of a log is gone, and it does not read SQL Server's own error log.

Event Viewer: one computer at a time

Event Viewer can open another computer's logs: right-click Event Viewer (Local) and choose Connect to Another Computer. The whole console then shows that one computer. To check ten servers you connect ten times and repeat the same filter each time.

A custom view (Create Custom View in the Actions pane) saves a filter over several logs, such as errors in Application and System for the last 24 hours. It runs against the computer Event Viewer is connected to. It is still useful here: build the filter in the dialog, open the XML tab, and you have a query that Get-WinEvent -FilterXml accepts as it is.

Windows Event Forwarding (WEF) is Windows' own way to bring events from many computers into one. It is more setup than a search:

  • WinRM configured on the collector and on every source computer, and the Windows Event Collector service on the collector (winrm qc and wecutil qc).
  • A subscription that says which events to send. A collector-initiated one lists the computers; a source-initiated one is pushed to the sources by Group Policy.
  • Outside a domain, certificates on both sides and HTTPS on port 5986.

The forwarded events land in the collector's ForwardedEvents log, which you then search like any other log. Microsoft's guidance is to keep a collector-initiated subscription to 500 computers or fewer. WEF only moves event log entries: log files and SQL Server's error log are not covered.

Search several servers with PowerShell

Get-WinEvent reads another computer's logs with -ComputerName, but it takes one name at a time, so the script loops. -FilterHashtable does the filtering on the server, which is much faster than sending every event back and filtering with Where-Object. The hashtable has no key for message text, so the text check runs in the script after the events arrive.

param(
    [string[]]$ComputerName = @('SERVER01', 'SERVER02', 'SERVER03'),
    [string[]]$LogName      = @('Application', 'System'),
    [int[]]$Level           = @(1, 2),                 # 1 Critical, 2 Error, 3 Warning, 4 Information
    [datetime]$StartTime    = (Get-Date).AddHours(-24),
    [string]$Text           = '',                      # part of the message to look for; empty = all
    [string]$CsvPath        = "$env:USERPROFILE\Desktop\EventSearch.csv"
)

$filter  = @{ LogName = $LogName; Level = $Level; StartTime = $StartTime }
$found   = [System.Collections.Generic.List[object]]::new()
$failed  = [System.Collections.Generic.List[object]]::new()

foreach ($computer in $ComputerName) {
    try {
        $events = Get-WinEvent -ComputerName $computer -FilterHashtable $filter -ErrorAction Stop
    }
    catch {
        # "No events were found" is an error in Get-WinEvent; here it just means nothing matched.
        if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { continue }
        $failed.Add([pscustomobject]@{ Computer = $computer; Error = $_.Exception.Message })
        continue
    }

    foreach ($e in $events) {
        # The message text is not part of the filter, so it is checked here, case-insensitive.
        if ($Text -and -not ($e.Message -and
            $e.Message.IndexOf($Text, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) { continue }

        $found.Add([pscustomobject]@{
            Time     = $e.TimeCreated
            Computer = $computer
            Log      = $e.LogName
            Level    = $e.LevelDisplayName
            Source   = $e.ProviderName
            EventId  = $e.Id
            Message  = $e.Message
        })
    }
}

$sorted = $found | Sort-Object Time -Descending
$sorted | Format-Table Time, Computer, Level, Source, EventId, Message -AutoSize
$sorted | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
"{0} events from {1} computers written to {2}" -f $found.Count, $ComputerName.Count, $CsvPath

foreach ($f in $failed) { Write-Warning "$($f.Computer): $($f.Error)" }

Save it as C:\Scripts\Search-EventLogs.ps1 and run it with your own server names:

C:\Scripts\Search-EventLogs.ps1 -ComputerName SQL01, SQL02, WEB01 -Text 'timeout' -StartTime (Get-Date).AddDays(-3)

# Or read the server list from a file, one name per line, and include warnings:
C:\Scripts\Search-EventLogs.ps1 -ComputerName (Get-Content C:\Scripts\servers.txt) -Level 1,2,3 -Text 'disk'
  • -ComputerName: the servers to search. Use the names you would use in Event Viewer.
  • -LogName: the logs to read (default Application and System).
  • -Level: 1 Critical, 2 Error, 3 Warning, 4 Information. The default is Critical and Error.
  • -StartTime: how far back to look (default 24 hours).
  • -Text: part of the message to look for, not case-sensitive. Leave it out to get every event that matches the other filters.
  • -CsvPath: where the results go. The table on screen is the same list, newest first, with a Computer column.

Keep the time window and levels tight. Only those filters run on the server; every event that passes them travels over the network before the text check, so "all Information events for a month" can be slow on a busy server.

Tested on Windows PowerShell 5.1 on Windows 11, reading the test computer by its own name and as localhost: the results from both names came back in one table sorted newest first and in the CSV; the text search matched the same rows in upper and lower case; a filter that matched nothing gave an empty result and no error; a log name that does not exist was reported as a warning and the script carried on. The remote parts (other servers, firewall rules, Invoke-Command) are from Microsoft's documentation and were not run.

Faster on many servers: Invoke-Command

The loop above reads one server after another. Invoke-Command runs the same search on the servers, up to 32 at a time by default (-ThrottleLimit), and only the matching events come back. Each result carries a PSComputerName property:

$servers = Get-Content C:\Scripts\servers.txt

$search = {
    param([string]$Text, [datetime]$Since)
    $filter = @{ LogName = 'Application', 'System'; Level = 1, 2; StartTime = $Since }
    try   { $events = Get-WinEvent -FilterHashtable $filter -ErrorAction Stop }
    catch {
        if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { return }
        throw
    }
    $events |
        Where-Object { $_.Message -and $_.Message.IndexOf($Text, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 } |
        Select-Object TimeCreated, LogName, LevelDisplayName, ProviderName, Id, Message
}

Invoke-Command -ComputerName $servers -ScriptBlock $search -ArgumentList 'timeout', (Get-Date).AddDays(-1) |
    Sort-Object TimeCreated -Descending |
    Select-Object PSComputerName, TimeCreated, LogName, LevelDisplayName, ProviderName, Id, Message |
    Export-Csv "$env:USERPROFILE\Desktop\EventSearch.csv" -NoTypeInformation -Encoding UTF8

The trade-off is what it needs: Invoke-Command uses PowerShell remoting over WinRM, not the Event Log's own remote access. See the next section.

What a remote read needs

Get-WinEvent -ComputerName does not use PowerShell remoting. It talks to the Event Log service on the server over RPC, so it works on servers where remoting is off. It needs three things on each server:

  • Permission. Your account (or the account the script runs as) in the server's local Event Log Readers group, whose members can read the event logs, or in Administrators. Domain controllers have a domain-wide Event Log Readers group in the Builtin container.
  • The firewall. The inbound rules in the Remote Event Log Management group (RPC, RPC-EPMAP and NP-In) turned on. On the Windows 11 computer we tested on, all six rules in that group were off.
  • RPC through the network. Any firewall between you and the server must let through TCP port 135 (the RPC endpoint mapper) and the dynamic port the server hands out after that.

An administrator can set the first two up on a server like this (replace the account name with yours):

# Run on each server, in an elevated PowerShell window.
Add-LocalGroupMember -Group 'Event Log Readers' -Member 'CONTOSO\svc-logsearch'
Enable-NetFirewallRule -DisplayGroup 'Remote Event Log Management'

On many servers, do the same through Group Policy instead of one by one. Enable-NetFirewallRule without a filter turns on every rule on the computer, so always give it -DisplayGroup or a rule name.

Invoke-Command needs something else: WinRM listening on the server (on by default on Windows Server 2012 and later), the WinRM ports 5985/5986 open, and an account in the server's Administrators or Remote Management Users group. The search then runs on the server as that account, so a non-administrator also needs Event Log Readers there.

Common errors

  • "The RPC server is unavailable." The script could not reach the Event Log service. Microsoft documents this error for a server whose Remote Event Log Management firewall rules are not enabled. Other causes: a wrong name or DNS entry, the server is off, or a firewall in between blocks port 135 or the dynamic RPC port. Test-NetConnection SERVER01 -Port 135 checks the first port only.
  • Access denied. The connection worked but the account cannot read the log. Add it to Event Log Readers on that server, or run the script as an account that is. Get-WinEvent also has -Credential for another account.
  • "No events were found that match the specified selection criteria." This is not a failure. Get-WinEvent reports an empty result as an error (its id is NoMatchingEventsFound). The script catches exactly that error and moves on, and reports everything else. Do not use -ErrorAction SilentlyContinue for this: it hides the RPC and access errors too, and a server you could not read looks the same as a server with nothing to find.
  • "There is not an event log on the SERVER01 computer that matches ..." One of the log names in -LogName does not exist on that server (for example a log that only exists where a product is installed). The whole call fails for that server, so search such a log in a separate run.

Where the script stops

  • Only what is still in the log. Event logs have a maximum size, and the usual setting (LogMode Circular) overwrites the oldest events when the log is full. On a busy server that can be days. The script keeps nothing between runs, so last month's error is gone if the log has wrapped since. To see how far back each log goes:
    Get-WinEvent -ListLog Application, System -ComputerName SERVER01 |
        Select-Object LogName, LogMode, MaximumSizeInBytes, RecordCount
    
    Get-WinEvent -LogName System -ComputerName SERVER01 -Oldest -MaxEvents 1 |
        Select-Object TimeCreated
  • No SQL Server error log. SQL Server writes its own error log (the ERRORLOG file), which Get-WinEvent does not read. Microsoft documents sp_readerrorlog for searching it, one instance at a time:
    USE [master];
    EXECUTE sp_readerrorlog 0, 1, N'timeout';
    The first number is the log (0 is the current one, 1 the one before), the second is 1 for SQL Server or 2 for SQL Server Agent, then up to two search strings. It needs securityadmin or VIEW SERVER STATE (SQL Server 2022 and later also accept VIEW ANY ERROR LOG). A new error log starts every time SQL Server starts, and by default only six old ones are kept. You will see xp_readerrorlog in many scripts; it has no reference page on Microsoft Learn, so this guide uses the documented procedure.
  • No log files. IIS logs, application logs and other text files are not in the Event Log. That is a separate search.
  • A search, not a record. Every run reads the servers again, and nothing tells you when a new error appears.

Questions

Can Event Viewer search several servers at once?

No. Event Viewer connects to one computer at a time (Connect to Another Computer), and a custom view runs on that computer. To search many servers at once, loop over them with Get-WinEvent -ComputerName in PowerShell, forward their events to one collector with Windows Event Forwarding, or use a log collector.

Does Get-WinEvent -ComputerName need PowerShell remoting?

No. It reads the logs through the Event Log service over RPC, so it works with remoting turned off. The server needs your account in its Event Log Readers group and the Remote Event Log Management firewall rules on. Invoke-Command is different: it uses PowerShell remoting over WinRM.

Why do I get "The RPC server is unavailable"?

The script could not reach the Event Log service on that server. The usual causes are the Remote Event Log Management firewall rules being off on the server, a firewall in between that blocks port 135 or the dynamic RPC port, a wrong name, or the server being down.

Why does Get-WinEvent throw an error when nothing matches?

Get-WinEvent reports an empty result as the error "No events were found that match the specified selection criteria." Catch that one error (its id starts with NoMatchingEventsFound) and treat it as zero results. Do not hide all errors with -ErrorAction SilentlyContinue, or a server you could not read looks like a server with nothing to find.

How far back can I search an Event Log?

Only as far as the log still holds. Event logs have a maximum size, and the usual setting overwrites the oldest events when the log is full, which on a busy server can be a few days. Get-WinEvent -LogName System -Oldest -MaxEvents 1 shows the oldest event that is left. To search further back, the events must be copied somewhere first, for example by Windows Event Forwarding or a collector such as LogTrawler.

Can I search the SQL Server error log the same way?

Not with Get-WinEvent: SQL Server writes its error log to a file, not to the Event Log. Use sp_readerrorlog on each instance, for example EXECUTE sp_readerrorlog 0, 1, N'timeout'; to search the current log. By default SQL Server keeps the current log and six older ones, and starts a new one every time it starts.

Related

Sources (Microsoft Learn): Get-WinEvent, Creating Get-WinEvent queries with FilterHashtable, Windows Event Log: accessing remote computers, Configure remote management in Server Manager, Active Directory security groups (Event Log Readers), Add-LocalGroupMember, Enable-NetFirewallRule, RPC error troubleshooting, Test-NetConnection, Security considerations for PowerShell remoting, about_Remote_Requirements, Invoke-Command, Windows Event Collector, Setting up a source-initiated subscription, WinRM best practices, How to select computers in Event Viewer, sp_readerrorlog, Configure SQL Server error logs.

An unhandled error has occurred. Reload 🗙