Skip to content

Commit 1e4468a

Browse files
committed
Make MSBuildLogger much more capable
1 parent e3cc7d3 commit 1e4468a

File tree

1 file changed

+157
-19
lines changed

1 file changed

+157
-19
lines changed

src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs

Lines changed: 157 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,50 @@
1010

1111
namespace Microsoft.NET.Build.Containers.Logging;
1212

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+
1335
/// <summary>
1436
/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper.
1537
/// </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>
1657
internal sealed class MSBuildLogger : ILogger
1758
{
1859
private static readonly IDisposable Scope = new DummyDisposable();
@@ -38,18 +79,20 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
3879
switch (logLevel)
3980
{
4081
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);
4283
break;
4384
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;
4487
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);
4689
break;
4790
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);
4992
break;
5093
case LogLevel.Error:
5194
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);
5396
break;
5497
case LogLevel.None:
5598
break;
@@ -58,8 +101,9 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
58101
}
59102
}
60103

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)
62105
{
106+
MSBuildMessageParameters message = default;
63107
using var builder = new SpanBasedStringBuilder();
64108
var categoryBlock = string.Concat("[".AsSpan(), category.AsSpan(), "] ".AsSpan());
65109
builder.Append(categoryBlock);
@@ -72,37 +116,131 @@ public static string FormatMessage<TState>(string category, TState state, Except
72116
// scope will be our dictionary thing we need to probe into
73117
scopeProvider.ForEachScope((scope, state) =>
74118
{
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+
}
77178

78179
if (scope is IDictionary<string, object> dict)
79180
{
80181
foreach (var kvp in dict)
81182
{
82-
if (kvp.Key == "{OriginalFormat}")
183+
switch (kvp.Key)
83184
{
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+
}
87223

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.
94227

95-
builder.Append($" {kvp.Key}={kvp.Value}");
228+
builder.Append($" {kvp.Key}={kvp.Value}");
229+
continue;
230+
}
96231
}
97232
}
98233
else if (scope is string s)
99234
{
100235
builder.Append($" {s}");
101236
}
237+
238+
102239
}, state);
103240
}
104241

105-
return builder.ToString();
242+
message.message = builder.ToString();
243+
return message;
106244
}
107245

108246
/// <summary>

0 commit comments

Comments
 (0)