I configured my project to use Serilog for logging using dependecy injection.
I use the following schema in the classes constructor:
namespace FlickPopper.API.Controllers {
public class ValuesController : Controller {
private ILogger<ValuesController> _logger;
public MyClass(ILogger<ValuesController> logger) {
_logger = logger;
}
}
}
In this way, serilog creates the logger calling to Log.ForContext<classname>
My settings are:
"Serilog": {
"WriteTo": [
{
"Name": "RollingFile",
"Args": {
"pathFormat": "Logs\\log-{Date}.txt",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext" ]
}
So, the logs seems like this:
2018-01-26 22:20:08 [INF] [FlickPopper.API.Controllers.ValuesController] Get all values
It's any way to format the SourceContext property to show only the assembly name in the logs, somthing like this?
2018-01-26 22:20:08 [INF] [FlickPopper.API] Get all values
When you use injected ILogger<T>, under the hood Serilog logger is created by SerilogLoggerProvider that implements ILoggerProvider interface. This interface has the only method:
public interface ILoggerProvider : IDisposable
{
ILogger CreateLogger(string categoryName);
}
The passed categoryName is used as value for {SourceContext} property in message format. And ASP.NET Core passes it as fully-qualified name (e.g. FlickPopper.API.Controllers.ValuesController).
So this string value should be fixed not in Serilog code or configuration, but in ASP.NET Core logging infrastructure.
The responsible class for creation of that value in first place is Logger<T> class. Instances of Logger<T> are instantiated when you inject ILogger<T> into your classes. Here is source code of its constructor:
public class Logger<T> : ILogger<T>
{
public Logger(ILoggerFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
_logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T)));
}
// ...
}
That call to TypeNameHelper.GetTypeDisplayName(typeof(T)) returns fully-qualified name that then is passed to ILoggerFactory and eventually to SerilogLoggerProvider.
So if you want to change that behavior and adjust the categoryName passed to ILoggerFactory, you should have your own implementation of ILogger<T> that makes required call to ILoggerFactory.CreateLogger(). It's not that difficult, because Logger<T> class is very thin and is based on Microsoft.Extensions.Logging.Logger implementation. Here is the class that is a copy of Logger<T> except for one line producing logger category name:
public class LoggerEx<T> : ILogger<T>
{
private readonly ILogger _logger;
public LoggerEx(ILoggerFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
_logger = factory.CreateLogger(typeof(T).Assembly.GetName().Name);
}
IDisposable ILogger.BeginScope<TState>(TState state)
{
return _logger.BeginScope(state);
}
bool ILogger.IsEnabled(LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
You should also replace the standard implementation of ILogger<T> with this one in your services registration:
services.AddSingleton(typeof(ILogger<>), typeof(LoggerEx<>));
Now the instances of LoggerEx<T> will be injected to the controllers and {SourceContext} will have a value you built:
2018-01-27 09:54:21 [INF] [TestProject.TestApplication] Hello!
I had a similar problem, except I wanted to include only the class/interface name.
I solved this by creating a custom enricher.
// Create logger
var logger = new LoggerConfiguration()
// ...
.Enrich.With(new SourceContextEnricher())
// ...
.CreateLogger();
public class SourceContextEnricher : ILogEventEnricher {
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) {
if (logEvent.Properties.TryGetValue("SourceContext", out var property)) {
var scalarValue = property as ScalarValue;
var value = scalarValue?.Value as string;
if (value?.StartsWith("FlickPopper.API.") ?? false) {
var lastElement = value.Split(".").LastOrDefault();
if (!string.IsNullOrWhiteSpace(lastElement)) {
logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(lastElement)));
}
}
}
}
}
Related
In .NET5/.NET6 we typically get our services from the ServiceProvider directly.
But in some cases this is not possible and we need to create a service factory. We inject the service factory into our services and then, within the service, the service factory creates instances on demand. A public example would be HttpClientFactory (for demonstrating the thought).
The usual use case would be if a service needs to have multiple instances of a service and the instances need to be (pre)configured within the using service.
My way to implement this looks as follows:
// appsettings.json
"MyServiceFactory": {
"Instances": {
"Instance1": {
"Option1": "Hi",
"Option2": "there"
},
"Instance2": {
"Option1": "Thanks",
"Option2": "for helping"
}
}
}
// the options class to bind
public class MyServiceFactoryOptions
{
public Dictionary<string, MyServiceOptions> Instances { get; set; } = new Dictionary<string, MyServiceOptions>();
}
// the factory implementation
public class MyServiceFactory : IMyServiceFactory
{
public readonly ILoggerFactory LoggerFactory;
public readonly ILogger<MyServiceFactory> Logger;
public readonly MyServiceFactoryOptions Options;
public MyServiceFactory(ILogger<MyServiceFactory> logger, IOptions<MyServiceFactoryOptions> options, ILoggerFactory loggerFactory)
{
LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Options = options.Value;
}
public IMyService CreateInstance(string name)
{
if (Options.Instances.TryGetValue(name, out MyServiceOptions options))
{
ILogger<MyService> logger = LoggerFactory.CreateLogger<MyService>();
IOptions<MyService> iOptions = Microsoft.Extensions.Options.Options.Create(options);
return new MyService(logger, iOptions);
}
else
{
throw new Exception($"Config is missing instance configuration for {name}");
}
}
}
//the service registration
services
.AddOptions<MyServiceFactoryOptions>()
.BindConfiguration("MyServiceFactory");
services.TryAddTransient<MyServiceFactory>();
And the CreateInstance(string name) method actually works creating the instances with the given configuration as desired.
The problem rises when MyService needs to be disposed when the ASP.NET server is closed.
Typically
the (my) service implements IDisposable
the instance is created via the ServiceProvider
the instance is injected via the constructor
Dispose get's called when the server is shut down
In my implementation Dispose seems to be never called.
A hint why that my could be comes from Andrew Locks's arcticle - Four ways to dispose IDisposable.
As I understand it right now the problem is that the instances are not created via the ServiceProvider and therefor Dispose is not called.
Am I right and is there a way to get Dispose called?
I have defined services in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
string connectionString = Configuration.GetConnectionString("DbConnection");
services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
services.AddControllersWithViews();
services.AddScoped<IAdsService, AdsService>();
services.AddScoped<ILogger, ConsoleLogger>();
services.AddScoped<ILogger, FileLogger>();
services.AddScoped<IAdsRepository, AdsRepository>();
}
This is my implementation of Logger:
public class ConsoleLogger: ILogger
{
public void Log(LogData data) => Console.WriteLine(data.ToString());
}
public class FileLogger : ILogger
{
private string Path = "logs";
public void Log(LogData data)
{
if (!Directory.Exists(Path))
{
DirectoryInfo di = Directory.CreateDirectory(Path);
}
File.AppendAllText(Path + "/logs.txt", data.ToString());
}
}
I want to use different loggers for different controllers:
e.g.
private readonly ILogger _logger;
private readonly IAdsService _adsService;
public AdController(IAdsService adsService, ILogger logger)
{
_adsService = adsService;
_logger = logger;
}
However it only takes FileLogger - how to specify my controller to use ConsoleLogger in Startup?
What you are looking for is a feature called context-based injection, which is something that is not easily implemented with MS.DI. That's not to say it's impossible, but depending on your needs, it might require a lot of configuration.
One way to achieve this, though, it by configuring any component that requires an alternative logger explicitly using a lambda using the ActivatorUtilities class. Here's an example:
private static void AddServices(IServiceCollection services)
{
// The default logger
services.AddScoped<ILogger, FileLogger>();
// Additional 'alternative' loggers
services.AddScoped<ConsoleLogger>();
// Configure a component that requires an alternative logger
services.AddTransient<AdController>(c =>
ActivatorUtilities.CreateInstance<AdController>(c,
c.GetRequiredService<ConsoleLogger>()));
}
In this example:
FileLogger is registered as ILogger allowing any 'normal' component that depends on ILogger to get injected with FileLogger.
ConsoleLogger is registered as itself, allowing it to be requested als alternative logger
AdController is registered using the ActivatorUtilities so that ActivatorUtilities is responsible for creating a new AdController where a resolved ConsoleLogger is supplied to ActivatorUtilities. This allows ActivatorUtilities to supply ConsoleLogger to the first constructor parameter that is assignable from ConsoleLogger. This basically means that ConsoleLogger is supplied to the ILogger argument of AdController.
To test this code, try this:
public interface IAdsService { }
public class AdsService : IAdsService { }
public interface ILogger { }
public class ConsoleLogger : ILogger { }
public class FileLogger : ILogger { }
public class AdController
{
public AdController(IAdsService adsService, ILogger logger) => this.Logger = logger;
public ILogger Logger { get; }
}
class Program
{
static void Main(string[] args)
{
var services = new ServiceCollection();
services.AddTransient<IAdsService, AdsService>();
AddServices(services);
var provider = services.BuildServiceProvider();
using (var scope = provider.CreateScope())
{
var controller =
scope.ServiceProvider.GetRequiredService<AdController>();
Console.WriteLine(controller.Logger.GetType().Name);
}
}
}
There are several downsides to this approach, the most important being that this solution might not scale well. There is no good way to take a more convention-based approach where you say "use ConsoleLogger for any component that follows the following definition X". You must specify each component that uses an alternative logger explicitly.
If these limitations cause maintainability issues, try using a different DI Container that natively supports this feature.
Essentially, there are two different ways I get ILogger instances. One works perfectly fine, the other doesn't.
I have an Azure Function like this:
class AzureFunctionClass {
private readonly ISomeClass _someclass;
public AzureFunctionClass(ISomeClass someClass){
_someclass = someClass;
}
public Task<IActionResult> AzureFunction(ILogger log){
log.LogInformation("This works, I see this message when run");
_someclass.ExecuteMethod();
}
}
Another class, not containing Azure functions, like this:
class SomeClass : ISomeClass {
private readonly ILogger<SomeClass> _log;
public SomeClass(ILogger log){
_log = log;
}
public void ExecuteMethod(){
_log.LogInformation("This doesn't crash so _log isn't null, but it
doesn't write anything");
}
}
Startup.cs:
class Startup : IWebJobsStartup {
public void Configure(IWebJobsBuilder builder){
builder.Services.AddScoped<ISomeClass, SomeClass>();
builder.Services.AddTransient(typeof(ILogger<>), typeof(Logger<>));
builder.Services.AddScoped<ILogger<SomeClass>, Logger<SomeClass>>();
}
}
And no, I'm afraid that AzureFunctionClass cannot just pass its ILogger instance to ISomeClass as a parameter.
I've also looked everywhere for log files, such as in Azure Storage Explorer, to see if it's possibly just not writing to the Azure Portal console. Every log file I found had logs for the working case described above, and none of them had logs for the other case.
Current syntax shown has some issues with this injected dependencies.
class SomeClass : ISomeClass {
private readonly ILogger _log;
public SomeClass(ILogger<SomeClass> log) {
_log = log;
}
public void ExecuteMethod() {
_log.LogInformation("This doesn't crash so _log isn't null, but it doesn't write anything");
}
}
Second issue is that logging is added by default and your manually added settings are overriding the default setup.
class Startup : FunctionsStartup {
public override void Configure(IFunctionsHostBuilder builder) {
builder.Services.AddScoped<AzureFunctionClass>();
builder.Services.AddScoped<ISomeClass, SomeClass>();
//...
}
}
Technically all you needed to add was your function class and its dependencies.
Reference Use dependency injection in .NET Azure Functions
At present, the function runtime has a bug due to which it filters out any log that is created with a category that doesn't start with string Function..
See these GitHub issues:
#4425 - ILogger is not injected when using new DI functionality
#4345 - Remove filters for ILoggers created by customer DI
The logger injected in the function method is done by the function runtime which creates the logger with category set to Function.<FunctionName>.User. So this gets logged properly. But the logger that is injected into the constructor is done by the asp.net core DI framework, which sets the category name for the logger as Type.FullName (type in your example case is SomeClass). Because it's fullname doesn't start with Function, the lines logged with this category are filtered out.
There are two ways to workaround this.
Option 1: Change host.json to not filter logs from your namespace
{
"version": "2.0",
"logging": {
"logLevel": {
"<YourNameSpace>": "Information"
}
}
}
Option 2: Inject ILoggerFactory in your ctor, and create a logger with a category that won't get filtered
class SomeClass : ISomeClass {
private readonly ILogger _log;
public SomeClass(ILoggerFactory loggerFactory){ // Note that we inject ILoggerFactory
this._log = loggerFactory.CreateLogger(
LogCategories.CreateFunctionUserCategory(this.GetType().FullName)); // Must use CreateFunctionUserCategory to create the log category name otherwise the log gets filtered out.
}
public void ExecuteMethod(){
_log.LogInformation("This should get logged correctly.");
}
}
Note that, ILogger is already registered into DI framework by the function runtime (as mentioned in NKosi's answer), so those lines can be removed.
I am writing a simple Azure function.
I have installed the AzureFunctions.Autofac nuget package, and would like to use this as my DI library.
I have set up the following AutofacConfig class to register my types:
public class AutofacConfig
{
public AutofacConfig(string functionName)
{
DependencyInjection.Initialize(builder =>
{
//do all of you initialization here
//db client
builder.RegisterType<EventComponent>()
.As<IComponent<EventModel>>().SingleInstance();
}, functionName);
}
}
Here is my EventComponent class, to which I would like to inject the ILogger instance provided.
public class EventComponent : IComponent<EventModel>
{
private ILogger _log;
public EventComponent(ILogger logger)
{
_log = logger;
}
}
Here is how I inject my EventComponent:
[FunctionName("AddEvent")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, ILogger log, [Inject]IComponent<EventModel> component)
{
log.LogInformation("C# HTTP trigger function processed a request.");
await component.Add(new EventModel() { Id = Guid.NewGuid(), Description = $"Test description nr: {new Random().Next(1, 100000)}", User = "Test User" });
return req.CreateResponse(HttpStatusCode.OK);
}
The problem is, I get an exception on the above, because Autofac cannot resolve the parameter Microsoft.Extensions.Logging.ILogger.
Here is the exception message:
Exception binding parameter 'component'... Cannot resolve parameter 'Microsoft.Extensions.Logging.ILogger logger' of constructor 'Void .ctor(Microsoft.Extensions.Logging.ILogger)'. (See inner exception for details.) -> None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'Event.Function.Components.EventComponent' can be invoked with the available services and parameters:\r\nCannot resolve parameter 'Microsoft.Extensions.Logging.ILogger logger' of constructor 'Void .ctor(Microsoft.Extensions.Logging.ILogger)'.",
How can I inject the ILogger instance into my EventComponent class?
In Azure Functions V2, the ILogger is injected by default. Also, here are two very nice articles on dependency inject in Azure Functions.
https://blog.mexia.com.au/dependency-injections-on-azure-functions-v2
and http://codingsoul.de/2018/01/19/azure-function-dependency-injection-with-autofac/
I found your question when looking for the same thing. Have you found a solution?
Because I don't think that is possible. ILogger log is injected by the framework and I don't see how it could be referenced from your AutofacConfig-class.
How I resolved this was by changing the EventComponent-class to use Setter-injection instead of Constructor-injection, like this:
public class EventComponent : IComponent<EventModel>
{
public ILogger Log { get; set; }
}
and change your function to set the Log-property:
[FunctionName("AddEvent")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, ILogger log, [Inject]IComponent<EventModel> component)
{
log.LogInformation("C# HTTP trigger function processed a request.");
component.Log = log;
await component.Add(new EventModel() { Id = Guid.NewGuid(), Description = $"Test description nr: {new Random().Next(1, 100000)}", User = "Test User" });
return req.CreateResponse(HttpStatusCode.OK);
}
The downside is that you need to remember to set that value at the start of every function that uses that class, but the injection works.
If you want to inject the ILogger into a function app you need to do the following:
Add the correct log level and namespace to your host.json
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true
}
},
"logLevel": {
"YourNameSpace": "Information"
}
Inject ILogger<T> where T is your function app class name/type. In this sample my function app class name is Api.
public class TestService : ITestService
{
private readonly ILogger<Api> _logger;
public TestService(ILogger<Api> logger)
{
_logger = logger;
}
public void LogSomething(string message)
{
_logger.LogInformation(message);
}
}
Given several types of class constructor injection, i.e.;
public class DataService :IDataService
{
public DataService(ILogger logger) { ... }
}
and,
public class Logger
{
public Logger(IDataService service) { ... }
}
or, should I do this instead;
public class DataService : IDataService, ILogger, IDisposable
{
public DataService() { ... }
}
However, I actually don't like to do this on every repository classes or other classes that needs data services and logging at the same time;
public class SomeRepository : IRepostiory
{
public SomeRepository (IDataService service, ILogger logger) { ... }
}
I'm fine with this model;
public interface IRepository : ILogger { ... }
or,
public interface IDataService : ILogger { ... }
Which one is preferred as best practice design? Also, how do we determine if we would like to log the data service process, and at the same time we also would like to log other components that injected ILogger services or using repository?
This is a tongue in cheek answer, but there is no "right" way to implement ILogger. You should probably use the existing framework to construct an ILogger, or hand it over to NLog, log4net, or Serilog. You shouldn't directly implement ILogger unless you plan to build your own logging framework. You should use the logging libary's extension methods to wireup the ILogger Here is an article about this.
Here is a minimal example of how to get console logging up and running:
var hostBuilder = Host.CreateDefaultBuilder().
ConfigureLogging((builderContext, loggingBuilder) =>
{
loggingBuilder.AddConsole((options) =>
{
//This displays arguments from the scope
options.IncludeScopes = true;
});
});
var host = hostBuilder.Build();
var logger = host.Services.GetRequiredService<ILogger<LogTest>>();
//This specifies that every time a log message is logged, the correlation id will be logged as part of it
using (logger.BeginScope("Correlation ID: {correlationID}", 123))
{
logger.LogInformation("Test");
logger.LogInformation("Test2");
}
I'll like to add that the BeginScope method could take any kind of object, not only string. You may pass a more complex object that could represent better the context/scope, like User Id, HostName, CorrelationId, Tennant Id ...
In my case where a created an external Api Logger service, and thus create my own ILogger where BeginScope with those values make more sense.
public IDisposable BeginScope<TState>(TState state)
{
_scopeContextManager = state as ScopeContextManager;
return default!;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter)
{
// use _scopeContextManager
}
I struggled for a long time before figuring this out.