diff --git a/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs b/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs new file mode 100644 index 000000000..c719cef3e --- /dev/null +++ b/src/NzbDrone.Common/Extensions/ExceptionExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Extensions +{ + public static class ExceptionExtensions + { + public static T WithData(this T ex, string key, string value) where T : Exception + { + ex.AddData(key, value); + + return ex; + } + public static T WithData(this T ex, string key, int value) where T : Exception + { + ex.AddData(key, value.ToString()); + + return ex; + } + + public static T WithData(this T ex, string key, Http.HttpUri value) where T : Exception + { + ex.AddData(key, value.ToString()); + + return ex; + } + + + public static T WithData(this T ex, Http.HttpResponse response, int maxSampleLength = 512) where T : Exception + { + if (response == null || response.Content == null) return ex; + + var contentSample = response.Content.Substring(0, Math.Min(response.Content.Length, 512)); + + if (response.Headers != null) + { + ex.AddData("ContentType", response.Headers.ContentType ?? string.Empty); + } + ex.AddData("ContentLength", response.Content.Length.ToString()); + ex.AddData("ContentSample", contentSample); + + return ex; + } + + + private static void AddData(this Exception ex, string key, string value) + { + if (value.IsNullOrWhiteSpace()) return; + + ex.Data[key] = value; + } + } +} diff --git a/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs new file mode 100644 index 000000000..f33f4587b --- /dev/null +++ b/src/NzbDrone.Common/Instrumentation/CleansingJsonVisitor.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Common.Instrumentation +{ + public class CleansingJsonVisitor : JsonVisitor + { + public override void Visit(JArray json) + { + for (var i = 0; i < json.Count; i++) + { + if (json[i].Type == JTokenType.String) + { + var text = json[i].Value(); + json[i] = new JValue(CleanseLogMessage.Cleanse(text)); + } + } + foreach (JToken token in json) + { + Visit(token); + } + } + + public override void Visit(JProperty property) + { + if (property.Value.Type == JTokenType.String) + { + property.Value = CleanseValue(property.Value as JValue); + } + else + { + base.Visit(property); + } + } + + private JValue CleanseValue(JValue value) + { + var text = value.Value(); + var cleansed = CleanseLogMessage.Cleanse(text); + return new JValue(cleansed); + } + } +} diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs new file mode 100644 index 000000000..0815c0d48 --- /dev/null +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryPacketCleanser.cs @@ -0,0 +1,31 @@ +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; + } + } + } +} diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index 744d8063a..176884a31 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -111,18 +111,10 @@ namespace NzbDrone.Common.Instrumentation.Sentry return; } - var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => CleanseLogMessage.Cleanse(x.Value.ToString())); + var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()); _client.Logger = logEvent.LoggerName; - string cleansedMessage = CleanseLogMessage.Cleanse(logEvent.Message); - string cleansedFormattedMessage = cleansedMessage; - - if (logEvent.Parameters != null) - { - cleansedFormattedMessage = CleanseLogMessage.Cleanse(string.Format(logEvent.Message, logEvent.Parameters)); - } - - var sentryMessage = new SentryMessage(cleansedFormattedMessage); + var sentryMessage = new SentryMessage(logEvent.Message, logEvent.Parameters); var sentryEvent = new SentryEvent(logEvent.Exception) { @@ -133,7 +125,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry { logEvent.Level.ToString(), logEvent.LoggerName, - cleansedMessage + logEvent.Message } }; diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs index fb639fb2f..3ba6b499c 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SonarrJsonPacketFactory.cs @@ -6,6 +6,13 @@ namespace NzbDrone.Common.Instrumentation.Sentry { public class SonarrJsonPacketFactory : IJsonPacketFactory { + private readonly SentryPacketCleanser _cleanser; + + public SonarrJsonPacketFactory() + { + _cleanser = new SentryPacketCleanser(); + } + private static string ShortenPath(string path) { @@ -37,6 +44,8 @@ namespace NzbDrone.Common.Instrumentation.Sentry frame.Filename = ShortenPath(frame.Filename); } } + + _cleanser.CleansePacket(packet); } catch (Exception) { @@ -46,7 +55,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry return packet; } - [Obsolete] public JsonPacket Create(string project, SentryMessage message, ErrorLevel level = ErrorLevel.Info, IDictionary tags = null, string[] fingerprint = null, object extra = null) @@ -61,4 +69,4 @@ namespace NzbDrone.Common.Instrumentation.Sentry throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 34ee38755..928b0e1bb 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -136,6 +136,7 @@ + @@ -175,12 +176,14 @@ + + @@ -205,6 +208,7 @@ + diff --git a/src/NzbDrone.Common/Serializer/JsonVisitor.cs b/src/NzbDrone.Common/Serializer/JsonVisitor.cs new file mode 100644 index 000000000..87fdeeeec --- /dev/null +++ b/src/NzbDrone.Common/Serializer/JsonVisitor.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace NzbDrone.Common.Serializer +{ + + public class JsonVisitor + { + protected void Dispatch(JToken json) + { + switch (json.Type) + { + case JTokenType.Object: + Visit(json as JObject); + break; + + case JTokenType.Array: + Visit(json as JArray); + break; + + case JTokenType.Raw: + Visit(json as JRaw); + break; + + case JTokenType.Constructor: + Visit(json as JConstructor); + break; + + case JTokenType.Property: + Visit(json as JProperty); + break; + + case JTokenType.Comment: + case JTokenType.Integer: + case JTokenType.Float: + case JTokenType.String: + case JTokenType.Boolean: + case JTokenType.Null: + case JTokenType.Undefined: + case JTokenType.Date: + case JTokenType.Bytes: + case JTokenType.Guid: + case JTokenType.Uri: + case JTokenType.TimeSpan: + Visit(json as JValue); + break; + + default: + break; + } + } + + public virtual void Visit(JToken json) + { + Dispatch(json); + } + + public virtual void Visit(JContainer json) + { + Dispatch(json); + } + + public virtual void Visit(JArray json) + { + foreach (JToken token in json) + { + Visit(token); + } + } + public virtual void Visit(JConstructor json) + { + } + + public virtual void Visit(JObject json) + { + foreach (JProperty property in json.Properties()) + { + Visit(property); + } + } + + public virtual void Visit(JProperty property) + { + Visit(property.Value); + } + + public virtual void Visit(JValue value) + { + + } + } +} diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 1405eb0d4..75ad1354e 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -243,6 +243,7 @@ namespace NzbDrone.Core.Indexers catch (CloudFlareCaptchaException ex) { _indexerStatusService.RecordFailure(Definition.Id); + ex.WithData("FeedUrl", url); if (ex.IsExpired) { _logger.Error(ex, "Expired CAPTCHA token for {0}, please refresh in indexer settings.", this); @@ -257,11 +258,11 @@ namespace NzbDrone.Core.Indexers _indexerStatusService.RecordFailure(Definition.Id); _logger.Warn(ex, "{0}", url); } - catch (Exception feedEx) + catch (Exception ex) { _indexerStatusService.RecordFailure(Definition.Id); - feedEx.Data.Add("FeedUrl", url); - _logger.Error(feedEx, "An error occurred while processing feed. {0}", url); + ex.WithData("FeedUrl", url); + _logger.Error(ex, "An error occurred while processing feed. {0}", url); } return CleanupReleases(releases); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs index a3eac6d23..6e870ec3d 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs @@ -69,6 +69,8 @@ namespace NzbDrone.Core.Indexers.Newznab catch (XmlException ex) { _logger.Debug(ex, "Failed to parse newznab api capabilities for {0}.", indexerSettings.Url); + + ex.WithData(response); throw; } catch (Exception ex) diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index 1cd46e754..93304f2fd 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -65,12 +65,14 @@ namespace NzbDrone.Core.Indexers } catch (UnsupportedFeedException itemEx) { - itemEx.Data.Add("Item", item.Title()); + itemEx.WithData("FeedUrl", indexerResponse.Request.Url); + itemEx.WithData("ItemTitle", item.Title()); throw; } catch (Exception itemEx) { - itemEx.Data.Add("Item", item.Title()); + itemEx.WithData("FeedUrl", indexerResponse.Request.Url); + itemEx.WithData("ItemTitle", item.Title()); _logger.Error(itemEx, "An error occurred while processing feed item from {0}", indexerResponse.Request.Url); } } @@ -95,8 +97,7 @@ namespace NzbDrone.Core.Indexers var contentSample = indexerResponse.Content.Substring(0, Math.Min(indexerResponse.Content.Length, 512)); _logger.Debug("Truncated response content (originally {0} characters): {1}", indexerResponse.Content.Length, contentSample); - ex.Data.Add("ContentLength", indexerResponse.Content.Length); - ex.Data.Add("ContentSample", contentSample); + ex.WithData(indexerResponse.HttpResponse); throw; } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssSettingsDetector.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssSettingsDetector.cs index c20c09b2a..53ee117cc 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssSettingsDetector.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssSettingsDetector.cs @@ -40,23 +40,31 @@ namespace NzbDrone.Core.Indexers.TorrentRss { _logger.Debug("Evaluating TorrentRss feed '{0}'", indexerSettings.BaseUrl); - var requestGenerator = new TorrentRssIndexerRequestGenerator { Settings = indexerSettings }; - var request = requestGenerator.GetRecentRequests().GetAllTiers().First().First(); - - HttpResponse httpResponse = null; try { - httpResponse = _httpClient.Execute(request.HttpRequest); + var requestGenerator = new TorrentRssIndexerRequestGenerator { Settings = indexerSettings }; + var request = requestGenerator.GetRecentRequests().GetAllTiers().First().First(); + + HttpResponse httpResponse = null; + try + { + httpResponse = _httpClient.Execute(request.HttpRequest); + } + catch (Exception ex) + { + _logger.Warn(ex, string.Format("Unable to connect to indexer {0}: {1}", request.Url, ex.Message)); + return null; + } + + var indexerResponse = new IndexerResponse(request, httpResponse); + return GetParserSettings(indexerResponse, indexerSettings); } catch (Exception ex) { - _logger.Warn(ex, string.Format("Unable to connect to indexer {0}: {1}", request.Url, ex.Message)); - return null; + ex.WithData("FeedUrl", indexerSettings.BaseUrl); + throw; } - - var indexerResponse = new IndexerResponse(request, httpResponse); - return GetParserSettings(indexerResponse, indexerSettings); - } + } private TorrentRssIndexerParserSettings GetParserSettings(IndexerResponse response, TorrentRssIndexerSettings indexerSettings) { @@ -140,7 +148,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss _logger.Trace("Feed doesn't have Seeders in Description, disabling option."); parser.ParseSeedersInDescription = settings.ParseSeedersInDescription = false; } - + if (!releases.Any(r => r.Size < ValidSizeThreshold)) { _logger.Trace("Feed has valid size in enclosure.");