diff --git a/NLog.Targets.Sentry.UnitTests/NLog.Targets.Sentry.UnitTests.csproj b/NLog.Targets.Sentry.UnitTests/NLog.Targets.Sentry.UnitTests.csproj index 1059b63..c780a9c 100644 --- a/NLog.Targets.Sentry.UnitTests/NLog.Targets.Sentry.UnitTests.csproj +++ b/NLog.Targets.Sentry.UnitTests/NLog.Targets.Sentry.UnitTests.csproj @@ -42,8 +42,9 @@ ..\packages\NUnit.2.6.4\lib\nunit.framework.dll - - ..\packages\SharpRaven.1.4.3\lib\net45\SharpRaven.dll + + ..\packages\SharpRaven.2.1.0\lib\net45\SharpRaven.dll + True diff --git a/NLog.Targets.Sentry.UnitTests/SentryTargetTests.cs b/NLog.Targets.Sentry.UnitTests/SentryTargetTests.cs index f6453f9..8884bdd 100644 --- a/NLog.Targets.Sentry.UnitTests/SentryTargetTests.cs +++ b/NLog.Targets.Sentry.UnitTests/SentryTargetTests.cs @@ -40,29 +40,38 @@ public void TestPublicConstructor() [Test] public void TestBadDsn() { - Assert.Throws(() => new SentryTarget(null) { Dsn = "http://localhost" }); + var sentryTarget = new SentryTarget { Dsn = "http://localhost" }; + var configuration = new LoggingConfiguration(); + configuration.AddTarget("NLogSentry", sentryTarget); + configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, sentryTarget)); + LogManager.Configuration = configuration; + try + { + LogManager.GetCurrentClassLogger().Info("Test"); + Assert.Fail("Expected exception not raised"); + } + catch (NLogRuntimeException ex) + { + Assert.IsInstanceOf(ex.InnerException); + } } [Test] public void TestLoggingToSentry() { var sentryClient = new Mock(); - ErrorLevel lErrorLevel = ErrorLevel.Debug; - IDictionary lTags = null; - Exception lException = null; + SentryEvent lastSentryEvent = null; sentryClient - .Setup(x => x.CaptureException(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) - .Callback((Exception exception, SentryMessage msg, ErrorLevel lvl, IDictionary d, object extra) => + .Setup(x => x.Capture(It.IsAny())) + .Callback((SentryEvent sentryEvent) => { - lException = exception; - lErrorLevel = lvl; - lTags = d; + lastSentryEvent = sentryEvent; }) .Returns("Done"); // Setup NLog - var sentryTarget = new SentryTarget(sentryClient.Object) + var sentryTarget = new SentryTarget(() => sentryClient.Object) { Dsn = "http://25e27038b1df4930b93c96c170d95527:d87ac60bb07b4be8908845b23e914dae@test/4", }; @@ -81,9 +90,9 @@ public void TestLoggingToSentry() logger.ErrorException("Error Message", e); } - Assert.IsTrue(lException.Message == "Oh No!"); - Assert.IsTrue(lTags == null); - Assert.IsTrue(lErrorLevel == ErrorLevel.Error); + Assert.IsTrue(lastSentryEvent.Message == "Oh No!"); + Assert.IsEmpty(lastSentryEvent.Tags); + Assert.IsTrue(lastSentryEvent.Level == ErrorLevel.Error); } @@ -93,22 +102,18 @@ public void TestLoggingToSentry() public void TestLoggingToSentry_SendLogEventInfoPropertiesAsTags() { var sentryClient = new Mock(); - ErrorLevel lErrorLevel = ErrorLevel.Debug; - IDictionary lTags = null; - Exception lException = null; + SentryEvent lastSentryEvent = null; sentryClient - .Setup(x => x.CaptureException(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) - .Callback((Exception exception, SentryMessage msg, ErrorLevel lvl, IDictionary d, object extra) => + .Setup(x => x.Capture(It.IsAny())) + .Callback((SentryEvent sentryEvent) => { - lException = exception; - lErrorLevel = lvl; - lTags = d; + lastSentryEvent = sentryEvent; }) .Returns("Done"); // Setup NLog - var sentryTarget = new SentryTarget(sentryClient.Object) + var sentryTarget = new SentryTarget(() => sentryClient.Object) { Dsn = "http://25e27038b1df4930b93c96c170d95527:d87ac60bb07b4be8908845b23e914dae@test/4", SendLogEventInfoPropertiesAsTags = true, @@ -133,9 +138,76 @@ public void TestLoggingToSentry_SendLogEventInfoPropertiesAsTags() logger.Log(logEventInfo); } - Assert.IsTrue(lException.Message == "Oh No!"); - Assert.IsTrue(lTags != null); - Assert.IsTrue(lErrorLevel == ErrorLevel.Error); + Assert.IsTrue(lastSentryEvent.Message == "Oh No!"); + CollectionAssert.AreEqual(new Dictionary { { "tag1", tag1Value } }, lastSentryEvent.Tags); + Assert.IsTrue(lastSentryEvent.Level == ErrorLevel.Error); + } + + + [Test] + public void TestLoggingToSentry_SendSpecifiedPropertiesAsTags() + { + var sentryClient = new Mock(); + SentryEvent lastSentryEvent = null; + + sentryClient + .Setup(x => x.Capture(It.IsAny())) + .Callback((SentryEvent sentryEvent) => + { + lastSentryEvent = sentryEvent; + }) + .Returns("Done"); + + // Setup NLog + var tag1 = "tag1"; + var tag2 = "tag2"; + var tag1Value = "abcde"; + var tag2Value = "fghij"; + + var sentryTarget = new SentryTarget(() => sentryClient.Object) + { + Dsn = "http://25e27038b1df4930b93c96c170d95527:d87ac60bb07b4be8908845b23e914dae@test/4", + TagProperties = tag1, + }; + var configuration = new LoggingConfiguration(); + configuration.AddTarget("NLogSentry", sentryTarget); + configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, sentryTarget)); + LogManager.Configuration = configuration; + + + try + { + throw new Exception("Oh No!"); + } + catch (Exception e) + { + var logger = LogManager.GetCurrentClassLogger(); + + var logEventInfo = LogEventInfo.Create(LogLevel.Error, "default", "Error Message", e); + logEventInfo.Properties.Add(tag1, tag1Value); + logEventInfo.Properties.Add(tag2, tag2Value); + logger.Log(logEventInfo); + } + + Assert.IsTrue(lastSentryEvent.Message == "Oh No!"); + CollectionAssert.AreEqual(new Dictionary { { tag1, tag1Value } }, lastSentryEvent.Tags); + CollectionAssert.AreEqual(new Dictionary { { tag2, tag2Value } }, (Dictionary)lastSentryEvent.Extra); + Assert.IsTrue(lastSentryEvent.Level == ErrorLevel.Error); + } + + [TestCase("Trace", 0, ErrorLevel.Debug)] + [TestCase("Debug", 1, ErrorLevel.Debug)] + [TestCase("Info", 2, ErrorLevel.Info)] + [TestCase("Warn", 3, ErrorLevel.Warning)] + [TestCase("Error", 4, ErrorLevel.Error)] + [TestCase("Fatal", 5, ErrorLevel.Fatal)] + [TestCase("Off", 6, null)] + public void TestLevelMappings(string name, int ordinal, ErrorLevel? expectedErrorLevel) + { + var level = LogLevel.FromString(name); + Assert.AreEqual(level, LogLevel.FromOrdinal(ordinal)); + var errorLevel = SentryTarget.TryGetErrorLevel(level); + Assert.AreEqual(expectedErrorLevel, errorLevel); } } } diff --git a/NLog.Targets.Sentry.UnitTests/packages.config b/NLog.Targets.Sentry.UnitTests/packages.config index 94a48ab..93c7dd8 100644 --- a/NLog.Targets.Sentry.UnitTests/packages.config +++ b/NLog.Targets.Sentry.UnitTests/packages.config @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/NLog.Targets.Sentry/NLog.Targets.Sentry.csproj b/NLog.Targets.Sentry/NLog.Targets.Sentry.csproj index db60fb9..22a3647 100755 --- a/NLog.Targets.Sentry/NLog.Targets.Sentry.csproj +++ b/NLog.Targets.Sentry/NLog.Targets.Sentry.csproj @@ -36,8 +36,9 @@ ..\packages\NLog.3.2.0.0\lib\net45\NLog.dll - - ..\packages\SharpRaven.1.4.3\lib\net45\SharpRaven.dll + + ..\packages\SharpRaven.2.1.0\lib\net45\SharpRaven.dll + True diff --git a/NLog.Targets.Sentry/Packages.config b/NLog.Targets.Sentry/Packages.config index 1349288..64026af 100755 --- a/NLog.Targets.Sentry/Packages.config +++ b/NLog.Targets.Sentry/Packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/NLog.Targets.Sentry/SentryTarget.cs b/NLog.Targets.Sentry/SentryTarget.cs index 3ebbc2b..22832fc 100755 --- a/NLog.Targets.Sentry/SentryTarget.cs +++ b/NLog.Targets.Sentry/SentryTarget.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using NLog.Common; using NLog.Config; +using NLog.Layouts; using SharpRaven; using SharpRaven.Data; @@ -11,37 +11,20 @@ namespace NLog.Targets // ReSharper restore CheckNamespace { [Target("Sentry")] - public class SentryTarget : TargetWithLayout + public class SentryTarget : Target { - private Dsn dsn; - private readonly Lazy client; - - /// - /// Map of NLog log levels to Raven/Sentry log levels - /// - protected static readonly IDictionary LoggingLevelMap = new Dictionary - { - {LogLevel.Debug, ErrorLevel.Debug}, - {LogLevel.Error, ErrorLevel.Error}, - {LogLevel.Fatal, ErrorLevel.Fatal}, - {LogLevel.Info, ErrorLevel.Info}, - {LogLevel.Trace, ErrorLevel.Debug}, - {LogLevel.Warn, ErrorLevel.Warning}, - }; + private readonly Func ravenClientFactory; /// /// The DSN for the Sentry host /// [RequiredParameter] - public string Dsn - { - get { return dsn == null ? null : dsn.ToString(); } - set { dsn = new Dsn(value); } - } + public string Dsn { get; set; } /// /// Determines whether events with no exceptions will be send to Sentry or not /// + [Obsolete("Use target filter conditions instead. See: https://github.com/NLog/NLog/wiki/Conditions")] public bool IgnoreEventsWithNoException { get; set; } /// @@ -49,21 +32,25 @@ public string Dsn /// public bool SendLogEventInfoPropertiesAsTags { get; set; } + /// + /// A comma separated list of NLog property names to be used as tags. + /// + public string TagProperties { get; set; } + /// /// Constructor /// public SentryTarget() { - client = new Lazy(() => new RavenClient(dsn)); } /// /// Internal constructor, used for unit-testing /// - /// A - internal SentryTarget(IRavenClient ravenClient) : this() + /// Constructor of a + internal SentryTarget(Func createRavenClient) { - client = new Lazy(() => ravenClient); + this.ravenClientFactory = createRavenClient; } /// @@ -72,34 +59,133 @@ internal SentryTarget(IRavenClient ravenClient) : this() /// Logging event to be written out. protected override void Write(LogEventInfo logEvent) { - try + var sentryEvent = ToSentryEvent(logEvent); + if (sentryEvent == null) return; + var client = CreateClient(logEvent); + client.Capture(sentryEvent); + } + + private IRavenClient CreateClient(LogEventInfo logEvent) + { + var client = ravenClientFactory != null ? ravenClientFactory() : new RavenClient(new Dsn(Dsn)); + client.Logger = logEvent.LoggerName; + return client; + } + + private SentryEvent ToSentryEvent(LogEventInfo logEvent) + { + var level = TryGetErrorLevel(logEvent.Level); + // Level is set to "Off", so exit. + if (level == null) { - var tags = SendLogEventInfoPropertiesAsTags - ? logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()) - : null; + return null; + } - var extras = SendLogEventInfoPropertiesAsTags - ? null - : logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString()); + var sentryEvent = CreateSentryEvent(logEvent); + if (IgnoreEventsWithNoException && sentryEvent.Exception == null) + { + return null; + } + + sentryEvent.Level = level.Value; + AppendEventDetails(sentryEvent, logEvent.Properties); + return sentryEvent; + } - client.Value.Logger = logEvent.LoggerName; + private static SentryEvent CreateSentryEvent(LogEventInfo logEvent) + { + if (logEvent.Exception != null) + { + return new SentryEvent(logEvent.Exception); + } + else + { + return new SentryEvent(new SentryMessage(logEvent.FormattedMessage)); + } + } - // If the log event did not contain an exception and we're not ignoring - // those kinds of events then we'll send a "Message" to Sentry - if (logEvent.Exception == null && !IgnoreEventsWithNoException) + private void AppendEventDetails(SentryEvent sentryEvent, IDictionary properties) + { + var propertiesAsStrings = ConvertPropertiesToStrings(properties); + if (SendLogEventInfoPropertiesAsTags) + { + foreach (var tag in propertiesAsStrings) { - var sentryMessage = new SentryMessage(Layout.Render(logEvent)); - client.Value.CaptureMessage(sentryMessage, LoggingLevelMap[logEvent.Level], extra: extras, tags: tags); + sentryEvent.Tags.Add(tag); } - else if (logEvent.Exception != null) + } + else + { + foreach (var tagPropertyKey in RenderTagProperties()) { - var sentryMessage = new SentryMessage(logEvent.FormattedMessage); - client.Value.CaptureException(logEvent.Exception, extra: extras, level: LoggingLevelMap[logEvent.Level], message: sentryMessage, tags: tags); + string propertyValue; + if (propertiesAsStrings.TryGetValue(tagPropertyKey, out propertyValue)) + { + sentryEvent.Tags.Add(tagPropertyKey, propertyValue); + propertiesAsStrings.Remove(tagPropertyKey); + } } + + sentryEvent.Extra = propertiesAsStrings; + } + } + + private IEnumerable RenderTagProperties() + { + if (string.IsNullOrWhiteSpace(TagProperties)) + { + return Enumerable.Empty(); + } + + return new HashSet(TagProperties.Split(',').Select(s => s.Trim())); + } + + private static Dictionary ConvertPropertiesToStrings(IDictionary properties) + { + return ( + from property in properties + let stringKey = ToStringOrNull(property.Key) + let stringValue = ToStringOrNull(property.Value) + where stringKey != null && stringValue != null + group stringValue by stringKey) + .ToDictionary(x => x.Key, x => string.Join(",", x)); + } + + private static string ToStringOrNull(object obj) + { + if (obj == null) + { + return null; } - catch (Exception e) + + return obj.ToString(); + } + + internal static ErrorLevel? TryGetErrorLevel(LogLevel level) + { + if (level == null) + { + return null; + } + + // For ordinals, see https://github.com/NLog/NLog/blob/master/src/NLog/LogLevel.cs + switch (level.Ordinal) { - InternalLogger.Error("Unable to send Sentry request: {0}", e.Message); + case 0: // Trace + case 1: // Debug + return ErrorLevel.Debug; + case 2: + return ErrorLevel.Info; + case 3: + return ErrorLevel.Warning; + case 4: + return ErrorLevel.Error; + case 5: + return ErrorLevel.Fatal; + case 6: // Off + return null; + default: + throw new Exception(string.Format("Unable to map NLog LogLevel of {0} to a Sentry ErrorLevel", level)); } } }