- Centralize sanitization into a custom Serilog enricher - More log sites pass the exception object in - Console output now consistently only prints the mssage, but not the stack trace. - File output always outputs the stack trace. Additionally, there are fixes to several SonarLint issues.pull/259/head
parent
925d54368a
commit
cb56ab5737
@ -1,18 +0,0 @@
|
|||||||
using Serilog.Core;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Logging;
|
|
||||||
|
|
||||||
public class ExceptionMessageEnricher : ILogEventEnricher
|
|
||||||
{
|
|
||||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
|
||||||
{
|
|
||||||
var msg = logEvent.Exception?.Message;
|
|
||||||
if (string.IsNullOrEmpty(msg))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ExceptionMessage", msg));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,43 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Recyclarr.Http;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Logging;
|
||||||
|
|
||||||
|
public class FlurlExceptionSanitizingEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||||
|
{
|
||||||
|
if (logEvent.Exception is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitizedMessage = Sanitize.ExceptionMessage(logEvent.Exception);
|
||||||
|
|
||||||
|
// Use a builder to handle whether to use a newline character for the full exception message without checking if
|
||||||
|
// the sanitized message is null or whitespace more than once.
|
||||||
|
var fullBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(sanitizedMessage))
|
||||||
|
{
|
||||||
|
MakeProperty("SanitizedExceptionMessage", sanitizedMessage);
|
||||||
|
fullBuilder.Append($"{sanitizedMessage}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (logEvent.Exception.StackTrace is not null)
|
||||||
|
{
|
||||||
|
fullBuilder.Append(logEvent.Exception.StackTrace);
|
||||||
|
MakeProperty("SanitizedExceptionFull", fullBuilder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void MakeProperty(string propertyName, object value)
|
||||||
|
{
|
||||||
|
logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty(propertyName, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
namespace Recyclarr.Cli.Processors.Config;
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
public class FileExistsException(string attemptedPath) : Exception
|
public class FileExistsException(string attemptedPath) : Exception($"File already exists: {attemptedPath}")
|
||||||
{
|
{
|
||||||
public string AttemptedPath { get; } = attemptedPath;
|
public string AttemptedPath { get; } = attemptedPath;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
namespace Recyclarr.Common.Extensions;
|
||||||
|
|
||||||
|
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,3 +1,3 @@
|
|||||||
namespace Recyclarr.Config.Parsing.ErrorHandling;
|
namespace Recyclarr.Config.Parsing.ErrorHandling;
|
||||||
|
|
||||||
public class NoConfigurationFilesException : Exception;
|
public class NoConfigurationFilesException() : Exception("No configuration files found");
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Flurl.Http;
|
using Flurl;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
|
||||||
namespace Recyclarr.Http;
|
namespace Recyclarr.Http;
|
||||||
|
|
||||||
public static partial class FlurlExtensions
|
public static partial class Sanitize
|
||||||
{
|
{
|
||||||
public static string SanitizedExceptionMessage(this FlurlHttpException exception)
|
public static string Message(string message)
|
||||||
{
|
{
|
||||||
// Replace full URLs
|
// Replace full URLs
|
||||||
var result = UrlRegex().Replace(exception.Message, Sanitize);
|
var result = UrlRegex().Replace(message, SanitizeMatch);
|
||||||
|
|
||||||
// There are sometimes parenthetical parts of the message that contain the host but are not
|
// There are sometimes parenthetical parts of the message that contain the host but are not
|
||||||
// detected as true URLs. Just strip those out completely.
|
// detected as true URLs. Just strip those out completely.
|
||||||
return HostRegex().Replace(result, "");
|
return HostRegex().Replace(result, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Sanitize(Match match)
|
public static string ExceptionMessage(Exception exception)
|
||||||
{
|
{
|
||||||
return FlurlLogging.SanitizeUrl(match.Value).ToString() ?? match.Value;
|
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]+)?\)")]
|
[GeneratedRegex(@"\([-a-zA-Z0-9@:%._+~#=]{1,256}(?::[0-9]+)?\)")]
|
@ -1,11 +1,8 @@
|
|||||||
namespace Recyclarr.VersionControl;
|
namespace Recyclarr.VersionControl;
|
||||||
|
|
||||||
public class GitCmdException(int exitCode, string error) : Exception("Git command failed with a non-zero exit code")
|
public class GitCmdException(int exitCode, string errorMessage) : Exception(errorMessage)
|
||||||
{
|
{
|
||||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
|
||||||
public string Error { get; } = error;
|
|
||||||
public int ExitCode { get; } = exitCode;
|
public int ExitCode { get; } = exitCode;
|
||||||
// ReSharper restore UnusedAutoPropertyAccessor.Global
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InvalidGitRepoException(string? message) : Exception(message);
|
public class InvalidGitRepoException(string? message) : Exception(message);
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
using JetBrains.Annotations;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
using YamlDotNet.Serialization.NodeDeserializers;
|
|
||||||
|
|
||||||
// ReSharper disable UnusedMember.Global
|
|
||||||
|
|
||||||
namespace Recyclarr.Yaml.YamlDotNet;
|
|
||||||
|
|
||||||
public static class YamlDotNetExtensions
|
|
||||||
{
|
|
||||||
public static T? DeserializeType<T>(this IDeserializer deserializer, string data)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
var extractor = deserializer.Deserialize<RootExtractor<T>>(data);
|
|
||||||
return extractor.RootObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DeserializerBuilder WithRequiredPropertyValidation(this DeserializerBuilder builder)
|
|
||||||
{
|
|
||||||
return builder
|
|
||||||
.WithNodeDeserializer(inner => new ValidatingDeserializer(inner),
|
|
||||||
s => s.InsteadOf<ObjectNodeDeserializer>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
|
|
||||||
private sealed class RootExtractor<T>
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
public T? RootObject { get; }
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue