Your ROOT_URL in app.ini is https://git.cloudchain.link/ but you are visiting https://dash.bss.nz/open-source-mirrors/recyclarr/commit/a4bb339f07475085dc965f363a30c1cd2679c587 You should set ROOT_URL correctly, otherwise the web may not work correctly.

refactor: Improve Spectre.Console and logging initialization

This commit introduces significant changes to the initialization process
of Spectre.Console and the logging system:

1. Logger Initialization:
   - Implement a two-phase initialization for the LoggerFactory.
   - First phase creates an ILogger writing to the CLI console without
     runtime configuration.
   - Second phase wraps the initial logger with additional sinks (e.g.,
     file logging) based on CLI arguments.

2. Spectre.Console Setup:
   - Refactor AutofacTypeRegistrar to store registrations in lists.
   - Implement Build() method to register types with Autofac when
     called.
   - This approach better aligns with Autofac's registration and
     resolution separation.

3. Global Setup Tasks:
   - Introduce AppDataDirSetupTask and LoggerSetupTask.
   - Modify IGlobalSetupTask interface to accept BaseCommandSettings.
   - Update existing setup tasks to conform to the new interface.

4. Error Handling:
   - Implement top-level exception handling in Program.Main().
   - Remove IFlurlHttpExceptionHandler interface, simplify
     FlurlHttpExceptionHandler.

5. Logging Improvements:
   - Move console logging setup to LoggerFactory.
   - Introduce IndirectLoggerDecorator to allow dynamic logger updates.
   - Simplify log template management.

6. Dependency Injection:
   - Update CompositionRoot to reflect new logger and setup task
     structure.
   - Remove LoggingAutofacModule, integrate its functionality into
     CompositionRoot.

These changes improve the flexibility and maintainability of the
application's startup process, particularly in handling logging and CLI
argument processing. The new structure allows for more dynamic
configuration of services based on runtime parameters.
pull/351/head
Robert Dailey 6 months ago
parent e4feb92980
commit a4bb339f07

@ -4,7 +4,7 @@ using Autofac;
using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using Recyclarr.Cache;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration;
@ -29,6 +29,7 @@ using Recyclarr.Settings;
using Recyclarr.TrashGuide;
using Recyclarr.VersionControl;
using Recyclarr.Yaml;
using Serilog.Core;
using Spectre.Console.Cli;
namespace Recyclarr.Cli;
@ -42,7 +43,7 @@ public static class CompositionRoot
// Needed for Autofac.Extras.Ordering
builder.RegisterSource<OrderedRegistrationSource>();
RegisterLogger(builder, thisAssembly);
RegisterLogger(builder);
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<ConfigAutofacModule>();
@ -89,22 +90,30 @@ public static class CompositionRoot
.OrderByRegistration();
}
private static void RegisterLogger(ContainerBuilder builder, Assembly thisAssembly)
private static void RegisterLogger(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(thisAssembly)
.AssignableTo<ILogConfigurator>()
// Log Configurators
builder.RegisterTypes(
typeof(FileLogSinkConfigurator))
.As<ILogConfigurator>();
builder.RegisterModule<LoggingAutofacModule>();
builder.RegisterType<LoggingLevelSwitch>().SingleInstance();
builder.RegisterType<LoggerFactory>().SingleInstance();
builder.RegisterType<IndirectLoggerDecorator>().As<ILogger>();
builder.RegisterType<LogJanitor>();
}
private static void CliRegistrations(ContainerBuilder builder)
{
builder.RegisterType<AutofacTypeRegistrar>().As<ITypeRegistrar>();
builder.RegisterType<CommandApp>();
builder.RegisterType<CommandSetupInterceptor>().As<ICommandInterceptor>();
builder.RegisterComposite<CompositeGlobalSetupTask, IGlobalSetupTask>();
builder.RegisterTypes(
typeof(AppDataDirSetupTask), // This must be first; ILogger creation depends on IAppPaths
typeof(LoggerSetupTask),
typeof(ProgramInformationDisplayTask),
typeof(JanitorCleanupTask))
.As<IGlobalSetupTask>()

@ -0,0 +1,47 @@
using Autofac;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console;
internal class AutofacTypeRegistrar(ILifetimeScope scope) : ITypeRegistrar
{
private readonly List<(Type, Type)> _typeRegistrations = [];
private readonly List<(Type, object)> _instanceRegistrations = [];
private readonly List<(Type, Func<object>)> _lazyRegistrations = [];
public void Register(Type service, Type implementation)
{
_typeRegistrations.Add((service, implementation));
}
public void RegisterInstance(Type service, object implementation)
{
_instanceRegistrations.Add((service, implementation));
}
public void RegisterLazy(Type service, Func<object> factory)
{
_lazyRegistrations.Add((service, factory));
}
public ITypeResolver Build()
{
return new AutofacTypeResolver(scope.BeginLifetimeScope(builder =>
{
foreach (var (service, impl) in _typeRegistrations)
{
builder.RegisterType(impl).As(service).SingleInstance();
}
foreach (var (service, implementation) in _instanceRegistrations)
{
builder.RegisterInstance(implementation).As(service);
}
foreach (var (service, factory) in _lazyRegistrations)
{
builder.Register(_ => factory()).As(service).SingleInstance();
}
}));
}
}

@ -1,7 +1,7 @@
using Autofac;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Helpers;
namespace Recyclarr.Cli.Console;
internal class AutofacTypeResolver(ILifetimeScope scope) : ITypeResolver
{

@ -1,11 +0,0 @@
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console;
public static class CommandConfiguratorExtensions
{
public static ICommandConfigurator WithExample(this ICommandConfigurator cli, params string[] args)
{
return cli.WithExample(args);
}
}

@ -2,7 +2,7 @@ using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Recyclarr.Cli.Console.Helpers;
namespace Recyclarr.Cli.Console;
// Taken from: https://github.com/spectreconsole/spectre.console/issues/701#issuecomment-1081834778
internal sealed class ConsoleAppCancellationTokenSource : IDisposable

@ -1,28 +0,0 @@
using Autofac;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Helpers;
internal class AutofacTypeRegistrar(ContainerBuilder builder)
: ITypeRegistrar
{
public void Register(Type service, Type implementation)
{
builder.RegisterType(implementation).As(service).SingleInstance();
}
public void RegisterInstance(Type service, object implementation)
{
builder.RegisterInstance(implementation).As(service);
}
public void RegisterLazy(Type service, Func<object> factory)
{
builder.Register(_ => factory()).As(service).SingleInstance();
}
public ITypeResolver Build()
{
return new AutofacTypeResolver(builder.Build());
}
}

@ -0,0 +1,16 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Platform;
namespace Recyclarr.Cli.Console.Setup;
public class AppDataDirSetupTask(IAppDataSetup appDataSetup) : IGlobalSetupTask
{
public void OnStart(BaseCommandSettings cmd)
{
appDataSetup.SetAppDataDirectoryOverride(cmd.AppData ?? "");
}
public void OnFinish()
{
}
}

@ -1,43 +1,31 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Platform;
using Serilog.Core;
using Serilog.Events;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Helpers;
namespace Recyclarr.Cli.Console.Setup;
internal sealed class CommandSetupInterceptor : ICommandInterceptor, IDisposable
{
private readonly ConsoleAppCancellationTokenSource _ct = new();
private readonly LoggingLevelSwitch _loggingLevelSwitch;
private readonly IAppDataSetup _appDataSetup;
private readonly Lazy<IGlobalSetupTask> _globalTaskSetup;
public CommandSetupInterceptor(
Lazy<ILogger> log,
LoggingLevelSwitch loggingLevelSwitch,
IAppDataSetup appDataSetup,
Lazy<IGlobalSetupTask> globalTaskSetup)
{
_loggingLevelSwitch = loggingLevelSwitch;
_appDataSetup = appDataSetup;
_globalTaskSetup = globalTaskSetup;
_ct.CancelPressed.Subscribe(_ => log.Value.Information("Exiting due to signal interrupt"));
}
// Executed on CLI startup
public void Intercept(CommandContext context, CommandSettings settings)
{
switch (settings)
if (settings is not BaseCommandSettings cmd)
{
case BaseCommandSettings cmd:
HandleBaseCommand(cmd);
break;
throw new InvalidOperationException("Command settings must be of type BaseCommandSettings");
}
_globalTaskSetup.Value.OnStart();
cmd.CancellationToken = _ct.Token;
_globalTaskSetup.Value.OnStart(cmd);
}
// Executed on CLI exit
@ -46,19 +34,6 @@ internal sealed class CommandSetupInterceptor : ICommandInterceptor, IDisposable
_globalTaskSetup.Value.OnFinish();
}
private void HandleBaseCommand(BaseCommandSettings cmd)
{
_appDataSetup.SetAppDataDirectoryOverride(cmd.AppData ?? "");
cmd.CancellationToken = _ct.Token;
_loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{
true => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
}
public void Dispose()
{
_ct.Dispose();

@ -1,15 +1,23 @@
using Recyclarr.Cli.Console.Commands;
namespace Recyclarr.Cli.Console.Setup;
[UsedImplicitly]
public class CompositeGlobalSetupTask(IOrderedEnumerable<IGlobalSetupTask> tasks) : IGlobalSetupTask
public class CompositeGlobalSetupTask(IOrderedEnumerable<Lazy<IGlobalSetupTask>> tasks) : IGlobalSetupTask
{
public void OnStart()
public void OnStart(BaseCommandSettings cmd)
{
tasks.ForEach(x => x.OnStart());
foreach (var task in tasks)
{
task.Value.OnStart(cmd);
}
}
public void OnFinish()
{
tasks.Reverse().ForEach(x => x.OnFinish());
foreach (var task in tasks.Reverse())
{
task.Value.OnFinish();
}
}
}

@ -1,12 +1,9 @@
using Recyclarr.Cli.Console.Commands;
namespace Recyclarr.Cli.Console.Setup;
public interface IGlobalSetupTask
{
void OnStart()
{
}
void OnFinish()
{
}
void OnStart(BaseCommandSettings cmd);
void OnFinish();
}

@ -1,3 +1,4 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Logging;
using Recyclarr.Settings;
@ -6,6 +7,10 @@ namespace Recyclarr.Cli.Console.Setup;
public class JanitorCleanupTask(LogJanitor janitor, ILogger log, ISettingsProvider settingsProvider)
: IGlobalSetupTask
{
public void OnStart(BaseCommandSettings cmd)
{
}
public void OnFinish()
{
var maxFiles = settingsProvider.Settings.LogJanitor.MaxFiles;

@ -0,0 +1,30 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Logging;
using Recyclarr.Logging;
using Serilog.Core;
using Serilog.Events;
namespace Recyclarr.Cli.Console.Setup;
public class LoggerSetupTask(
LoggingLevelSwitch loggingLevelSwitch,
LoggerFactory loggerFactory,
IEnumerable<ILogConfigurator> logConfigurators)
: IGlobalSetupTask
{
public void OnStart(BaseCommandSettings cmd)
{
loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{
true => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
loggerFactory.AddLogConfiguration(logConfigurators);
}
public void OnFinish()
{
throw new NotImplementedException();
}
}

@ -1,12 +1,17 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Platform;
namespace Recyclarr.Cli.Console.Setup;
public class ProgramInformationDisplayTask(ILogger log, IAppPaths paths) : IGlobalSetupTask
{
public void OnStart()
public void OnStart(BaseCommandSettings cmd)
{
log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
log.Debug("App Data Dir: {AppData}", paths.AppDataDirectory);
}
public void OnFinish()
{
}
}

@ -1,23 +0,0 @@
using Recyclarr.Logging;
using Recyclarr.Platform;
using Serilog.Core;
using Serilog.Templates;
using Serilog.Templates.Themes;
namespace Recyclarr.Cli.Logging;
internal class ConsoleLogSinkConfigurator(LoggingLevelSwitch levelSwitch, IEnvironment env) : ILogConfigurator
{
public void Configure(LoggerConfiguration config)
{
config.WriteTo.Console(BuildExpressionTemplate(), levelSwitch: levelSwitch);
}
private ExpressionTemplate BuildExpressionTemplate()
{
var template = "[{@l:u3}] " + LogTemplates.Base;
var raw = !string.IsNullOrEmpty(env.GetEnvironmentVariable("NO_COLOR"));
return new ExpressionTemplate(template, theme: raw ? null : TemplateTheme.Code);
}
}

@ -32,7 +32,7 @@ internal class FileLogSinkConfigurator(IAppPaths paths) : ILogConfigurator
private static ExpressionTemplate BuildExpressionTemplate()
{
var template = "[{@t:HH:mm:ss} {@l:u3}] " + LogTemplates.Base +
var template = "[{@t:HH:mm:ss} {@l:u3}] " + LogSetup.BaseTemplate +
"{Inspect(@x).StackTrace}";
return new ExpressionTemplate(template);

@ -0,0 +1,11 @@
using Serilog.Events;
namespace Recyclarr.Cli.Logging;
internal class IndirectLoggerDecorator(LoggerFactory loggerFactory) : ILogger
{
public void Write(LogEvent logEvent)
{
loggerFactory.Logger.Write(logEvent);
}
}

@ -0,0 +1,37 @@
using Recyclarr.Logging;
using Recyclarr.Platform;
using Serilog.Core;
using Serilog.Templates;
using Serilog.Templates.Themes;
namespace Recyclarr.Cli.Logging;
public class LoggerFactory(IEnvironment env, LoggingLevelSwitch levelSwitch)
{
public ILogger Logger { get; private set; } = LogSetup.BaseConfiguration()
.WriteTo.Console(BuildExpressionTemplate(env), levelSwitch: levelSwitch)
.CreateLogger();
private static ExpressionTemplate BuildExpressionTemplate(IEnvironment env)
{
var template = "[{@l:u3}] " + LogSetup.BaseTemplate;
var raw = !string.IsNullOrEmpty(env.GetEnvironmentVariable("NO_COLOR"));
return new ExpressionTemplate(template, theme: raw ? null : TemplateTheme.Code);
}
public void AddLogConfiguration(IEnumerable<ILogConfigurator> configurators)
{
var config = LogSetup.BaseConfiguration()
.WriteTo.Logger(Logger);
// throw new InvalidOperationException("testing only"); // testing only
foreach (var configurator in configurators)
{
configurator.Configure(config);
}
Logger = config.CreateLogger();
}
}

@ -8,7 +8,7 @@ using YamlDotNet.Core;
namespace Recyclarr.Cli.Processors.ErrorHandling;
public class ConsoleExceptionHandler(ILogger log, IFlurlHttpExceptionHandler httpExceptionHandler)
public class ConsoleExceptionHandler(ILogger log)
{
public async Task<bool> HandleException(Exception sourceException)
{
@ -20,6 +20,7 @@ public class ConsoleExceptionHandler(ILogger log, IFlurlHttpExceptionHandler htt
case FlurlHttpException e:
log.Error(e, "HTTP error");
var httpExceptionHandler = new FlurlHttpExceptionHandler(log);
await httpExceptionHandler.ProcessServiceErrorMessages(new ServiceErrorMessageExtractor(e));
break;

@ -3,7 +3,7 @@ using Recyclarr.Common.Extensions;
namespace Recyclarr.Cli.Processors.ErrorHandling;
public class FlurlHttpExceptionHandler(ILogger log) : IFlurlHttpExceptionHandler
public class FlurlHttpExceptionHandler(ILogger log)
{
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public async Task ProcessServiceErrorMessages(IServiceErrorMessageExtractor extractor)

@ -1,6 +0,0 @@
namespace Recyclarr.Cli.Processors.ErrorHandling;
public interface IFlurlHttpExceptionHandler
{
Task ProcessServiceErrorMessages(IServiceErrorMessageExtractor extractor);
}

@ -14,7 +14,7 @@ public class ServiceProcessorsAutofacModule : Module
base.Load(builder);
builder.RegisterType<ConsoleExceptionHandler>();
builder.RegisterType<FlurlHttpExceptionHandler>().As<IFlurlHttpExceptionHandler>();
builder.RegisterType<FlurlHttpExceptionHandler>();
// Sync
builder.RegisterType<SyncProcessor>().As<ISyncProcessor>();

@ -1,48 +1,49 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
using Recyclarr.Cli.Console;
using Recyclarr.Cli.Console.Helpers;
using Spectre.Console;
using Recyclarr.Cli.Processors;
using Recyclarr.Cli.Processors.ErrorHandling;
using Spectre.Console.Cli;
namespace Recyclarr.Cli;
internal static class Program
{
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification =
"Top level catch-all to translate exceptions; lack of specificity is intentional")]
public static async Task<int> Main(string[] args)
{
var builder = new ContainerBuilder();
CompositionRoot.Setup(builder);
var scope = builder.Build();
var app = new CommandApp(new AutofacTypeRegistrar(builder));
app.Configure(config =>
try
{
#if DEBUG
config.PropagateExceptions();
config.ValidateExamples();
#endif
var app = scope.Resolve<CommandApp>();
app.Configure(config =>
{
#if DEBUG
config.ValidateExamples();
#endif
config.Settings.StrictParsing = true;
config.PropagateExceptions();
config.UseStrictParsing();
config.SetApplicationName("recyclarr");
config.SetApplicationVersion(
$"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})");
config.SetApplicationName("recyclarr");
config.SetApplicationVersion(
$"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})");
config.SetExceptionHandler((ex, resolver) =>
{
var log = (ILogger?) resolver?.Resolve(typeof(ILogger));
if (log is null)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
}
else
{
log.Error(ex, "Non-recoverable Exception");
}
CliSetup.Commands(config);
});
CliSetup.Commands(config);
});
return await app.RunAsync(args);
return await app.RunAsync(args);
}
catch (Exception e)
{
var log = scope.Resolve<ILogger>();
var exceptionHandler = new ConsoleExceptionHandler(log);
await exceptionHandler.HandleException(e);
return (int) ExitStatus.Failed;
}
}
}

@ -3,7 +3,7 @@ using Serilog.Events;
namespace Recyclarr.Logging;
internal class FlurlExceptionSanitizingEnricher : ILogEventEnricher
public class FlurlExceptionSanitizingEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{

@ -0,0 +1,27 @@
using Serilog;
using Serilog.Events;
namespace Recyclarr.Logging;
public static class LogSetup
{
public static string BaseTemplate { get; } = GetBaseTemplateString();
public static LoggerConfiguration BaseConfiguration()
{
return new LoggerConfiguration()
.MinimumLevel.Is(LogEventLevel.Verbose)
.Enrich.FromLogContext()
.Enrich.With<FlurlExceptionSanitizingEnricher>();
}
private static string GetBaseTemplateString()
{
var scope = LogProperty.Scope;
return
$"{{#if {scope} is not null}}{{{scope}}}: {{#end}}" +
"{@m}" +
"{#if SanitizedExceptionMessage is not null}: {SanitizedExceptionMessage}{#end}\n";
}
}

@ -1,16 +0,0 @@
namespace Recyclarr.Logging;
public static class LogTemplates
{
public static string Base { get; } = GetBaseTemplateString();
private static string GetBaseTemplateString()
{
var scope = LogProperty.Scope;
return
$"{{#if {scope} is not null}}{{{scope}}}: {{#end}}" +
"{@m}" +
"{#if SanitizedExceptionMessage is not null}: {SanitizedExceptionMessage}{#end}\n";
}
}

@ -1,22 +0,0 @@
using Serilog;
using Serilog.Events;
namespace Recyclarr.Logging;
public class LoggerFactory(IEnumerable<ILogConfigurator> configurators)
{
public ILogger Create()
{
var config = new LoggerConfiguration()
.MinimumLevel.Is(LogEventLevel.Verbose)
.Enrich.FromLogContext()
.Enrich.With<FlurlExceptionSanitizingEnricher>();
foreach (var configurator in configurators)
{
configurator.Configure(config);
}
return config.CreateLogger();
}
}

@ -1,18 +0,0 @@
using Autofac;
using Serilog;
using Serilog.Core;
using Module = Autofac.Module;
namespace Recyclarr.Logging;
public class LoggingAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<LoggingLevelSwitch>().SingleInstance();
builder.RegisterType<LoggerFactory>();
builder.Register(c => c.Resolve<LoggerFactory>().Create()).As<ILogger>().SingleInstance();
}
}

@ -7,16 +7,6 @@ namespace Recyclarr.Cli.IntegrationTests;
[TestFixture]
internal class BaseCommandSetupIntegrationTest : CliIntegrationFixture
{
[Test]
public void Base_command_startup_tasks_are_registered()
{
var registrations = Resolve<IEnumerable<IGlobalSetupTask>>();
registrations.Select(x => x.GetType()).Should().BeEquivalentTo([
typeof(JanitorCleanupTask),
typeof(ProgramInformationDisplayTask)
]);
}
[Test]
public void Log_janitor_cleans_up_user_specified_max_files()
{

Loading…
Cancel
Save