Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Unable to create ILogger that only exists in specific IServiceScope #678

Closed
Aurel opened this issue Aug 2, 2017 · 3 comments
Closed

Unable to create ILogger that only exists in specific IServiceScope #678

Aurel opened this issue Aug 2, 2017 · 3 comments

Comments

@Aurel
Copy link

Aurel commented Aug 2, 2017

I've been working on a web app feature that does a bunch of task scheduling for operations that run on background threads. In order to do this, I've got some sort of Singleton object that contains an operation queue, and when one is done, we pop off the queue and perform the next Operation. Since an operation is async, I needed a way of having this running on a background thread, which means having a separate scope from the current HTTP request, which will end.

As such, I've been using IServiceScopeFactory in order to kick off a new application lifetime scope, as something like this:

using (var scope = _scopeFactory.CreateScope())
{
	await CurrentOperation.Invoke(scope);
}

Inside of the operations, I have logic that allows them to take the current IServiceScope, and grab whatever services they need from the current scope, specifically now that it's detached from any scope generated by an HTTP request. This works fine for pretty much everything except logging.

The dream we had was that any scoped sub-service requested down within this background task could also request a logger, and we'd be able to capture all logging events that happened only within that specific scope.

It's easy to write a custom ILogger and ILoggerProvider, but there's no current way that I can see for us to have an instance of an ILogger that knows to write to a specific operation running only within a specific IServiceScope. I've tried approaches where my ILogger is given an instance of a ServiceProvider, but that's still the top-level Service provider, and there's no way to get one for my current scope.

Since the implementation of ILoggerFactory is a singleton, and Logger just stores a mapping of string to ILogger, it seems like there is currently no clean way for someone to create an ILogger that has access to the current scope, short of doing something extremely convoluted.

I'm still using 1.1, and I know there's been some changes in the 2.0 Preview (#626) that have added some more DI capabilities, but just my initial look at the code doesn't seem to indicate that there's any way to create a logger that can react to the current scope it's being called on. Am I missing something, or is this a feature that would be useful to be implemented?

Note: I know that I can easily register a different Logging service that has nothing to do with the logger, and have it be scoped, taking in a reference to ILoggerFactory, but then I would be missing out on any other logs in services down the line that were using the existing Logging system, which is exactly what I need to capture. I need all logs that were executed within my custom scope. Additionally, since there's no HTTP request here, I can't grab the scope from an IHttpContextAccessor in a Logger. The only option was to have some form of Scoped logger, but I don't see a way for that to work with the current system.

Example

Here's a rather silly example that you wouldn't want to do, but for the sake of the example...

// This comes from DI normally but for the sake of example, let's forgive the new
IScopeServiceFactory serviceFactory = new ConcreteScopeServiceFactory();
using (var scope = serviceFactory.CreateScope())
{
	scope.ServiceProvider.GetService<MyCurrentScopeObject>().CurrentScope = "SomeId";
	scope.ServiceProvider.GetService<MyCustomService>().DoSomething();
}
public class MyCustomService
{
	public MyCustomService(RandomOtherService ros, ILoggerFactory loggerFactory)
	{
		_logger = loggerFactory.Create<MyCustomService>();
		//... save these to service.
	}
	public void DoSomething()
	{
		_logger.Log( /* Log something */);
	}
}
public class MyCurrentScopeObject 
{
	public string CurrentScope { get; set; }
	public List<string> LogList { get; set; }
}
public class MyCustomLogger : ILogger
{
	public MyCustomLogger(...) {} // This comes not from DI, but from a LoggerProvider
	public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
	{
		// if I had access to MyCurrentScopeObject I would do something with it. 
		_myCurrentScopeObject.LogList.Add( ... );
	}
}

Since the MyCustomLogger in the code above has to be created with new from an ILoggerProvider there's no way for a scoped DI object of a MyCurrentScopeObject to be passed in!

@davidfowl
Copy link
Member

You'll need to create an async local that stores your current scope and access that from the ILogger implementation. It would be the moral equivalent of the IHttpContextAccessor but for your custom scope.

@Aurel
Copy link
Author

Aurel commented Aug 16, 2017

Thanks @davidfowl - I suppose I've now got an implementation working with an AsyncLocal created right when my scope kicks off. I was hoping to avoid something like that, but, whatever - it solves the problem and does what it needs to. I suppose this can be closed, unless you all think there's value in actually thinking about a Scoped ILogger instance, which would fit in with a lot of the other moves toward being more DI-friendly that the logging code has been making.

@davidfowl
Copy link
Member

davidfowl commented Aug 16, 2017

The logger instance can't be scoped. There is no concept of a "current" scope in DI so it wouldn't really work. LoggerProviders and loggers are singletons

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants