-
Notifications
You must be signed in to change notification settings - Fork 1.1k
push improved MSBuildLogger to a separate project in case we want to spin it off #49751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
395a0e5
0febdbe
e116342
72276d3
f057254
8720d27
7fe2868
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,266 @@ | ||||||
// Copyright (c) .NET Foundation and contributors. All rights reserved. | ||||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||||||
// | ||||||
|
||||||
using Microsoft.Build.Framework; | ||||||
using Microsoft.Build.Utilities; | ||||||
using Microsoft.NET.StringTools; | ||||||
|
||||||
namespace Microsoft.Extensions.Logging.MSBuild; | ||||||
|
||||||
/// <summary> | ||||||
/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper. | ||||||
/// </summary> | ||||||
/// <remarks> | ||||||
/// 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. | ||||||
/// 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. | ||||||
/// Those specific keys are: | ||||||
/// <list type="bullet"> | ||||||
/// <item><term>Subcategory</term></item> | ||||||
/// <item><term>Code</term></item> | ||||||
/// <item><term>HelpKeyword</term></item> | ||||||
/// <item><term>File</term></item> | ||||||
/// <item><term>LineNumber</term></item> | ||||||
/// <item><term>ColumnNumber</term></item> | ||||||
/// <item><term>EndLineNumber</term></item> | ||||||
/// <item><term>EndColumnNumber</term></item> | ||||||
/// <item><term>{OriginalFormat}</term><description>(usually provided by the underlying logging framework)</description></item> | ||||||
/// </list> | ||||||
/// | ||||||
/// 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, | ||||||
/// they will be extracted and used to format the message correctly for MSBuild. | ||||||
/// </remarks> | ||||||
public sealed class MSBuildLogger : ILogger | ||||||
{ | ||||||
private static readonly IDisposable Scope = new DummyDisposable(); | ||||||
|
||||||
private readonly TaskLoggingHelper _loggingHelper; | ||||||
private readonly string _category; | ||||||
private IExternalScopeProvider? _scopeProvider; | ||||||
|
||||||
public MSBuildLogger(string category, TaskLoggingHelper loggingHelperToWrap, IExternalScopeProvider? scopeProvider = null) | ||||||
{ | ||||||
_category = category; | ||||||
_loggingHelper = loggingHelperToWrap; | ||||||
_scopeProvider = scopeProvider; | ||||||
} | ||||||
|
||||||
IDisposable ILogger.BeginScope<TState>(TState state) => _scopeProvider?.Push(state) ?? Scope; | ||||||
|
||||||
public bool IsEnabled(LogLevel logLevel) => | ||||||
logLevel switch | ||||||
{ | ||||||
LogLevel.Trace => _loggingHelper.LogsMessagesOfImportance(MessageImportance.Low), | ||||||
LogLevel.Debug => _loggingHelper.LogsMessagesOfImportance(MessageImportance.Normal), | ||||||
LogLevel.Information => _loggingHelper.LogsMessagesOfImportance(MessageImportance.High), | ||||||
LogLevel.Warning or LogLevel.Error or LogLevel.Critical => true, | ||||||
LogLevel.None => false, | ||||||
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null) | ||||||
}; | ||||||
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) | ||||||
{ | ||||||
var message = FormatMessage(_category, state, exception, formatter, _scopeProvider); | ||||||
switch (logLevel) | ||||||
{ | ||||||
case LogLevel.Trace: | ||||||
_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); | ||||||
break; | ||||||
case LogLevel.Debug: | ||||||
_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); | ||||||
break; | ||||||
case LogLevel.Information: | ||||||
_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); | ||||||
break; | ||||||
case LogLevel.Warning: | ||||||
_loggingHelper.LogWarning(message.subcategory, message.code, message.helpKeyword, message.file, message.lineNumber ?? 0, message.columnNumber ?? 0, message.endLineNumber ?? 0, message.endColumnNumber ?? 0, message.message); | ||||||
break; | ||||||
case LogLevel.Error: | ||||||
case LogLevel.Critical: | ||||||
_loggingHelper.LogError(message.subcategory, message.code, message.helpKeyword, message.file, message.lineNumber ?? 0, message.columnNumber ?? 0, message.endLineNumber ?? 0, message.endColumnNumber ?? 0, message.message); | ||||||
break; | ||||||
case LogLevel.None: | ||||||
break; | ||||||
default: | ||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
private static MSBuildMessageParameters FormatMessage<TState>(string category, TState state, Exception? exception, Func<TState, Exception?, string> formatter, IExternalScopeProvider? scopeProvider) | ||||||
{ | ||||||
MSBuildMessageParameters message = default; | ||||||
using var builder = new SpanBasedStringBuilder(); | ||||||
var categoryBlock = string.Concat("[".AsSpan(), category.AsSpan(), "] ".AsSpan()); | ||||||
builder.Append(categoryBlock); | ||||||
var formatted = formatter(state, exception); | ||||||
builder.Append(formatted); | ||||||
|
||||||
if (scopeProvider is not null) | ||||||
{ | ||||||
// state will be a FormattedLogValues instance | ||||||
// scope will be our dictionary thing we need to probe into | ||||||
scopeProvider.ForEachScope((scope, state) => | ||||||
{ | ||||||
var stateItems = (state as IReadOnlyList<KeyValuePair<string, object?>>)!; | ||||||
baronfel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
string originalFormat = null!; | ||||||
|
||||||
foreach (var kvp in stateItems) | ||||||
baronfel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
{ | ||||||
switch (kvp.Key) | ||||||
{ | ||||||
case "{OriginalFormat}": | ||||||
// If the key is {OriginalFormat}, we will use it to set the originalFormat variable. | ||||||
// This is used to avoid appending the same key again in the message. | ||||||
if (kvp.Value is string format) | ||||||
{ | ||||||
originalFormat = format; | ||||||
} | ||||||
continue; | ||||||
case "Subcategory": | ||||||
message.subcategory = kvp.Value as string; | ||||||
continue; | ||||||
case "Code": | ||||||
message.code = kvp.Value as string; | ||||||
continue; | ||||||
case "HelpKeyword": | ||||||
message.helpKeyword = kvp.Value as string; | ||||||
continue; | ||||||
baronfel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
case "File": | ||||||
message.file = kvp.Value as string; | ||||||
continue; | ||||||
case "LineNumber": | ||||||
if (kvp.Value is int lineNumber) | ||||||
message.lineNumber = lineNumber; | ||||||
continue; | ||||||
case "ColumnNumber": | ||||||
if (kvp.Value is int columnNumber) | ||||||
message.columnNumber = columnNumber; | ||||||
continue; | ||||||
case "EndLineNumber": | ||||||
if (kvp.Value is int endLineNumber) | ||||||
message.endLineNumber = endLineNumber; | ||||||
continue; | ||||||
case "EndColumnNumber": | ||||||
if (kvp.Value is int endColumnNumber) | ||||||
message.endColumnNumber = endColumnNumber; | ||||||
continue; | ||||||
default: | ||||||
var wrappedKey = "{" + kvp.Key + "}"; | ||||||
if (originalFormat.Contains(wrappedKey)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential null reference exception when originalFormat is null. The variable is declared as 'null!' but can remain null if no {OriginalFormat} key is found, causing a null reference when calling Contains().
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
{ | ||||||
// If the key is part of the format string of the original format, we don't need to append it again. | ||||||
continue; | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the message template includes formatting like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally agree - I should possibly look at interpreting this via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CompositeFormat.Parse method does not support named parameter placeholders like log message templates have. The CompositeFormat class does not have public members for examining the structure of the format string. I don't think CompositeFormat is useful here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was hoping it would be slightly more capable. In the past I've used MessageTemplates for this kind of thing, but there would be source-build concerns with using an external library. Might have to punt for v1 of this - at least if it stays in the SDK repo directly. |
||||||
|
||||||
// Otherwise, append the key and value to the message. | ||||||
// if MSbuild had a property bag concept on the message APIs, | ||||||
// we could use that instead of appending to the message. | ||||||
|
||||||
builder.Append($" {kvp.Key}={kvp.Value}"); | ||||||
continue; | ||||||
} | ||||||
} | ||||||
|
||||||
if (scope is IDictionary<string, object> dict) | ||||||
{ | ||||||
foreach (var kvp in dict) | ||||||
{ | ||||||
switch (kvp.Key) | ||||||
{ | ||||||
// map all of the keys we decide are special and map to MSbuild message concepts | ||||||
case "{OriginalFormat}": | ||||||
continue; | ||||||
case "Subcategory": | ||||||
message.subcategory = kvp.Value as string; | ||||||
continue; | ||||||
case "Code": | ||||||
message.code = kvp.Value as string; | ||||||
continue; | ||||||
case "HelpKeyword": | ||||||
message.helpKeyword = kvp.Value as string; | ||||||
continue; | ||||||
case "File": | ||||||
message.file = kvp.Value as string; | ||||||
continue; | ||||||
case "LineNumber": | ||||||
if (kvp.Value is int lineNumber) | ||||||
message.lineNumber = lineNumber; | ||||||
continue; | ||||||
case "ColumnNumber": | ||||||
if (kvp.Value is int columnNumber) | ||||||
message.columnNumber = columnNumber; | ||||||
continue; | ||||||
case "EndLineNumber": | ||||||
if (kvp.Value is int endLineNumber) | ||||||
message.endLineNumber = endLineNumber; | ||||||
continue; | ||||||
case "EndColumnNumber": | ||||||
if (kvp.Value is int endColumnNumber) | ||||||
message.endColumnNumber = endColumnNumber; | ||||||
continue; | ||||||
default: | ||||||
var wrappedKey = "{" + kvp.Key + "}"; | ||||||
if (originalFormat.Contains(wrappedKey)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential null reference exception when originalFormat is null. This is the same issue as line 140 - originalFormat can be null when no {OriginalFormat} key is found in the state items.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
{ | ||||||
// If the key is part of the format string of the original format, we don't need to append it again. | ||||||
continue; | ||||||
} | ||||||
baronfel marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
// Otherwise, append the key and value to the message. | ||||||
// if MSbuild had a property bag concept on the message APIs, | ||||||
// we could use that instead of appending to the message. | ||||||
|
||||||
builder.Append($" {kvp.Key}={kvp.Value}"); | ||||||
continue; | ||||||
} | ||||||
} | ||||||
} | ||||||
else if (scope is string s) | ||||||
{ | ||||||
builder.Append($" {s}"); | ||||||
} | ||||||
|
||||||
|
||||||
}, state); | ||||||
} | ||||||
|
||||||
message.message = builder.ToString(); | ||||||
return message; | ||||||
} | ||||||
|
||||||
|
||||||
/// <summary> | ||||||
/// 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. | ||||||
/// </summary> | ||||||
/// <param name="subcategory"></param> | ||||||
/// <param name="code"></param> | ||||||
/// <param name="helpKeyword"></param> | ||||||
/// <param name="file"></param> | ||||||
/// <param name="lineNumber"></param> | ||||||
/// <param name="columnNumber"></param> | ||||||
/// <param name="endLineNumber"></param> | ||||||
/// <param name="endColumnNumber"></param> | ||||||
/// <param name="message"></param> | ||||||
private record struct MSBuildMessageParameters(string? subcategory, | ||||||
string? code, | ||||||
string? helpKeyword, | ||||||
string? file, | ||||||
int? lineNumber, | ||||||
int? columnNumber, | ||||||
int? endLineNumber, | ||||||
int? endColumnNumber, | ||||||
string message); | ||||||
|
||||||
/// <summary> | ||||||
/// A simple disposable to describe scopes with <see cref="ILogger.BeginScope"/>. | ||||||
/// </summary> | ||||||
private sealed class DummyDisposable : IDisposable | ||||||
{ | ||||||
public void Dispose() { } | ||||||
} | ||||||
|
||||||
internal void SetScopeProvider(IExternalScopeProvider scopeProvider) | ||||||
{ | ||||||
_scopeProvider = scopeProvider; | ||||||
} | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.