Login With Github

Play With The Log Components in ASP.NET Core

Introduction

The log components are the most frequently used components for programmers, providing with the necessary information to develop debuggers. A generic log interface ILogger is built in the ASP.NET Core, which implements a variety of built-in log providers, such as

  • Console
  • Debug
  • EventSource
  • EventLog
  • TraceSource
  • Azure App Service

In addition to the built-in log providers, ASP.NET Core also supports a variety of third-party log tools, such as

Developers can specify the log provider in the ASP.Net Core freely and send the log to a specified location.

We will introduce the general log interface of the ASP.Net Core in this blog. And we will implement some custom log providers in the end.

The Built-in Log Provider Provided by The System

Log Level

There are six log levels available in the ASP.NET Core, and they are Trace, Debug, Information, Warning, Error, and Critical. The following are their usage scenarios.

Log Level Common Usage Scenarios
Trace

It's used to record some information that is helpful for programmers to debug programs.
It may contain some sensitive informations, so it should be avoided enable Trace logs in production environments

Debug

It's used to record the short-term usefulness during the development and debugging phases.
Developers should try to avoid enabling Debug logs in production environments, if they do not want to troubleshoot in production environments temporarily.

Information It's used to record some of the application's processes. For example, it can record the url of the current api request.
Warning

It's used to record the unusual or unexpected event information that has occurred in the application.
The information may contain error messages or conditions that cause errors, such as, files not found.

Error It's used to record error and exception information generated by some action in the application.
Critical It's used to record some issues which need to be fixed immediately. For example, data is lost or there is not enough disk space.

How to create logs

In order to create a log, we first need to get a generic interface object which is used to implement ILogger<log category> through dependency injection.

Let's injecte an ILogger logger into the constructor ValuesController in the the following code.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
	private readonly ILogger<ValuesController> _logger = null;

	public ValuesController(ILogger<ValuesController> logger)
	{
		_logger = logger;
	}

	// GET api/values
	[HttpGet]
	public ActionResult<IEnumerable<string>> Get()
	{
		_logger.LogInformation("[Test Log]Getting items.");
		return new string[] { "value1", "value2" };
	}

}

Then let's add a log with the type of Information, which is "[Test Log]Getting items", using the LogInformation method provided by the ILogger interface.

Note: In order to provide 6 methods to output logs, there are 6 different log levels in ILogger.

  • LogTrace
  • LogDebug
  • LogInformation
  • LogWarning
  • LogError
  • LogCritical!

Now let's use the Kestral server to startup the project.

The logs generated by the project are as follows, and the logs we output manually will appear in the console.

Log configuration

You may doubt that why the logs are generated normally though we havn't inject any log providers into Startup.cs. This is due to the fact that two log providers have been added by default using the WebHost.CreateDefaultBuilder method in Program.cs.

Default log providers

Usually, we'll see the following code in Program.cs when creating a new ASP.NET Core WebApi project.

public class Program
{
	public static void Main(string[] args)
	{
		CreateWebHostBuilder(args).Build().Run();
	}

	public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
		WebHost.CreateDefaultBuilder(args)
			.UseStartup<Startup>();
}

Let's take a look at the source code of WebHost.CreateDefaultBuilder

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
	var builder = new WebHostBuilder()
		.UseKestrel()
		.UseContentRoot(Directory.GetCurrentDirectory())
		.ConfigureAppConfiguration((hostingContext, config) =>
		{
			var env = hostingContext.HostingEnvironment;

			config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
				  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

			if (env.IsDevelopment())
			{
				var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
				if (appAssembly != null)
				{
					config.AddUserSecrets(appAssembly, optional: true);
				}
			}

			config.AddEnvironmentVariables();

			if (args != null)
			{
				config.AddCommandLine(args);
			}
		})
		.ConfigureLogging((hostingContext, logging) =>
		{
			logging.UseConfiguration(hostingContext.Configuration.GetSection("Logging"));
			logging.AddConsole();
			logging.AddDebug();
		})
		.UseIISIntegration()
		.UseDefaultServiceProvider((context, options) =>
		{
			options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
		})
		.ConfigureServices(services =>
		{
			services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
		});

	return builder;
}

You will find that the log providers with the types of Console and Debug are configured by default in logging.AddConsole and logging.AddDebug in the code, and that is the reason why the logs are generated normally though we havn't injected any log providers.

Add the log provider manually

After reading the above code, you should have understanded how to add other built-in log providers. What we only need to do is to use the ConfigureLogging method in Program.cs to configure the log provider we need.

For example:

public class Program
{
	public static void Main(string[] args)
	{
		CreateWebHostBuilder(args).Build().Run();
	}

	public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
		WebHost.CreateDefaultBuilder(args)
		.ConfigureLogging((hostingContext, logging) =>
		{
			logging.AddConsole();
			logging.AddDebug();
			logging.AddEventSourceLogger();
		})
		.UseStartup<Startup>();
}

In addition to adding the log provider in Program.cs, we can also add the log provider in Startup.cs.

