Skip to content

Commit 1cc9e57

Browse files
committed
Tweak log message template to separate path from line num with ':'
This is probably a legal path char on Linux but on Windows it isn't. Also, Pester uses a similar format: <path>: line <num> Update PSES log analyzer to parse template. Add some more analysis commands.
1 parent 0373ee7 commit 1cc9e57

File tree

6 files changed

+184
-47
lines changed

6 files changed

+184
-47
lines changed

src/PowerShellEditorServices/Utility/PsesLogger.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class PsesLogger : ILogger
1515
/// The standard log template for all log entries.
1616
/// </summary>
1717
private static readonly string s_logMessageTemplate =
18-
"[{LogLevelName:l}] tid:{ThreadId} in '{CallerName:l}' {CallerSourceFile:l} (line {CallerLineNumber}):{IndentedLogMsg:l}";
18+
"[{LogLevelName:l}] tid:{ThreadId} in '{CallerName:l}' {CallerSourceFile:l}: line {CallerLineNumber}{IndentedLogMsg:l}";
1919

2020
/// <summary>
2121
/// The name of the ERROR log level.

tools/PsesLogAnalyzer/Analyze.ps1

+121-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
function Get-PsesRpcNotificationMessage {
2+
[CmdletBinding(DefaultParameterSetName = "PsesLogEntry")]
3+
param(
4+
# Specifies a path to one or more PSES EditorServices log files.
5+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Path")]
6+
[Alias("PSPath")]
7+
[ValidateNotNullOrEmpty()]
8+
[string]
9+
$Path,
10+
11+
# Specifies PsesLogEntry objects to analyze.
12+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "PsesLogEntry", ValueFromPipeline = $true)]
13+
[ValidateNotNull()]
14+
[psobject[]]
15+
$LogEntry,
16+
17+
# Specifies a filter for either client or server sourced notifications. By default both are output.
18+
[Parameter()]
19+
[ValidateSet('Client', 'Server')]
20+
[string]
21+
$Source
22+
)
23+
24+
begin {
25+
if ($PSCmdlet.ParameterSetName -eq "Path") {
26+
$logEntries = Parse-PsesLog $Path
27+
}
28+
}
29+
30+
process {
31+
if ($PSCmdlet.ParameterSetName -eq "PsesLogEntry") {
32+
$logEntries = $LogEntry
33+
}
34+
35+
foreach ($entry in $logEntries) {
36+
if ($entry.LogMessageType -eq 'Notification') {
37+
if (!$Source -or ($entry.Message.Source -eq $Source)) {
38+
$entry
39+
}
40+
}
41+
}
42+
}
43+
}
44+
145
function Get-PsesRpcMessageResponseTime {
246
[CmdletBinding(DefaultParameterSetName="Path")]
347
param(
@@ -31,11 +75,11 @@ function Get-PsesRpcMessageResponseTime {
3175
# Populate $requests hashtable with request timestamps
3276
$requests = @{}
3377
$logEntries |
34-
Where-Object MessageType -match Request |
78+
Where-Object LogMessageType -match Request |
3579
Foreach-Object { $requests[$_.Message.Id] = $_.Timestamp }
3680

3781
$res = $logEntries |
38-
Where-Object MessageType -match Response |
82+
Where-Object LogMessageType -match Response |
3983
Foreach-Object {
4084
$elapsedMilliseconds = [int]($_.Timestamp - $requests[$_.Message.Id]).TotalMilliseconds
4185
[PsesLogEntryElapsed]::new($_, $elapsedMilliseconds)
@@ -44,3 +88,78 @@ function Get-PsesRpcMessageResponseTime {
4488
$res
4589
}
4690
}
91+
92+
function Get-PsesScriptAnalysisCompletionTime {
93+
[CmdletBinding(DefaultParameterSetName = "PsesLogEntry")]
94+
param(
95+
# Specifies a path to one or more PSES EditorServices log files.
96+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Path")]
97+
[Alias("PSPath")]
98+
[ValidateNotNullOrEmpty()]
99+
[string]
100+
$Path,
101+
102+
# Specifies PsesLogEntry objects to analyze.
103+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "PsesLogEntry", ValueFromPipeline = $true)]
104+
[ValidateNotNull()]
105+
[psobject[]]
106+
$LogEntry
107+
)
108+
109+
begin {
110+
if ($PSCmdlet.ParameterSetName -eq "Path") {
111+
$logEntries = Parse-PsesLog $Path
112+
}
113+
}
114+
115+
process {
116+
if ($PSCmdlet.ParameterSetName -eq "PsesLogEntry") {
117+
$logEntries = $LogEntry
118+
}
119+
120+
foreach ($entry in $logEntries) {
121+
if (($entry.LogMessageType -eq 'Log') -and ($entry.Message.Data -match '^\s*Script analysis of.*\[(?<ms>\d+)ms\]\s*$')) {
122+
$elapsedMilliseconds = [int]$matches["ms"]
123+
[PsesLogEntryElapsed]::new($entry, $elapsedMilliseconds)
124+
}
125+
}
126+
}
127+
}
128+
129+
function Get-PsesIntelliSenseCompletionTime {
130+
[CmdletBinding(DefaultParameterSetName = "PsesLogEntry")]
131+
param(
132+
# Specifies a path to one or more PSES EditorServices log files.
133+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Path")]
134+
[Alias("PSPath")]
135+
[ValidateNotNullOrEmpty()]
136+
[string]
137+
$Path,
138+
139+
# Specifies PsesLogEntry objects to analyze.
140+
[Parameter(Mandatory = $true, Position = 0, ParameterSetName = "PsesLogEntry", ValueFromPipeline = $true)]
141+
[ValidateNotNull()]
142+
[psobject[]]
143+
$LogEntry
144+
)
145+
146+
begin {
147+
if ($PSCmdlet.ParameterSetName -eq "Path") {
148+
$logEntries = Parse-PsesLog $Path
149+
}
150+
}
151+
152+
process {
153+
if ($PSCmdlet.ParameterSetName -eq "PsesLogEntry") {
154+
$logEntries = $LogEntry
155+
}
156+
157+
foreach ($entry in $logEntries) {
158+
# IntelliSense completed in 320ms.
159+
if (($entry.LogMessageType -eq 'Log') -and ($entry.Message.Data -match '^\s*IntelliSense completed in\s+(?<ms>\d+)ms.\s*$')) {
160+
$elapsedMilliseconds = [int]$matches["ms"]
161+
[PsesLogEntryElapsed]::new($entry, $elapsedMilliseconds)
162+
}
163+
}
164+
}
165+
}

tools/PsesLogAnalyzer/Parse-PsesLog.ps1

+42-33
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
$peekBuf = $null
3-
$currentLineNum = 1
3+
$currentLineNum = 0
44
$logEntryIndex = 0
55

66
function Parse-PsesLog {
@@ -44,7 +44,7 @@ function Parse-PsesLog {
4444
$script:logEntryIndex = 0
4545

4646
if ($OldLogFormat) {
47-
# Example old log entry start
47+
# Example old log entry start:
4848
# 2018-11-15 19:49:06.979 [NORMAL] C:\PowerShellEditorServices\src\PowerShellEditorServices.Host\EditorServicesHost.cs: In method 'StartLogging', line 160:
4949
$logEntryRegex =
5050
[regex]::new(
@@ -53,10 +53,10 @@ function Parse-PsesLog {
5353
}
5454
else {
5555
# Example new log entry start:
56-
# 2018-11-24 12:26:58.302 [DIAGNOSTIC] tid:28 in 'ReadMessage' C:\Users\Keith\GitHub\rkeithhill\PowerShellEditorServices\src\PowerShellEditorServices.Protocol\MessageProtocol\MessageReader.cs:114:
56+
# 2018-11-24 12:26:58.302 [DIAGNOSTIC] tid:28 in 'ReadMessage' C:\Users\Keith\GitHub\rkeithhill\PowerShellEditorServices\src\PowerShellEditorServices.Protocol\MessageProtocol\MessageReader.cs: line 114
5757
$logEntryRegex =
5858
[regex]::new(
59-
'^(?<ts>[^\[]+)\[(?<lev>([^\]]+))\]\s+tid:(?<tid>\d+)\s+in\s+''(?<meth>\w+)''\s+(?<file>..[^:]+):(?<line>\d+)',
59+
'^(?<ts>[^\[]+)\[(?<lev>([^\]]+))\]\s+tid:(?<tid>\d+)\s+in\s+''(?<meth>\w+)''\s+(?<file>..[^:]+):\s+line\s+(?<line>\d+)',
6060
[System.Text.RegularExpressions.RegexOptions]::Compiled -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
6161
}
6262

@@ -101,7 +101,7 @@ function Parse-PsesLog {
101101
}
102102

103103
while ($line -notmatch $logEntryRegex) {
104-
Write-Warning "Ignoring line: '$line'"
104+
Write-Warning "Ignoring line:${currentLineNum} '$line'"
105105
$line = nextLine
106106
}
107107

@@ -117,10 +117,10 @@ function Parse-PsesLog {
117117
[string]$file = $matches["file"]
118118
[int]$lineNumber = $matches["line"]
119119

120-
$message = parseMessage $method
120+
$message = parseLogMessage $method
121121

122122
[PsesLogEntry]::new($script:logEntryIndex, $timestamp, $timestampStr, $logLevel, $threadId, $method,
123-
$file, $lineNumber, $message.MessageType, $message.Message)
123+
$file, $lineNumber, $message.LogMessageType, $message.LogMessage)
124124

125125
if ($DebugTimingInfo) {
126126
$sw.Stop()
@@ -132,10 +132,10 @@ function Parse-PsesLog {
132132
$script:logEntryIndex++
133133
}
134134

135-
function parseMessage([string]$Method) {
135+
function parseLogMessage([string]$Method) {
136136
$result = [PSCustomObject]@{
137-
MessageType = [PsesMessageType]::Log
138-
Message = $null
137+
LogMessageType = [PsesLogMessageType]::Log
138+
LogMessage = $null
139139
}
140140

141141
$line = nextLine
@@ -145,45 +145,54 @@ function Parse-PsesLog {
145145
}
146146

147147
if (($Method -eq 'ReadMessage') -and
148-
($line -match '\s+Received Request ''(?<msg>[^'']+)'' with id (?<id>\d+)')) {
149-
$result.MessageType = [PsesMessageType]::Request
148+
($line -match '^\s+Received Request ''(?<msg>[^'']+)'' with id (?<id>\d+)')) {
149+
$result.LogMessageType = [PsesLogMessageType]::Request
150150
$msg = $matches["msg"]
151151
$id = $matches["id"]
152-
$json = parseJsonMessageBody
153-
$result.Message = [PsesJsonRpcMessage]::new($msg, $id, $json.Data, $json.DataSize)
152+
$json = parseLogMessageBodyAsJson
153+
$result.LogMessage = [PsesJsonRpcMessage]::new($msg, $id, $json.Data, $json.DataSize)
154154
}
155155
elseif (($Method -eq 'ReadMessage') -and
156-
($line -match '\s+Received event ''(?<msg>[^'']+)''')) {
157-
$result.MessageType = [PsesMessageType]::Notification
156+
($line -match '^\s+Received event ''(?<msg>[^'']+)''')) {
157+
$result.LogMessageType = [PsesLogMessageType]::Notification
158158
$msg = $matches["msg"]
159-
$json = parseJsonMessageBody
160-
$result.Message = [PsesNotificationMessage]::new($msg, [PsesNotificationSource]::Client, $json.Data, $json.DataSize)
159+
$json = parseLogMessageBodyAsJson
160+
$result.LogMessage = [PsesNotificationMessage]::new($msg, [PsesNotificationSource]::Client, $json.Data, $json.DataSize)
161161
}
162162
elseif (($Method -eq 'WriteMessage') -and
163-
($line -match '\s+Writing Response ''(?<msg>[^'']+)'' with id (?<id>\d+)')) {
164-
$result.MessageType = [PsesMessageType]::Response
163+
($line -match '^\s+Writing Response ''(?<msg>[^'']+)'' with id (?<id>\d+)')) {
164+
$result.LogMessageType = [PsesLogMessageType]::Response
165165
$msg = $matches["msg"]
166166
$id = $matches["id"]
167-
$json = parseJsonMessageBody
168-
$result.Message = [PsesJsonRpcMessage]::new($msg, $id, $json.Data, $json.DataSize)
167+
$json = parseLogMessageBodyAsJson
168+
$result.LogMessage = [PsesJsonRpcMessage]::new($msg, $id, $json.Data, $json.DataSize)
169169
}
170170
elseif (($Method -eq 'WriteMessage') -and
171-
($line -match '\s+Writing event ''(?<msg>[^'']+)''')) {
172-
$result.MessageType = [PsesMessageType]::Notification
171+
($line -match '^\s+Writing event ''(?<msg>[^'']+)''')) {
172+
$result.LogMessageType = [PsesLogMessageType]::Notification
173173
$msg = $matches["msg"]
174-
$json = parseJsonMessageBody
175-
$result.Message = [PsesNotificationMessage]::new($msg, [PsesNotificationSource]::Server, $json.Data, $json.DataSize)
174+
$json = parseLogMessageBodyAsJson
175+
$result.LogMessage = [PsesNotificationMessage]::new($msg, [PsesNotificationSource]::Server, $json.Data, $json.DataSize)
176176
}
177177
else {
178-
$result.MessageType = [PsesMessageType]::Log
179-
$body = parseMessageBody $line
180-
$result.Message = [PsesLogMessage]::new($body)
178+
if ($line -match '^\s+Exception: ') {
179+
$result.LogMessageType = [PsesLogMessageType]::Exception
180+
}
181+
elseif ($line -match '^\s+Handled exception: ') {
182+
$result.LogMessageType = [PsesLogMessageType]::HandledException
183+
}
184+
else {
185+
$result.LogMessageType = [PsesLogMessageType]::Log
186+
}
187+
188+
$body = parseLogMessageBody $line
189+
$result.LogMessage = [PsesLogMessage]::new($body)
181190
}
182191

183192
$result
184193
}
185194

186-
function parseMessageBody([string]$startLine = '', [switch]$Discard) {
195+
function parseLogMessageBody([string]$startLine = '', [switch]$Discard) {
187196
if (!$Discard) {
188197
$strBld = [System.Text.StringBuilder]::new($startLine, 4096)
189198
$newLine = "`r`n"
@@ -219,7 +228,7 @@ function Parse-PsesLog {
219228
}
220229
}
221230

222-
function parseJsonMessageBody() {
231+
function parseLogMessageBodyAsJson() {
223232
$result = [PSCustomObject]@{
224233
Data = $null
225234
DataSize = 0
@@ -228,11 +237,11 @@ function Parse-PsesLog {
228237
$obj = $null
229238

230239
if ($SkipRpcMessageBody) {
231-
parseMessageBody -Discard
240+
parseLogMessageBody -Discard
232241
return $result
233242
}
234243
else {
235-
$result.Data = parseMessageBody
244+
$result.Data = parseLogMessageBody
236245
$result.DataSize = $result.Data.Length
237246
}
238247

tools/PsesLogAnalyzer/PsesLogAnalyzer.format.ps1xml

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
<Alignment>right</Alignment>
2424
</TableColumnHeader>
2525
<TableColumnHeader>
26-
<Label>MessageType</Label>
27-
<Width>13</Width>
26+
<Label>LogMessageType</Label>
27+
<Width>15</Width>
2828
<Alignment>left</Alignment>
2929
</TableColumnHeader>
3030
<TableColumnHeader>
@@ -45,7 +45,7 @@
4545
<PropertyName>ThreadId</PropertyName>
4646
</TableColumnItem>
4747
<TableColumnItem>
48-
<PropertyName>MessageType</PropertyName>
48+
<PropertyName>LogMessageType</PropertyName>
4949
</TableColumnItem>
5050
<TableColumnItem>
5151
<ScriptBlock>($_.Message -split "`r`n")[0]</ScriptBlock>

tools/PsesLogAnalyzer/PsesLogAnalyzer.psd1

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ FormatsToProcess = @('PsesLogAnalyzer.format.ps1xml')
7171
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
7272
FunctionsToExport = @(
7373
'Parse-PsesLog',
74-
'Get-PsesRpcMessageResponseTime'
74+
'Get-PsesRpcMessageResponseTime',
75+
'Get-PsesRpcNotificationMessage',
76+
'Get-PsesScriptAnalysisCompletionTime',
77+
'Get-PsesIntelliSenseCompletionTime'
7578
)
7679

7780
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.

0 commit comments

Comments
 (0)