Parsing Event Log Subscription Runtime Status using PowerShell and Regex
The Story
Since Event Log Subscription doesn’t have a module or a .NET class,
interacting with its settings and status has to be done either via UI or
the command line utility, wecutil.exe
I was especially interested in getting the computers runtime status, to
see which machines are failing (and why).
Let’s look at the output generated by wecutil gr
:
As we can see, every server’s status is represented by multiple lines,
which rules out easy one-line-at-a-time parsing.
Since Wecutil has no script-friendly output (e.g. CSV), I had to use multiline regex parsing.
This is done by joining the lines using ``n (since PowerShell returns
an array of rows rather than the complete string), using the .NET class
Regex` (System.Text.RegularExpressions.Regex)
and creating a complicated regex pattern to parse by.
My full script also supports remoting (using
Invoke-Command)
and parsing all of the fields into meaningful objects (e.g. extracting
the error messages out of the XML).
The really interseting part is the regex pattern, especially these
parts:
- Nonecapturing groups: Using
(?:)
means the group isn’t kept for data extraction - Positive lookbehind: both the beginning and the end of my string
contain line-breaks, so I had to use lookbehind
(?\<=)
or lookahead(?=)
(or both) groups to avoid “competition” for the same line-break between two close matches. - Capturing ALL characters: apperantly,
.
captures every character except\n
(newline) so(?:.|\\n)
will match every character. Although this difference isn’t visible when dealing with single line expressions (since you don’t encounter line breaks), it’s vital when handling multiline ones. - Optional segments: The fields
ErrorMessage
,ErrorTime
andNextRetryTime
only appear whenLastError
isn’t 0, so we can’t mandate them in the pattern. On the other hand, we do want to capture them when they’re present. Surrounding them all with(?:
text)?
will ensure that the entire expression won’t fall if they’re not present but populate the right capturing groups if they are. - Reaching the real end: The last line isn’t terminated with a
newline but rather with End-Of-String, so our string ends with
(?:\n|$)
to match either.
The Script
param(
[string]$computerName,
[parameter(Mandatory=$true)]
[string]$subscriptionName
)
$wecUtil={wecutil gr $args[0]}
$splatTable=@{}
if($computeRName) {$splatTable['ComputerName']=$computerName}
$in = Invoke-Command @splatTable $wecUtil -ArgumentList $subscriptionName
$in2 = ($in -join "`n")
$pattern = '(?<=\n)\t{2}(?!\t)(.+)\n\t{3}RunTimeStatus: (.+)\n\t{3}LastError: (.+)(?:\n\t{3}ErrorMessage: ((?:\n|.)+?)\n\t{3}ErrorTime: (.+)\n\t{3}NextRetryTime: (.+))?(?:\n|$)'
[regex]::Matches($in2,$pattern,'Multiline') | %{
$arr = $_.Groups | select -s 1 -exp value
5| select `
@{Name='Name';Expression={$arr[0]}},
@{Name='RunTimeStatus';Expression={$arr[1]}},
@{Name='LastError';Expression={[int]$arr[2]}},
@{Name='ErrorMessage';Expression={
$msg = ([xml]$arr[3]).WSManFault.Message
if($msg -is [string]) {$msg}
else {$msg.ProviderFault.ProviderError.'#text'}
}},
@{Name='ErrorTime';Expression={[datetime]$arr[4]}},
@{Name='NextRetryTime';Expression={[datetime]$arr[5]}}
}