In Startup.cs, we can add a third parameter ILoggerFactory loggerFactory into the Configure method and use this parameter to add a log provider.

For example:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	loggerFactory.AddConsole();
	loggerFactory.AddDebug();
	
	app.UseMvc();
}

Configuration files and log level filtering

By default, ASP.NET Core will read the configuration of the logs from the Logging property in appSetting.json (of course you can also read the configuration from other files). Here we have set the minimum log levels generated by different log providers.

{
  "Logging": {
    "Debug": {
      "LogLevel": {
        "Default": "Information"
      }
    },
    "Console": {
      "LogLevel": {
        "Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
        "Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
        "Microsoft.AspNetCore.Mvc.Razor": "Error",
        "Default": "Trace"
      }
    },
    "LogLevel": {
      "Default": "Debug"
    }
  }
}

The Debug in the above code indicates the Debug Log Provider, the Console indicates the Console Log Provider, and the last LogLevel indicates that the other log providers are generic.

The Default setting in Debug is Information, which means that the minimum level of the logs generated in Debug is Information, so the logs below the Information level won't be output. The configuration in the Console is the same.

Custom Log Components

After learning the above basics, we should have get a simple understanding for the built-in log providers. Now let's try to customize 2 log providers.

In ASP.NET Core, we can create our own log providers by implementing ILogger, ILoggerProvider interfaces.

Write a console log component in a custom style

Here we'll add a log which will be output in the console, and the difference with the built-in Console type log is that we need to set different colors for different log types.

First let's create a new Api project ColoredConsoleLoggerSample

Then let's create a font color configuration class ColoredConsoleLoggerConfiguration for different log levels, and here is the code.

public class ColoredConsoleLoggerConfiguration
{
	public LogLevel LogLevel { get; set; } = LogLevel.Warning;
	public ConsoleColor Color { get; set; } = ConsoleColor.Yellow;
}

It defines different font colors for different log types in the class.

Then we create a log class ColoredConsoleLogger, which implements the ILogger interface, and the code is as follows.

public class ColoredConsoleLogger : ILogger
{
	private readonly string _name;
	private readonly ColoredConsoleLoggerConfiguration _config;

	public ColoredConsoleLogger(string name, ColoredConsoleLoggerConfiguration config)
	{
		_name = name;
		_config = config;
	}

	public IDisposable BeginScope<TState>(TState state)
	{
		return null;
	}

	public bool IsEnabled(LogLevel logLevel)
	{
		return logLevel == _config.LogLevel;
	}

	public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
	{
		if (!IsEnabled(logLevel))
		{
			return;
		}
		
		var color = Console.ForegroundColor;
		Console.ForegroundColor = _config.Color;
		Console.WriteLine($"{logLevel.ToString()} - {_name} - {formatter(state, exception)}");
		Console.ForegroundColor = color;
	}
}

Note:

  • ColoredConsoleLogger is only for one log level.
  • Here we use the IsEnable method to determine that the log will be output as long as the currently generated log level is the same as the log level defined in the ColoredConsoleLogger.
  • Log is the method defined in the ILogger interface, and the log will be output by the method.
  • Here we'll record the original font color of the current console before entering the log. When the output log is completed, we'll restore the font color to the original color.

Then we add a Logger provider class ColoredConsoleLoggerProvider, and the code is as follows:

public class ColoredConsoleLoggerProvider : ILoggerProvider
{
	private readonly ColoredConsoleLoggerConfiguration _config;

	public ColoredConsoleLoggerProvider(ColoredConsoleLoggerConfiguration config)
	{
		_config = config;
	}

	public ILogger CreateLogger(string categoryName)
	{
		return new ColoredConsoleLogger(categoryName, _config);
	}

	public void Dispose()
	{

	}
}

Note:

  • ColoredConsoleLoggerProvider is only for one log level
  • CreateLogger is a method defined in the ILoggerProvider interface, which is used to return a log generator.

Finally, let's modify the Configure method in Startup.cs, and the code is as follows.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	
	loggerFactory.AddProvider(new ColoredConsoleLoggerProvider(new ColoredConsoleLoggerConfiguration
	{
		LogLevel = LogLevel.Information,
		Color = ConsoleColor.Blue
	}));
	loggerFactory.AddProvider(new ColoredConsoleLoggerProvider(new ColoredConsoleLoggerConfiguration
	{
		LogLevel = LogLevel.Debug,
		Color = ConsoleColor.Gray
	}));

	app.UseMvc();
}

Now we have added 2 log providers for the Information level log and the Debug level log respectively.

Final effect

Our logs are displayed as what we presupposed the font colors before.

Write a Real-time Log Component which is Integrated with SignalR

Next we are going to customize a log provider which is integrated with SignalR. The logs generated are expected to be pushed to a web page via a SignalR server.

First let's create an ASP.NET Core WebApi project, which is named SignalrServer. Then right-click on the project property and modify App Url to http://localhost:5000.

Then create a LogHub class, which inherits from the Hub class, and the code is as follows.

public class LogHub : Hub
{
	public async Task WriteLog(Log log)
	{
		await Clients.All.SendAsync("showLog", log);
	}
}

