diff --git a/NzbDrone.Common/Contract/ExceptionReport.cs b/NzbDrone.Common/Contract/ExceptionReport.cs index 893f00af8..f044e0513 100644 --- a/NzbDrone.Common/Contract/ExceptionReport.cs +++ b/NzbDrone.Common/Contract/ExceptionReport.cs @@ -20,14 +20,13 @@ namespace NzbDrone.Common.Contract { var dic = new Dictionary { - {"ExType", Type.NullCheck()}, - {"Logger", Logger.NullCheck()}, - {"Message", LogMessage.NullCheck()}, - {"Str", String.NullCheck()} + {"ExType", Type.NullSafe()}, + {"Logger", Logger.NullSafe()}, + {"Message", LogMessage.NullSafe()}, + {"Str", String.NullSafe()} }; return dic; } } - } diff --git a/NzbDrone.Common/Contract/ExceptionReportResponse.cs b/NzbDrone.Common/Contract/ExceptionReportResponse.cs new file mode 100644 index 000000000..0b6d05375 --- /dev/null +++ b/NzbDrone.Common/Contract/ExceptionReportResponse.cs @@ -0,0 +1,11 @@ +using System.Linq; +using Newtonsoft.Json; + +namespace NzbDrone.Common.Contract +{ + public class ExceptionReportResponse + { + [JsonProperty("id")] + public int ExceptionId { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Contract/ParseErrorReport.cs b/NzbDrone.Common/Contract/ParseErrorReport.cs index 6f22e2855..19b5ad6ef 100644 --- a/NzbDrone.Common/Contract/ParseErrorReport.cs +++ b/NzbDrone.Common/Contract/ParseErrorReport.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Common.Contract { var dic = new Dictionary { - {"Title", Title.NullCheck()}, + {"Title", Title.NullSafe()}, }; return dic; diff --git a/NzbDrone.Common/NzbDrone.Common.csproj b/NzbDrone.Common/NzbDrone.Common.csproj index b9e06a43b..9ddbfb235 100644 --- a/NzbDrone.Common/NzbDrone.Common.csproj +++ b/NzbDrone.Common/NzbDrone.Common.csproj @@ -54,6 +54,7 @@ + diff --git a/NzbDrone.Common/StringExtention.cs b/NzbDrone.Common/StringExtention.cs index d3e35bbc5..4a0e9059e 100644 --- a/NzbDrone.Common/StringExtention.cs +++ b/NzbDrone.Common/StringExtention.cs @@ -6,15 +6,15 @@ namespace NzbDrone.Common public static class StringExtention { - public static object NullCheck(this object target) + public static object NullSafe(this object target) { if (target != null) return target; return "[NULL]"; } - public static string NullCheck(this string target) + public static string NullSafe(this string target) { - return ((object)target).NullCheck().ToString(); + return ((object)target).NullSafe().ToString(); } } } \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ExceptionController.cs b/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ExceptionController.cs new file mode 100644 index 000000000..d3331a02d --- /dev/null +++ b/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ExceptionController.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Web.Mvc; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Contract; +using NzbDrone.Services.Service.Repository.Reporting; +using Services.PetaPoco; + +namespace NzbDrone.Services.Service.Controllers +{ + public class ExceptionController : Controller + { + private readonly IDatabase _database; + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + private const string OK = "OK"; + + public ExceptionController(IDatabase database) + { + _database = database; + } + + [HttpPost] + public JsonResult ReportNew(ExceptionReport exceptionReport) + { + try + { + var exceptionId = GetExceptionDetailId(exceptionReport); + + var exceptionInstance = new ExceptionInstance + { + ExceptionDetail = exceptionId, + IsProduction = exceptionReport.IsProduction, + LogMessage = exceptionReport.LogMessage, + Timestamp = DateTime.Now + }; + + _database.Insert(exceptionInstance); + + return new JsonResult { Data = new ExceptionReportResponse { ExceptionId = exceptionId } }; + } + catch (Exception e) + { + logger.FatalException("Error has occurred while logging exception", e); + throw; + } + } + + + private int GetExceptionDetailId(ExceptionReport exceptionReport) + { + var reportHash = Hash(exceptionReport.Version + exceptionReport.String + exceptionReport.Logger); + var id = _database.FirstOrDefault("SELECT Id FROM Exceptions WHERE Hash =@0", reportHash); + + if (id == 0) + { + var exeptionDetail = new ExceptionDetail(); + exeptionDetail.Hash = reportHash; + exeptionDetail.Logger = exceptionReport.Logger; + exeptionDetail.String = exceptionReport.String; + exeptionDetail.Type = exceptionReport.Type; + exeptionDetail.Version = exceptionReport.Version; + + id = Convert.ToInt32(_database.Insert(exeptionDetail)); + } + + return id; + } + + private static string Hash(string input) + { + uint mCrc = 0xffffffff; + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input); + foreach (byte myByte in bytes) + { + mCrc ^= ((uint)(myByte) << 24); + for (var i = 0; i < 8; i++) + { + if ((Convert.ToUInt32(mCrc) & 0x80000000) == 0x80000000) + { + mCrc = (mCrc << 1) ^ 0x04C11DB7; + } + else + { + mCrc <<= 1; + } + } + } + return String.Format("{0:x8}", mCrc); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ReportingController.cs b/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ReportingController.cs index 3a5afc059..f3a336a6b 100644 --- a/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ReportingController.cs +++ b/NzbDrone.Services/NzbDrone.Services.Service/Controllers/ReportingController.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Services.Service.Controllers [HttpPost] public JsonResult ParseError(ParseErrorReport parseErrorReport) { - logger.Trace(parseErrorReport.NullCheck()); + logger.Trace(parseErrorReport.NullSafe()); if (ParseErrorExists(parseErrorReport.Title)) return Json(OK); @@ -63,7 +63,7 @@ namespace NzbDrone.Services.Service.Controllers } catch (Exception) { - logger.Trace(exceptionReport.NullCheck()); + logger.Trace(exceptionReport.NullSafe()); throw; } diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Migrations/Migration20120229.cs b/NzbDrone.Services/NzbDrone.Services.Service/Migrations/Migration20120229.cs new file mode 100644 index 000000000..852c44cb3 --- /dev/null +++ b/NzbDrone.Services/NzbDrone.Services.Service/Migrations/Migration20120229.cs @@ -0,0 +1,38 @@ +using System; +using System.Data; +using System.Linq; +using Migrator.Framework; + +namespace NzbDrone.Services.Service.Migrations +{ + [Migration(20120229)] + public class Migration20120229 : Migration + { + public override void Up() + { + + Database.AddTable("ExceptionInstances", new Column("Id", DbType.Int64, ColumnProperty.PrimaryKeyWithIdentity), + new Column("ExceptionDetail", DbType.Int16, ColumnProperty.NotNull), + new Column("LogMessage", DbType.String, 3000, ColumnProperty.NotNull), + MigrationsHelper.TimestampColumn, + MigrationsHelper.ProductionColumn); + + Database.AddTable("Exceptions", new Column("Id", DbType.Int64, ColumnProperty.PrimaryKeyWithIdentity), + new Column("Logger", DbType.String, ColumnProperty.NotNull), + new Column("Type", DbType.String, ColumnProperty.NotNull), + new Column("String", DbType.String, ColumnProperty.NotNull), + new Column("Hash", DbType.String, ColumnProperty.NotNull), + MigrationsHelper.VersionColumn); + + + Database.ExecuteNonQuery("ALTER TABLE ExceptionReports ALTER COLUMN String NTEXT"); + Database.ExecuteNonQuery("ALTER TABLE Exceptions ALTER COLUMN String NTEXT"); + + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Service/NzbDrone.Services.Service.csproj b/NzbDrone.Services/NzbDrone.Services.Service/NzbDrone.Services.Service.csproj index db988fd2d..1658c38d2 100644 --- a/NzbDrone.Services/NzbDrone.Services.Service/NzbDrone.Services.Service.csproj +++ b/NzbDrone.Services/NzbDrone.Services.Service/NzbDrone.Services.Service.csproj @@ -209,8 +209,12 @@ + + + + diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionDetail.cs b/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionDetail.cs new file mode 100644 index 000000000..512f40679 --- /dev/null +++ b/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionDetail.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using Services.PetaPoco; + +namespace NzbDrone.Services.Service.Repository.Reporting +{ + [TableName("Exceptions")] + public class ExceptionDetail + { + public int Id { get; set; } + public string Logger { get; set; } + public string Type { get; set; } + public string String { get; set; } + public string Hash { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionInstance.cs b/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionInstance.cs new file mode 100644 index 000000000..452388f87 --- /dev/null +++ b/NzbDrone.Services/NzbDrone.Services.Service/Repository/Reporting/ExceptionInstance.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using Services.PetaPoco; + +namespace NzbDrone.Services.Service.Repository.Reporting +{ + [TableName("ExceptionInstances")] + public class ExceptionInstance + { + public long Id { get; set; } + public int ExceptionDetail { get; set; } + public string LogMessage { get; set; } + public DateTime Timestamp { get; set; } + public bool IsProduction { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Tests/ExceptionControllerFixture.cs b/NzbDrone.Services/NzbDrone.Services.Tests/ExceptionControllerFixture.cs new file mode 100644 index 000000000..808a804b6 --- /dev/null +++ b/NzbDrone.Services/NzbDrone.Services.Tests/ExceptionControllerFixture.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Contract; +using NzbDrone.Services.Service.Controllers; +using NzbDrone.Services.Service.Repository.Reporting; +using NzbDrone.Services.Tests.Framework; + +namespace NzbDrone.Services.Tests +{ + [TestFixture] + public class ExceptionControllerFixture : ServicesTestBase + { + + ExceptionController Controller + { + get + { + return Mocker.Resolve(); + } + } + + private static ExceptionReport CreateExceptionReport() + { + return new ExceptionReport + { + IsProduction = true, + Version = "1.1.2.323456", + UGuid = Guid.NewGuid(), + Logger = "NzbDrone.Logger.Name", + LogMessage = @"Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message", + String = @"Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message", + + Type = typeof(InvalidOperationException).Name + }; + } + + + [Test] + public void ReportNew_should_save_instance() + { + var exceptionReport = CreateExceptionReport(); + + WithRealDb(); + + Controller.ReportNew(exceptionReport); + + var exceptionInstance = Db.Fetch(); + exceptionInstance.Should().HaveCount(1); + exceptionInstance.Single().Id.Should().BeGreaterThan(0); + exceptionInstance.Single().ExceptionDetail.Should().BeGreaterThan(0); + exceptionInstance.Single().IsProduction.Should().Be(exceptionReport.IsProduction); + exceptionInstance.Single().Timestamp.Should().BeWithin(TimeSpan.FromSeconds(4)).Before(DateTime.Now); + exceptionInstance.Single().LogMessage.Should().Be(exceptionReport.LogMessage); + } + + [Test] + public void ReportNew_should_return_exception_id() + { + var exceptionReport = CreateExceptionReport(); + + WithRealDb(); + + var response = Controller.ReportNew(exceptionReport); + + response.Data.Should().BeOfType(); + ((ExceptionReportResponse)response.Data).ExceptionId.Should().BeGreaterThan(0); + } + + + [Test] + public void Reporting_exception_more_than_once_should_create_single_detail_with_multiple_instances() + { + var exceptionReport = CreateExceptionReport(); + + WithRealDb(); + + var response1 = Controller.ReportNew(exceptionReport); + var response2 = Controller.ReportNew(exceptionReport); + var response3 = Controller.ReportNew(exceptionReport); + + var detail = Db.Fetch(); + var instances = Db.Fetch(); + + detail.Should().HaveCount(1); + instances.Should().HaveCount(3); + + instances.Should().OnlyContain(c => c.ExceptionDetail == detail.Single().Id); + + } + } +} \ No newline at end of file diff --git a/NzbDrone.Services/NzbDrone.Services.Tests/NzbDrone.Services.Tests.csproj b/NzbDrone.Services/NzbDrone.Services.Tests/NzbDrone.Services.Tests.csproj index 7e972431e..63b078b3b 100644 --- a/NzbDrone.Services/NzbDrone.Services.Tests/NzbDrone.Services.Tests.csproj +++ b/NzbDrone.Services/NzbDrone.Services.Tests/NzbDrone.Services.Tests.csproj @@ -65,6 +65,7 @@ + diff --git a/NzbDrone.Services/NzbDrone.Services.Tests/ReportingControllerFixture.cs b/NzbDrone.Services/NzbDrone.Services.Tests/ReportingControllerFixture.cs index 1783d6600..c8da31737 100644 --- a/NzbDrone.Services/NzbDrone.Services.Tests/ReportingControllerFixture.cs +++ b/NzbDrone.Services/NzbDrone.Services.Tests/ReportingControllerFixture.cs @@ -48,6 +48,24 @@ namespace NzbDrone.Services.Tests Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message", String = @"Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message + Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message Long message Long message Long messageLong messageLong messageLong messageLong messageLong messageLong messageLong messageLong message