mirror of https://github.com/Sonarr/Sonarr
New: Replace SharpRaven with new Sentry SDK
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
This commit is contained in:
parent
de31dfb11e
commit
f9dc2fb6d5
6
build.sh
6
build.sh
|
@ -196,9 +196,9 @@ PackageMono()
|
||||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
|
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
|
||||||
|
|
||||||
echo "Adding unix System.Runtime.InteropServices.RuntimeInformation.dll (for SharpRaven)"
|
# Is blacklisted by mono from loading from appdir, instead loading from mono GAC.
|
||||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux
|
echo "Remove System.Runtime.InteropServices.RuntimeInformation.dll (uses win32 interop)"
|
||||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux/Sonarr.Update
|
rm $outputFolderLinux/System.Runtime.InteropServices.RuntimeInformation.dll
|
||||||
|
|
||||||
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
||||||
rm $outputFolderLinux/Sonarr.exe*
|
rm $outputFolderLinux/Sonarr.exe*
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NzbDrone.Common.Instrumentation.Sentry;
|
||||||
|
using System;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Test.InstrumentationTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SentryTargetFixture : TestBase
|
||||||
|
{
|
||||||
|
private SentryTarget Subject;
|
||||||
|
|
||||||
|
private static LogLevel[] AllLevels = LogLevel.AllLevels.ToArray();
|
||||||
|
private static LogLevel[] SentryLevels = LogLevel.AllLevels.Where(x => x >= LogLevel.Error).ToArray();
|
||||||
|
private static LogLevel[] OtherLevels = AllLevels.Except(SentryLevels).ToArray();
|
||||||
|
|
||||||
|
private static Exception[] FilteredExceptions = new Exception[] {
|
||||||
|
new UnauthorizedAccessException(),
|
||||||
|
new TinyIoC.TinyIoCResolutionException(typeof(string))
|
||||||
|
};
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
||||||
|
}
|
||||||
|
|
||||||
|
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||||
|
{
|
||||||
|
return LogEventInfo.Create(level, "SentryTest", ex, CultureInfo.InvariantCulture, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource("AllLevels")]
|
||||||
|
public void log_without_error_is_not_sentry_event(LogLevel level)
|
||||||
|
{
|
||||||
|
Subject.IsSentryMessage(GivenLogEvent(level, null, "test")).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource("SentryLevels")]
|
||||||
|
public void error_or_worse_with_exception_is_sentry_event(LogLevel level)
|
||||||
|
{
|
||||||
|
Subject.IsSentryMessage(GivenLogEvent(level, new Exception(), "test")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource("OtherLevels")]
|
||||||
|
public void less_than_error_with_exception_is_not_sentry_event(LogLevel level)
|
||||||
|
{
|
||||||
|
Subject.IsSentryMessage(GivenLogEvent(level, new Exception(), "test")).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource("FilteredExceptions")]
|
||||||
|
public void should_filter_event_for_filtered_exception_types(Exception ex)
|
||||||
|
{
|
||||||
|
var log = GivenLogEvent(LogLevel.Error, ex, "test");
|
||||||
|
Subject.IsSentryMessage(log).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource("FilteredExceptions")]
|
||||||
|
public void should_not_filter_event_for_filtered_exception_types_if_filtering_disabled(Exception ex)
|
||||||
|
{
|
||||||
|
Subject.FilterEvents = false;
|
||||||
|
var log = GivenLogEvent(LogLevel.Error, ex, "test");
|
||||||
|
Subject.IsSentryMessage(log).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, TestCaseSource(typeof(SentryTarget), "FilteredExceptionMessages")]
|
||||||
|
public void should_filter_event_for_filtered_exception_messages(string message)
|
||||||
|
{
|
||||||
|
var log = GivenLogEvent(LogLevel.Error, new Exception("aaaaaaa" + message + "bbbbbbb"), "test");
|
||||||
|
Subject.IsSentryMessage(log).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("A message that isn't filtered")]
|
||||||
|
[TestCase("Error")]
|
||||||
|
public void should_not_filter_event_for_exception_messages_that_are_not_filtered(string message)
|
||||||
|
{
|
||||||
|
var log = GivenLogEvent(LogLevel.Error, new Exception(message), "test");
|
||||||
|
Subject.IsSentryMessage(log).Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
using SharpRaven.Data;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
|
||||||
{
|
|
||||||
public class MachineNameUserFactory : ISentryUserFactory
|
|
||||||
{
|
|
||||||
public SentryUser Create()
|
|
||||||
{
|
|
||||||
return new SentryUser(HashUtil.AnonymousToken());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Sentry;
|
||||||
|
using Sentry.Protocol;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
|
{
|
||||||
|
public static class SentryCleanser
|
||||||
|
{
|
||||||
|
public static SentryEvent CleanseEvent(SentryEvent sentryEvent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message);
|
||||||
|
|
||||||
|
if (sentryEvent.Fingerprint != null)
|
||||||
|
{
|
||||||
|
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||||
|
sentryEvent.SetFingerprint(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentryEvent.Extra != null)
|
||||||
|
{
|
||||||
|
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
||||||
|
sentryEvent.SetExtras(extras);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var exception in sentryEvent.SentryExceptions)
|
||||||
|
{
|
||||||
|
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||||
|
foreach (var frame in exception.Stacktrace.Frames)
|
||||||
|
{
|
||||||
|
frame.FileName = ShortenPath(frame.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return sentryEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Breadcrumb CleanseBreadcrumb(Breadcrumb b)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var message = CleanseLogMessage.Cleanse(b.Message);
|
||||||
|
var data = b.Data?.ToDictionary(x => x.Key, y => CleanseLogMessage.Cleanse(y.Value));
|
||||||
|
return new Breadcrumb(message, b.Type, data, b.Category, b.Level);
|
||||||
|
}
|
||||||
|
catch(Exception)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ShortenPath(string path)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the paths in the stacktrace depend on where it was compiled,
|
||||||
|
// not the current OS
|
||||||
|
var rootDirs = new [] { "\\src\\", "/src/" };
|
||||||
|
foreach (var rootDir in rootDirs)
|
||||||
|
{
|
||||||
|
var index = path.IndexOf(rootDir, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
return path.Substring(index + rootDir.Length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
|
||||||
{
|
|
||||||
public class SentryPacketCleanser
|
|
||||||
{
|
|
||||||
public void CleansePacket(SonarrSentryPacket packet)
|
|
||||||
{
|
|
||||||
packet.Message = CleanseLogMessage.Cleanse(packet.Message);
|
|
||||||
|
|
||||||
if (packet.Fingerprint != null)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < packet.Fingerprint.Length; i++)
|
|
||||||
{
|
|
||||||
packet.Fingerprint[i] = CleanseLogMessage.Cleanse(packet.Fingerprint[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packet.Extra != null)
|
|
||||||
{
|
|
||||||
var target = JObject.FromObject(packet.Extra);
|
|
||||||
new CleansingJsonVisitor().Visit(target);
|
|
||||||
packet.Extra = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,52 +4,123 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Data.SQLite;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Common;
|
using NLog.Common;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using SharpRaven;
|
using Sentry;
|
||||||
using SharpRaven.Data;
|
using Sentry.Protocol;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
{
|
{
|
||||||
[Target("Sentry")]
|
[Target("Sentry")]
|
||||||
public class SentryTarget : TargetWithLayout
|
public class SentryTarget : TargetWithLayout
|
||||||
{
|
{
|
||||||
private readonly RavenClient _client;
|
// don't report uninformative SQLite exceptions
|
||||||
|
// busy/locked are benign https://forums.sonarr.tv/t/owin-sqlite-error-5-database-is-locked/5423/11
|
||||||
private static readonly IDictionary<LogLevel, ErrorLevel> LoggingLevelMap = new Dictionary<LogLevel, ErrorLevel>
|
// The others will be user configuration problems and silt up Sentry
|
||||||
{
|
private static readonly HashSet<SQLiteErrorCode> FilteredSQLiteErrors = new HashSet<SQLiteErrorCode> {
|
||||||
{LogLevel.Debug, ErrorLevel.Debug},
|
SQLiteErrorCode.Busy,
|
||||||
{LogLevel.Error, ErrorLevel.Error},
|
SQLiteErrorCode.Locked,
|
||||||
{LogLevel.Fatal, ErrorLevel.Fatal},
|
SQLiteErrorCode.Perm,
|
||||||
{LogLevel.Info, ErrorLevel.Info},
|
SQLiteErrorCode.ReadOnly,
|
||||||
{LogLevel.Trace, ErrorLevel.Debug},
|
SQLiteErrorCode.IoErr,
|
||||||
{LogLevel.Warn, ErrorLevel.Warning},
|
SQLiteErrorCode.Corrupt,
|
||||||
|
SQLiteErrorCode.Full,
|
||||||
|
SQLiteErrorCode.CantOpen,
|
||||||
|
SQLiteErrorCode.Auth
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// use string and not Type so we don't need a reference to the project
|
||||||
|
// where these are defined
|
||||||
|
private static readonly HashSet<string> FilteredExceptionTypeNames = new HashSet<string> {
|
||||||
|
// UnauthorizedAccessExceptions will just be user configuration issues
|
||||||
|
"UnauthorizedAccessException",
|
||||||
|
// Filter out people stuck in boot loops
|
||||||
|
"CorruptDatabaseException",
|
||||||
|
// This also filters some people in boot loops
|
||||||
|
"TinyIoCResolutionException"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly List<string> FilteredExceptionMessages = new List<string> {
|
||||||
|
// Swallow the many, many exceptions flowing through from Jackett
|
||||||
|
"Jackett.Common.IndexerException"
|
||||||
|
};
|
||||||
|
|
||||||
|
// exception types in this list will additionally have the exception message added to the
|
||||||
|
// sentry fingerprint. Make sure that this message doesn't vary by exception
|
||||||
|
// (e.g. containing a path or a url) so that the sentry grouping is sensible
|
||||||
|
private static readonly HashSet<string> IncludeExceptionMessageTypes = new HashSet<string> {
|
||||||
|
"SQLiteException"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IDictionary<LogLevel, SentryLevel> LoggingLevelMap = new Dictionary<LogLevel, SentryLevel>
|
||||||
|
{
|
||||||
|
{LogLevel.Debug, SentryLevel.Debug},
|
||||||
|
{LogLevel.Error, SentryLevel.Error},
|
||||||
|
{LogLevel.Fatal, SentryLevel.Fatal},
|
||||||
|
{LogLevel.Info, SentryLevel.Info},
|
||||||
|
{LogLevel.Trace, SentryLevel.Debug},
|
||||||
|
{LogLevel.Warn, SentryLevel.Warning},
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IDictionary<LogLevel, BreadcrumbLevel> BreadcrumbLevelMap = new Dictionary<LogLevel, BreadcrumbLevel>
|
||||||
|
{
|
||||||
|
{LogLevel.Debug, BreadcrumbLevel.Debug},
|
||||||
|
{LogLevel.Error, BreadcrumbLevel.Error},
|
||||||
|
{LogLevel.Fatal, BreadcrumbLevel.Critical},
|
||||||
|
{LogLevel.Info, BreadcrumbLevel.Info},
|
||||||
|
{LogLevel.Trace, BreadcrumbLevel.Debug},
|
||||||
|
{LogLevel.Warn, BreadcrumbLevel.Warning},
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly IDisposable _sdk;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
private readonly SentryDebounce _debounce;
|
private readonly SentryDebounce _debounce;
|
||||||
private bool _unauthorized;
|
private bool _unauthorized;
|
||||||
|
|
||||||
|
public bool FilterEvents { get; set; }
|
||||||
|
public string UpdateBranch { get; set; }
|
||||||
|
public Version DatabaseVersion { get; set; }
|
||||||
|
public int DatabaseMigration { get; set; }
|
||||||
|
|
||||||
public SentryTarget(string dsn)
|
public SentryTarget(string dsn)
|
||||||
{
|
{
|
||||||
_client = new RavenClient(new Dsn(dsn), new SonarrJsonPacketFactory(), new SentryRequestFactory(), new MachineNameUserFactory())
|
_sdk = SentrySdk.Init(o =>
|
||||||
{
|
{
|
||||||
Compression = true,
|
o.Dsn = new Dsn(dsn);
|
||||||
Environment = RuntimeInfo.IsProduction ? "production" : "development",
|
o.AttachStacktrace = true;
|
||||||
Release = BuildInfo.Release,
|
o.MaxBreadcrumbs = 200;
|
||||||
ErrorOnCapture = OnError
|
o.SendDefaultPii = true;
|
||||||
};
|
o.Debug = false;
|
||||||
|
o.DiagnosticsLevel = SentryLevel.Debug;
|
||||||
|
o.Release = BuildInfo.Release;
|
||||||
_client.Tags.Add("osfamily", OsInfo.Os.ToString());
|
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
_client.Tags.Add("runtime", PlatformInfo.PlatformName);
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
_client.Tags.Add("culture", Thread.CurrentThread.CurrentCulture.Name);
|
});
|
||||||
_client.Tags.Add("branch", BuildInfo.Branch);
|
|
||||||
_client.Tags.Add("version", BuildInfo.Version.ToString());
|
|
||||||
|
|
||||||
|
SentrySdk.ConfigureScope(scope =>
|
||||||
|
{
|
||||||
|
scope.User = new User {
|
||||||
|
Username = HashUtil.AnonymousToken()
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.SetTag("osfamily", OsInfo.Os.ToString());
|
||||||
|
scope.SetTag("runtime", PlatformInfo.PlatformName);
|
||||||
|
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
|
||||||
|
scope.SetTag("branch", BuildInfo.Branch);
|
||||||
|
scope.SetTag("version", BuildInfo.Version.ToString());
|
||||||
|
scope.SetTag("production", RuntimeInfo.IsProduction.ToString());
|
||||||
|
});
|
||||||
|
|
||||||
_debounce = new SentryDebounce();
|
_debounce = new SentryDebounce();
|
||||||
|
|
||||||
|
// initialize to true and reconfigure later
|
||||||
|
// Otherwise it will default to false and any errors occuring
|
||||||
|
// before config file gets read will not be filtered
|
||||||
|
FilterEvents = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnError(Exception ex)
|
private void OnError(Exception ex)
|
||||||
|
@ -79,28 +150,31 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
|
|
||||||
var fingerPrint = new List<string>
|
var fingerPrint = new List<string>
|
||||||
{
|
{
|
||||||
logEvent.Level.Ordinal.ToString(),
|
logEvent.Level.ToString(),
|
||||||
logEvent.LoggerName
|
logEvent.LoggerName,
|
||||||
|
logEvent.Message
|
||||||
};
|
};
|
||||||
|
|
||||||
var ex = logEvent.Exception;
|
var ex = logEvent.Exception;
|
||||||
|
|
||||||
if (ex != null)
|
if (ex != null)
|
||||||
{
|
{
|
||||||
var exception = ex.GetType().Name;
|
fingerPrint.Add(ex.GetType().FullName);
|
||||||
|
fingerPrint.Add(ex.TargetSite.ToString());
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
exception += ex.InnerException.GetType().Name;
|
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||||
|
}
|
||||||
|
else if (IncludeExceptionMessageTypes.Contains(ex.GetType().Name))
|
||||||
|
{
|
||||||
|
fingerPrint.Add(ex?.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerPrint.Add(exception);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fingerPrint;
|
return fingerPrint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsSentryMessage(LogEventInfo logEvent)
|
public bool IsSentryMessage(LogEventInfo logEvent)
|
||||||
{
|
{
|
||||||
if (logEvent.Properties.ContainsKey("Sentry"))
|
if (logEvent.Properties.ContainsKey("Sentry"))
|
||||||
{
|
{
|
||||||
|
@ -109,6 +183,24 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
|
|
||||||
if (logEvent.Level >= LogLevel.Error && logEvent.Exception != null)
|
if (logEvent.Level >= LogLevel.Error && logEvent.Exception != null)
|
||||||
{
|
{
|
||||||
|
if (FilterEvents)
|
||||||
|
{
|
||||||
|
if (logEvent.Exception is SQLiteException sqliteException && FilteredSQLiteErrors.Contains(sqliteException.ResultCode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FilteredExceptionTypeNames.Contains(logEvent.Exception.GetType().Name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FilteredExceptionMessages.Any(x => logEvent.Exception.Message.Contains(x)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +217,8 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(logEvent.FormattedMessage, logEvent.LoggerName, level: BreadcrumbLevelMap[logEvent.Level]);
|
||||||
|
|
||||||
// don't report non-critical events without exceptions
|
// don't report non-critical events without exceptions
|
||||||
if (!IsSentryMessage(logEvent))
|
if (!IsSentryMessage(logEvent))
|
||||||
{
|
{
|
||||||
|
@ -137,9 +231,8 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString());
|
var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => (object)x.Value.ToString());
|
||||||
extras.Remove("Sentry");
|
extras.Remove("Sentry");
|
||||||
_client.Logger = logEvent.LoggerName;
|
|
||||||
|
|
||||||
if (logEvent.Exception != null)
|
if (logEvent.Exception != null)
|
||||||
{
|
{
|
||||||
|
@ -149,46 +242,58 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sentryMessage = new SentryMessage(logEvent.Message, logEvent.Parameters);
|
|
||||||
|
|
||||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||||
{
|
{
|
||||||
Level = LoggingLevelMap[logEvent.Level],
|
Level = LoggingLevelMap[logEvent.Level],
|
||||||
Message = sentryMessage,
|
Logger = logEvent.LoggerName,
|
||||||
Extra = extras,
|
Message = logEvent.FormattedMessage,
|
||||||
Fingerprint =
|
Environment = UpdateBranch
|
||||||
{
|
|
||||||
logEvent.Level.ToString(),
|
|
||||||
logEvent.LoggerName,
|
|
||||||
logEvent.Message
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (logEvent.Exception != null)
|
sentryEvent.SetExtras(extras);
|
||||||
{
|
sentryEvent.SetFingerprint(fingerPrint);
|
||||||
sentryEvent.Fingerprint.Add(logEvent.Exception.GetType().FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logEvent.Properties.ContainsKey("Sentry"))
|
|
||||||
{
|
|
||||||
sentryEvent.Fingerprint.Clear();
|
|
||||||
Array.ForEach((string[])logEvent.Properties["Sentry"], sentryEvent.Fingerprint.Add);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// this can't be in the constructor as at that point OsInfo won't have
|
||||||
|
// populated these values yet
|
||||||
var osName = Environment.GetEnvironmentVariable("OS_NAME");
|
var osName = Environment.GetEnvironmentVariable("OS_NAME");
|
||||||
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
|
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
|
||||||
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
|
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
|
||||||
|
|
||||||
sentryEvent.Tags.Add("os_name", osName);
|
sentryEvent.SetTag("os_name", osName);
|
||||||
sentryEvent.Tags.Add("os_version", $"{osName} {osVersion}");
|
sentryEvent.SetTag("os_version", $"{osName} {osVersion}");
|
||||||
sentryEvent.Tags.Add("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");
|
sentryEvent.SetTag("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");
|
||||||
|
if (DatabaseVersion != default(Version))
|
||||||
|
{
|
||||||
|
sentryEvent.SetTag("sqlite_version", $"{DatabaseVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
_client.Capture(sentryEvent);
|
SentrySdk.CaptureEvent(sentryEvent);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
OnError(e);
|
OnError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/2496311/implementing-idisposable-on-a-subclass-when-the-parent-also-implements-idisposab
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
// Only do something if we're not already disposed
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
// If disposing == true, we're being called from a call to base.Dispose(). In this case, we Dispose() our logger
|
||||||
|
// If we're being called from a finalizer, our logger will get finalized as well, so no need to do anything.
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_sdk?.Dispose();
|
||||||
|
}
|
||||||
|
// Flag us as disposed. This allows us to handle multiple calls to Dispose() as well as ObjectDisposedException
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should always be safe to call multiple times!
|
||||||
|
// We could include it in the check for disposed above, but I left it out to demonstrate that it's safe
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using SharpRaven.Data;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
|
||||||
{
|
|
||||||
public class SonarrJsonPacketFactory : IJsonPacketFactory
|
|
||||||
{
|
|
||||||
private readonly SentryPacketCleanser _cleanser;
|
|
||||||
|
|
||||||
public SonarrJsonPacketFactory()
|
|
||||||
{
|
|
||||||
_cleanser = new SentryPacketCleanser();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ShortenPath(string path)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = path.IndexOf("\\src\\", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
if (index <= 0)
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Substring(index + "\\src".Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonPacket Create(string project, SentryEvent @event)
|
|
||||||
{
|
|
||||||
var packet = new SonarrSentryPacket(project, @event);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var exception in packet.Exceptions)
|
|
||||||
{
|
|
||||||
foreach (var frame in exception.Stacktrace.Frames)
|
|
||||||
{
|
|
||||||
frame.Filename = ShortenPath(frame.Filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanser.CleansePacket(packet);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public JsonPacket Create(string project, SentryMessage message, ErrorLevel level = ErrorLevel.Info, IDictionary<string, string> tags = null,
|
|
||||||
string[] fingerprint = null, object extra = null)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete]
|
|
||||||
public JsonPacket Create(string project, Exception exception, SentryMessage message = null, ErrorLevel level = ErrorLevel.Error,
|
|
||||||
IDictionary<string, string> tags = null, string[] fingerprint = null, object extra = null)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using Newtonsoft.Json;
|
|
||||||
using SharpRaven.Data;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
|
||||||
{
|
|
||||||
public class SonarrSentryPacket : JsonPacket
|
|
||||||
{
|
|
||||||
private readonly JsonSerializerSettings _setting;
|
|
||||||
|
|
||||||
public SonarrSentryPacket(string project, SentryEvent @event) :
|
|
||||||
base(project, @event)
|
|
||||||
{
|
|
||||||
_setting = new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString(Formatting formatting)
|
|
||||||
{
|
|
||||||
return JsonConvert.SerializeObject(this, formatting, _setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return ToString(Formatting.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@
|
||||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
<PackageReference Include="NLog" Version="4.6.6" />
|
<PackageReference Include="NLog" Version="4.6.6" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="Sentry" Version="1.2.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -17,6 +17,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System.Configuration.Install" />
|
<Reference Include="System.Configuration.Install" />
|
||||||
|
<Reference Include="System.Data.SQLite">
|
||||||
|
<HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="System.ServiceProcess" />
|
<Reference Include="System.ServiceProcess" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Libraries.Test
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class RuntimeInformationFixture : TestBase
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_report_correct_osplatform()
|
||||||
|
{
|
||||||
|
var isWindows = OsInfo.IsWindows;
|
||||||
|
var isLinux = OsInfo.IsLinux;
|
||||||
|
var isOsx = OsInfo.IsOsx;
|
||||||
|
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.Windows).Should().Be(isWindows);
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.Linux).Should().Be(isLinux && !isOsx);
|
||||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX).Should().Be(isOsx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue