Primary motivation for this change is to create separation of concerns for log sink configuration. The LoggerFactory was becoming too large and had too many responsibilities. Later on this will help facilitate extension of log functionality while respecting the Open Closed Principle.pull/336/head
parent
d6783093ce
commit
06b68772bd
@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Logging;
|
||||
using Recyclarr.Platform;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
|
||||
namespace Recyclarr.Cli.Logging;
|
||||
|
||||
internal class FileLogSinkConfigurator(IAppPaths paths) : ILogConfigurator
|
||||
{
|
||||
public void Configure(LoggerConfiguration config)
|
||||
{
|
||||
var logFilePrefix = $"recyclarr_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}";
|
||||
var logDir = paths.LogDirectory;
|
||||
var template = BuildExpressionTemplate();
|
||||
|
||||
config
|
||||
.WriteTo.Logger(c => c
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(template, LogFilePath("debug")))
|
||||
.WriteTo.Logger(c => c
|
||||
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Verbose)
|
||||
.WriteTo.File(template, LogFilePath("verbose")));
|
||||
|
||||
return;
|
||||
|
||||
string LogFilePath(string type)
|
||||
{
|
||||
return logDir.File($"{logFilePrefix}.{type}.log").FullName;
|
||||
}
|
||||
}
|
||||
|
||||
private static ExpressionTemplate BuildExpressionTemplate()
|
||||
{
|
||||
var template = "[{@t:HH:mm:ss} {@l:u3}] " + LogTemplates.Base +
|
||||
"{Inspect(@x).StackTrace}";
|
||||
|
||||
return new ExpressionTemplate(template);
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Platform;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
using Serilog.Templates.Themes;
|
||||
|
||||
namespace Recyclarr.Cli.Logging;
|
||||
|
||||
public class LoggerFactory(
|
||||
IAppPaths paths,
|
||||
LoggingLevelSwitch levelSwitch,
|
||||
IEnvironment env,
|
||||
IEnumerable<ILogEventSink> sinks)
|
||||
{
|
||||
private static string GetBaseTemplateString()
|
||||
{
|
||||
var scope = LogProperty.Scope;
|
||||
|
||||
return
|
||||
$"{{#if {scope} is not null}}{{{scope}}}: {{#end}}" +
|
||||
"{@m}";
|
||||
}
|
||||
|
||||
private ExpressionTemplate GetConsoleTemplate()
|
||||
{
|
||||
var template = "[{@l:u3}] " + GetBaseTemplateString() +
|
||||
"{#if SanitizedExceptionMessage is not null}: {SanitizedExceptionMessage}{#end}\n";
|
||||
|
||||
var raw = !string.IsNullOrEmpty(env.GetEnvironmentVariable("NO_COLOR"));
|
||||
return new ExpressionTemplate(template, theme: raw ? null : TemplateTheme.Code);
|
||||
}
|
||||
|
||||
private static ExpressionTemplate GetFileTemplate()
|
||||
{
|
||||
var template = "[{@t:HH:mm:ss} {@l:u3}] " + GetBaseTemplateString() +
|
||||
"{#if SanitizedExceptionMessage is not null}: {SanitizedExceptionMessage}{#end}\n" +
|
||||
"{Inspect(@x).StackTrace}";
|
||||
|
||||
return new ExpressionTemplate(template);
|
||||
}
|
||||
|
||||
public ILogger Create()
|
||||
{
|
||||
var logFilePrefix = $"recyclarr_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}";
|
||||
var logDir = paths.LogDirectory;
|
||||
|
||||
var config = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(LogEventLevel.Verbose)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.With<FlurlExceptionSanitizingEnricher>()
|
||||
.WriteTo.Console(GetConsoleTemplate(), levelSwitch: levelSwitch)
|
||||
.WriteTo.Logger(c => c
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(GetFileTemplate(), LogFilePath("debug")))
|
||||
.WriteTo.Logger(c => c
|
||||
.Filter.ByIncludingOnly(e => e.Level == LogEventLevel.Verbose)
|
||||
.WriteTo.File(GetFileTemplate(), LogFilePath("verbose")));
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
config.WriteTo.Sink(sink, levelSwitch: levelSwitch);
|
||||
}
|
||||
|
||||
return config.CreateLogger();
|
||||
|
||||
string LogFilePath(string type)
|
||||
{
|
||||
return logDir.File($"{logFilePrefix}.{type}.log").FullName;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using Recyclarr.Common.Extensions;
|
||||
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
public static string FullMessage(this Exception ex)
|
||||
{
|
||||
if (ex is AggregateException aex)
|
||||
{
|
||||
return aex.InnerExceptions.Aggregate("[ ", (total, next) => $"{total}[{next.FullMessage()}] ") + "]";
|
||||
}
|
||||
|
||||
var msg = ex.Message.Replace(", see inner exception.", "").Trim();
|
||||
var innerMsg = ex.InnerException?.FullMessage();
|
||||
if (innerMsg != null && !innerMsg.ContainsIgnoreCase(msg) && !msg.ContainsIgnoreCase(innerMsg))
|
||||
{
|
||||
msg = $"{msg} [ {innerMsg} ]";
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
using Recyclarr.Http;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Recyclarr.Cli.Logging;
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
internal class FlurlExceptionSanitizingEnricher : ILogEventEnricher
|
||||
{
|
@ -0,0 +1,8 @@
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
public interface ILogConfigurator
|
||||
{
|
||||
void Configure(LoggerConfiguration config);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
public static class LogProperty
|
||||
{
|
||||
public static string Scope => nameof(Scope);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
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";
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Flurl;
|
||||
|
||||
namespace Recyclarr.Logging;
|
||||
|
||||
public static partial class Sanitize
|
||||
{
|
||||
public static string Message(string message)
|
||||
{
|
||||
// Replace full URLs
|
||||
var result = UrlRegex().Replace(message, SanitizeMatch);
|
||||
|
||||
// There are sometimes parenthetical parts of the message that contain the host but are not
|
||||
// detected as true URLs. Just strip those out completely.
|
||||
return HostRegex().Replace(result, "");
|
||||
}
|
||||
|
||||
public static string ExceptionMessage(Exception exception)
|
||||
{
|
||||
return Message(exception.FullMessage());
|
||||
}
|
||||
|
||||
public static Url Url(Url url)
|
||||
{
|
||||
// Replace hostname for user privacy
|
||||
url.Host = "REDACTED";
|
||||
return url;
|
||||
}
|
||||
|
||||
private static string SanitizeMatch(Match match)
|
||||
{
|
||||
return Url(match.Value).ToString() ?? match.Value;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\([-a-zA-Z0-9@:%._+~#=]{1,256}(?::[0-9]+)?\)")]
|
||||
private static partial Regex HostRegex();
|
||||
|
||||
[GeneratedRegex(@"https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}(:[0-9]+)?\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)")]
|
||||
private static partial Regex UrlRegex();
|
||||
}
|
Loading…
Reference in new issue