ASP.NET Core Logging

2024-07-01

筆記 ASP.NET Core Logging 的基本要點。

logo

Logging

We can use primary constructor to inject ILogger into our controller, and use it to log messages.

[Route("api/[controller]")]
public class LogController(ILogger<LogController> _logger) : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        var message = "Get Method Invoked";
        _logger.LogInformation(message);
        return Ok(message);
    }
}

Kestrel Log will be console window.

info is Level, Controllers.LogController is Category, Get Method Invoked is Message, [0] is EventId.

IIS Express will log will be output window.

Debug Model will log will be Debug output window.

Log Level

Log Level Description
5 Critical
4 Error
3 Warning
2 Information
1 Debug
0 Trace

The default log level is Information, if we want to change it, we can do it in appsettings.json (in development environment, we should cahnge appsettings.Development.json).

{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
[HttpGet("level")]
public IActionResult Level()
{
    var message = "GetLogLevel Method Invoked";
    _logger.LogCritical(message);
    _logger.LogError(message);
    _logger.LogWarning(message);
    _logger.LogInformation(message);
    _logger.LogDebug(message);
    _logger.LogTrace(message);
    return Ok(message);
}

We can custom message with multiple parameters.

var message = "GetLogLevel Method Invoked {}";
_logger.LogCritical(message, Request.Path);

Log Category

Instead of ILogger, we ca use logfactory, the benefit is we can use the same category for multiple controllers.

public class LogController(ILoggerFactory factory) : ControllerBase
{
    readonly ILogger _logger = factory.CreateLogger("MyLog");
    ...
}

With LogFactory to create logger, then we can use log filter to filter log messages in program.cs

builder.Logging.AddFilter((category, level) =>
{
    return category == "MyLog" && level >= LogLevel.Warning;
});

💡 However the best practice is to set log level in appsettings.json or appsettings.Development.json, by the logger name.

We disable Default log level, and set MyLog log level to Warning.

{
  "Logging": {
    "LogLevel": {
      "MyLog": "Warning",
      "Default": "None",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

We even can use namespace as category to filter log messages 😉

readonly ILogger _logger = factory.CreateLogger("MyNamespace.MyClass");
{
  "Logging": {
    "LogLevel": {
      "MyNamespace.MyClass": "Warning",
      "Default": "None",
    }
  }
}

Log to File

We can not log to file directly, but we can log to file by using NLog library.

Getting started with ASP.NET Core 6

<ItemGroup>
  <PackageReference Include="NLog.Web.AspNetCore" Version="5.*" />
  <PackageReference Include="NLog" Version="5.*" />
</ItemGroup>

And then add a nlog.config file to the root of the project.

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="c:\temp\internal-nlog-AspNetCore.txt">

	<!-- enable asp.net core layout renderers -->
	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>

	<!-- the targets to write to -->
	<targets>
		<!-- File Target for all log messages with basic details -->
		<target xsi:type="File" name="allfile" fileName="c:\temp\nlog-AspNetCore-all-${shortdate}.log"
				layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />

		<!-- File Target for own log messages with extra web details using some ASP.NET core renderers -->
		<target xsi:type="File" name="ownFile-web" fileName="c:\temp\nlog-AspNetCore-own-${shortdate}.log"
				layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

		<!--Console Target for hosting lifetime messages to improve Docker / Visual Studio startup detection -->
		<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
	</targets>

	<!-- rules to map from logger name to target -->
	<rules>
		<!--All logs, including from Microsoft-->
		<logger name="*" minlevel="Trace" writeTo="allfile" />

		<!--Output hosting lifetime messages to console target for faster startup detection -->
		<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />

		<!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
		<logger name="Microsoft.*" maxlevel="Info" final="true" />
		<logger name="System.Net.Http.*" maxlevel="Info" final="true" />

		<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
	</rules>
</nlog>

The default log path will be "c:\temp".

program.cs

builder.Host.UseNLog(new NLogAspNetCoreOptions() { RemoveLoggerFactoryFilter = false });

Factory Pattern (Multiple Logger)

We can use factory pattern to create multiple loggers.

public class LoggerFactory
{
    private readonly IServiceProvider _serviceProvider;

    public LoggerFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ICustomLogger CreateLogger(string loggerType)
    {
        switch (loggerType.ToLower())
        {
            case "console":
                return _serviceProvider.GetRequiredService<ConsoleCustomLogger>();
            case "debug":
                return _serviceProvider.GetRequiredService<DebugCustomLogger>();
            default:
                throw new ArgumentException("Invalid logger type", nameof(loggerType));
        }
    }
}

program.cs

builder.Services.AddTransient<ConsoleCustomLogger>();
builder.Services.AddTransient<DebugCustomLogger>();
builder.Services.AddTransient<LoggerFactory>();