Asynchronous logging scope in .NET Core

Viewed 2153

In .NETCore I want to support such scenario where I wrap code in some context like:

using(new LoggingContext("Method 1"))
{

}

Those contexts can also be embedded in one another

using(new LoggingContext("Method 1"))
{
   Method2();
}

void Method2() 
{
   using(new LoggingContext("Method 2"))
   {
      logger.Log("ERROR")
   }
}

After calling logger.Log("ERROR") I want it to log passed message along with those stacked contexts identifiers like:

Method 1 > Method 2 > ERROR

It is very easy in single thread scenario - it requires a globally accessible stack of contexts. In .NET Core it is for example some public static AsyncLocal<Stack<LoggingContext>>.

However I don't know what is the best way to achieve similar behavior in multithreaded scenario like this:

using(LoggingContext("Method 1"))
{
    Task.WaitAll(
       new Task(() => Method("Method A", "A"), 
       new Task(() => Method("Method B", "B")))
}

void Method(string contextName, string message)
{
   using(new LoggingContext(contextName)
   {
       _logger.Log(message)
   }
}

The desired log would look like this:

Method 1 > Method A > A
Method 1 > Method B > B

Of course one task can start in another task and situation gets even more complicated.

One more thing to note: For some legacy reasons it is impossible to pass dependency everywhere I create LoggingContext it must be some static solution.

The only idea I can think of is to use ExecutionContext.SupressFlow whenever I create a new Task and copy current stack to the new AsyncLocal object but is it a good solution? Messing with execution context seems a bit unsafe for me.

1 Answers

One option would be to manually clone or extend your parent logging context when you create a new task:

using(var parentContext = LoggingContext("Method 1"))
{
    Task.WaitAll(
       new Task(() => {
          using(parentContext.Clone()) 
          {
              Method("Method A", "A");
          }
       }), 
       ...
}

It's not pretty, but you could write some helper methods to handle the messy bits. For example:

using(var parentContext = LoggingContext("Method 1"))
{
    Task.WaitAll(
       parentContext.StartTask(() => Method("Method A", "A")),
       parentContext.StartTask(() => Method("Method B", "B")));
}
Related