diff --git a/README.md b/README.md
index 034dc34..b90e943 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ log4net-loggly
Custom log4net appenders for importing logging events to loggly. It’s asynchronous and will send logs in the background without blocking your application. Check out Loggly's [.Net logging documentation](https://www.loggly.com/docs/net-logs/) to learn more.
-Note: This library also has a support for .NET Core applications. Please see the section [.NET Core Support](README.md#net-core-support) below.
+**Note:** This library supports both .NET 4.0 and .NET Standard 2.0. Please see the section **[.NET Core Support](README.md#net-core-support)** below.
Download log4net-loggly package from NuGet. Use the following command.
@@ -14,79 +14,86 @@ 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.
+If you want to append **GlobalContext** and/or **LogicalThreadContext** properties to your log you need to define the list of context properties in the configuration.
-For GlobalContext Properties use
-``````
+For GlobalContext Properties use ``
-For LogicalThreadContext Properties
-``````
+For LogicalThreadContext Properties ``
-You can also use **layout** with in the Config to render logs according to your Pattern Layouts
-
-
-
-
-
-By default, library uses Loggly /bulk end point (https://www.loggly.com/docs/http-bulk-endpoint/). To use /inputs endpoint, add the following configuration in config file.
-
-```
-
-```
-
-Set number of inner exceptions that should be sent to loggly, if you don't want the default value.
-```
-
+You can also use **layout** to render logs according to your Pattern Layouts
+```xml
+
+
+
```
-Add the following entry in your AssemblyInfo.cs
-```
+Add the following entry to your AssemblyInfo.cs
+```csharp
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
```
Alternatively, you can add the following code in your Main method or in Global.asax file
-
-```
+```csharp
log4net.Config.XmlConfigurator.Configure();
```
Create an object of the Log class using LogManager
-
- var logger = LogManager.GetLogger(typeof(Class));
+```csharp
+var logger = LogManager.GetLogger(typeof(Class));
+```
Send logs to Loggly using the following code
+```csharp
+// send plain string
+logger.Info("log message");
-```
- logger.Info("log message");
-```
+// send an exception
+logger.Error("your log message", new Exception("your exception message"));
+
+// send dictionary as JSON object
+var items = new Dictionary();
+items.Add("key1","value1");
+items.Add("key2", "value2");
+logger.Info(items);
-For Console Application
+// send any object as JSON object
+logger.Debug(new { Property = "This is anonymous object", Property2 = "with two properties" });
+```
-You should add the following statement at the end of your Main method as the log4net-loggly library is asynchronous so there needs to be time for the threads the complete logging before the application exits.
+## Flushing logs on application shutdown
+Library is buffering and sending log messages to Loggly asynchronously in background. That means that some logs may be still in buffer when the application terminates. To make sure that all logs have been sent you need to cleanly shutdown log4net logger using following code:
+```csharp
+logger.Logger.Repository.Shutdown();
```
-Console.ReadKey();
+This flushes any pending messages.
+
+## Advanced configuration
+
+Library by default serializes and sends 4 levels of inner exceptions in case of warn/error log. If you want to change this number just add following configuration to config file to `` section
+```xml
+
```
-### .NET Core Support:
-Prerequisites:
+### .NET Core Support:
+
+**Prerequisites:**
- Since this library support .NET Core target framework 2.0, make sure you are using either version 15.3.0 or higher of Visual Studio IDE 2017 or Visual Studio Code.
@@ -94,25 +101,25 @@ Console.ReadKey();
- You may also have to install the .NET Core cross-platform development workload (in the Other Toolsets section). Please see the more details [here](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x).
-Once you are done with the environment setup, now you are all set to create your application in .NET Core target framework 2.0. Please follow the points below-
+Once you are done with the environment setup, now you are all set to create your application in .NET Core 2.0. Please follow the points below.
-- If you are using Visual Studio 2017 IDE then you can create a new .NET Core project by selecting New Project from File menu.
+- If you are using **Visual Studio 2017 IDE** then you can create a new .NET Core project by selecting **New Project** from **File** menu.
-- Visual Studio Code users can create a new project by running the below command on the project workspace terminal-
+- **Visual Studio Code** users can create a new project by running the below command on the project workspace terminal-
```
dotnet new console -o Application_Name
```
-The dotnet command creates a new application of type console for you. The -o parameter creates a directory named where your app is stored, and populates it with the required files.
+The **dotnet** command creates a new application of type **console** for you. The **-o** parameter creates a directory named **Application_Name** where your app is stored, and populates it with the required files.
-- If you are using Visual Studio 2017 IDE then you have to install the package log4net-loggly into your project from NuGet by running the command on Package Manager Console as shown below-
+- If you are using **Visual Studio 2017 IDE** then you have to install the package **log4net-loggly** into your project from **NuGet** by running the command on **Package Manager Console** as shown below-
```
Install-Package log4net-loggly
```
-- If you are using Visual Studio Code then run the below command on the terminal to install the log4net-loggly package.
+- If you are using **Visual Studio Code** then run the below command on the terminal to install the **log4net-loggly** package.
```
dotnet add package log4net-loggly
@@ -120,11 +127,11 @@ dotnet add package log4net-loggly
- Now when you create an applicaton in .NET Core, there is no App.config file exist already in the project so you have to create one.
- (a) For Visual Studio 2017 users, you should right click on your project and create a Application Configuration File "App.config" on the root level of your project.
+ (a) For **Visual Studio 2017** users, you should right click on your project and create a **Application Configuration File** "App.config" on the root level of your project.
- (b) For Visual Studio Code users, you should simply create the same configuration file on the the folder structure where your another files exists.
+ (b) For **Visual Studio Code** users, you should simply create the same configuration file on the the folder structure where your another files exists.
-- You should simply add the below configuration code in your App.config file to configure LogglyAppender in your application. Make sure the configSections block is the first element of the configuration in app.config. This is a requirement set by .NET.
+- You should simply add the below configuration code in your App.config file to configure LogglyAppender in your application. Make sure the **configSections** block is the first element of the configuration in app.config. This is a requirement set by .NET.
```xml
@@ -139,8 +146,8 @@ dotnet add package log4net-loggly
-
-
+
+
@@ -148,15 +155,15 @@ dotnet add package log4net-loggly
```
-Note: If you are using Visual Studio 2017 IDE then your application will not be able to read configurations from this App.config file until you do the following-
+**Note: If you are using Visual Studio 2017 IDE then your application will not be able to read configurations from this App.config file until you do the following-**
-- Right click on your App.config file from Solution Explorer, go to Properties and select the Copy to Output Directory to Copy always, click Apply and hit the OK button.
+- Right click on your **App.config** file from Solution Explorer, go to **Properties** and select the **Copy to Output Directory** to **Copy always**, click Apply and hit the OK button.
- If you are using Visual Studio Code then you don't need to do the extra settings for App.config file.
+ If you are using **Visual Studio Code** then you don't need to do the extra settings for App.config file.
- As compare to .NET Frameworks, in .NET Core you don't need any AssemblyInfo.cs file to add the below code in-
-```
+```csharp
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
```
@@ -164,45 +171,18 @@ The above code line allows the application to read the configuration from App.co
- Add the following code inside the main method of your application file i.e. Program.cs-
-```
+```csharp
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
log4net.Config.XmlConfigurator.Configure(logRepository, new FileInfo("App.config"));
```
-After adding the above code you can simply create an object of the Log class using LogManager and start logging any plaintext, exceptions, or JSON events as shown below-
-
-```
-var logger = LogManager.GetLogger(typeof(Class));
-//Send plaintext
-logger.Info("your log message");
-
-//Send an exception
-logger.Error("your log message", new Exception("your exception message"));
-//Send a JSON object
-var items = new Dictionary();
-items.Add("key1","value1");
-items.Add("key2", "value2");
-logger.Info(items);
-```
-Running the application in Visual Studio 2017 IDE is easy since you just need to click on the Start button to run your application.
+Running the application in **Visual Studio 2017 IDE** is easy since you just need to click on the **Start** button to run your application.
-If you are using Visual Studio Code then you have to run the below command on the terminal to run your .NET Core application-
+If you are using **Visual Studio Code** then you have to run the below command on the terminal to run your .NET Core application-
```
dotnet run
```
-And that's it. After doing this, you will see your .NET Core application logs flowing into Loggly.
-
-
-Added handling for LoggingEvent properties
-
-Support for properties tied to a specific event and not a ThreadContext which is shared across the entire thread.
-
-Added test cases project
-
-- Added unit test cases project in library to test consistency for new feature.
-
-- User can select test cases project in Visual Studio and can simply run all test cases from Test Explorer.
-
+And that's it. After doing this, you will see your .NET Core application logs flowing into Loggly.
\ No newline at end of file
diff --git a/source/log4net-loggly-console/App.config b/source/log4net-loggly-console/App.config
index 12d32e3..ca4d9a2 100644
--- a/source/log4net-loggly-console/App.config
+++ b/source/log4net-loggly-console/App.config
@@ -16,6 +16,9 @@
+
diff --git a/source/log4net-loggly-console/Program.cs b/source/log4net-loggly-console/Program.cs
index 5d8e41a..56ac300 100644
--- a/source/log4net-loggly-console/Program.cs
+++ b/source/log4net-loggly-console/Program.cs
@@ -27,8 +27,9 @@ static void Main(string[] argArray)
{
GlobalContext.Properties["GlobalContextPropertySample"] = new GlobalContextTest();
+ var currentFileName = Path.GetFileName(Assembly.GetExecutingAssembly().Location);
var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
- log4net.Config.XmlConfigurator.Configure(logRepository,new FileInfo("app.config"));
+ log4net.Config.XmlConfigurator.Configure(logRepository, new FileInfo(currentFileName + ".config"));
var log = LogManager.GetLogger(typeof(Program));
@@ -88,7 +89,11 @@ static void Main(string[] argArray)
parent.Children = new List { child1, child2 };
log.Info(parent);
- log.Debug("zzzz");
+ log.Debug(@"This
+is
+some
+multiline
+log");
log.InfoFormat("Loggly is the best {0} to collect Logs.", "service");
log.Info(new { type1 = "newcustomtype", value1 = "newcustomvalue" });
log.Info(new TestObject());
@@ -124,7 +129,8 @@ static void Main(string[] argArray)
log.Error("Exception", e);
}
- Console.ReadKey();
+ log.Info("This is the last message. Program will terminate now.");
+ log.Logger.Repository.Shutdown();
}
}
}
diff --git a/source/log4net-loggly-stress-tool/App.config b/source/log4net-loggly-stress-tool/App.config
new file mode 100644
index 0000000..d3b47fc
--- /dev/null
+++ b/source/log4net-loggly-stress-tool/App.config
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/log4net-loggly-stress-tool/CommandLineArgs.cs b/source/log4net-loggly-stress-tool/CommandLineArgs.cs
new file mode 100644
index 0000000..de2ca6e
--- /dev/null
+++ b/source/log4net-loggly-stress-tool/CommandLineArgs.cs
@@ -0,0 +1,95 @@
+using System;
+
+namespace log4net_loggly_stress_tool
+{
+ internal class CommandLineArgs
+ {
+ public int NumEvents { get; private set; } = 1000;
+ public int NumLoggingThreads { get; private set; } = 1;
+ public int ExceptionFrequency { get; private set; } = 0;
+ public TimeSpan SendDelay { get; private set; } = TimeSpan.Zero;
+ public int LogsPerSecond { get; set; } = 0;
+
+ public static CommandLineArgs Parse(string[] args)
+ {
+ var result = new CommandLineArgs();
+
+ try
+ {
+ for (int i = 0; i < args.Length; i++)
+ {
+ switch (args[i])
+ {
+ case "-n":
+ case "--num-events":
+ i++;
+ result.NumEvents = int.Parse(args[i]);
+ if (result.NumEvents < 1)
+ throw new ArgumentException("Number of events must be >= 1");
+ break;
+ case "-t":
+ case "--num-threads":
+ i++;
+ result.NumLoggingThreads = int.Parse(args[i]);
+ if (result.NumLoggingThreads < 1)
+ throw new ArgumentException("Number of threads must be >= 1");
+ break;
+ case "-d":
+ case "--send-delay":
+ {
+ i++;
+ var value = int.Parse(args[i]);
+ if (value < 0)
+ throw new ArgumentException("Delay must be >= 0");
+ result.SendDelay = TimeSpan.FromMilliseconds(value);
+ }
+ break;
+ case "-l":
+ case "--logs-per-second":
+ {
+ i++;
+ var value = int.Parse(args[i]);
+ if (value <= 0)
+ throw new ArgumentException("Logs-per-second must be > 0");
+ result.LogsPerSecond = value;
+ }
+ break;
+ case "-e":
+ case "--exception-every":
+ i++;
+ result.ExceptionFrequency = int.Parse(args[i]);
+ if (result.ExceptionFrequency < 0)
+ throw new ArgumentException("Exception frequency must be >= 0");
+ break;
+ default:
+ PrintHelp();
+ break;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ PrintHelp();
+ }
+
+ return result;
+ }
+
+ private static void PrintHelp()
+ {
+ Console.WriteLine(@"
+Loggly log4net logger stress testing tool.
+Tool is generating log messages in one or more threads and logs them to logger.
+Fake HTTP layer is used to fake sending data to Loggly, no data are really sent out.
+
+Usage: log4net-loggly-stress-tool.exe [-n|--num-threads ] [-d|--send-delay ] [-e|--exception-every ]
+ -n|--num-events - Number of events to send. Must be > 0. Default: 1000
+ -l|--logs-per-second - How many logs per second should be generated. Total number is still defined by -n, this value just slows down the generator to given frequency.
+ -t|--num-threads - Number of threads used to generate logs. Must be > 0. Default: 1.
+ -d|--send-delay - Delay for one simulated send to Loggly servers in milliseconds. Must be >= 0. Default: 0
+ -e|--exception-every - Log error with exception every N logs. Must be >= 0. Default: 0 - never");
+
+ Environment.Exit(0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/log4net-loggly-stress-tool/Program.cs b/source/log4net-loggly-stress-tool/Program.cs
new file mode 100644
index 0000000..36908ae
--- /dev/null
+++ b/source/log4net-loggly-stress-tool/Program.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using log4net;
+using log4net.loggly;
+
+namespace log4net_loggly_stress_tool
+{
+ public class Program
+ {
+ private static ILog _log;
+
+ private static long _count = 0;
+
+ public static void Main(string[] args)
+ {
+ var commandLine = CommandLineArgs.Parse(args);
+
+ // use test HTTP layer
+ LogglyClient.WebRequestFactory = (config, url) => new TestHttpClient(commandLine.SendDelay);
+
+ var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
+ log4net.Config.XmlConfigurator.Configure(logRepository);
+
+ _log = LogManager.GetLogger(typeof(Program));
+
+ SetupThreadContext();
+
+ var exception = GetTestException();
+
+ Console.WriteLine("Running test in {0} threads with {1} ms delay for send and exception every {2} messages.",
+ commandLine.NumLoggingThreads, commandLine.SendDelay, commandLine.ExceptionFrequency);
+
+ var watch = Stopwatch.StartNew();
+ var tasks = new List(commandLine.NumLoggingThreads);
+ int logsPerSecondPerThread = (int)Math.Ceiling((double)commandLine.LogsPerSecond / commandLine.NumLoggingThreads);
+ for (int i = 0; i < commandLine.NumLoggingThreads; i++)
+ {
+ tasks.Add(Task.Factory.StartNew(() => SendContinuously(commandLine, exception, logsPerSecondPerThread), TaskCreationOptions.LongRunning));
+ }
+
+ Task.WaitAll(tasks.ToArray());
+
+ watch.Stop();
+
+ Console.WriteLine("Test finished. Elapsed: {0}, Throughput: {1} logs/s", watch.Elapsed, _count*1000 / watch.Elapsed.TotalMilliseconds);
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey();
+ }
+
+ private static void SendContinuously(CommandLineArgs commandLine, Exception exception, int logsPerSecond)
+ {
+ long currentCount;
+ Stopwatch watch = Stopwatch.StartNew();
+ int countThisSecond = 0;
+ while ((currentCount = Interlocked.Increment(ref _count)) <= commandLine.NumEvents)
+ {
+ if (currentCount % 1000 == 0)
+ {
+ Console.WriteLine("Sent: {0}", currentCount);
+ }
+
+ if (commandLine.ExceptionFrequency > 0 && currentCount % commandLine.ExceptionFrequency == 0)
+ {
+ _log.Error(
+ $"Test message {currentCount} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus fermentum ligula " +
+ "et ante tincidunt venenatis. Ut pretium, mi laoreet fringilla egestas, mauris quam lacinia dolor, " +
+ "eu maximus nisi mauris vel lorem. Duis a ex eu orci consectetur congue sed sit amet ligula. " +
+ "Aenean congue mollis quam volutpat varius.", exception);
+ }
+ else
+ {
+ _log.Info(
+ $"Test message {currentCount}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus fermentum ligula " +
+ "et ante tincidunt venenatis. Ut pretium, mi laoreet fringilla egestas, mauris quam lacinia dolor, " +
+ "eu maximus nisi mauris vel lorem. Duis a ex eu orci consectetur congue sed sit amet ligula. " +
+ "Aenean congue mollis quam volutpat varius.");
+ }
+
+ // if rate limiting is applied then sleep remainder of the current second before moving on
+ if (logsPerSecond > 0 && ++countThisSecond >= logsPerSecond)
+ {
+ Thread.Sleep(1000 - Math.Min(1000, (int)watch.ElapsedMilliseconds));
+ countThisSecond = 0;
+ watch.Restart();
+ }
+ }
+ }
+
+
+ private static Exception GetTestException()
+ {
+ Exception exception;
+ try
+ {
+ try
+ {
+ throw new ArgumentException("inner exception");
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException("outer exception", e);
+ }
+ }
+ catch (Exception e)
+ {
+ exception = e;
+ }
+
+ return exception;
+ }
+
+ private static void SetupThreadContext()
+ {
+ ThreadContext.Properties["ThreadProperty1"] = DateTime.Now;
+ ThreadContext.Properties["ThreadProperty2"] = new TestClass
+ {
+ IntProperty = 123,
+ StringProperty =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus fermentum ligula et ante tincidunt venenatis. " +
+ "Ut pretium, mi laoreet fringilla egestas, mauris quam lacinia dolor, eu maximus nisi mauris vel lorem. " +
+ "Duis a ex eu orci consectetur congue sed sit amet ligula. Aenean congue mollis quam volutpat varius.",
+ DatetimeProperty = DateTime.Now
+ };
+ }
+
+ private class TestClass
+ {
+ public int IntProperty { get; set; }
+ public string StringProperty { get; set; }
+ public DateTime DatetimeProperty { get; set; }
+ }
+ }
+}
diff --git a/source/log4net-loggly-stress-tool/TestHttpClient.cs b/source/log4net-loggly-stress-tool/TestHttpClient.cs
new file mode 100644
index 0000000..15104c2
--- /dev/null
+++ b/source/log4net-loggly-stress-tool/TestHttpClient.cs
@@ -0,0 +1,32 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+
+namespace log4net_loggly_stress_tool
+{
+ internal class TestHttpClient : WebRequest
+ {
+ private readonly TimeSpan _sendDelay;
+
+ public TestHttpClient(TimeSpan sendDelay)
+ {
+ _sendDelay = sendDelay;
+ }
+
+ public override WebResponse GetResponse()
+ {
+ Thread.Sleep(_sendDelay);
+ return new TestResponse();
+ }
+
+ public override Stream GetRequestStream()
+ {
+ return new MemoryStream();
+ }
+
+ private class TestResponse : WebResponse
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/log4net-loggly-stress-tool/log4net-loggly-stress-tool.csproj b/source/log4net-loggly-stress-tool/log4net-loggly-stress-tool.csproj
new file mode 100644
index 0000000..2b0ba9f
--- /dev/null
+++ b/source/log4net-loggly-stress-tool/log4net-loggly-stress-tool.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net45
+ log4net_loggly_stress_tool
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/log4net-loggly.UnitTests/App.config b/source/log4net-loggly.UnitTests/App.config
new file mode 100644
index 0000000..2630ca5
--- /dev/null
+++ b/source/log4net-loggly.UnitTests/App.config
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/log4net-loggly.UnitTests/IntegrationTest.cs b/source/log4net-loggly.UnitTests/IntegrationTest.cs
new file mode 100644
index 0000000..278b09b
--- /dev/null
+++ b/source/log4net-loggly.UnitTests/IntegrationTest.cs
@@ -0,0 +1,559 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using FluentAssertions;
+using FluentAssertions.Json;
+using JetBrains.Annotations;
+using log4net;
+using log4net.Core;
+using log4net.loggly;
+using log4net.Util;
+using Moq;
+using Newtonsoft.Json.Linq;
+using Xunit;
+// ReSharper disable UnusedAutoPropertyAccessor.Local
+
+namespace log4net_loggly.UnitTests
+{
+ [UsedImplicitly]
+ [SuppressMessage("ReSharper", "StringLiteralTypo")]
+ public abstract class IntegrationTest
+ {
+ protected internal const string TestThreadName = "MyTestThread";
+
+ private readonly ManualResetEvent _messageSent;
+ protected ILog _log;
+ protected readonly LogglyAppender _logglyAppender;
+ private readonly MemoryStream _messageStream;
+
+ protected IntegrationTest()
+ {
+ // setup HTTP client mock so that we can wait for sent message and inspect it
+ _messageSent = new ManualResetEvent(false);
+
+ _messageStream = new MemoryStream();
+ var webRequestMock = new Mock();
+ webRequestMock.Setup(x => x.GetRequestStream()).Returns(_messageStream);
+ webRequestMock.Setup(x => x.GetResponse())
+ .Returns(() =>
+ {
+ _messageSent.Set();
+ return Mock.Of();
+ });
+
+ // use mocked web request
+ LogglyClient.WebRequestFactory = (config, url) => webRequestMock.Object;
+
+ var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
+ var currentFileName = Path.GetFileName(Assembly.GetExecutingAssembly().Location);
+ log4net.Config.XmlConfigurator.Configure(logRepository, new FileInfo(currentFileName + ".config"));
+
+ _log = LogManager.GetLogger(GetType());
+
+ var appenders = logRepository.GetAppenders();
+ _logglyAppender = (LogglyAppender)appenders[0];
+
+ ThreadContext.Properties.Clear();
+ LogicalThreadContext.Properties.Clear();
+ GlobalContext.Properties.Clear();
+
+ // thread name can be set just once so we need this safeguard
+ if (string.IsNullOrEmpty(Thread.CurrentThread.Name))
+ {
+ Thread.CurrentThread.Name = TestThreadName;
+ }
+ }
+
+ [Fact]
+ public void LogContainsThreadName()
+ {
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ThreadName.Should().Be(TestThreadName);
+ }
+
+ [Fact]
+ public void LogContainsHostName()
+ {
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.Hostname.Should().Be(Environment.MachineName);
+ }
+
+ [Fact]
+ public void LogContainsLoggerName()
+ {
+ _log = LogManager.GetLogger(Assembly.GetExecutingAssembly(), "MyTestLogger");
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.LoggerName.Should().Be("MyTestLogger");
+ }
+
+ [Fact]
+ public void LogContainsProcessName()
+ {
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.Process.Should().Be(Process.GetCurrentProcess().ProcessName);
+ }
+
+ [Fact]
+ public void LogContainsTimestampInLocalTime()
+ {
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ var timestamp = DateTime.Parse(message.Timestamp);
+ timestamp.Should().BeWithin(TimeSpan.FromSeconds(5)).Before(DateTime.Now);
+ }
+
+ [Theory]
+ [MemberData(nameof(LogLevels))]
+ public void LogContainsLogLevel(Level level)
+ {
+ _log.Logger.Log(GetType(), level, "test message", null);
+
+ var message = WaitForSentMessage();
+ message.Level.Should().Be(level.Name);
+ }
+
+ [Fact]
+ public void LogContainsPassedException()
+ {
+ Exception thrownException;
+ try
+ {
+ throw new InvalidOperationException("test exception");
+ }
+ catch (Exception e)
+ {
+ thrownException = e;
+ _log.Error("test message", e);
+ }
+
+ var message = WaitForSentMessage();
+ var exception = message.ExtraProperties.Should().HaveElement("exception", "logged exception should be in the data").Which;
+ AssertException(exception, thrownException);
+ }
+
+ [Fact]
+ public void LogContainsInnerException()
+ {
+ Exception thrownException;
+ try
+ {
+ try
+ {
+ throw new ArgumentException("inner exception");
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException("test exception", e);
+ }
+ }
+ catch (Exception e)
+ {
+ thrownException = e;
+ _log.Error("test message", e);
+ }
+
+ var message = WaitForSentMessage();
+ var exception = message.ExtraProperties.Should().HaveElement("exception", "logged exception should be in the data").Which;
+ AssertException(exception, thrownException);
+ AssertException(exception["innerException"], thrownException.InnerException);
+ }
+
+ [Fact]
+ public void LogContainsEventContextProperties()
+ {
+ var expectedJson = @"
+{
+""MyProperty1"": ""MyValue1"",
+""MyProperty2"": {
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123,
+ ""Parent"": null
+ }
+}";
+ var data = new LoggingEventData
+ {
+ Level = Level.Info,
+ Message = "test message",
+ Properties = new PropertiesDictionary()
+ };
+ data.Properties["MyProperty1"] = "MyValue1";
+ data.Properties["MyProperty2"] = new TestItem { IntProperty = 123, StringProperty = "test string" };
+
+ _log.Logger.Log(new LoggingEvent(data));
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsThreadContextProperties()
+ {
+ ThreadContext.Properties["MyProperty1"] = "MyValue1";
+ ThreadContext.Properties["MyProperty2"] = new TestItem { IntProperty = 123, StringProperty = "test string" };
+ var expectedJson = @"
+{
+""MyProperty1"": ""MyValue1"",
+""MyProperty2"": {
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123,
+ ""Parent"": null
+ }
+}";
+
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsSelectedLogicalThreadContextProperties()
+ {
+ LogicalThreadContext.Properties["lkey1"] = "MyValue1";
+ LogicalThreadContext.Properties["lkey2"] = new TestItem { IntProperty = 123, StringProperty = "test string" };
+ LogicalThreadContext.Properties["lkey3"] = "this won't be in the log";
+ // only properties defines in app.config in will be included
+ var expectedJson = @"
+{
+""lkey1"": ""MyValue1"",
+""lkey2"": {
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123,
+ ""Parent"": null
+ }
+}";
+
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsSelectedGlobalContextProperties()
+ {
+ GlobalContext.Properties["gkey1"] = "MyValue1";
+ GlobalContext.Properties["gkey2"] = new TestItem { IntProperty = 123, StringProperty = "test string" };
+ GlobalContext.Properties["gkey3"] = "this won't be in the log";
+ // only properties defines in app.config in will be included
+ var expectedJson = @"
+{
+""gkey1"": ""MyValue1"",
+""gkey2"": {
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123,
+ ""Parent"": null
+ }
+}";
+
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsThreadContextStacks()
+ {
+ using (ThreadContext.Stacks["TestStack1"].Push("TestStackValue1"))
+ {
+ using (ThreadContext.Stacks["TestStack2"].Push("TestStackValue2"))
+ using (ThreadContext.Stacks["TestStack1"].Push("TestStackValue3"))
+ {
+ _log.Info("test message");
+ }
+ }
+ var expectedJson = @"
+{
+""TestStack1"": ""TestStackValue1 TestStackValue3"",
+""TestStack2"": ""TestStackValue2""
+}";
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsLogicalThreadContextStacks()
+ {
+ using (LogicalThreadContext.Stacks["lkey1"].Push("TestStackValue1"))
+ {
+ using (LogicalThreadContext.Stacks["lkey2"].Push("TestStackValue2"))
+ using (LogicalThreadContext.Stacks["lkey1"].Push("TestStackValue3"))
+ {
+ _log.Info("test message");
+ }
+ }
+ var expectedJson = @"
+{
+""lkey1"": ""TestStackValue1 TestStackValue3"",
+""lkey2"": ""TestStackValue2""
+}";
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void EventContextHasHighestPriority()
+ {
+ GlobalContext.Properties["CommonProperty"] = "GlobalContext";
+ ThreadContext.Properties["CommonProperty"] = "ThreadContext";
+ LogicalThreadContext.Properties["CommonProperty"] = "LogicalThreadContext";
+ var data = new LoggingEventData
+ {
+ Level = Level.Info,
+ Message = "test message",
+ Properties = new PropertiesDictionary()
+ };
+ data.Properties["CommonProperty"] = "EventContext";
+ _log.Logger.Log(new LoggingEvent(data));
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().HaveElement("CommonProperty")
+ .Which.Value().Should().Be("EventContext");
+ }
+
+ [Fact]
+ public void LogicalThreadContextHasSecondHighestPriority()
+ {
+ GlobalContext.Properties["CommonProperty"] = "GlobalContext";
+ ThreadContext.Properties["CommonProperty"] = "ThreadContext";
+ LogicalThreadContext.Properties["CommonProperty"] = "LogicalThreadContext";
+ // no event properties here
+
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().HaveElement("CommonProperty")
+ .Which.Value().Should().Be("LogicalThreadContext");
+ }
+
+ [Fact]
+ public void ThreadContextHaveThirdHighestPriority()
+ {
+ GlobalContext.Properties["CommonProperty"] = "GlobalContext";
+ ThreadContext.Properties["CommonProperty"] = "ThreadContext";
+ // no event or logical thread context properties here
+
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().HaveElement("CommonProperty")
+ .Which.Value().Should().Be("ThreadContext");
+ }
+
+ [Fact]
+ public void PropertiesFromDifferentContextsAreMerged()
+ {
+ GlobalContext.Properties["gkey1"] = "GlobalContext";
+ ThreadContext.Properties["tkey1"] = "ThreadContext";
+ LogicalThreadContext.Properties["lkey1"] = "LogicalThreadContext";
+ var data = new LoggingEventData
+ {
+ Level = Level.Info,
+ Message = "test message",
+ Properties = new PropertiesDictionary()
+ };
+ data.Properties["ekey1"] = "EventContext";
+ var expectedJson = @"
+{
+""gkey1"": ""GlobalContext"",
+""tkey1"": ""ThreadContext"",
+""lkey1"": ""LogicalThreadContext"",
+""ekey1"": ""EventContext"",
+}";
+
+ _log.Logger.Log(new LoggingEvent(data));
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void SendPlainString_DoesNotHaveAnyExtraProperties()
+ {
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().HaveCount(0);
+ }
+
+ [Fact]
+ public void SendObject_SendsItAsJson()
+ {
+ var expectedJson = @"
+{
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123,
+ ""Parent"": null
+}";
+ var item = new TestItem { StringProperty = "test string", IntProperty = 123 };
+ _log.Info(item);
+ var message = WaitForSentMessage();
+
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void SendAnonymousObject_SendsItAsJson()
+ {
+ var expectedJson = @"
+{
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123
+}";
+ _log.Info(new { StringProperty = "test string", IntProperty = 123 });
+ var message = WaitForSentMessage();
+
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void SendNestedObjects_SendsItAsJson()
+ {
+ var expectedJson = @"
+{
+ ""ParentStringProperty"": ""parent"",
+ ""Child"": {
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123
+ }
+}";
+ var item = new TestItem { StringProperty = "test string", IntProperty = 123 };
+ var parent = new TestParentItem { ParentStringProperty = "parent", Child = item };
+ item.Parent = parent;
+ _log.Info(parent);
+ var message = WaitForSentMessage();
+
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void SendJsonString_SendsItAsJson()
+ {
+ var expectedJson = @"
+{
+ ""StringProperty"": ""test string"",
+ ""IntProperty"": 123
+}";
+
+ _log.Info("{\"StringProperty\": \"test string\", \"IntProperty\": 123}");
+
+ var message = WaitForSentMessage();
+ message.ExtraProperties.Should().BeEquivalentTo(JObject.Parse(expectedJson));
+ }
+
+ [Fact]
+ public void LogContainsFixedValues()
+ {
+ ThreadContext.Properties["TestFixValue"] = new TestFixingItem();
+ _log.Info("test message");
+
+ var message = WaitForSentMessage();
+ // TestFixingItem returns "volatile value" on ToString() but "fixed value" on GetFixedObject()
+ message.ExtraProperties["TestFixValue"].Should().HaveValue("fixed value", "type of this value requires fixing");
+ }
+
+ protected SentMessage WaitForSentMessage()
+ {
+ _messageSent.WaitOne(TimeSpan.FromSeconds(10)).Should().BeTrue("Log message should have been sent already.");
+ var message = Encoding.UTF8.GetString(_messageStream.ToArray());
+ return new SentMessage(message);
+ }
+
+ private void AssertException(JToken exception, Exception expectedException)
+ {
+ exception.Value("exceptionType").Should()
+ .Be(expectedException.GetType().FullName, "exception type should be correct");
+ exception.Value("exceptionMessage").Should().Be(expectedException.Message, "exception message should be correct");
+ exception.Value("stacktrace").Should().Contain(expectedException.StackTrace, "exception stack trace should be correct");
+ }
+
+ public static IEnumerable