diff --git a/README.md b/README.md index 325c261..034dc34 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ Add the following code in your web.config to configure LogglyAppender in your ap + ``` + To send **GlobalContext** and **LogicalThreadContext** properties in your log you need define the list of used properties in the configuration. For GlobalContext Properties use @@ -49,6 +51,10 @@ By default, library uses Loggly /bulk end point (https://www.loggly.com/docs/htt ``` +Set number of inner exceptions that should be sent to loggly, if you don't want the default value. +``` + +``` Add the following entry in your AssemblyInfo.cs ``` diff --git a/source/log4net-loggly-console/App.config b/source/log4net-loggly-console/App.config index 108e1ff..12d32e3 100644 --- a/source/log4net-loggly-console/App.config +++ b/source/log4net-loggly-console/App.config @@ -15,6 +15,7 @@ + diff --git a/source/log4net-loggly-console/Program.cs b/source/log4net-loggly-console/Program.cs index dd0f478..5d8e41a 100644 --- a/source/log4net-loggly-console/Program.cs +++ b/source/log4net-loggly-console/Program.cs @@ -94,6 +94,36 @@ static void Main(string[] argArray) log.Info(new TestObject()); log.Info(null); + try + { + try + { + try + { + try + { + throw new Exception("1"); + } + catch (Exception e) + { + throw new Exception("2", e); + } + } + catch (Exception e) + { + throw new Exception("3", e); + } + } + catch (Exception e) + { + throw new Exception("4", e); + } + } + catch (Exception e) + { + log.Error("Exception", e); + } + Console.ReadKey(); } } diff --git a/source/log4net-loggly.UnitTests/LogglyFormatterTest.cs b/source/log4net-loggly.UnitTests/LogglyFormatterTest.cs index 1f154e8..e2f695b 100644 --- a/source/log4net-loggly.UnitTests/LogglyFormatterTest.cs +++ b/source/log4net-loggly.UnitTests/LogglyFormatterTest.cs @@ -8,7 +8,7 @@ using log4net.Core; using log4net.loggly; using log4net.Repository; - using log4net_loggly.UnitTests.Models; + using Models; using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -304,6 +304,58 @@ public void ShouldSerializeTheException() stacktrace.Should().NotBeNull("because the exception has a stacktrace"); } + [Theory] + [InlineData(0, 0, 1)] + [InlineData(1, 1, 1)] + [InlineData(2, 1, 1)] + [InlineData(2, 2, 2)] + [InlineData(2, 2, 3)] + [InlineData(3, 3, 3)] + [InlineData(5, 5, 5)] + [InlineData(5, 5, 10)] + public void ShouldSerializeInnerExceptions(int configurationNumberOfInnerExceptions, int expectedNumberOfException, int innerExceptionsToCreate) + { + Exception ex = GetArgumentException(innerExceptionsToCreate + 1); + + var evt = new LoggingEvent( + GetType(), + Mock.Of(), + _fixture.Create("loggerName"), + _fixture.Create(), + _fixture.Create("message"), + ex); + var instance = _fixture.Create(); + _fixture.Freeze>().SetupGet(x => x.NumberOfInnerExceptions).Returns(configurationNumberOfInnerExceptions); + + var result = instance.ToJson(evt); + dynamic json = JObject.Parse(result); + + var exception = json.exception; + + ((object)exception).Should().NotBeNull("because an exception was specified in the event"); + + // Validate first level + var message = (string)exception.exceptionMessage; + var type = (string)exception.exceptionType; + var stacktrace = (string)exception.stacktrace; + AssertException(message, type, stacktrace, 0); + + // Validate inner exceptions + var count = 0; + var innerException = exception.innerException; + while (innerException != null) + { + count++; + message = (string)innerException.innerExceptionMessage; + type = (string)innerException.innerExceptionType; + stacktrace = (string)innerException.innerStacktrace; + AssertException(message, type, stacktrace, count); + innerException = innerException.innerException; + } + + count.Should().Be(expectedNumberOfException, "Expects all stacktraces"); + } + [Fact] public void ShouldSerializeThreadContextProperties() { @@ -355,6 +407,60 @@ public void ShouldSetMessagePropertyWhenMessageObjectIsString() message.Should().StartWith("message", "because the MessageObject property value is used"); } + + private static ArgumentException GetArgumentException(int numberOfExceptions) + { + try + { + if (--numberOfExceptions > 0) + { + try + { + GetNestedArgumentException(numberOfExceptions); + } + catch (ArgumentException e) + { + throw new ArgumentException("Exception 0", e); + } + } + else + { + throw new ArgumentException("Exception 0"); + } + } + catch (ArgumentException e) + { + return e; + } + return null; + } + + private static void GetNestedArgumentException(int numberOfExceptions, int deep = 0) + { + deep++; + if (--numberOfExceptions > 0) + { + try + { + GetNestedArgumentException(numberOfExceptions, deep); + } + catch (ArgumentException e) + { + throw new ArgumentException($"Exception {deep}", e); + } + } + else + { + throw new ArgumentException($"Exception {deep}"); + } + } + + private static void AssertException(string message, string type, string stacktrace, int stackLevel) + { + message.Should().Be($"Exception {stackLevel}", "because an argument exception has a default message"); + type.Should().Be(typeof(ArgumentException).FullName, "because we logged an argument exception"); + stacktrace.Should().NotBeNull("because the exception has a stacktrace"); + } } } } diff --git a/source/log4net-loggly/ILogglyAppenderConfig.cs b/source/log4net-loggly/ILogglyAppenderConfig.cs index 0238986..ea17176 100644 --- a/source/log4net-loggly/ILogglyAppenderConfig.cs +++ b/source/log4net-loggly/ILogglyAppenderConfig.cs @@ -1,15 +1,16 @@ -namespace log4net.loggly -{ - public interface ILogglyAppenderConfig - { - string RootUrl { get; set; } - string InputKey { get; set; } - string UserAgent { get; set; } - string LogMode { get; set; } - int TimeoutInSeconds { get; set; } - string Tag { get; set; } - string LogicalThreadContextKeys { get; set; } - string GlobalContextKeys { get; set; } - int BufferSize { get; set; } - } +namespace log4net.loggly +{ + public interface ILogglyAppenderConfig + { + string RootUrl { get; set; } + string InputKey { get; set; } + string UserAgent { get; set; } + string LogMode { get; set; } + int TimeoutInSeconds { get; set; } + string Tag { get; set; } + string LogicalThreadContextKeys { get; set; } + string GlobalContextKeys { get; set; } + int BufferSize { get; set; } + int NumberOfInnerExceptions { get; set; } + } } \ No newline at end of file diff --git a/source/log4net-loggly/LogglyAppender.cs b/source/log4net-loggly/LogglyAppender.cs index 2e6a745..4b189fa 100644 --- a/source/log4net-loggly/LogglyAppender.cs +++ b/source/log4net-loggly/LogglyAppender.cs @@ -1,105 +1,106 @@ -using log4net.Appender; -using log4net.Core; -using System; -using System.Collections.Generic; -using Timer = System.Timers; - - - -namespace log4net.loggly -{ - public class LogglyAppender : AppenderSkeleton - { - List lstLogs = new List(); - string[] arr = new string[100]; - public readonly string InputKeyProperty = "LogglyInputKey"; - public ILogglyFormatter Formatter = new LogglyFormatter(); - public ILogglyClient Client = new LogglyClient(); - public LogglySendBufferedLogs _sendBufferedLogs = new LogglySendBufferedLogs(); - private ILogglyAppenderConfig Config = new LogglyAppenderConfig(); - public string RootUrl { set { Config.RootUrl = value; } } - public string InputKey { set { Config.InputKey = value; } } - public string UserAgent { set { Config.UserAgent = value; } } - public string LogMode { set { Config.LogMode = value; } } - public int TimeoutInSeconds { set { Config.TimeoutInSeconds = value; } } - public string Tag { set { Config.Tag = value; } } - public string LogicalThreadContextKeys { set { Config.LogicalThreadContextKeys = value; } } - public string GlobalContextKeys { set { Config.GlobalContextKeys = value; } } - public int BufferSize { set { Config.BufferSize = value; } } - - private LogglyAsyncHandler LogglyAsync; - - public LogglyAppender() - { - LogglyAsync = new LogglyAsyncHandler(); - Timer.Timer t = new Timer.Timer(); - t.Interval = 5000; - t.Enabled = true; - t.Elapsed += t_Elapsed; - } - - void t_Elapsed(object sender, Timer.ElapsedEventArgs e) - { - if (lstLogs.Count != 0) - { - SendAllEvents(lstLogs.ToArray()); - } - _sendBufferedLogs.sendBufferedLogsToLoggly(Config, Config.LogMode == "bulk/"); - } - - protected override void Append(LoggingEvent loggingEvent) - { - SendLogAction(loggingEvent); - } - - private void SendLogAction(LoggingEvent loggingEvent) - { - //we should always format event in the same thread as - //many properties used in the event are associated with the current thread - //like threadname, ndc stacks, threadcontent properties etc. - - //initializing a string for the formatted log - string _formattedLog = string.Empty; - - //if Layout is null then format the log from the Loggly Client - if (this.Layout == null) - { - Formatter.AppendAdditionalLoggingInformation(Config, loggingEvent); - _formattedLog = Formatter.ToJson(loggingEvent); - } - else - { - _formattedLog = Formatter.ToJson(RenderLoggingEvent(loggingEvent), loggingEvent.TimeStamp); - } - - //check if logMode is bulk or inputs - if (Config.LogMode == "bulk/") - { - addToBulk(_formattedLog); - } - else if (Config.LogMode == "inputs/") - { - //sending _formattedLog to the async queue - LogglyAsync.PostMessage(_formattedLog, Config); - } - } - - public void addToBulk(string log) - { - // store all events into a array max lenght is 100 - lstLogs.Add(log.Replace("\n", "")); - if (lstLogs.Count == 100) - { - SendAllEvents(lstLogs.ToArray()); - } - } - - private void SendAllEvents(string[] events) - { - lstLogs.Clear(); - String bulkLog = String.Join(System.Environment.NewLine, events); - LogglyAsync.PostMessage(bulkLog, Config); - } - - } +using log4net.Appender; +using log4net.Core; +using System; +using System.Collections.Generic; +using Timer = System.Timers; + + + +namespace log4net.loggly +{ + public class LogglyAppender : AppenderSkeleton + { + List lstLogs = new List(); + string[] arr = new string[100]; + public readonly string InputKeyProperty = "LogglyInputKey"; + public ILogglyFormatter Formatter = new LogglyFormatter(); + public ILogglyClient Client = new LogglyClient(); + public LogglySendBufferedLogs _sendBufferedLogs = new LogglySendBufferedLogs(); + private ILogglyAppenderConfig Config = new LogglyAppenderConfig(); + public string RootUrl { set { Config.RootUrl = value; } } + public string InputKey { set { Config.InputKey = value; } } + public string UserAgent { set { Config.UserAgent = value; } } + public string LogMode { set { Config.LogMode = value; } } + public int TimeoutInSeconds { set { Config.TimeoutInSeconds = value; } } + public string Tag { set { Config.Tag = value; } } + public string LogicalThreadContextKeys { set { Config.LogicalThreadContextKeys = value; } } + public string GlobalContextKeys { set { Config.GlobalContextKeys = value; } } + public int BufferSize { set { Config.BufferSize = value; } } + public int NumberOfInnerExceptions { set { Config.NumberOfInnerExceptions = value; } } + + private LogglyAsyncHandler LogglyAsync; + + public LogglyAppender() + { + LogglyAsync = new LogglyAsyncHandler(); + Timer.Timer t = new Timer.Timer(); + t.Interval = 5000; + t.Enabled = true; + t.Elapsed += t_Elapsed; + } + + void t_Elapsed(object sender, Timer.ElapsedEventArgs e) + { + if (lstLogs.Count != 0) + { + SendAllEvents(lstLogs.ToArray()); + } + _sendBufferedLogs.sendBufferedLogsToLoggly(Config, Config.LogMode == "bulk/"); + } + + protected override void Append(LoggingEvent loggingEvent) + { + SendLogAction(loggingEvent); + } + + private void SendLogAction(LoggingEvent loggingEvent) + { + //we should always format event in the same thread as + //many properties used in the event are associated with the current thread + //like threadname, ndc stacks, threadcontent properties etc. + + //initializing a string for the formatted log + string _formattedLog = string.Empty; + + //if Layout is null then format the log from the Loggly Client + if (this.Layout == null) + { + Formatter.AppendAdditionalLoggingInformation(Config, loggingEvent); + _formattedLog = Formatter.ToJson(loggingEvent); + } + else + { + _formattedLog = Formatter.ToJson(RenderLoggingEvent(loggingEvent), loggingEvent.TimeStamp); + } + + //check if logMode is bulk or inputs + if (Config.LogMode == "bulk/") + { + addToBulk(_formattedLog); + } + else if (Config.LogMode == "inputs/") + { + //sending _formattedLog to the async queue + LogglyAsync.PostMessage(_formattedLog, Config); + } + } + + public void addToBulk(string log) + { + // store all events into a array max lenght is 100 + lstLogs.Add(log.Replace("\n", "")); + if (lstLogs.Count == 100) + { + SendAllEvents(lstLogs.ToArray()); + } + } + + private void SendAllEvents(string[] events) + { + lstLogs.Clear(); + String bulkLog = String.Join(System.Environment.NewLine, events); + LogglyAsync.PostMessage(bulkLog, Config); + } + + } } \ No newline at end of file diff --git a/source/log4net-loggly/LogglyAppenderConfig.cs b/source/log4net-loggly/LogglyAppenderConfig.cs index 587b9fb..5f1c4f4 100644 --- a/source/log4net-loggly/LogglyAppenderConfig.cs +++ b/source/log4net-loggly/LogglyAppenderConfig.cs @@ -1,58 +1,62 @@ -namespace log4net.loggly -{ - public class LogglyAppenderConfig : ILogglyAppenderConfig - { - private string _rootUrl; - private string _logMode; - public string RootUrl - { - get { return _rootUrl; } - set - { - //TODO: validate http and uri - _rootUrl = value; - if (!_rootUrl.EndsWith("/")) - { - _rootUrl += "/"; - } - } - } - - public string LogMode - { - get { return _logMode; } - set - { - _logMode = value; - if (!_logMode.EndsWith("/")) - { - _logMode = _logMode.ToLower() + "/"; - } - } - } - - public string InputKey { get; set; } - - public string UserAgent { get; set; } - - public int TimeoutInSeconds { get; set; } - - public string Tag { get; set; } - - public string LogicalThreadContextKeys { get; set; } - - public string GlobalContextKeys { get; set; } - - public int BufferSize { get; set; } - public LogglyAppenderConfig() - { - UserAgent = "loggly-log4net-appender"; - TimeoutInSeconds = 30; - Tag = "log4net"; - LogMode = "bulk"; - LogicalThreadContextKeys = null; - GlobalContextKeys = null; - BufferSize = 500; - } - } +namespace log4net.loggly +{ + public class LogglyAppenderConfig : ILogglyAppenderConfig + { + private string _rootUrl; + private string _logMode; + public string RootUrl + { + get { return _rootUrl; } + set + { + //TODO: validate http and uri + _rootUrl = value; + if (!_rootUrl.EndsWith("/")) + { + _rootUrl += "/"; + } + } + } + + public string LogMode + { + get { return _logMode; } + set + { + _logMode = value; + if (!_logMode.EndsWith("/")) + { + _logMode = _logMode.ToLower() + "/"; + } + } + } + + public string InputKey { get; set; } + + public string UserAgent { get; set; } + + public int TimeoutInSeconds { get; set; } + + public string Tag { get; set; } + + public string LogicalThreadContextKeys { get; set; } + + public string GlobalContextKeys { get; set; } + + public int BufferSize { get; set; } + + public int NumberOfInnerExceptions { get; set; } + + public LogglyAppenderConfig() + { + UserAgent = "loggly-log4net-appender"; + TimeoutInSeconds = 30; + Tag = "log4net"; + LogMode = "bulk"; + LogicalThreadContextKeys = null; + GlobalContextKeys = null; + BufferSize = 500; + NumberOfInnerExceptions = 1; + } + } } \ No newline at end of file diff --git a/source/log4net-loggly/LogglyFormatter.cs b/source/log4net-loggly/LogglyFormatter.cs index 752e6d1..b451dc2 100644 --- a/source/log4net-loggly/LogglyFormatter.cs +++ b/source/log4net-loggly/LogglyFormatter.cs @@ -80,20 +80,29 @@ private object GetExceptionInfo(LoggingEvent loggingEvent) exceptionInfo.exceptionType = loggingEvent.ExceptionObject.GetType().FullName; exceptionInfo.exceptionMessage = loggingEvent.ExceptionObject.Message; exceptionInfo.stacktrace = loggingEvent.ExceptionObject.StackTrace; + exceptionInfo.innerException = + GetInnerExceptions(loggingEvent.ExceptionObject.InnerException, Config.NumberOfInnerExceptions); + + return exceptionInfo; + } - //most of the times dotnet exceptions contain important messages in the inner exceptions - if (loggingEvent.ExceptionObject.InnerException != null) + /// + /// Return nested exceptions + /// + /// The inner exception + /// The number of inner exceptions that should be included. + /// + private object GetInnerExceptions(Exception innerException, int deep) + { + if (innerException == null || deep <= 0) return null; + dynamic ex = new { - dynamic innerException = new - { - innerExceptionType = loggingEvent.ExceptionObject.InnerException.GetType().FullName, - innerExceptionMessage = loggingEvent.ExceptionObject.InnerException.Message, - innerStacktrace = loggingEvent.ExceptionObject.InnerException.StackTrace - }; - exceptionInfo.innerException = innerException; - } - - return exceptionInfo; + innerExceptionType = innerException.GetType().FullName, + innerExceptionMessage = innerException.Message, + innerStacktrace = innerException.StackTrace, + innerException = --deep > 0 ? GetInnerExceptions(innerException.InnerException, deep) : null + }; + return ex; } ///