Quantcast
Channel: logging – .NET Liberty
Viewing all articles
Browse latest Browse all 4

ASP.NET 5 Why Use ILogger.IsEnabled

$
0
0

ASP.NET 5 comes with a new logging framework that integrates nicely with the dependency injection system. We can request

ILogger
instances in components like our controllers and ASP.NET 5 will handle creating and injecting these logger instances for us. For more information on how the new logging framework basics, check out my post here.

With such rich logging features integrated and ready to use it can be tempting to start logging out all kinds of bits of information. In my experience it has been hard to add too much logging: when debugging an issue having more logs is better than having not enough.

main-image

Luckily the new logging framework allows us to create log entries at different levels (

Debug
,
Verbose
,
Information
,
Warning
,
Error
, and
Critical
). This means I can go wild logging all kinds of useful information throughout my application, then I can adjust the output logging level in my
Startup.cs
to tailor to my current needs. Perhaps when running in Development I’d want to see all kinds of verbose messages, but in Production I’d want to see only warnings, errors, and critical messages.

It Can Be Expensive

This is all great, but there’s an important aspect of all this logging to consider: constructing log messages can sometimes be an expensive operation. For example, I may want to output a debug log entry that shows detailed user information that I had to fetch from a database or other service. If I’m only making this extra call just to construct a Debug log message, it would be wasteful to make that call when I’m running in Production where I don’t even have Debug logs enabled. Lets look at a concrete example.

Lets say I have a

UserInfoService
that will look up a user ID for me when I give it a username:
using System.Threading;

namespace ConditionalLogging.Services
{
    public class UserInfoService : IUserInfoService
    {
        public string GetUserId(string userName)
        {
            // Simulate some expensive operation
            Thread.Sleep(500);
            return "some-user-id";
        }
    }
}

In this example I’m using

Thread.Sleep
to simulate an expensive operation like communicating with a database or service. Now lets say I wanted to log out a user ID in my
HomeController
as a
Debug
level log entry:
using Microsoft.AspNet.Mvc;
using ConditionalLogging.Services;
using Microsoft.Framework.Logging;

namespace ConditionalLogging.Controllers
{
    public class HomeController : Controller
    {
        [FromServices]
        public IUserInfoService UserInfoService { get; set; }

        [FromServices]
        public ILogger<HomeController> Logger { get; set; }

        public IActionResult Index()
        {
            var userName = ActionContext.HttpContext.User.Identity.Name;
            Logger.LogDebug("User with ID {userId} requested Index.", UserInfoService.GetUserId(userName));
            return View();
        }
    }
}

This works fine when the log level is set to

Debug
. What happens if I adjust the log level (in
Startup.cs
) to be
Information
? My
Index
action will make the call to
GetUserId
, which will take 500 ms, then the 
ILogger
will ignore the log entry that I spent so much time preparing.

This should illustrate the problem quite well. For this reason most logging frameworks include some functionality to check whether a given log level is enabled or not, allowing us to check before spending time computing an expensive log entry. ASP.NET 5 is no exception. Before writing to the logger, I can call the

IsEnabled
method and check beforehand:
using Microsoft.AspNet.Mvc;
using ConditionalLogging.Services;
using Microsoft.Framework.Logging;

namespace ConditionalLogging.Controllers
{
    public class HomeController : Controller
    {
        [FromServices]
        public IUserInfoService UserInfoService { get; set; }

        [FromServices]
        public ILogger<HomeController> Logger { get; set; }

        public IActionResult Index()
        {
            var userName = ActionContext.HttpContext.User.Identity.Name;
            if (Logger.IsEnabled(LogLevel.Debug))
            {
                Logger.LogDebug("User with ID {userId} requested Index.", UserInfoService.GetUserId(userName));
            }
            return View();
        }
    }
}

Unit Testing This

awesome-camels

I can even add some unit tests to validate this works as expected (and to protect against me accidentally removing the if check later on).

First, I created a

TestLogger
to use for unit testing my controller. Here’s what that looks like:
using Microsoft.Framework.Logging;
using System;
using System.Collections.Generic;

