diff --git a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs
deleted file mode 100644
index e6b96093f53b..000000000000
--- a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLogger.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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.Extensions.Logging;
-using ILogger = Microsoft.Extensions.Logging.ILogger;
-
-namespace Microsoft.NET.Build.Containers.Logging;
-
-///
-/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper.
-///
-internal sealed class MSBuildLogger : ILogger
-{
- private static readonly IDisposable Scope = new DummyDisposable();
-
- private readonly TaskLoggingHelper _loggingHelper;
-
- public MSBuildLogger(string category, TaskLoggingHelper loggingHelperToWrap)
- {
- _loggingHelper = loggingHelperToWrap;
- }
-
- IDisposable ILogger.BeginScope(TState state) => Scope;
-
- public bool IsEnabled(LogLevel logLevel) => true;
-
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
- {
- switch (logLevel)
- {
- case LogLevel.Trace:
- _loggingHelper.LogMessage(MessageImportance.Low, formatter(state, exception));
- break;
- case LogLevel.Debug:
- case LogLevel.Information:
- _loggingHelper.LogMessage(MessageImportance.High, formatter(state, exception));
- break;
- case LogLevel.Warning:
- _loggingHelper.LogWarning(formatter(state, exception));
- break;
- case LogLevel.Error:
- case LogLevel.Critical:
- _loggingHelper.LogError(formatter(state, exception));
- break;
- case LogLevel.None:
- break;
- default:
- break;
- }
- }
-
- ///
- /// A simple disposable to describe scopes with .
- ///
- private sealed class DummyDisposable : IDisposable
- {
- public void Dispose() { }
- }
-}
diff --git a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs b/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs
deleted file mode 100644
index 8db4a7b2ae51..000000000000
--- a/src/Containers/Microsoft.NET.Build.Containers/Logging/MSBuildLoggerProvider.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.Utilities;
-using Microsoft.Extensions.Logging;
-
-namespace Microsoft.NET.Build.Containers.Logging;
-
-///
-/// An that creates s which passes
-/// all the logs to MSBuild's .
-///
-internal class MSBuildLoggerProvider : ILoggerProvider
-{
- private readonly TaskLoggingHelper _loggingHelper;
-
- public MSBuildLoggerProvider(TaskLoggingHelper loggingHelperToWrap)
- {
- _loggingHelper = loggingHelperToWrap;
- }
-
- public ILogger CreateLogger(string categoryName)
- {
- return new MSBuildLogger(categoryName, _loggingHelper);
- }
-
- public void Dispose() { }
-}
diff --git a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj
index c6aaaf98c24e..afdaf1ebab2f 100644
--- a/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj
+++ b/src/Containers/Microsoft.NET.Build.Containers/Microsoft.NET.Build.Containers.csproj
@@ -25,13 +25,13 @@
-
+
build
false
+
diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs
index 16e5b8392372..f103fe8c1246 100644
--- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs
+++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs
@@ -5,7 +5,7 @@
using System.Text.Json.Nodes;
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
-using Microsoft.NET.Build.Containers.Logging;
+using Microsoft.Extensions.Logging.MSBuild;
using Microsoft.NET.Build.Containers.Resources;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Task = System.Threading.Tasks.Task;
diff --git a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs
index 4b9993e06cf7..dd33472c27f5 100644
--- a/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs
+++ b/src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs
@@ -3,7 +3,7 @@
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
-using Microsoft.NET.Build.Containers.Logging;
+using Microsoft.Extensions.Logging.MSBuild;
using Microsoft.NET.Build.Containers.Resources;
using ILogger = Microsoft.Extensions.Logging.ILogger;
diff --git a/src/Containers/packaging/package.csproj b/src/Containers/packaging/package.csproj
index 5b6bf2b952b7..675c43792194 100644
--- a/src/Containers/packaging/package.csproj
+++ b/src/Containers/packaging/package.csproj
@@ -28,10 +28,6 @@
SetTargetFramework="TargetFramework=$(TargetFramework)"
OutputItemType="ContainerLibraryOutput"/>
-
-
@@ -86,7 +82,7 @@
$([MSBuild]::ValueOrDefault('%(_AllNetContainerTaskDependencies.NuGetPackageId)', '').Contains('Microsoft.Extensions'))
) and
%(_AllNetContainerTaskDependencies.NuGetIsFrameworkReference) != true" />
-
+
@@ -94,7 +90,6 @@
-
diff --git a/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLogger.cs b/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLogger.cs
new file mode 100644
index 000000000000..ba780177985b
--- /dev/null
+++ b/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLogger.cs
@@ -0,0 +1,282 @@
+// 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 System.Diagnostics;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.NET.StringTools;
+
+namespace Microsoft.Extensions.Logging.MSBuild;
+
+///
+/// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper.
+///
+///
+/// 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:
+///
+/// - Subcategory
+/// - Code
+/// - HelpKeyword
+/// - File
+/// - LineNumber
+/// - ColumnNumber
+/// - EndLineNumber
+/// - EndColumnNumber
+/// - {OriginalFormat}(usually provided by the underlying logging framework)
+///
+///
+/// So if you add these to the scope (e.g. via _logger.BeginScope(new Dictionary<string, object>{ ... }))
or on the message format itself,
+/// they will be extracted and used to format the message correctly for MSBuild.
+///
+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 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(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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.helpLink, 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.helpLink, 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(string category, TState state, Exception? exception, Func 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);
+
+ // any unprocessed state items will be appended to the message after scope processing
+ var unprocessedKeyValues = ProcessState(state, ref message, out string? originalFormat);
+
+ // scope will be our dictionary thing we need to probe into
+ scopeProvider?.ForEachScope((scope, state) => ProcessScope(scope, ref message, ref originalFormat, unprocessedKeyValues), state);
+
+ Debug.Assert(originalFormat is not null, "Original format should not be null at this point - either state or scope should have provided it.");
+
+ ApplyUnprocessedItemsToMessage(unprocessedKeyValues, originalFormat, builder);
+
+ message.message = builder.ToString();
+ return message;
+ }
+
+ private static void ProcessScope(object? scope, ref MSBuildMessageParameters message, ref string? originalFormat, List>? unprocessedKeyValues)
+ {
+ if (scope is IDictionary dict)
+ {
+ foreach (var kvp in dict)
+ {
+ switch (kvp.Key)
+ {
+ case "{OriginalFormat}":
+ if (originalFormat is null && 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;
+ case "HelpLink":
+ message.helpLink = 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:
+ unprocessedKeyValues ??= [];
+ unprocessedKeyValues.Add(kvp);
+ continue;
+ }
+ }
+ }
+ else if (scope is string s)
+ {
+ unprocessedKeyValues ??= [];
+ // If the scope is a string, we treat it as an unprocessed item
+ unprocessedKeyValues.Add(new KeyValuePair("Scope", s));
+ }
+ }
+
+ private static void ApplyUnprocessedItemsToMessage(List>? unprocessedStateItems, string originalFormat, SpanBasedStringBuilder builder)
+ {
+ // foreach unprocessed item, if the format string does not contain the key, append it to the message
+ // in key=value format using the builder
+ if (unprocessedStateItems is not null)
+ {
+ foreach (var kvp in unprocessedStateItems)
+ {
+ var wrappedKey = "{" + kvp.Key + "}";
+ if (!originalFormat.Contains(wrappedKey))
+ {
+ builder.Append($" {kvp.Key}={kvp.Value}");
+ }
+ }
+ }
+ }
+
+ private static List>? ProcessState(TState state, ref MSBuildMessageParameters message, out string? originalFormat)
+ {
+ originalFormat = null;
+ List>? unmappedStateItems = null;
+ if (state is IReadOnlyList> stateItems)
+ {
+ foreach (var kvp in stateItems)
+ {
+ 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;
+ case "HelpLink":
+ message.helpLink = 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:
+ unmappedStateItems ??= [];
+ unmappedStateItems.Add(kvp);
+ continue;
+ }
+ }
+ return unmappedStateItems;
+ }
+ else
+ {
+ // If the state is not a list, we just create an empty message.
+ message = new MSBuildMessageParameters();
+ }
+ return null;
+ }
+
+
+ ///
+ /// 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.
+ ///
+ private record struct MSBuildMessageParameters(string? subcategory,
+ string? code,
+ string? helpKeyword,
+ string? helpLink,
+ string? file,
+ int? lineNumber,
+ int? columnNumber,
+ int? endLineNumber,
+ int? endColumnNumber,
+ string message);
+
+ ///
+ /// A simple disposable to describe scopes with .
+ ///
+ private sealed class DummyDisposable : IDisposable
+ {
+ public void Dispose() { }
+ }
+
+ internal void SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+}
diff --git a/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs b/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs
new file mode 100644
index 000000000000..ccc8dd8fccba
--- /dev/null
+++ b/src/Microsoft.Extensions.Logging.MSBuild/MSBuildLoggerProvider.cs
@@ -0,0 +1,49 @@
+// 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.Utilities;
+
+namespace Microsoft.Extensions.Logging.MSBuild;
+
+///
+/// An that creates s which passes
+/// all the logs to MSBuild's .
+///
+public class MSBuildLoggerProvider : ILoggerProvider, ISupportExternalScope
+{
+ private readonly TaskLoggingHelper _loggingHelper;
+ private readonly Dictionary _loggers = [];
+ private IExternalScopeProvider? _scopeProvider;
+
+ public MSBuildLoggerProvider(TaskLoggingHelper loggingHelperToWrap)
+ {
+ _loggingHelper = loggingHelperToWrap;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ lock (_loggers)
+ {
+ if (!_loggers.TryGetValue(categoryName, out var logger))
+ {
+ logger = new MSBuildLogger(categoryName, _loggingHelper, _scopeProvider);
+ _loggers[categoryName] = logger;
+ }
+ return logger;
+ }
+ }
+
+ public void Dispose() { }
+
+ public void SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ lock (_loggers)
+ {
+ foreach (var logger in _loggers.Values)
+ {
+ logger.SetScopeProvider(scopeProvider);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Logging.MSBuild/Microsoft.Extensions.Logging.MSBuild.csproj b/src/Microsoft.Extensions.Logging.MSBuild/Microsoft.Extensions.Logging.MSBuild.csproj
new file mode 100644
index 000000000000..c61744712454
--- /dev/null
+++ b/src/Microsoft.Extensions.Logging.MSBuild/Microsoft.Extensions.Logging.MSBuild.csproj
@@ -0,0 +1,14 @@
+
+
+
+ $(SdkTargetFramework)
+ enable
+ true
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs b/test/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs
index 3aa57a3a7670..aec5b31d6651 100644
--- a/test/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs
+++ b/test/Microsoft.NET.Build.Containers.IntegrationTests/PackageTests.cs
@@ -49,7 +49,8 @@ public void SanityTest_NET_Build_ContainersDependencies()
};
IReadOnlyList knownProjectReferences = new List()
{
- "..\\..\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj"
+ "..\\..\\Cli\\Microsoft.DotNet.Cli.Utils\\Microsoft.DotNet.Cli.Utils.csproj",
+ "..\\..\\Microsoft.Extensions.Logging.MSBuild\\Microsoft.Extensions.Logging.MSBuild.csproj"
};
string projectFilePath = Path.Combine(TestContext.Current.TestExecutionDirectory, "Container", "ProjectFiles", "Microsoft.NET.Build.Containers.csproj");
@@ -86,6 +87,7 @@ public void PackageContentTest()
"containerize/Microsoft.Extensions.Logging.Abstractions.dll",
"containerize/Microsoft.Extensions.Logging.Configuration.dll",
"containerize/Microsoft.Extensions.Logging.Console.dll",
+ "containerize/Microsoft.Extensions.Logging.MSBuild.dll",
"containerize/Microsoft.Extensions.Logging.dll",
"containerize/Microsoft.Extensions.Options.ConfigurationExtensions.dll",
"containerize/Microsoft.Extensions.Options.dll",
@@ -118,6 +120,7 @@ public void PackageContentTest()
$"tasks/{netTFM}/Microsoft.Extensions.DependencyInjection.dll",
$"tasks/{netTFM}/Microsoft.Extensions.Logging.Abstractions.dll",
$"tasks/{netTFM}/Microsoft.Extensions.Logging.dll",
+ $"tasks/{netTFM}/Microsoft.Extensions.Logging.MSBuild.dll",
$"tasks/{netTFM}/Microsoft.Extensions.Options.dll",
$"tasks/{netTFM}/Microsoft.Extensions.Primitives.dll",
$"tasks/{netTFM}/Microsoft.NET.Build.Containers.deps.json",