Skip to content

Commit

Permalink
Remove ConfigurationBinder usage from Console Logging
Browse files Browse the repository at this point in the history
This allows ConfigurationBinder, and its dependencies like TypeConverter,
to be trimmed in an application that uses Console Logging, like an
ASP.NET API application.

Fix dotnet#81931
  • Loading branch information
eerhardt committed Feb 14, 2023
1 parent 7802c39 commit 583731d
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 21 deletions.
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

0 comments on commit 583731d

Please sign in to comment.