Skip to content

Commit fae2e39

Browse files
committed
push improved MSBuildLogger to a separate project in case we want to spin it off
1 parent 460bff1 commit fae2e39

File tree

7 files changed

+294
-71
lines changed

7 files changed

+294
-71
lines changed

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

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
</PropertyGroup>
2626

2727
<ItemGroup>
28-
<ProjectReference Include="..\..\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj"
29-
Condition="'$(TargetFramework)' != 'net472'">
28+
<ProjectReference Include="..\..\Cli\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj" Condition="'$(TargetFramework)' != 'net472'">
3029
<!-- This is referenced by the core CLI as well so it doesn't need to be redistributed
3130
in the containers task folder. -->
3231
<IncludeAssets>build</IncludeAssets>
@@ -104,6 +103,10 @@
104103
<AdditionalFiles Include="PublicAPI/$(TargetFramework)/PublicAPI.Unshipped.txt" />
105104
</ItemGroup>
106105

106+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
107+
<ProjectReference Include="..\..\Microsoft.Extensions.Logging.MSBuild\Microsoft.Extensions.Logging.MSBuild.csproj" />
108+
</ItemGroup>
109+
107110
<!-- This target adds all of our PackageReference and ProjectReference's runtime assets to our package output. -->
108111
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
109112
<ItemGroup>

src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using System.Text.Json.Nodes;
66
using Microsoft.Build.Framework;
77
using Microsoft.Extensions.Logging;
8-
using Microsoft.NET.Build.Containers.Logging;
8+
using Microsoft.Extensions.Logging.MSBuild;
99
using Microsoft.NET.Build.Containers.Resources;
1010
using ILogger = Microsoft.Extensions.Logging.ILogger;
1111
using Task = System.Threading.Tasks.Task;

