AutofacOnFunctions: New logging capabilities

After having released version 1 of AutofacOnFunctions some days ago, there are some new changes and a fitting new nuget package as well.

Logging in Azure Functions

When I started with Azure Functions I was concerned about many things. I did like the approach to be able to use attributes to trigger a function, a well thought through concept. Actually the static nature of an Azure Function doesn’t make anything better. First of all, I decided to try out how it is possible to use dependency injection in Azure Functions. Without that concept being available, it would be pretty hard to write sensible, robust, maintainable and extensible code. The second thing was logging. The logging capabilities of Azure Functions are quite okay up to the point in time, when the code leaves the Azure Function itself and tries to do the same thing in other classes. From the infrastructural point of view, this is questionable. Actually asp.net core had also been built up with dependency injection and extensible logging systems in mind, so would have Azure Function. Anyway. In this release, AutofacOnFunctions solves the gap for logging functionality for Azure Functions and all other classes.

Logging to AutofacOnFunctions based on Microsoft.Extensions.Logging.Abstractions

History of logging in Azure Functions

Let’s loose some word about the history of Azure Functions logging. Dependent on the version and usage of Azure Functions there are different logging systems in place.

TraceWriter

First logging mechanism that had been introduced to Azure Functions was the TraceWriter class. Adding the appropriate parameter directly into your Azure Function will provide logging capabilities:

[pastacode lang=”javascript” manual=”public%20static%20void%20Run(Message%20message%2C%20TraceWriter%20log)%0A%7B%0A%20%20log.Info(%22Function%20invoked.%22)%3B%0A%20%20%2F%2F%20…%0A%7D” message=”” highlight=”” provider=”manual”/]

The logging information will be available in Azure Portal as well as in debugging when watching the console. The logging is available in Kudu, and when you want to watch raw data, you can check the Table Storage, where the information is persisted.

ILogger

TraceWriter is an annoying dependence for every class, which is the prerequisite for logging with TraceWriter. It is even more annoying when there is the necessity to pass it around to let also classes, that are no Azure Function, log out information.

It ain’t no fun to pass around logger instances for all classes

In current versions of Azure Functions, Microsoft.Extensions.Logging.Abstractions ILogger implementation is used to provide logging capabilities. Same procedure must be adopted to make ILogger available to an Azure Function.

[pastacode lang=”javascript” manual=”public%20static%20void%20Run(Message%20message%2C%20ILogger%20log)%0A%7B%0A%20%20log.LogInformation(%22Function%20invoked.%22)%3B%0A%20%20%2F%2F%20…%0A%7D” message=”” highlight=”” provider=”manual”/]

While this is simple, this still targets only the Azure Function itself. How can a logger instance be created per class/ type and be directly injected?

Logging methodic

As stated in earlier blogs, I do like the approach to have dependency injection handle logging instances and the injection for me. You may check these links to have a closer look on how I do prefer to implement logging :

Actually, each class should get an own instance of ILogger. at least each type. This should be done without any hurdles, meaning the logger instance can be injected by ctor injection right into the class.

Logging should be hurdle free

ILogger for everything

Have a look onto this sample:

using Microsoft.Extensions.Logging;

namespace AutofacOnFunctions.Samples.NetStandard.Services.Functions
{
    internal class TestIt : ITestIt
    {
        private readonly ILogger<TestIt> _logger;

        public TestIt(ILogger<TestIt> logger)
        {
            _logger = logger;
            _logger.LogInformation("ctor");
        }
        public string Name { get; set; }

        public string CallMe()
        {
            _logger.LogInformation("callme");
            return "Dependency Injection Test method had been called.";
        }
    }
}

The approach is moreless the same that asp.net core meanwhile does. Just an ILogger instance with a generic type that describes for which class the logger is going to be created. The generic type may look some kind of redundant, but actually it makes the whole process of logging a lot easier. In that way, the logger class does know for which class it is going to create logging messages. There is no need to pass the type on creation of the logger class.

Certainly it is possible to just define a logger without a generic type:

using Microsoft.Extensions.Logging;

namespace AutofacOnFunctions.Samples.NetStandard.Services.Functions
{
    internal class TestItByName : ITestItByName
    {
        private readonly ILogger _logger;

        public TestItByName(ILogger logger)
        {
            _logger = logger;
            _logger.LogInformation("ctor");
        }

        public string CallMe()
        {
            _logger.LogInformation("callme");
            return "Dependency Injection Test method had been called. Resolution had been done by name.";
        }
    }
}

Azure Functions does not allow for MessageFormatting. So it seems not possible to add level, type of class and probably method to the output of a log message within Azure Functions. Additionally it has some filter mechanism to only show log messages that coming out of the function itself. The filter is implemented by naming pattern of the logger class to be generated. More details on this can be read in Dependency Injection and ILogger n Azure Functions. The non generic logger instances will automatically get the right naming for the logging instances. Generic logger instances can be generated and are fully functional, but the results will not be displayed in Azure Functions logs.

However, there is currently no other mechanism available than the ILoggerFactory implementation provided by WebJobs. The logging interface is really not the worst Microsoft ever released, it provides MessageTemplates and is quite comprehensive.

[Inject] loggers

Certainly you can do the following and use the [Inject] attribute to add a logger instance to an Azure Function:

using AutofacOnFunctions.Services.Ioc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;

namespace AutofacOnFunctions.Samples.NetStandard.Services.Functions
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
            HttpRequest req, [Inject] ITestIt testit,
            [Inject] ILogger logger)
        {
            logger.LogInformation("C# HTTP trigger function processed a request.");
            return new OkObjectResult($"Hello. Dependency injection sample returns '{testit.CallMe()}'");
        }
    }
}

This instance is going to be created and published by AutofacOnFunctions and not by the Azure Functions framework, but it actually does not make any difference. It is simple less code to write and probably faster to execute, to not use the Inject attribute for injecting logger instances to an Azure Function. Just use the default way and leave it out.

Don’t use [Inject] to add loggers to Azure Functions. It does not add any value.

Logging Framework support

Anyway, I did see a lot of approaches to not use the original logger implementation brought by Microsoft.Extensions.Logging.Abstractions but any other popular framework like serilog or log4net. What I really dislike is, that the logger has to be configured and loaded inside of the Azure Function. As mentioned before, the function should not care about the environment, it should fulfill it’s job. There must be some clever hook in place to have the possibility to configure logging frameworks before a function is going to be called. This is not part of this release, but it will be one of the next tasks to do.

Support for other logging frameworks will follow.