Skip to content
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

Remove ConfigurationBinder usage from Console Logging #82098

Merged
merged 3 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Configures a ConsoleFormatterOptions object from an IConfiguration.
/// </summary>
/// <remarks>
/// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
/// to be trimmed. This improves app size and startup.
/// </remarks>
[UnsupportedOSPlatform("browser")]
internal sealed class ConsoleFormatterConfigureOptions : IConfigureOptions<ConsoleFormatterOptions>
{
private readonly IConfiguration _configuration;

public ConsoleFormatterConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
{
_configuration = providerConfiguration.GetFormatterOptionsSection();
}

public void Configure(ConsoleFormatterOptions options) => Bind(_configuration, options);

public static void Bind(IConfiguration configuration, ConsoleFormatterOptions options)
{
if (configuration["IncludeScopes"] is string includeScopes)
{
options.IncludeScopes = bool.Parse(includeScopes);
}

if (configuration["TimestampFormat"] is string timestampFormat)
{
options.TimestampFormat = timestampFormat;
}

if (configuration["UseUtcTimestamp"] is string useUtcTimestamp)
{
options.UseUtcTimestamp = bool.Parse(useUtcTimestamp);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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
{
/// <summary>
/// Configures a ConsoleLoggerOptions object from an IConfiguration.
/// </summary>
/// <remarks>
/// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
/// to be trimmed. This improves app size and startup.
/// </remarks>
[UnsupportedOSPlatform("browser")]
internal sealed class ConsoleLoggerConfigureOptions : IConfigureOptions<ConsoleLoggerOptions>
{
private readonly IConfiguration _configuration;

public ConsoleLoggerConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
{
_configuration = providerConfiguration.Configuration;
}

public void Configure(ConsoleLoggerOptions options)
{
if (_configuration["DisableColors"] is string disableColors)
{
#pragma warning disable CS0618 // Type or member is obsolete
options.DisableColors = bool.Parse(disableColors);
#pragma warning restore CS0618 // Type or member is obsolete
}

if (_configuration["Format"] is string format)
{
#pragma warning disable CS0618 // Type or member is obsolete
options.Format = ParseEnum<ConsoleLoggerFormat>(format);
#pragma warning restore CS0618 // Type or member is obsolete
}

if (_configuration["FormatterName"] is string formatterName)
{
options.FormatterName = formatterName;
}

if (_configuration["IncludeScopes"] is string includeScopes)
{
#pragma warning disable CS0618 // Type or member is obsolete
options.IncludeScopes = bool.Parse(includeScopes);
#pragma warning restore CS0618 // Type or member is obsolete
}

if (_configuration["LogToStandardErrorThreshold"] is string logToStandardErrorThreshold)
{
options.LogToStandardErrorThreshold = ParseEnum<LogLevel>(logToStandardErrorThreshold);
}

if (_configuration["MaxQueueLength"] is string maxQueueLength)
{
options.MaxQueueLength = int.Parse(maxQueueLength, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}

if (_configuration["QueueFullMode"] is string queueFullMode)
{
options.QueueFullMode = ParseEnum<ConsoleLoggerQueueFullMode>(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 (_configuration["UseUtcTimestamp"] is string useUtcTimestamp)
{
#pragma warning disable CS0618 // Type or member is obsolete
options.UseUtcTimestamp = bool.Parse(useUtcTimestamp);
#pragma warning restore CS0618 // Type or member is obsolete
}
}

public static T ParseEnum<T>(string value) where T : struct =>
#if NETSTANDARD || NETFRAMEWORK
(T)Enum.Parse(typeof(T), value, ignoreCase: true);
#else
Enum.Parse<T>(value, ignoreCase: true);
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,18 @@ public static class ConsoleLoggerExtensions
/// Adds a console logger named 'Console' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
[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<JsonConsoleFormatter, JsonConsoleFormatterOptions>();
builder.AddConsoleFormatter<SystemdConsoleFormatter, ConsoleFormatterOptions>();
builder.AddConsoleFormatter<SimpleConsoleFormatter, SimpleConsoleFormatterOptions>();
builder.AddConsoleFormatter<JsonConsoleFormatter, JsonConsoleFormatterOptions, JsonConsoleFormatterConfigureOptions>();
builder.AddConsoleFormatter<SystemdConsoleFormatter, ConsoleFormatterOptions, ConsoleFormatterConfigureOptions>();
builder.AddConsoleFormatter<SimpleConsoleFormatter, SimpleConsoleFormatterOptions, SimpleConsoleFormatterConfigureOptions>();

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, ConsoleLoggerProvider>());
LoggerProviderOptions.RegisterProviderOptions<ConsoleLoggerOptions, ConsoleLoggerProvider>(builder.Services);

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<ConsoleLoggerOptions>, ConsoleLoggerConfigureOptions>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<ConsoleLoggerOptions>, LoggerProviderOptionsChangeTokenSource<ConsoleLoggerOptions, ConsoleLoggerProvider>>());

return builder;
}
Expand Down Expand Up @@ -135,13 +131,7 @@ private static ILoggingBuilder AddFormatterWithName(this ILoggingBuilder builder
where TOptions : ConsoleFormatterOptions
where TFormatter : ConsoleFormatter
{
builder.AddConfiguration();

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ConsoleFormatter, TFormatter>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<TOptions>, ConsoleLoggerFormatterConfigureOptions<TFormatter, TOptions>>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<TOptions>, ConsoleLoggerFormatterOptionsChangeTokenSource<TFormatter, TOptions>>());

return builder;
return AddConsoleFormatter<TFormatter, TOptions, ConsoleLoggerFormatterConfigureOptions<TFormatter, TOptions>>(builder);
}

/// <summary>
Expand All @@ -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<TOptions>
{
builder.AddConfiguration();

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ConsoleFormatter, TFormatter>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<TOptions>, TConfigureOptions>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<TOptions>, ConsoleLoggerFormatterOptionsChangeTokenSource<TFormatter, TOptions>>());

return builder;
}

internal static IConfiguration GetFormatterOptionsSection(this ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
{
return providerConfiguration.Configuration.GetSection("FormatterOptions");
}
}

[UnsupportedOSPlatform("browser")]
Expand All @@ -171,7 +180,7 @@ internal sealed class ConsoleLoggerFormatterConfigureOptions<TFormatter, [Dynami
[RequiresDynamicCode(ConsoleLoggerExtensions.RequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(ConsoleLoggerExtensions.TrimmingRequiresUnreferencedCodeMessage)]
public ConsoleLoggerFormatterConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration) :
base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
base(providerConfiguration.GetFormatterOptionsSection())
{
}
}
Expand All @@ -182,7 +191,7 @@ internal sealed class ConsoleLoggerFormatterOptionsChangeTokenSource<TFormatter,
where TFormatter : ConsoleFormatter
{
public ConsoleLoggerFormatterOptionsChangeTokenSource(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
: base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
: base(providerConfiguration.GetFormatterOptionsSection())
{
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
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
{
/// <summary>
/// Configures a JsonConsoleFormatterOptions object from an IConfiguration.
/// </summary>
/// <remarks>
/// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
/// to be trimmed. This improves app size and startup.
/// </remarks>
[UnsupportedOSPlatform("browser")]
internal sealed class JsonConsoleFormatterConfigureOptions : IConfigureOptions<JsonConsoleFormatterOptions>
{
private readonly IConfiguration _configuration;

public JsonConsoleFormatterConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> 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 (jsonWriterOptionsConfig["Indented"] is string indented)
{
jsonWriterOptions.Indented = bool.Parse(indented);
}

if (jsonWriterOptionsConfig["MaxDepth"] is string maxDepth)
{
jsonWriterOptions.MaxDepth = int.Parse(maxDepth, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}

if (jsonWriterOptionsConfig["SkipValidation"] is string skipValidation)
{
jsonWriterOptions.SkipValidation = bool.Parse(skipValidation);
}

options.JsonWriterOptions = jsonWriterOptions;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Configures a SimpleConsoleFormatterOptions object from an IConfiguration.
/// </summary>
/// <remarks>
/// Doesn't use ConfigurationBinder in order to allow ConfigurationBinder, and all its dependencies,
/// to be trimmed. This improves app size and startup.
/// </remarks>
[UnsupportedOSPlatform("browser")]
internal sealed class SimpleConsoleFormatterConfigureOptions : IConfigureOptions<SimpleConsoleFormatterOptions>
{
private readonly IConfiguration _configuration;

public SimpleConsoleFormatterConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
{
_configuration = providerConfiguration.GetFormatterOptionsSection();
}

public void Configure(SimpleConsoleFormatterOptions options)
{
ConsoleFormatterConfigureOptions.Bind(_configuration, options);

if (_configuration["ColorBehavior"] is string colorBehavior)
{
options.ColorBehavior = ConsoleLoggerConfigureOptions.ParseEnum<LoggerColorBehavior>(colorBehavior);
}

if (_configuration["SingleLine"] is string singleLine)
{
options.SingleLine = bool.Parse(singleLine);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TargetInvocationException>(() => serviceProvider.GetRequiredService<ILoggerProvider>());
Assert.Throws<ArgumentOutOfRangeException>(() => serviceProvider.GetRequiredService<ILoggerProvider>());
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1346,6 +1348,21 @@ public void ConsoleLoggerOptions_IncludeScopes_IsReadFromLoggingConfiguration()
Assert.True(formatter.FormatterOptions.IncludeScopes);
}

[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);
}

public static TheoryData<ConsoleLoggerFormat, LogLevel> FormatsAndLevels
{
get
Expand Down