10
10
11
11
namespace Microsoft . NET . Build . Containers . Logging ;
12
12
13
+ /// <summary>
14
+ /// a struct that maps to the parameters of the MSBuild LogX methods. We'll extract this from M.E.ILogger state/scope information so that we can be maximally compatible with the MSBuild logging system.
15
+ /// </summary>
16
+ /// <param name="subcategory"></param>
17
+ /// <param name="code"></param>
18
+ /// <param name="helpKeyword"></param>
19
+ /// <param name="file"></param>
20
+ /// <param name="lineNumber"></param>
21
+ /// <param name="columnNumber"></param>
22
+ /// <param name="endLineNumber"></param>
23
+ /// <param name="endColumnNumber"></param>
24
+ /// <param name="message"></param>
25
+ internal record struct MSBuildMessageParameters ( string ? subcategory ,
26
+ string ? code ,
27
+ string ? helpKeyword ,
28
+ string ? file ,
29
+ int ? lineNumber ,
30
+ int ? columnNumber ,
31
+ int ? endLineNumber ,
32
+ int ? endColumnNumber ,
33
+ string message ) ;
34
+
13
35
/// <summary>
14
36
/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper.
15
37
/// </summary>
38
+ /// <remarks>
39
+ /// This logger is designed to be used with MSBuild tasks, allowing logs to be written in a way that integrates with the MSBuild logging system.
40
+ /// It looks for specific property names in the state/scope parts of the message and maps them to the parameters of the MSBuild LogX methods.
41
+ /// Those specific keys are:
42
+ /// <list type="bullet">
43
+ /// <item><term>Subcategory</term></item>
44
+ /// <item><term>Code</term></item>
45
+ /// <item><term>HelpKeyword</term></item>
46
+ /// <item><term>File</term></item>
47
+ /// <item><term>LineNumber</term></item>
48
+ /// <item><term>ColumnNumber</term></item>
49
+ /// <item><term>EndLineNumber</term></item>
50
+ /// <item><term>EndColumnNumber</term></item>
51
+ /// <item><term>{OriginalFormat}</term><description>(usually provided by the underlying logging framework)</description></item>
52
+ /// </list>
53
+ ///
54
+ /// So if you add these to the scope (e.g. via <code lang="csharp">_logger.BeginScope(new Dictionary<string, object>{ ... }))</code> or on the message format itself,
55
+ /// they will be extracted and used to format the message correctly for MSBuild.
56
+ /// </remarks>
16
57
internal sealed class MSBuildLogger : ILogger
17
58
{
18
59
private static readonly IDisposable Scope = new DummyDisposable ( ) ;
@@ -38,18 +79,20 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
38
79
switch ( logLevel )
39
80
{
40
81
case LogLevel . Trace :
41
- _loggingHelper . LogMessage ( MessageImportance . Low , message ) ;
82
+ _loggingHelper . LogMessage ( message . subcategory , message . code , message . helpKeyword , message . file , message . lineNumber ?? 0 , message . columnNumber ?? 0 , message . endLineNumber ?? 0 , message . endColumnNumber ?? 0 , MessageImportance . Low , message . message ) ;
42
83
break ;
43
84
case LogLevel . Debug :
85
+ _loggingHelper . LogMessage ( message . subcategory , message . code , message . helpKeyword , message . file , message . lineNumber ?? 0 , message . columnNumber ?? 0 , message . endLineNumber ?? 0 , message . endColumnNumber ?? 0 , MessageImportance . Normal , message . message ) ;
86
+ break ;
44
87
case LogLevel . Information :
45
- _loggingHelper . LogMessage ( MessageImportance . High , message ) ;
88
+ _loggingHelper . LogMessage ( message . subcategory , message . code , message . helpKeyword , message . file , message . lineNumber ?? 0 , message . columnNumber ?? 0 , message . endLineNumber ?? 0 , message . endColumnNumber ?? 0 , MessageImportance . High , message . message ) ;
46
89
break ;
47
90
case LogLevel . Warning :
48
- _loggingHelper . LogWarning ( message ) ;
91
+ _loggingHelper . LogWarning ( message . subcategory , message . code , message . helpKeyword , message . file , message . lineNumber ?? 0 , message . columnNumber ?? 0 , message . endLineNumber ?? 0 , message . endColumnNumber ?? 0 , message . message ) ;
49
92
break ;
50
93
case LogLevel . Error :
51
94
case LogLevel . Critical :
52
- _loggingHelper . LogError ( message ) ;
95
+ _loggingHelper . LogError ( message . subcategory , message . code , message . helpKeyword , message . file , message . lineNumber ?? 0 , message . columnNumber ?? 0 , message . endLineNumber ?? 0 , message . endColumnNumber ?? 0 , message . message ) ;
53
96
break ;
54
97
case LogLevel . None :
55
98
break ;
@@ -58,8 +101,9 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
58
101
}
59
102
}
60
103
61
- public static string FormatMessage < TState > ( string category , TState state , Exception ? exception , Func < TState , Exception ? , string > formatter , IExternalScopeProvider ? scopeProvider )
104
+ public static MSBuildMessageParameters FormatMessage < TState > ( string category , TState state , Exception ? exception , Func < TState , Exception ? , string > formatter , IExternalScopeProvider ? scopeProvider )
62
105
{
106
+ MSBuildMessageParameters message = default ;
63
107
using var builder = new SpanBasedStringBuilder ( ) ;
64
108
var categoryBlock = string . Concat ( "[" . AsSpan ( ) , category . AsSpan ( ) , "] " . AsSpan ( ) ) ;
65
109
builder . Append ( categoryBlock ) ;
@@ -72,37 +116,131 @@ public static string FormatMessage<TState>(string category, TState state, Except
72
116
// scope will be our dictionary thing we need to probe into
73
117
scopeProvider . ForEachScope ( ( scope , state ) =>
74
118
{
75
- var stateItems = state as IReadOnlyList < KeyValuePair < string , object ? > > ;
76
- var originalFormat = ( stateItems ? . FirstOrDefault ( kvp => kvp . Key == "{OriginalFormat}" ) . Value as string ) ! ;
119
+ var stateItems = ( state as IReadOnlyList < KeyValuePair < string , object ? > > ) ! ;
120
+ string originalFormat = null ! ;
121
+
122
+ foreach ( var kvp in stateItems )
123
+ {
124
+ switch ( kvp . Key )
125
+ {
126
+ case "{OriginalFormat}" :
127
+ // If the key is {OriginalFormat}, we will use it to set the originalFormat variable.
128
+ // This is used to avoid appending the same key again in the message.
129
+ if ( kvp . Value is string format )
130
+ {
131
+ originalFormat = format ;
132
+ }
133
+ continue ;
134
+ case "Subcategory" :
135
+ message . subcategory = kvp . Value as string ;
136
+ continue ;
137
+ case "Code" :
138
+ message . code = kvp . Value as string ;
139
+ continue ;
140
+ case "HelpKeyword" :
141
+ message . helpKeyword = kvp . Value as string ;
142
+ continue ;
143
+ case "File" :
144
+ message . file = kvp . Value as string ;
145
+ continue ;
146
+ case "LineNumber" :
147
+ if ( kvp . Value is int lineNumber )
148
+ message . lineNumber = lineNumber ;
149
+ continue ;
150
+ case "ColumnNumber" :
151
+ if ( kvp . Value is int columnNumber )
152
+ message . columnNumber = columnNumber ;
153
+ continue ;
154
+ case "EndLineNumber" :
155
+ if ( kvp . Value is int endLineNumber )
156
+ message . endLineNumber = endLineNumber ;
157
+ continue ;
158
+ case "EndColumnNumber" :
159
+ if ( kvp . Value is int endColumnNumber )
160
+ message . endColumnNumber = endColumnNumber ;
161
+ continue ;
162
+ default :
163
+ var wrappedKey = "{" + kvp . Key + "}" ;
164
+ if ( originalFormat . Contains ( wrappedKey ) )
165
+ {
166
+ // If the key is part of the format string of the original format, we don't need to append it again.
167
+ continue ;
168
+ }
169
+
170
+ // Otherwise, append the key and value to the message.
171
+ // if MSbuild had a property bag concept on the message APIs,
172
+ // we could use that instead of appending to the message.
173
+
174
+ builder . Append ( $ " { kvp . Key } ={ kvp . Value } ") ;
175
+ continue ;
176
+ }
177
+ }
77
178
78
179
if ( scope is IDictionary < string , object > dict )
79
180
{
80
181
foreach ( var kvp in dict )
81
182
{
82
- if ( kvp . Key == "{OriginalFormat}" )
183
+ switch ( kvp . Key )
83
184
{
84
- // Skip the original format key
85
- continue ;
86
- }
185
+ // map all of the keys we decide are special and map to MSbuild message concepts
186
+ case "{OriginalFormat}" :
187
+ continue ;
188
+ case "Subcategory" :
189
+ message . subcategory = kvp . Value as string ;
190
+ continue ;
191
+ case "Code" :
192
+ message . code = kvp . Value as string ;
193
+ continue ;
194
+ case "HelpKeyword" :
195
+ message . helpKeyword = kvp . Value as string ;
196
+ continue ;
197
+ case "File" :
198
+ message . file = kvp . Value as string ;
199
+ continue ;
200
+ case "LineNumber" :
201
+ if ( kvp . Value is int lineNumber )
202
+ message . lineNumber = lineNumber ;
203
+ continue ;
204
+ case "ColumnNumber" :
205
+ if ( kvp . Value is int columnNumber )
206
+ message . columnNumber = columnNumber ;
207
+ continue ;
208
+ case "EndLineNumber" :
209
+ if ( kvp . Value is int endLineNumber )
210
+ message . endLineNumber = endLineNumber ;
211
+ continue ;
212
+ case "EndColumnNumber" :
213
+ if ( kvp . Value is int endColumnNumber )
214
+ message . endColumnNumber = endColumnNumber ;
215
+ continue ;
216
+ default :
217
+ var wrappedKey = "{" + kvp . Key + "}" ;
218
+ if ( originalFormat . Contains ( wrappedKey ) )
219
+ {
220
+ // If the key is part of the format string of the original format, we don't need to append it again.
221
+ continue ;
222
+ }
87
223
88
- var wrappedKey = "{" + kvp . Key + "}" ;
89
- if ( originalFormat . Contains ( wrappedKey ) )
90
- {
91
- // If the key is part of the format string of the original format, we don't need to append it again.
92
- continue ;
93
- }
224
+ // Otherwise, append the key and value to the message.
225
+ // if MSbuild had a property bag concept on the message APIs,
226
+ // we could use that instead of appending to the message.
94
227
95
- builder . Append ( $ " { kvp . Key } ={ kvp . Value } ") ;
228
+ builder . Append ( $ " { kvp . Key } ={ kvp . Value } ") ;
229
+ continue ;
230
+ }
96
231
}
97
232
}
98
233
else if ( scope is string s )
99
234
{
100
235
builder . Append ( $ " { s } ") ;
101
236
}
237
+
238
+
102
239
} , state ) ;
103
240
}
104
241
105
- return builder . ToString ( ) ;
242
+ message . message = builder . ToString ( ) ;
243
+ return message ;
106
244
}
107
245
108
246
/// <summary>
0 commit comments