Inject instance of ILogger in my component class Azure Functions using Autofac - c#

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);
}
}

Related

Azure Function, Dependency Injection, "there is no argument given that corresponds to the formal parameter", dotnet core

I am trying to call a repository class from an Azure function but I am getting an error
There is no argument given that corresponds to the formal parameter
I copied the repository class structure from a .NET Core Web API project and know this has to do with dependency injection.
The constructor of the repository class looks like this:
public CaseRepository(ILogger<CaseRepository> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
How can I pass this into the static method of an Azure Function as I do with the Web API call like this:
[FunctionName("TestFunction")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log, CaseRepository caseRepository)
{
// ...
}
You can define the dependency declaration in the Startup class as shown in this file and later instead of defining the function as static define it normal class function. In the class constructor inject the required dependency. See this for reference.
Startup class
[assembly: FunctionsStartup(typeof(YourNamespace.Startup))]
namespace YourNamespace
{
public class Startup : FunctionsStartup
{
builder.Services.AddSingleton<ICaseRepository, CaseRepository>();
}
}
Usage - here ICaseRepository is injected in the class containing the Azure functions.
public class TestFunction
{
private readonly ICaseRepository caseRepository;
public TestFunction(ICaseRepository caseRepository)
{
this.caseRepository= caseRepository;
}
[FunctionName("TestFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
// ... use caseRepository instance
}
}

Log message from Class library which is used in Azure function

In an Azure function I am able to add logs and it is added to function logs as expected. Below is sample code
[FunctionName("myfunction")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequestMessage req, ILogger log)
{
log.LogInformation("HTTP trigger function start"); // This works fine
MyClass class1 = new MyClass(); // this is from a class library referred in the function
class1.mymethod()
}
In the MyClass methods I want to log messages the same way I did in azure function. I tried creating a new log object of type Ilogger in my class library and calling LogInformation method with the required messages. But this is not working and I am getting error like "Value cannot be null. (Parameter 'logger') at Microsoft.Extensions.Logging.LoggerExtensions.Log"
So how we need to create new object of logger in class library so that it will log message to the same Azure function logs.
First, add a PackageReference for Microsoft.Extensions.Logging.Abstractions* to your class library project. Then, add a parameter to your class' constructor that accepts an ILogger (or an ILogger<YourClass> if you intend on using DI). Store the logger in a field and then use it when you need it.
public class MyClass {
private readonly ILogger _logger;
public MyClass(ILogger logger) =>
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
public void MyMethod(string name) {
_logger.LogInformation("Hello {Person}", name);
}
}
Then, pass the logger to your constructor.
FunctionName("myfunction")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequestMessage req, ILogger log) {
log.LogInformation("HTTP trigger function start");
var class1 = new MyClass(log);
class1.MyMethod("pinkfloydx33");
}
You can also make the logger optional by substituting NullLogger.Instance (or NullLogger<MyClass>.Instance) when one is not provided by the user:
public class MyClass {
private readonly ILogger _logger;
public MyClass(ILogger logger = null) =>
_logger = logger ?? NullLogger.Instance;
//....
}
Then you don't need to perform any null checks around your logging statements.
* Note that the latest version of Azure Functions only run on .NET Core 3.1. This means you can only reference the 3.x line of the Nuget package in your class library, and not the 5.x versions. While it will technically compile, the functions runtime/host will throw TypeInitializationException at runtime. Unfortunately as .NET5 is not LTS there won't be a new version of Azure Functions until .NET6. If you want to use your library from Azure Functions as well as "normal" projects targeted at .NET5, you'll have to cross-compile the library.

ILogger not Injected in Durable Functions v2.0

At the moment I'm trying to add an ILogger or ILogger<> to a Azure Durable Function so as to use logging in Activity Functions.
Logging in the Orchestration Function works fine and is injected in the method itself, but attempts at constructor injection for ILogger always results in a Null Exception.
builder.Services.AddLogging();
The above does not seem to work when added to the Startup file (Bootstrapper) and neither does variations on:
builder.Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
Anyone solved this?
Remove either of these lines from your Startup file:
builder.Services.AddLogging();
builder.Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
Then, wherever you are injecting your ILogger, add the type that your logger is being injected into using ILogger<T> i.e:
public class Function1
{
private readonly ILogger _logger;
public Function1(ILogger<Function1> logger)
{
_logger = logger;
}
[FunctionName("Function1")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
return name != null
? (ActionResult)new OkObjectResult($"Hello, {name}")
: new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}
}

Unable to use ILogger instance provided via DI

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.

serilog format SourceContext for showing only assembly name

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)));
}
}
}
}
}

Categories

Resources