如何使用 log4net 记录 Threadpool 线程的正确上下文?
我正在尝试找到一种方法来从一堆线程中记录有用的上下文。问题是,许多代码是在通过线程池线程到达的事件上处理的(据我所知),因此它们的名称与任何上下文都没有关系。这个问题可以用下面的代码来演示:
class Program
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
static void Main(string[] args)
{
new Thread(TestThis).Start("ThreadA");
new Thread(TestThis).Start("ThreadB");
Console.ReadLine();
}
private static void TestThis(object name)
{
var nameStr = (string)name;
Thread.CurrentThread.Name = nameStr;
log4net.ThreadContext.Properties["ThreadContext"] = nameStr;
log4net.LogicalThreadContext.Properties["LogicalThreadContext"] = nameStr;
log.Debug("From Thread itself");
ThreadPool.QueueUserWorkItem(x => log.Debug("From threadpool Thread: " + nameStr));
}
}
转换模式是:
%date [%thread] %-5level %logger [%property] - %message%newline
输出如下:
2010-05-21 15:08:02,357 [ThreadA] DEBUG LogicalContextTest.Program [{LogicalThreadContext=ThreadA, log4net:HostName=xxx, ThreadContext=ThreadA}] - From Thread itself
2010-05-21 15:08:02,357 [ThreadB] DEBUG LogicalContextTest.Program [{LogicalThreadContext=ThreadB, log4net:HostName=xxx, ThreadContext=ThreadB}] - From Thread itself
2010-05-21 15:08:02,404 [7] DEBUG LogicalContextTest.Program [{log4net:HostName=xxx}] - From threadpool Thread: ThreadA
2010-05-21 15:08:02,420 [16] DEBUG LogicalContextTest.Program [{log4net:HostName=xxx}] - From threadpool Thread: ThreadB
正如您所看到的,最后两行没有有用信息的名称来区分 2 个线程,除了手动将名称添加到消息中(我想避免)。如何将名称/上下文获取到线程池线程的日志中,而无需在每次调用时将其添加到消息中或需要在每次回调中再次设置属性。
I am trying to find a way to log useful context from a bunch of threads. The problem is that a lot of code is dealt with on Events that are arriving via threadpool threads (as far as I can tell) so their names are not in relation to any context. The problem can be demonstrated with the following code:
class Program
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
static void Main(string[] args)
{
new Thread(TestThis).Start("ThreadA");
new Thread(TestThis).Start("ThreadB");
Console.ReadLine();
}
private static void TestThis(object name)
{
var nameStr = (string)name;
Thread.CurrentThread.Name = nameStr;
log4net.ThreadContext.Properties["ThreadContext"] = nameStr;
log4net.LogicalThreadContext.Properties["LogicalThreadContext"] = nameStr;
log.Debug("From Thread itself");
ThreadPool.QueueUserWorkItem(x => log.Debug("From threadpool Thread: " + nameStr));
}
}
The Conversion pattern is:
%date [%thread] %-5level %logger [%property] - %message%newline
The output is like so:
2010-05-21 15:08:02,357 [ThreadA] DEBUG LogicalContextTest.Program [{LogicalThreadContext=ThreadA, log4net:HostName=xxx, ThreadContext=ThreadA}] - From Thread itself
2010-05-21 15:08:02,357 [ThreadB] DEBUG LogicalContextTest.Program [{LogicalThreadContext=ThreadB, log4net:HostName=xxx, ThreadContext=ThreadB}] - From Thread itself
2010-05-21 15:08:02,404 [7] DEBUG LogicalContextTest.Program [{log4net:HostName=xxx}] - From threadpool Thread: ThreadA
2010-05-21 15:08:02,420 [16] DEBUG LogicalContextTest.Program [{log4net:HostName=xxx}] - From threadpool Thread: ThreadB
As you can see the last two rows have no Names of useful information to distinguish the 2 threads, other than manually adding the name to the message (which I want to avoid). How can I get the Name/Context into the log for the threadpool threads without adding it to the message at every call or needing to set the property again in every callback.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
更新:
2014 年 12 月 11 日 - 请参阅我的帖子的第一部分:
log4net.ThreadContext 和 log4net.LogicalThreadContext 有什么区别?
最近的更新。 Log4Net 的 LogicalThreadContext 最近(在过去几年中)进行了一些更新,因此现在可以正常工作。链接帖子中的更新提供了一些详细信息。
结束更新。
这是一个可能对您有帮助的想法。部分问题在于 log4net 上下文对象(ThreadContext 和 LogicalThreadContext)不会将其属性“流”到“子”线程。 LogicalThreadContext 给人一种错误的印象,它确实如此,但事实并非如此。在内部它使用 CallContext.SetData 来存储其属性。通过 SetData 设置的数据附加到线程,但它不会被子线程“继承”。因此,如果您通过如下方式设置属性:
该属性将可通过 %property 模式转换器进行记录,并且在从您首先设置属性的同一线程记录时将包含一个值,但它不会包含值在从该线程生成的任何子线程中。
如果您可以通过 CallContext.LogicalSetData(请参阅上面的链接)保存属性,则属性将“流向”(或由)任何子线程继承。因此,如果您可以执行以下操作:
那么“MyLogicalData”将在您设置它的线程以及任何子线程中可用。
请参阅Jeffrey Richter 的这篇博客文章 了解有关使用 CallContext.LogicalSetData 的更多信息。
您可以通过 CallContext.LogicalSetData 轻松存储您自己的信息,并通过编写您自己的 PatternLayoutConverter.我附上了两个新的 PatternLayoutConverters 的一些示例代码。
第一个允许您记录存储在 Trace.CorrelationManager 中的信息 的LogicalOperationStack。布局转换器允许您记录 LogicalOperationStack 的顶部或整个 LogicalOperationStack。
第二个允许您记录通过 CallContext.LogicalSetData 存储的信息。正如所写,它使用 CallContext.LogicalGetData 使用固定名称提取值。可以轻松修改它以使用 Options 属性(如 LogicalOperationStack 转换器中所示)来指定要使用 CallContext.LogicalGetData 提取的特定值。
以下是如何配置:
这是我的测试代码:
这是输出:
当我进行此测试(以及我一直在进行的其他一些测试)时,我创建了自己的“上下文”堆栈对象(类似于 log4net 的“堆栈”)通过 CallContext.LogicalSetData 而不是通过 CallContext.SetData (这就是 log4net 存储它的方式)来存储我的堆栈。我发现当我有多个 ThreadPool 线程时,我的堆栈变得混乱。也许是因为当子上下文退出时将数据合并回父上下文。我没想到会出现这种情况,因为在我的测试中,我在进入 ThreadPool 线程时显式地将新值推送到线程池线程,并在退出时将其弹出。使用基于 Trace.CorrelationManager.LogicalOperationStack 的实现(我在其上编写了一个抽象)进行的类似测试似乎表现正确。我想也许自动流动(向下和向后)逻辑正在考虑 CorrelationManager,因为它是系统中的“已知”对象???
输出中需要注意的一些事项:
Trace.CorrelationManager 信息通过 CallContext.LogicalSetData 存储,因此它“流”到子线程。 TestThis 使用 Trace.CorrelationManager.StartLogicalOperation 将逻辑操作(根据传入的名称命名)“推送”到 LogicalOperationStack 上。 ThreadPool线程中的第一个logger.Debug语句显示ThreadPool线程继承了与父线程相同的LogicalOperationStack。在 ThreadPool 线程内部,我启动一个新的逻辑操作,该操作堆叠到继承的 LogicalOperationStack 上。您可以在第二个 logger.Debug 输出中看到结果。最后,在离开之前,我停止了逻辑运算。第三个 logger.Debug 输出显示。
从输出中可以看到,CallContext.LogicalSetData 也“流”到了子线程。在我的测试代码中,我选择在 ThreadPool 线程内的 LogicalSetData 中设置一个新值,然后在离开之前清理它 (FreeNamedDataSlot)。
请随意尝试这些图案布局转换器,看看是否可以获得您正在寻找的结果。正如我所演示的,您至少应该能够在日志输出中反映哪些 ThreadPool 线程是由哪些其他(父?)线程启动/使用的。
请注意,即使在某些环境中使用 CallContext.LogicalSetData 也存在一些问题:
“子”逻辑数据被合并回“父”逻辑数据:
EndInvoke 更改当前 CallContext - 为什么?
嵌套多线程操作跟踪
(不是问题,但关于 Trace.CorrelationManager.ActivityId 和任务并行库的好文章):
任务并行库中的任务如何影响 ActivityID?
有关 ASP.Net 上下文中各种“上下文”存储机制问题的链接博客文章
http://piers7.blogspot.com/2005/11/threadstatic-callcontext-and_02.html
[编辑]
我发现在大量(或者甚至可能不那么严重 - 我的测试使用各种线程/任务/并行技术执行 DoLongRunningWork),使用线程可能会导致 CallContext.LogicalSetData 的某些数据集不正常。
请参阅有关使用 Trace.CorrelationManager.ActivityId 的问题 在 StackOverflow 上。我发布了有关使用 Trace.CorrelationManager.LogicalOperationStack 的答案以及我的一些观察结果。
后来我用我对这个问题的回答作为我自己的问题的基础在线程/任务/并行上下文中使用 Trace.CorrelationManager.LogicalOperationStack。
我还发布了非常相似Microsoft 并行扩展论坛上的问题。
你可以阅读这些帖子看看我的观察。简单总结一下:
使用这样的代码模式:
无论 DoLongRunningWork 是由显式 Thread/ThreadPool 线程/Tasks/Parallel(.For 或 .Invoke) 启动,LogicalOperationStack 的内容都保持一致。
使用如下代码模式:
LogicalOperationStack 的内容保持一致,除非 DoLongRunningWork 由 Parallel.For 或 Parallel.Invoke 启动。原因似乎与 Parallel.For 和 Parallel.Invoke 使用主线程作为执行并行操作的线程之一有关。
这意味着,如果您想将整个并行(或线程)操作作为单个逻辑操作,并将每次迭代(即委托的每次调用)作为嵌套在外部操作中的逻辑操作,我测试的大多数技术(线程/线程池/任务)正常工作。在每次迭代时,LogicalOperationStack 都会反映出存在一个外部任务(用于主线程)和一个内部任务(委托)。
如果您使用 Parallel.For 或 Parallel.Invoke,LogicalOperationStack 将无法正常工作。在上面链接的帖子的示例代码中,LogicalOperationStack 不应有超过 2 个条目。一个用于主线程,一个用于委托。当使用 Parallel.For 或 Parallel.Invoke 时,LogicalOperationStack 最终将获得超过 2 个条目。
使用 CallContext.LogicalSetData 的效果甚至更糟(至少如果尝试通过使用 LogicalSetData 存储 Stack 来模拟 LogicalOperationStack 的话)。使用与上面类似的调用模式(具有封闭逻辑操作和委托逻辑操作的调用模式),用 LogicalSetData 存储并以相同方式维护的堆栈(据我所知)几乎在所有情况下都会被损坏。
CallContext.LogicalSetData 对于更简单的类型或未在“逻辑线程”内修改的类型可能效果更好。如果我用 LogicalSetData 存储值字典(类似于 log4net.LogicalThreadContext.Properties),它可能会被子线程/任务/等成功继承。
我没有任何很好的解释来解释为什么会发生这种情况,也没有最好的解决方法。我测试“上下文”的方式可能有点过分,也可能没有。
如果您确实对此进行了更多研究,您可以尝试我在上面的链接中发布的测试程序。测试程序仅测试LogicalOperationStack。我通过创建支持 IContextStack 等接口的上下文抽象,使用更复杂的代码执行了类似的测试。一种实现使用通过 CallContext.LogicalSetData 存储的 Stack(类似于 log4net 的 LogicalThreadContext.Stacks 的存储方式,除了我使用 LogicalSetData 而不是 SetData)。另一个实现通过 Trace.CorrelationManager.LogicalOperationStack 实现该接口。这使我可以轻松地使用不同的上下文实现运行相同的测试。
这是我的 IContextStack 接口:
这是基于 LogicalOperationStack 的实现:
这是基于 CallContext.LogicalSetData 的实现:
这是两者使用的 StackPopper:
需要消化的内容很多,但也许您会发现其中一些有用!
UPDATE:
12/11/2014 - See the first part of my post here:
What is the difference between log4net.ThreadContext and log4net.LogicalThreadContext?
for a recent update. Log4Net's LogicalThreadContext has been updated somewhat recently (in the last couple of years) so that it works correctly now. The updated in the linked post gives some details.
END UPDATE.
Here is an idea that might help you. Part of the problem is that the log4net context objects (ThreadContext and LogicalThreadContext) do not "flow" their properties to "child" threads. LogicalThreadContext gives the false impression that it does, but it doesn't. Internally it uses CallContext.SetData to store its properties. Data set via SetData is attached TO THE THREAD, but it is NOT "inherited" by child threads. So, if you set a property via like this:
That property will be loggable via the %property pattern converter and will contain a value when logging from the same thread where you set the property in the first place, but it will not contain a value in any child threads that are spawned from that thread.
If you could save your properties via CallContext.LogicalSetData (see link above), then the properties are "flowed" to (or inherited by) any child threads. So, if you could do something like this:
Then "MyLogicalData" will be available in the thread where you set it as well as in any child threads.
See this blog posting by Jeffrey Richter for more info on using CallContext.LogicalSetData.
You can easily store your own information via CallContext.LogicalSetData AND have it available for logging by log4net by writing your own PatternLayoutConverter. I have attached some sample code for two new PatternLayoutConverters.
The first one allows you to log information stored in the Trace.CorrelationManager's LogicalOperationStack. The layout converter allows you to log the top of the LogicalOperationStack or the entire LogicalOperationStack.
The second one allows you to log information stored via CallContext.LogicalSetData. As written, it pulls a value using CallContext.LogicalGetData using a fixed name. It could easily be modified to use the Options property (as demonstrated in the LogicalOperationStack converter) to specify a particular value to pull using CallContext.LogicalGetData.
Here is how to configure:
Here is my test code:
Here is the output:
When I did this test (and some other testing I have been working on), I created my own "context" stack object (similar to log4net's "stack" implementation) by storing my Stack via CallContext.LogicalSetData rather than via CallContext.SetData (which is how log4net stores it). I found that my stack got jumbled up when I had several ThreadPool threads. Maybe it was from merging the data back to the parent context when the child context exited. I would not have thought that would be the case as in my test I explicitly pushed the new value on entry to the ThreadPool thread and popped it on exit. A similar test with a Trace.CorrelationManager.LogicalOperationStack-based implementation (I wrote an abstraction over it) seemed to behave correctly. I guess that maybe the automatic flowing (down AND back) logic is accounting for CorrelationManager since it is a "known" object in the system???
Some things to note in the output:
Trace.CorrelationManager inforamation is stored via CallContext.LogicalSetData so it is "flowed" to child threads. TestThis uses Trace.CorrelationManager.StartLogicalOperation to "push" a logical operation (named for the passed in name) onto the LogicalOperationStack. The first logger.Debug statement in the ThreadPool thread shows that the ThreadPool thread has inherited the same LogicalOperationStack as the parent thread. Inside of the ThreadPool thread, I start a new logical operation which is stacked onto the inherited LogicalOperationStack. You can see the result of that in the second logger.Debug output. Finally, before leaving, I stop the logical operation. The third logger.Debug output shows that.
As you can see from the output, CallContext.LogicalSetData is also "flowed" to child threads. In my test code I chose to set a new value into LogicalSetData inside the ThreadPool thread and then clean it up before leaving (FreeNamedDataSlot).
Feel free to try these pattern layout converters and see if you can achieve the results that you are looking for. As I have demonstrated, you should at least be able to reflect in your logging output which ThreadPool threads were started/used by which other (parent?) threads.
Note there are some issues even with CallContext.LogicalSetData in certain environments:
"Child" logical data is merged back into "Parent" logical data:
EndInvoke changes current CallContext - why?
Nested multithread operations tracing
(Not an issue, but a good post about Trace.CorrelationManager.ActivityId and Task Parallel Library):
How do Tasks in the Task Parallel Library affect ActivityID?
An oft-linked blog posting about problems with various "context" storage mechanisms in the context of ASP.Net
http://piers7.blogspot.com/2005/11/threadstatic-callcontext-and_02.html
[EDIT]
I have found that maintaining the correct context while heavily (or maybe even not so heavily - my test executes DoLongRunningWork using various Thread/Task/Parallel techniques) using threads can throw some data set with CallContext.LogicalSetData out of whack.
See this question about using Trace.CorrelationManager.ActivityId here on StackOverflow. I posted an answer about using Trace.CorrelationManager.LogicalOperationStack and some of my observations.
Later I used my answer to that question as the basis for my own question about using Trace.CorrelationManager.LogicalOperationStack in the context of Threads/Tasks/Parallel.
I also posted a very similar question on Microsoft's Parallel Extensions forum.
You can read those posts see my observations. To summarize briefly:
With a code pattern like this:
The contents of the LogicalOperationStack remain consistent whether DoLongRunningWork is kicked off by explicit Thread/ThreadPool threads/Tasks/Parallel(.For or .Invoke).
With a code pattern like this:
The contents of the LogicalOperationStack remain consistent EXCEPT when DoLongRunningWork is kicked off by Parallel.For or Parallel.Invoke. The reason seems to be related to the fact that Parallel.For and Parallel.Invoke use the main thread as one of the threads to execute the parallel operations.
That means that if you want to enclose the entire parallelized (or threaded) operation as a single logical operation and each iteration (i.e. each invocation of the delegate) as a logical operation nested within the outer operation, most techiques that I tested (Thread/ThreadPool/Task) work correctly. At each iteration, the LogicalOperationStack reflects that there is an outer task (for the main thread) and an inner task (the delegate).
If you use Parallel.For or Parallel.Invoke, the LogicalOperationStack does not work correctly. In the sample code in the posts that linked above, the LogicalOperationStack should never have more than 2 entries. One for the main thread and one for the delegate. When using Parallel.For or Parallel.Invoke the LogicalOperationStack will eventually get many more than 2 entries.
Using CallContext.LogicalSetData fares even worse (at least if trying to emulate LogicalOperationStack by storing a Stack with LogicalSetData). With a similar call pattern as above (the one with the enclosing logical operation as well as the delegate logical operation), a Stack stored with LogicalSetData and maintained identically (as far as I can tell) will get corrupted in almost all cases.
CallContext.LogicalSetData might work better for simpler types or for types that are not modified within the "logical thread". If I were to store a dictionary of values with LogicalSetData (similar to log4net.LogicalThreadContext.Properties), it would probably be inherited successfully by child threads/Tasks/etc.
I don't have any great explanations for exactly why this is happening or the best way to work around it. It might be that the way I was testing "context" went a little overboard, or it might not.
If you do look into this some more, you might try out the test programs that I posted in the links above. The test programs only test the LogicalOperationStack. I have performed similar tests with more complicated code by making a context abstraction that supports an interface like IContextStack. One implementation uses a Stack stored via CallContext.LogicalSetData (similar to how log4net's LogicalThreadContext.Stacks is stored, except that I used LogicalSetData rather than SetData). The other implementation implements that interface over Trace.CorrelationManager.LogicalOperationStack. That makes it easy for me to run the same tests with different context implementations.
Here is my IContextStack interface:
Here is the LogicalOperationStack-based implementaiton:
Here is the CallContext.LogicalSetData-based implementation:
Here is the StackPopper used by both:
It's a lot to digest, but maybe you will find some of this useful!
log4net 中的上下文信息是针对每个线程的,因此每次启动新线程时,都必须向其中添加上下文信息。您可以使用属性,也可以使用 NDC。 NDC 也是针对每个线程的,因此您仍然需要在某个时刻将其添加到每个线程的上下文中,这可能是也可能不是您正在寻找的内容。不过,它可以让您免于将其添加到消息本身中。在您的示例中,它会是这样的:
这是 文档的链接对于 NDC。
总而言之,效果与使用属性类似,就像示例中的效果一样。唯一的区别是 NDC 可以堆叠,以便每次将值压入堆栈时,它都会连接到消息。它还支持 using 语句,这使得代码更清晰。
The context information in log4net is per thread, so every time you start a new thread you have to add your context information to it. You can use properties, or you can use the NDC. The NDC is per thread too, so you would still have to add it to each thread's context at some point, which might or might not be what you're looking for. It would save you from adding it to the message itself though. In your example it would be something like this:
Here's a link to the documentation for NDC.
All in all the effect is similar to using properties, just like what you have in your example. The only difference is that the NDC can be stacked so that every time that you push a value on the stack it will get concatenated to the message. It also supports the using statement, which makes for cleaner code.
从我的角度来看,唯一的可能性是更改模块内的线程创建,否则您无法添加任何相关上下文。
如果您可以更改代码,那么您将创建一个从所使用的 System.Threading 类(例如示例中的 Thread)继承的类,并调用超类并添加相关的日志记录上下文。
还有一些其他可能的技巧,但这将是一个干净的方法,没有任何肮脏的技巧。
From my pov the only possibility would be to change the thread creation inside the modules as otherwise you can not add any relevant context.
If you can change the code, then you would create a class which would inherit from the used System.Threading class (e.g. Thread in your example) and would call the super class and add the relevant logging context.
There are some other tricks possible, but this would be a clean approach without any dirty tricks.
一种选择是代替单个静态记录器实例,您可以通过使用 ThreadStatic 属性并在属性 getter 中初始化。然后,您可以将上下文添加到记录器,一旦设置了上下文,它将应用于每个日志条目。
但是,您仍然会遇到在每个线程中设置上下文的问题。为此,我建议抽象记录器的创建。使用工厂方法并需要调用 CreateLogger() 来检索记录器的实例。在工厂内,使用 ThreadStatic 并在初始化记录器时设置 ThreadContext 属性。
它确实需要一些代码修改,但不需要太多。
更精细的选择是使用 AOP(面向方面编程)框架,例如 LinFu 从外部注入您想要的日志记录行为。
One option is instead of a single static logger instance, you could create one for each thread by marking it with the ThreadStatic attribute and initializing in a property getter. Then you can add your context to the logger and it will be applied to each log entry once the context is set.
However, you'd still have the issue of getting the context set within each thread. For that, I recommend abstracting the creation of your loggers. Use a factory method and require a call to CreateLogger() to retrieve an instance of the logger. Within the factory, use ThreadStatic and set the ThreadContext property when a logger is initialized.
It does require a little code modification, but not a ton.
A more elaborate option is to use an AOP (Aspect Oriented Programming) framework such as LinFu to inject your desired logging behavior externally.