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;
}
///