namespace ConditionalLogging.Tests
{
    public class TestLogger<T> : ILogger<T>
    {
        private LogLevel _logLevel;

        public IList<string> LogMessages { get; } = new List<string>();

        public TestLogger(LogLevel logLevel)
        {
            _logLevel = logLevel;
        }

        public IDisposable BeginScopeImpl(object state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _logLevel;
        }

        public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
        {
            var message = string.Empty;
            if (null != formatter)
            {
                message = formatter(state, exception);
            }
            else
            {
                message = LogFormatter.Formatter(state, exception);
            }
            LogMessages.Add(message);
        }
    }
}

There’s two important parts here. First, I can pass in a

LogLevel
to the constructor so that
IsEnabled
can return true or false depending on how I want to use it in my test. Second, I’m recording the log entries to a list that I can access in the unit test to validate the messages (if any) that were written to the log during the test.

Next, I needed to create a fake

IUserInfoService
that simply records invocations for me. Like above, I’ll use this list of invocations in my unit test to validate whether the
GetUserId
method was called or not.
using ConditionalLogging.Services;
using System.Collections.Generic;

namespace ConditionalLogging.Tests
{
    public class TestUserInfoService : IUserInfoService
    {
        public IList<string> GetUserInvocations { get; } = new List<string>();

        public string GetUserId(string userName)
        {
            GetUserInvocations.Add(userName);
            return "test-user-id";
        }
    }
}

Now I’m ready to add my unit tests. First I added the xUnit framework dependencies to my project as described in my previous post on unit testing with xUnit. Next I added my first test to a new file called

Tests/Controllers/HomeControllerTest.cs
:
[Fact]
public void index_action_with_log_level_information_should_not_call_getuserid()
{
    // Arrange
    var userInfoService = new TestUserInfoService();
    var logger = new TestLogger<HomeController>(LogLevel.Information);
    var subject = new HomeController()
    {
        Logger = logger,
        UserInfoService = userInfoService,
        ActionContext = new ActionContext
        {
            HttpContext = new DefaultHttpContext()
        }
    };

    // Act
    var result = subject.Index();

    // Assert
    Assert.Empty(logger.LogMessages);
    Assert.Empty(userInfoService.GetUserInvocations);
}

This one is pretty simple. I create a fake logger and set its log level to

LogLevel.Information
. When I invoke the Index action on my controller, I expect no log messages to be emitted and no calls to
GetUserId
.

Now I added a positive test case where the log level was set to

LogLevel.Debug
:
[Fact]
public void index_action_with_log_level_debug_should_call_getuserid()
{
    // Arrange
    var userInfoService = new TestUserInfoService();
    var logger = new TestLogger<HomeController>(LogLevel.Debug);
    var actionContext = new ActionContext
    {
        HttpContext = new DefaultHttpContext
        {
            User = new GenericPrincipal(new GenericIdentity("test-user"), null)
        }
    };

    var subject = new HomeController()
    {
        Logger = logger,
        UserInfoService = userInfoService,
        ActionContext = actionContext
    };

    // Act
    var result = subject.Index();

    // Assert
    Assert.Equal("User with ID test-user-id requested Index.", logger.LogMessages.Single());
    Assert.Equal("test-user", userInfoService.GetUserInvocations.Single());
}

This one is only slightly more involved: I had to set the user identity inside the

ActionContext
(since it is being accessed in my controller and passed into the
IUserInfoService.GetUserId
method. Now I was able to assert that the correct log message was emitted and the
IUserInfoService
was invoked the correct number of times with the correct arguments.

Done

We touched on two topics in this post: first we recognized that for some log entries that are expensive to calculate, it makes sense to check

ILogger.IsEnabled
beforehand so we don’t waste time and resources needlessly.

Next we looked at how we could unit test this to validate that our logic is working as expected and help prevent breaking our code in the future. Even ignoring logging, the techniques used for unit testing in this post are applicable to unit testing our ASP.NET 5 MVC 6 applications in other areas.


Viewing all articles
Browse latest Browse all 4

Trending Articles