Skip to content

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

baronfel
Copy link
Member

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.

@Copilot Copilot AI review requested due to automatic review settings July 12, 2025 19:39
@baronfel baronfel requested a review from a team July 12, 2025 19:39
Copy link
Contributor

@Copilot Copilot AI left a 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 new Microsoft.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))
Copy link
Preview

Copilot AI Jul 12, 2025

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().

Suggested change
if (originalFormat.Contains(wrappedKey))
if (originalFormat != null && originalFormat.Contains(wrappedKey))

Copilot uses AI. Check for mistakes.

Copy link
Contributor

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))
Copy link
Preview

Copilot AI Jul 12, 2025

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.

Suggested change
if (originalFormat.Contains(wrappedKey))
if (originalFormat != null && originalFormat.Contains(wrappedKey))

Copilot uses AI. Check for mistakes.

@baronfel baronfel requested a review from a team as a code owner July 12, 2025 21:06
Comment on lines 148 to 153
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;
}
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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))
Copy link
Contributor

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.

@KalleOlaviNiemitalo
Copy link
Contributor

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?

@baronfel
Copy link
Member Author

How do you create the state objects that are passed to ILogger.Log 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 ILogger.BeginScope<TState>(TState state) - here's a specific example from the linked PR that I'm extracting this from. Serilog has good docs here, but basically it seems to be up to the specific logger how to handle various kinds of state - single primitive structures like strings you could add as some kind of tag, but collections/maps of key/value pairs are a natural fit for adding to a property bag of some kind, etc.

@KalleOlaviNiemitalo
Copy link
Contributor

The state objects are created via ILogger.BeginScope<TState>(TState state)

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.

- here's a specific example from the linked PR that I'm extracting this from.

That one doesn't seem to have any of the attributes that MSBuildLogger would recognise.

@baronfel
Copy link
Member Author

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.

@surayya-MS surayya-MS self-assigned this Jul 14, 2025
@baronfel baronfel force-pushed the superpowered-msbuild-logger branch from 53678d6 to 7fe2868 Compare July 14, 2025 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants