JPBlanc recommends grouping the records in a comment on the question, and the Group-Object cmdlet indeed offers a conceptually elegant solution:
Note: The assumption is that if a given keyword only has one entry, it is always the starting entry.
Select-String 'Starting thread:|Thread finished;' file.log |
Group-Object { (-split $_)[-1] } | Where-Object { $_.Count % 2 -eq 1 }
The Select-String call extracts only the lines of interest (a thread starting, a thread finishing), using a regex (regular expression)
The Group-Object call groups the resulting lines by the last ([-1]) whitespace-separated token (-split ...) on each line ($_), i.e., the keywords.
Where-Object then returns only those resulting that have an odd number of entries, i.e., those that aren't paired, representing the started-but-not-finished threads.
This yields something like the following:
Count Name Group
----- ---- -----
1 KEYWORD3 {/Users/jdoe/file.log:5:28.8.2018 08:59:16 Starting thread: KEYWORD3}
This is probably not the format you want, but given that the outputs are objects, as is typical in PowerShell, you can easily process them to your liking programmatically.
Technically, the above command outputs [Microsoft.PowerShell.Commands.GroupInfo] instances whose .Group property in this case contains [Microsoft.PowerShell.Commands.MatchInfo] instances, as output by Select-String.
The following code extends the one above to produce custom output that reports how much time has elapsed since each unfinished thread has started:
$now = Get-Date
Select-String 'Starting thread:|Thread finished;' file.log |
Group-Object { (-split $_)[-1] } | Where-Object { $_.Count % 2 -eq 1 } | ForEach-Object {
foreach ($matchInfo in $_.Group) { # loop over started-only lines
$tokens = -split $matchInfo.Line # split into tokens by whitespace
$date, $time = $tokens[0..1] # extract date and time (first 2 tokens)
$keyword = $tokens[-1] # extract keyword (last token)
# Parse date+time into a [datetime] instance.
# Note: Depending on the current culture, [datetime]::Parse("$date $time") may do.
$start = [datetime]::ParseExact("$date $time", 'd\.M\.yyyy HH:mm:ss', [cultureinfo]::InvariantCulture)
# Custom output string containing how long ago the thread was started:
"Thread $keyword hasn't finished yet; time elapsed since it started: " +
($now - $start).ToString('g')
}
}
This yields something like the following:
Thread KEYWORD3 hasn't finished yet; time elapsed since it started: 2:03:35.347563
2:03:35.347563 (2 hours, 3 minutes, ...) is the string representation of a [TimeSpan] instance that is the result of subtracting two points in time ([datetime] instances).