I have what is either a bug in log4net, or a misunderstanding on my part.
I'm trying to use LogicalThreadContext
to associate some data with a call context and have it propagate to any log statements made by any thread in that context. That is the purported advantage of LogicalThreadContext
over ThreadContext
.
I wasn't able to get the propagation to work, so I put together a simple unit test to see whether it would work, and it doesn't. Here it is:
[Fact]
public void log4net_logical_thread_context_test()
{
XmlConfigurator.Configure();
var log = LogManager.GetLogger(GetType());
var waitHandle = new ManualResetEvent(false);
using (LogicalThreadContext.Stacks["foo"].Push("Some contextual info"))
{
log.Debug("START");
ThreadPool.QueueUserWorkItem(delegate
{
log.Debug("A DIFFERENT THREAD");
waitHandle.Set();
});
waitHandle.WaitOne();
log.Debug("STOP");
}
}
My log4net configuration looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="FileAppender" type="log4net.Appender.FileAppender">
<file value="log.txt" />
<appendToFile value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%thread]|[%property{foo}]|%message%newline"/>
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="FileAppender" />
</root>
</log4net>
</configuration>
And my output looks like this:
[xUnit.net STA Test Execution Thread]|[Some contextual info]|START
[32]|[(null)]|A DIFFERENT THREAD
[xUnit.net STA Test Execution Thread]|[Some contextual info]|STOP
As you can see, the data I push onto the LTC stack is only present in the logging statements made on the same thread. The log statement made by the background thread is lacking the contextual data. Debugging through the test I could see that, indeed, that LogicalThreadContext.Stacks.Count
is zero on the background thread.
Digging into the log4net source, I found it utilizing the CallContext
class. This class does what it says on the tin - it allows context for the current "call" to be stored and retrieved. How it does this at a low level, I have no idea.
CallContext
has two sets of methods with which context information can be stored and retrieved: GetData
/SetData
and LogicalGetData
/LogicalSetData
. The documentation is very light on details regarding the difference between these two sets of methods, but the examples use GetData
/SetData
. And so does log4net's LogicalThreadContext
.
A quick test showed that GetData
/SetData
exhibits the same problem - data does not propagate across threads. I thought I'd give LogicalGetData
/LogicalSetData
a go instead:
[Fact]
public void call_context_test()
{
XmlConfigurator.Configure();
var log = LogManager.GetLogger(GetType());
var count = 5;
var waitHandles = new ManualResetEvent[count];
for (var i = 0; i < count; ++i)
{
waitHandles[i] = new ManualResetEvent(false);
var localI = i;
// on a bg thread, set some call context data
ThreadPool.QueueUserWorkItem(delegate
{
CallContext.LogicalSetData("name", "value " + localI);
log.DebugFormat("Set call context data to '{0}'", CallContext.LogicalGetData("name"));
var localWaitHandle = new ManualResetEvent(false);
// then on another bg thread, make sure the logical call context value is correct with respect to the "owning" bg thread
ThreadPool.QueueUserWorkItem(delegate
{
var value = CallContext.LogicalGetData("name");
log.DebugFormat("Retrieved call context data '{0}'", value);
Assert.Equal("value " + localI, value);
localWaitHandle.Set();
});
localWaitHandle.WaitOne();
waitHandles[localI].Set();
});
}
foreach (var waitHandle in waitHandles)
{
waitHandle.WaitOne();
}
}
This test passes - contextual information is successfully propagated across threads when using LogicalGetData
/LogicalSetData
.
So my question is this: has log4net gotten this wrong? Or is there something I'm missing?
UPDATE: I also tried doing a custom build of log4net with its LogicalThreadContextProperties
class altered as per my findings above. I re-ran my initial test and it worked. This just strikes me as too obvious a problem for a product used by so many people, so I have to assume I'm missing something.