diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs new file mode 100644 index 00000000000000..106c3c8263e64e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterConfigureOptions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Configures a ConsoleFormatterOptions object from an IConfiguration. + /// + /// + /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies, + /// to be trimmed. This improves app size and startup. + /// + [UnsupportedOSPlatform("browser")] + internal sealed class ConsoleFormatterConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + + public ConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration) + { + _configuration = providerConfiguration.GetFormatterOptionsSection(); + } + + public void Configure(ConsoleFormatterOptions options) => Bind(_configuration, options); + + public static void Bind(IConfiguration configuration, ConsoleFormatterOptions options) + { + if (ConsoleLoggerConfigureOptions.ParseBool(configuration, "IncludeScopes", out bool includeScopes)) + { + options.IncludeScopes = includeScopes; + } + + if (configuration["TimestampFormat"] is string timestampFormat) + { + options.TimestampFormat = timestampFormat; + } + + if (ConsoleLoggerConfigureOptions.ParseBool(configuration, "UseUtcTimestamp", out bool useUtcTimestamp)) + { + options.UseUtcTimestamp = useUtcTimestamp; + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs new file mode 100644 index 00000000000000..c0bd42be6f97be --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerConfigureOptions.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Runtime.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Configures a ConsoleLoggerOptions object from an IConfiguration. + /// + /// + /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies, + /// to be trimmed. This improves app size and startup. + /// + [UnsupportedOSPlatform("browser")] + internal sealed class ConsoleLoggerConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + + public ConsoleLoggerConfigureOptions(ILoggerProviderConfiguration providerConfiguration) + { + _configuration = providerConfiguration.Configuration; + } + + public void Configure(ConsoleLoggerOptions options) + { + if (ParseBool(_configuration, "DisableColors", out bool disableColors)) + { +#pragma warning disable CS0618 // Type or member is obsolete + options.DisableColors = disableColors; +#pragma warning restore CS0618 // Type or member is obsolete + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (ParseEnum(_configuration, "Format", out ConsoleLoggerFormat format)) + { + options.Format = format; + } +#pragma warning restore CS0618 // Type or member is obsolete + + if (_configuration["FormatterName"] is string formatterName) + { + options.FormatterName = formatterName; + } + + if (ParseBool(_configuration, "IncludeScopes", out bool includeScopes)) + { +#pragma warning disable CS0618 // Type or member is obsolete + options.IncludeScopes = includeScopes; +#pragma warning restore CS0618 // Type or member is obsolete + } + + if (ParseEnum(_configuration, "LogToStandardErrorThreshold", out LogLevel logToStandardErrorThreshold)) + { + options.LogToStandardErrorThreshold = logToStandardErrorThreshold; + } + + if (ParseInt(_configuration, "MaxQueueLength", out int maxQueueLength)) + { + options.MaxQueueLength = maxQueueLength; + } + + if (ParseEnum(_configuration, "QueueFullMode", out ConsoleLoggerQueueFullMode queueFullMode)) + { + options.QueueFullMode = queueFullMode; + } + + if (_configuration["TimestampFormat"] is string timestampFormat) + { +#pragma warning disable CS0618 // Type or member is obsolete + options.TimestampFormat = timestampFormat; +#pragma warning restore CS0618 // Type or member is obsolete + } + + if (ParseBool(_configuration, "UseUtcTimestamp", out bool useUtcTimestamp)) + { +#pragma warning disable CS0618 // Type or member is obsolete + options.UseUtcTimestamp = useUtcTimestamp; +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + /// + /// Parses the configuration value at the specified key into a bool. + /// + /// true if the value was successfully found and parsed. false if the key wasn't found. + /// Thrown when invalid data was found at the specified configuration key. + public static bool ParseBool(IConfiguration configuration, string key, out bool value) + { + if (configuration[key] is string valueString) + { + try + { + value = bool.Parse(valueString); + return true; + } + catch (Exception e) + { + ThrowInvalidConfigurationException(configuration, key, typeof(bool), e); + } + } + + value = default; + return false; + } + + /// + /// Parses the configuration value at the specified key into an enum. + /// + /// true if the value was successfully found and parsed. false if the key wasn't found. + /// Thrown when invalid data was found at the specified configuration key. + public static bool ParseEnum(IConfiguration configuration, string key, out T value) where T : struct + { + if (configuration[key] is string valueString) + { + try + { + value = +#if NETFRAMEWORK || NETSTANDARD2_0 + (T)Enum.Parse(typeof(T), valueString, ignoreCase: true); +#else + Enum.Parse(valueString, ignoreCase: true); +#endif + return true; + } + catch (Exception e) + { + ThrowInvalidConfigurationException(configuration, key, typeof(T), e); + } + } + + value = default; + return false; + } + + /// + /// Parses the configuration value at the specified key into an int. + /// + /// true if the value was successfully found and parsed. false if the key wasn't found. + /// Thrown when invalid data was found at the specified configuration key. + public static bool ParseInt(IConfiguration configuration, string key, out int value) + { + if (configuration[key] is string valueString) + { + try + { + value = int.Parse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); + return true; + } + catch (Exception e) + { + ThrowInvalidConfigurationException(configuration, key, typeof(int), e); + } + } + + value = default; + return false; + } + + private static void ThrowInvalidConfigurationException(IConfiguration configuration, string key, Type valueType, Exception innerException) + { + IConfigurationSection section = configuration.GetSection(key); + throw new InvalidOperationException(SR.Format(SR.InvalidConfigurationData, section.Path, valueType), innerException); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs index 160e099097f8c0..da4ff6bf0c991e 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerExtensions.cs @@ -25,22 +25,18 @@ public static class ConsoleLoggerExtensions /// Adds a console logger named 'Console' to the factory. /// /// The to use. - [UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", - Justification = "AddConsoleFormatter and RegisterProviderOptions are only called with Options types which only have simple properties.")] - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", - Justification = "AddConsoleFormatter and RegisterProviderOptions are only dangerous when the Options type cannot be statically analyzed, but that is not the case here. " + - "The DynamicallyAccessedMembers annotations on them will make sure to preserve the right members from the different options objects.")] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonWriterOptions))] public static ILoggingBuilder AddConsole(this ILoggingBuilder builder) { builder.AddConfiguration(); - builder.AddConsoleFormatter(); - builder.AddConsoleFormatter(); - builder.AddConsoleFormatter(); + builder.AddConsoleFormatter(); + builder.AddConsoleFormatter(); + builder.AddConsoleFormatter(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - LoggerProviderOptions.RegisterProviderOptions(builder.Services); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerConfigureOptions>()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, LoggerProviderOptionsChangeTokenSource>()); return builder; } @@ -135,13 +131,7 @@ private static ILoggingBuilder AddFormatterWithName(this ILoggingBuilder builder where TOptions : ConsoleFormatterOptions where TFormatter : ConsoleFormatter { - builder.AddConfiguration(); - - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterConfigureOptions>()); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterOptionsChangeTokenSource>()); - - return builder; + return AddConsoleFormatter>(builder); } /// @@ -161,6 +151,25 @@ private static ILoggingBuilder AddFormatterWithName(this ILoggingBuilder builder builder.Services.Configure(configure); return builder; } + + private static ILoggingBuilder AddConsoleFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFormatter, TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TConfigureOptions>(this ILoggingBuilder builder) + where TOptions : ConsoleFormatterOptions + where TFormatter : ConsoleFormatter + where TConfigureOptions : class, IConfigureOptions + { + builder.AddConfiguration(); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, TConfigureOptions>()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConsoleLoggerFormatterOptionsChangeTokenSource>()); + + return builder; + } + + internal static IConfiguration GetFormatterOptionsSection(this ILoggerProviderConfiguration providerConfiguration) + { + return providerConfiguration.Configuration.GetSection("FormatterOptions"); + } } [UnsupportedOSPlatform("browser")] @@ -171,7 +180,7 @@ internal sealed class ConsoleLoggerFormatterConfigureOptions providerConfiguration) : - base(providerConfiguration.Configuration.GetSection("FormatterOptions")) + base(providerConfiguration.GetFormatterOptionsSection()) { } } @@ -182,7 +191,7 @@ internal sealed class ConsoleLoggerFormatterOptionsChangeTokenSource providerConfiguration) - : base(providerConfiguration.Configuration.GetSection("FormatterOptions")) + : base(providerConfiguration.GetFormatterOptionsSection()) { } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs new file mode 100644 index 00000000000000..acfb948f56a95b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterConfigureOptions.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Configures a JsonConsoleFormatterOptions object from an IConfiguration. + /// + /// + /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies, + /// to be trimmed. This improves app size and startup. + /// + [UnsupportedOSPlatform("browser")] + internal sealed class JsonConsoleFormatterConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + + public JsonConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration) + { + _configuration = providerConfiguration.GetFormatterOptionsSection(); + } + + public void Configure(JsonConsoleFormatterOptions options) + { + ConsoleFormatterConfigureOptions.Bind(_configuration, options); + + if (_configuration.GetSection("JsonWriterOptions") is IConfigurationSection jsonWriterOptionsConfig) + { + JsonWriterOptions jsonWriterOptions = options.JsonWriterOptions; + + if (ConsoleLoggerConfigureOptions.ParseBool(jsonWriterOptionsConfig, "Indented", out bool indented)) + { + jsonWriterOptions.Indented = indented; + } + + if (ConsoleLoggerConfigureOptions.ParseInt(jsonWriterOptionsConfig, "MaxDepth", out int maxDepth)) + { + jsonWriterOptions.MaxDepth = maxDepth; + } + + if (ConsoleLoggerConfigureOptions.ParseBool(jsonWriterOptionsConfig, "SkipValidation", out bool skipValidation)) + { + jsonWriterOptions.SkipValidation = skipValidation; + } + + options.JsonWriterOptions = jsonWriterOptions; + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx index c3d2473f20cefa..8d925d89baf50f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx @@ -129,4 +129,7 @@ {0} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this. You may change `ConsoleLoggerQueueFullMode` to stop dropping messages. + + Failed to convert configuration value at '{0}' to type '{1}'. + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs new file mode 100644 index 00000000000000..a8f9a6cf88f212 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterConfigureOptions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Configuration; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Configures a SimpleConsoleFormatterOptions object from an IConfiguration. + /// + /// + /// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies, + /// to be trimmed. This improves app size and startup. + /// + [UnsupportedOSPlatform("browser")] + internal sealed class SimpleConsoleFormatterConfigureOptions : IConfigureOptions + { + private readonly IConfiguration _configuration; + + public SimpleConsoleFormatterConfigureOptions(ILoggerProviderConfiguration providerConfiguration) + { + _configuration = providerConfiguration.GetFormatterOptionsSection(); + } + + public void Configure(SimpleConsoleFormatterOptions options) + { + ConsoleFormatterConfigureOptions.Bind(_configuration, options); + + if (ConsoleLoggerConfigureOptions.ParseEnum(_configuration, "ColorBehavior", out LoggerColorBehavior colorBehavior)) + { + options.ColorBehavior = colorBehavior; + } + + if (ConsoleLoggerConfigureOptions.ParseBool(_configuration, "SingleLine", out bool singleLine)) + { + options.SingleLine = singleLine; + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs new file mode 100644 index 00000000000000..c30e842d3fc3ee --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerConfigureOptions.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Logging.Console.Test +{ + public class ConsoleLoggerConfigureOptions + { + [Fact] + public void EnsureConsoleLoggerOptions_ConfigureOptions_SupportsAllProperties() + { + // NOTE: if this test fails, it is because a property was added to one of the following types. + // When adding a new property to one of these types, ensure the corresponding + // IConfigureOptions class is updated for the new property. + + BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; + Assert.Equal(9, typeof(ConsoleLoggerOptions).GetProperties(flags).Length); + Assert.Equal(3, typeof(ConsoleFormatterOptions).GetProperties(flags).Length); + Assert.Equal(5, typeof(SimpleConsoleFormatterOptions).GetProperties(flags).Length); + Assert.Equal(4, typeof(JsonConsoleFormatterOptions).GetProperties(flags).Length); + Assert.Equal(4, typeof(JsonWriterOptions).GetProperties(flags).Length); + } + + [Theory] + [InlineData("Console:LogToStandardErrorThreshold", "invalid")] + [InlineData("Console:MaxQueueLength", "notANumber")] + [InlineData("Console:QueueFullMode", "invalid")] + [InlineData("Console:FormatterOptions:IncludeScopes", "not a bool")] + [InlineData("Console:FormatterOptions:UseUtcTimestamp", "not a bool")] + [InlineData("Console:FormatterOptions:ColorBehavior", "not a behavior")] + [InlineData("Console:FormatterOptions:SingleLine", "not a bool")] + [InlineData("Console:FormatterOptions:JsonWriterOptions:Indented", "not a bool")] + [InlineData("Console:FormatterOptions:JsonWriterOptions:MaxDepth", "not an int")] + [InlineData("Console:FormatterOptions:JsonWriterOptions:SkipValidation", "not a bool")] + public void ConsoleLoggerConfigureOptions_InvalidConfigurationData(string key, string value) + { + var configuration = new ConfigurationManager(); + configuration.AddInMemoryCollection(new[] { new KeyValuePair(key, value) }); + + IServiceProvider serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .AddConfiguration(configuration) + .AddConsole()) + .BuildServiceProvider(); + + InvalidOperationException e = Assert.Throws(() => serviceProvider.GetRequiredService()); + + // "Console:" is stripped off from the config path since that config section is read by the ConsoleLogger, and not part of the Options path. + string configPath = key.Substring("Console:".Length); + Assert.Contains(configPath, e.Message); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs index 38771eb2584993..99b49170ed8e1d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerExtensionsTests.cs @@ -374,8 +374,7 @@ public void AddConsole_MaxQueueLengthSetToNegativeOrZero_Throws(int invalidMaxQu ) .BuildServiceProvider(); - // the configuration binder throws TargetInvocationException when setting options property MaxQueueLength throws exception - Assert.Throws(() => serviceProvider.GetRequiredService()); + Assert.Throws(() => serviceProvider.GetRequiredService()); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs index 0994980590122b..8202204fcf4bb8 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; +using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration;