-
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR extracts an improved MSBuild logging component into a separate project to potentially spin it off as a standalone package. The main enhancement is the ability to use properties from Microsoft.Extensions.Logging.ILogger messages as inputs to MSBuild's logging parameters (code, file, line, etc.), while preserving state information for binlog and other MSBuild loggers.
- Moves MSBuild logging functionality from
Microsoft.NET.Build.Containers
to a newMicrosoft.Extensions.Logging.MSBuild
project - Enhances the logger to support extracting MSBuild-specific parameters from logging state/scope
- Updates namespace and visibility from internal to public for the extracted components
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
src/Microsoft.Extensions.Logging.MSBuild/Microsoft.Extensions.Logging.MSBuild.csproj |
New project file defining the extracted MSBuild logging library |
src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs |
Updated provider with scope support and public visibility |
src/Microsoft.Extensions.Logging.MSBuild/MSBuildLogger.cs |
Enhanced logger with parameter extraction from state/scope |
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs |
Updated namespace import to use new project |
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs |
Updated namespace import to use new project |
src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj |
Added project reference to new logging library |
src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs |
Removed old implementation |
Comments suppressed due to low confidence (1)
continue; | ||
default: | ||
var wrappedKey = "{" + kvp.Key + "}"; | ||
if (originalFormat.Contains(wrappedKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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().
if (originalFormat.Contains(wrappedKey)) | |
if (originalFormat != null && originalFormat.Contains(wrappedKey)) |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
originalFormat
is also null if "{OriginalFormat}" is a key in the list but the foreach (var kvp in stateItems)
loop has not reached it yet. There is no documented requirement that "{OriginalFormat}" be in the first element of the list.
continue; | ||
default: | ||
var wrappedKey = "{" + kvp.Key + "}"; | ||
if (originalFormat.Contains(wrappedKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.
if (originalFormat.Contains(wrappedKey)) | |
if (originalFormat != null && originalFormat.Contains(wrappedKey)) |
Copilot uses AI. Check for mistakes.
src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj
Outdated
Show resolved
Hide resolved
fae2e39
to
395a0e5
Compare
{ | ||
// state will be a FormattedLogValues instance | ||
// scope will be our dictionary thing we need to probe into | ||
scopeProvider.ForEachScope((scope, state) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if anyone would try to use the spun-off MSBuildLogger with log buffering, whose implementation in the Microsoft.Extensions.Telemetry package does not support scopes. Perhaps it is possible to work around that by implementing ILogEnricher to pull the attributes from the logger scopes into the log entries themselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting question - to be very honest with you I don't think this particular MSBuild/MEL bridge would even be used in a 'general' sense, only in the very specific scenario of "a Task wants a unified logging abstraction for code that runs during the Task". In the extreme, this logger could potentially be set up/configured by the Engine itself and made ambiently available as an ILogger
to Task implementations. That's all hypothetical and not designed at the moment though.
var wrappedKey = "{" + kvp.Key + "}"; | ||
if (originalFormat.Contains(wrappedKey)) | ||
{ | ||
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
If the message template includes formatting like {CreationDate:O}
, this won't match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree - I should possibly look at interpreting this via CompositeFormat
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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.
continue; | ||
default: | ||
var wrappedKey = "{" + kvp.Key + "}"; | ||
if (originalFormat.Contains(wrappedKey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
originalFormat
is also null if "{OriginalFormat}" is a key in the list but the foreach (var kvp in stateItems)
loop has not reached it yet. There is no documented requirement that "{OriginalFormat}" be in the first element of the list.
src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs
Outdated
Show resolved
Hide resolved
How do you create the state objects that are passed to ILogger.Log<TState> and then analysed by this logger? Methods like LoggerExtensions.LogInformation and LoggerMessage.Define parse the log message template to learn the names of attributes, so if you use those and want to give e.g. "File" to MSBuild, then you have to include a "{File}" placeholder in the template. But then the formatted log message also includes the file name, and MSBuild adds the file name again, so the user sees it twice. Is source-generated logging different in this respect? |
The state objects are created via |
Oh so if you don't want to include an attribute in the formatted log message, then you place it in the state of the logger scope, not in the state of the log entry.
That one doesn't seem to have any of the attributes that MSBuildLogger would recognise. |
Correct - I didn't really make use of the MSBuild-specific attributes in that PR. They just kind of emerged from the fact that I could now walk state/scope in a structured way and do something with the attributes I found. We have this problem in the SDK codebase more generally when we log in our Tasks where we have to call special "LogXFromFormat" methods because those know how to extract specific kinds of data from the shapes of our localized strings - "wouldn't it be nice if that was done in a more 'structured' way" was basically the genesis of this entire PR. |
if (!_loggers.TryGetValue(categoryName, out var logger)) | ||
{ | ||
logger = new MSBuildLogger(categoryName, _loggingHelper, _scopeProvider); | ||
lock (_loggers) | ||
{ | ||
_loggers[categoryName] = logger; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of locking is still not thread-safe for Dictionary<TKey, TValue>, as the lock does not prevent reading and writing in parallel. It would be safe for Hashtable, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda wondered if I should just use Concurrentdictionary.GetOrAdd for this, but I've heard/seen horror stories from ConcurrentDictionary internal locking...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option here is, make MSBuildLoggerProvider.CreateLogger create a new instance of MSBuildLogger on each call and not add it to any collection. Then, SetExternalScopeProvider would not be able to notify the MSBuildLogger instances, so each MSBuildLogger would instead have to keep a reference to MSBuildLoggerProvider and read the scope provider from there whenever it's needed. This would cost one more indirection in each method of ILogger but the locking would no longer be needed. Unnecessary instances of MSBuildLogger might get created but the garbage collector would clean them up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In normal use though, LoggerFactory.CreateLogger already does lock (_sync)
around the ILoggerProvider.CreateLogger calls, so there extra lock in MSBuildLoggerProvider.CreateLogger won't have any contention; I don't think ConcurrentDictionary there would help performance.
This is part of reducing the diff in #49556.
During that PR, I implemented a number of improvements to the simple Microsoft.Extensions.Logging.Abstractions.ILogger wrapper we had that ties that ILogger to MSBuild's ILogger interfaces.
The main feature is being able to use state/props from MEL.ILogger messages as inputs to MSbuild's various parameters on the
LogX
overloads - things like code, file, line, etc can now all be set by props. State is also persisted and attached to messages so ambient context is available in the binlog and other MSBuild Loggers even if not directly used in the Message.