src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using Microsoft.Build.Framework;
55
using Microsoft.Extensions.Logging;
6-
using Microsoft.NET.Build.Containers.Logging;
6+
using Microsoft.Extensions.Logging.MSBuild;
77
using Microsoft.NET.Build.Containers.Resources;
88
using ILogger = Microsoft.Extensions.Logging.ILogger;
99

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
//
4+
5+
using Microsoft.Build.Framework;
6+
using Microsoft.Build.Utilities;
7+
using Microsoft.NET.StringTools;
8+
9+
namespace Microsoft.Extensions.Logging.MSBuild;
10+
11+
/// <summary>
12+
/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper.
13+
/// </summary>
14+
/// <remarks>
15+
/// 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.
16+
/// 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.
17+
/// Those specific keys are:
18+
/// <list type="bullet">
19+
/// <item><term>Subcategory</term></item>
20+
/// <item><term>Code</term></item>
21+
/// <item><term>HelpKeyword</term></item>
22+
/// <item><term>File</term></item>
23+
/// <item><term>LineNumber</term></item>
24+
/// <item><term>ColumnNumber</term></item>
25+
/// <item><term>EndLineNumber</term></item>
26+
/// <item><term>EndColumnNumber</term></item>
27+
/// <item><term>{OriginalFormat}</term><description>(usually provided by the underlying logging framework)</description></item>
28+
/// </list>
29+
///
30+
/// So if you add these to the scope (e.g. via <code lang="csharp">_logger.BeginScope(new Dictionary&lt;string, object&gt;{ ... }))</code> or on the message format itself,
31+
/// they will be extracted and used to format the message correctly for MSBuild.
32+
/// </remarks>
33+
public sealed class MSBuildLogger : ILogger
34+
{
35+
private static readonly IDisposable Scope = new DummyDisposable();
36+
37+
private readonly TaskLoggingHelper _loggingHelper;
38+
private readonly string _category;
39+
private IExternalScopeProvider? _scopeProvider;
40+
41+
public MSBuildLogger(string category, TaskLoggingHelper loggingHelperToWrap, IExternalScopeProvider? scopeProvider = null)
42+
{
43+
_category = category;
44+
_loggingHelper = loggingHelperToWrap;
45+
_scopeProvider = scopeProvider;
46+
}
47+
48+
IDisposable ILogger.BeginScope<TState>(TState state) => _scopeProvider?.Push(state) ?? Scope;
49+
50+
public bool IsEnabled(LogLevel logLevel) => true;
51+
52+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
53+
{
54+
var message = FormatMessage(_category, state, exception, formatter, _scopeProvider);
55+
switch (logLevel)
56+
{
57+
case LogLevel.Trace:
58+
_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);
59+
break;
60+
case LogLevel.Debug:
61+
_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);
62+
break;
63+
case LogLevel.Information:
64+
_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);
65+
break;
66+
case LogLevel.Warning:
67+
_loggingHelper.LogWarning(message.subcategory, message.code, message.helpKeyword, message.file, message.lineNumber ?? 0, message.columnNumber ?? 0, message.endLineNumber ?? 0, message.endColumnNumber ?? 0, message.message);
68+
break;
69+
case LogLevel.Error:
70+
case LogLevel.Critical:
71+
_loggingHelper.LogError(message.subcategory, message.code, message.helpKeyword, message.file, message.lineNumber ?? 0, message.columnNumber ?? 0, message.endLineNumber ?? 0, message.endColumnNumber ?? 0, message.message);
72+
break;
73+
case LogLevel.None:
74+
break;
75+
default:
76+
break;
77+
}
78+
}
79+
80+
private static MSBuildMessageParameters FormatMessage<TState>(string category, TState state, Exception? exception, Func<TState, Exception?, string> formatter, IExternalScopeProvider? scopeProvider)
81+
{
82+
MSBuildMessageParameters message = default;
83+
using var builder = new SpanBasedStringBuilder();
84+
var categoryBlock = string.Concat("[".AsSpan(), category.AsSpan(), "] ".AsSpan());
85+
builder.Append(categoryBlock);
86+
var formatted = formatter(state, exception);
87+
builder.Append(formatted);
88+
89+
if (scopeProvider is not null)
90+
{
91+
// state will be a FormattedLogValues instance
92+
// scope will be our dictionary thing we need to probe into
93+
scopeProvider.ForEachScope((scope, state) =>
94+
{
95+
var stateItems = (state as IReadOnlyList<KeyValuePair<string, object?>>)!;
96+
string originalFormat = null!;
97+
98+
foreach (var kvp in stateItems)
99+
{
100+
switch (kvp.Key)
101+
{
102+
case "{OriginalFormat}":
103+
// If the key is {OriginalFormat}, we will use it to set the originalFormat variable.
104+
// This is used to avoid appending the same key again in the message.
105+
if (kvp.Value is string format)
106+
{
107+
originalFormat = format;
108+
}
109+
continue;
110+
case "Subcategory":
111+
message.subcategory = kvp.Value as string;
112+
continue;
113+
case "Code":
114+
message.code = kvp.Value as string;
115+
continue;
116+
case "HelpKeyword":
117+
message.helpKeyword = kvp.Value as string;
118+
continue;
119+
case "File":
120+
message.file = kvp.Value as string;
121+
continue;
122+
case "LineNumber":
123+
if (kvp.Value is int lineNumber)
124+
message.lineNumber = lineNumber;
125+
continue;
126+
case "ColumnNumber":
127+
if (kvp.Value is int columnNumber)
128+
message.columnNumber = columnNumber;
129+
continue;
130+
case "EndLineNumber":
131+
if (kvp.Value is int endLineNumber)
132+
message.endLineNumber = endLineNumber;
133+
continue;
134+
case "EndColumnNumber":
135+
if (kvp.Value is int endColumnNumber)
136+
message.endColumnNumber = endColumnNumber;
137+
continue;
138+
default:
139+
var wrappedKey = "{" + kvp.Key + "}";
140+
if (originalFormat.Contains(wrappedKey))
141+
{
142+
// If the key is part of the format string of the original format, we don't need to append it again.
143+
continue;
144+
}
145+
146+
// Otherwise, append the key and value to the message.
147+
// if MSbuild had a property bag concept on the message APIs,
148+
// we could use that instead of appending to the message.
149+
150+
builder.Append($" {kvp.Key}={kvp.Value}");
151+
continue;
152+
}
153+
}
154+
155+
if (scope is IDictionary<string, object> dict)
156+
{
157+
foreach (var kvp in dict)
158+
{
159+
switch (kvp.Key)
160+
{
161+
// map all of the keys we decide are special and map to MSbuild message concepts
162+
case "{OriginalFormat}":
163+
continue;
164+
case "Subcategory":
165+
message.subcategory = kvp.Value as string;
166+
continue;
167+
case "Code":
168+
message.code = kvp.Value as string;
169+
continue;
170+
case "HelpKeyword":
171+
message.helpKeyword = kvp.Value as string;
172+
continue;
173+
case "File":
174+
message.file = kvp.Value as string;
175+
continue;
176+
case "LineNumber":
177+
if (kvp.Value is int lineNumber)
178+
message.lineNumber = lineNumber;
179+
continue;
180+
case "ColumnNumber":
181+
if (kvp.Value is int columnNumber)
182+
message.columnNumber = columnNumber;
183+
continue;
184+
case "EndLineNumber":
185+
if (kvp.Value is int endLineNumber)
186+
message.endLineNumber = endLineNumber;
187+
continue;
188+
case "EndColumnNumber":
189+
if (kvp.Value is int endColumnNumber)
190+
message.endColumnNumber = endColumnNumber;
191+
continue;
192+
default:
193+
var wrappedKey = "{" + kvp.Key + "}";
194+
if (originalFormat.Contains(wrappedKey))
195+
{
196+
// If the key is part of the format string of the original format, we don't need to append it again.
197+
continue;
198+
}
199+
200+
// Otherwise, append the key and value to the message.
201+
// if MSbuild had a property bag concept on the message APIs,
202+
// we could use that instead of appending to the message.
203+
204+
builder.Append($" {kvp.Key}={kvp.Value}");
205+
continue;
206+
}
207+
}
208+
}
209+
else if (scope is string s)
210+
{
211+
builder.Append($" {s}");
212+
}
213+
214+
215+
}, state);
216+
}
217+
218+
message.message = builder.ToString();
219+
return message;
220+
}
221+
222+
223+
/// <summary>
224+
/// 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.
225+
/// </summary>
226+
/// <param name="subcategory"></param>
227+
/// <param name="code"></param>
228+
/// <param name="helpKeyword"></param>
229+
/// <param name="file"></param>
230+
/// <param name="lineNumber"></param>
231+
/// <param name="columnNumber"></param>
232+
/// <param name="endLineNumber"></param>
233+
/// <param name="endColumnNumber"></param>
234+
/// <param name="message"></param>
235+
private record struct MSBuildMessageParameters(string? subcategory,
236+
string? code,
237+
string? helpKeyword,
238+
string? file,
239+
int? lineNumber,
240+
int? columnNumber,
241+
int? endLineNumber,
242+
int? endColumnNumber,
243+
string message);
244+
245+
/// <summary>
246+
/// A simple disposable to describe scopes with <see cref="ILogger.BeginScope"/>.
247+
/// </summary>
248+
private sealed class DummyDisposable : IDisposable
249+
{
250+
public void Dispose() { }
251+
}
252+
253+
internal void SetScopeProvider(IExternalScopeProvider scopeProvider)
254+
{
255+
_scopeProvider = scopeProvider;
256+
}
257+
}

0 commit comments

Comments
 (0)