public class Log
{
	public LogLevel Level { get; set; }

	public string Content { get; set; }
}

Note:

Here we create a method to write logs, which will push the logs to all the clients connected to the SignalR server, and call the clients' showLog method to display the pushed log information.

Then modify the Startup.cs file, and the code is as follows:

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddCors();
		services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
		
		services.AddSignalR();
	}

	// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
	{
		if (env.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
		}
		app.UseCors(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials());
		app.UseSignalR(routes =>
		{
			routes.MapHub<LogHub>("/logHub");
		});

		

		app.UseMvc();
	}
}

Note:

  • The SignalR service is registered via service.AddSignalR.
  • A logHub will be registered via the app.UserSignalR method.
  • Here we have enabled CORS because of the need to provide cross-domain access.

Now let's create another ASP.NET Core WebApi project SignalRLoggerSample.

Then right-click on the project property and set the App URL to http://localhost:5001

Use the Package Console Manager to install Microsoft.AspNetCore.SignalR.Client.

PM> install-package Microsoft.AspNetCore.SignalR.Client

To create a SignalR log provider, we create a SignalRLogger class and a SignalRLoggerProvider class.

SignalRLogger.cs

public class SignalRLogger : ILogger
{
	HubConnection connection;

	public SignalRLogger()
	{
		connection = new HubConnectionBuilder()
		 .WithUrl("http://localhost:5000/LogHub")
		 .Build();
	}

	public IDisposable BeginScope<TState>(TState state)
	{
		return null;
	}

	public bool IsEnabled(LogLevel logLevel)
	{
		return true;
	}

	public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
	{
		if (!IsEnabled(logLevel))
		{
			return;
		}

		connection.StartAsync().Wait();
		var task = connection.SendAsync("writeLog", new { Level = logLevel, Content = formatter(state, exception) });
		task.Wait();
	}
}

SignalRLoggerProvider.cs

public class SignalRLoggerProvider : ILoggerProvider
{
	public SignalRLoggerProvider()
	{
	}

	public ILogger CreateLogger(string categoryName)
	{
		return new SignalRLogger();
	}

	public void Dispose()
	{

	}
}

Note:

  • Here a SignalR connection is created by using HubConnectionBuilder.
  • After the connection is started successfully, we use the connection. SendAsync method to send the log information generated currently to the SignalR server.

After that, let's create an index.html in the wwwroot folder, which introduces the js libraries for jquery and signalr, and specify that the connected signalR server is http://localhost:5000/logHub

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="jquery-1.10.2.min.js"></script>
    <script src="signalr.min.js"></script>
</head>
<body>
    <h1>Logs</h1>
    <div id="content" style="border:1px solid #0094ff">

    </div>
    <script type="text/javascript">
        var levels = [
            { level: 0, name: 'Trace', backgroundColor: 'gray' },
            { level: 1, name: 'Debug', backgroundColor: 'green' },
            { level: 2, name: 'Information', backgroundColor: 'blue' },
            { level: 3, name: 'Warning', backgroundColor: 'yellow' },
            { level: 4, name: 'Error', backgroundColor: 'orange' },
            { level: 5, name: 'Critical', backgroundColor: 'red' },
        ];

        function getLevelName(level) {
            return levels.find(function (o) {
                return o.level == level;
            }).name;
        }

        function getLevelColor(level) {
            return levels.find(function (o) {
                return o.level == level;
            }).backgroundColor;
        }

        var connection = new signalR.HubConnectionBuilder().withUrl("http://localhost:5000/logHub").build();

        connection.on("showLog", function (message) {
            var div = "<div style='background-color:" + getLevelColor(message.level)+"'>[" + getLevelName(message.level) + "]:" + message.content + "</div>";
            $("#content").append(div);
        });

        connection.start().catch(function (err) {
            return console.error(err.toString());
        });
    </script>
</body>
</html>

Then modify the ValuesController file, and the code is as follows.

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
	private ILogger<ValuesController> _logger = null;

	public ValuesController(ILogger<ValuesController> logger)
	{
		_logger = logger;
	}

	// GET api/values
	[HttpGet]
	public ActionResult<IEnumerable<string>> Get()
	{
		_logger.LogTrace("User call the /api/values api");
		_logger.LogDebug("User call the /api/values api");
		_logger.LogInformation("User call the /api/values api");
		_logger.LogWarning("User call the /api/values api");
		_logger.LogError("User call the /api/values api");
		_logger.LogCritical("User call the /api/values api");
		return new string[] { "value1", "value2" };
	}
}

Note:

  • A log for the ValueController class is created here.
  • When the user requests /api/values, it will output 6 different levels of logs.

At last, let's modify the Configure method in Startup.cs and add the SignalRLoggerProvider to the pipeline using the method we introduced earlier.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	app.UseStaticFiles();

	loggerFactory.AddProvider(new SignalRLoggerProvider());

	app.UseMvc();

}

Final effect

Start the SignalRServer and then SignalRLoggerSample, and the effect is as follows.

References

0 Comment

temp