Logging API In ASP.NET Core - Day Twelve

In the previous article of this series, we discussed about the different types of configuration processing concepts within .NET Core applications. Now, in this article, we will discuss how to implement logging in the ASP.NET Core application. Before starting the discussion about ASP.NET Core logging framework, we need to understand why it is required and also how it is implemented in the previous versions of .NET.
 
If you want to look at the previous articles of thse series, please visit the links given below.

What's logging and tracing?

Logging (also called tracing) is used to record information about a program's execution for debugging and testing purposes. Developers, testers, and support engineers often use logging and tracing techniques to identify software problems, for post-deployment debugging, monitoring live systems, and auditing purposes.

Logging usually involves writing text messages to log files or sending data to monitoring applications. Advanced and modern logging tools also support logging of complex data structures, call stacks, threading behavior and also support real-time monitoring of applications over a network or on a local machine.

For the above logging and tracing purpose, the below mentioned third party logging framework or library is mainly used which still can be applicable within ASP.NET Core application.

  1. Log4Net
  2. NLog
  3. SeriLog

Built-in ASP.NET Core Logging

ASP.NET Core now has a built-in logging framework which we can use. It is designed as a logging API that developers can use to capture built-in ASP.NET logging as well as for their own custom logging. The logging API supports multiple output providers and is extensible to potentially be able to send your application logging anywhere.

Other logging frameworks like NLog and Serilog have even written providers for it. So, you can use the ILoggerFactory and it ends up working sort of the same. Logging acts as a facade above an actual logging library. By using it in this way, it also allows you to leverage all the power of a library, like NLog, to overcome any limitations the built-in Microsoft.Extensions.Logging API may have. The biggest of those is being able to actually write your logs to a file on disk!

How to add Logging Provider

Actually, logging provider is always required to fire some action to log data which can be displayed in the console or stored within a physical file located in the physical machine or cloud environment. To use a logging provider in ASP.NET Core, we need to install Microsoft.Extensions.Logging.Abstractions package from NuGet Manager. Basically, this package contains two interfaces – ILogger and ILoggerFactory.

ASP.NET Core dependency injection (DI) provides the ILoggerFactory instance. The AddConsole and AddDebug extension methods are defined in the Microsoft.Extensions.Logging.Console and Microsoft.Extensions.Logging.Debug packages. Each extension method calls the ILoggerFactory.AddProvider method, passing in an instance of the provider.

How to create Logs Data

To create logs in the application, we need to get an instance of ILogger object using Dependency Injection and store that data within a field. After this, we need to call the logging methods on those logger objects.

  1. public class TodoController : Controller  
  2. {  
  3.    private readonly ITodoRepository _todoRepository;  
  4.    private readonly ILogger _logger;  
  5.    public TodoController(ITodoRepository todoRepository,  
  6.    ILogger<TodoController> logger)  
  7.    {  
  8.       _todoRepository = todoRepository;  
  9.       _logger = logger;  
  10.    }  
  11.    …………….  
  12. }  
  1. public IActionResult GetById(string id)  
  2. {  
  3.    _logger.LogInformation(LoggingEvents.GET_ITEM, "Getting item {ID}", id);  
  4.    var item = _todoRepository.Find(id);  
  5.    if (item == null)  
  6.    {  
  7.       _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "GetById({ID}) NOT FOUND", id);  
  8.       return NotFound();  
  9.    }  
  10.    return new ObjectResult(item);  
  11. }  

What is Log Category?

A category is specified with each log that we create in our application. The category may be any string, but a best convention is to use the fully qualified name of the class from which the logs are written. For example: "EmployeeApi.Controllers.SaveController".

We specify the category when we create a logger object or request one from DI, and the category is automatically included with every log written by that logger. We can specify the category explicitly or we can use an extension method that derives the category from the type.

To specify the category explicitly, call CreateLogger on an ILoggerFactory instance, as shown below.
  1. public class TodoController : Controller  
  2. {  
  3.    private readonly ITodoRepository _todoRepository;  
  4.    private readonly ILogger _logger;  
  5.    public TodoController(ITodoRepository todoRepository,  
  6.    ILoggerFactory logger)  
  7.    {  
  8.       _todoRepository = todoRepository;  
  9.       _logger = logger.CreateLogger("TodoApi.Controllers.TodoController");  
  10.    }  
  11.    ………………..  
  12. }  

About Log Level

Each time we write a log, we specify its LogLevel as per its importance. The log level indicates the degree of severity or importance. For example, we might write an Information log when a method ends normally; a Warning log when a method returns a 404 return code; and an Error log when you catch an unexpected exception.

  1. public IActionResult GetById(string id)  
  2. {  
  3.    _logger.LogInformation(LoggingEvents.GET_ITEM, "Getting item {ID}", id);  
  4.    var item = _todoRepository.Find(id);  
  5.    if (item == null)  
  6.    {  
  7.       _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "GetById({ID}) NOT FOUND", id);  
  8.       return NotFound();  
  9.    }  
  10.    return new ObjectResult(item);  
  11. }  

The below table defines the different log levels mentioned within ASP.NET Core.

Log LevelLevel valueDescription
Trace0For information that is valuable only to a developer debugging an issue. These messages may contain sensitive application data and so should not be enabled in a production environment. Disabled by default.
Debug1For information that has short-term usefulness during development and debugging
Information2For tracking the general flow of the application
Warning3For abnormal or unexpected events in the application flow. These may include errors or other conditions that do not cause the application to stop
Error4For errors and exceptions that cannot be handled. These messages indicate a failure in the current activity or operation (such as the current HTTP request),
Critical5For failures that require immediate attention. Examples: data loss scenarios, out of disk space

Given below is the built-in Logging Provider in ASP.NET Core.

  1. Console
  2. Debug
  3. EventSource
  4. EventLog
  5. TraceSource
  6. Azure App Service
TodoController.cs
  1. using Microsoft.AspNetCore.Mvc;  
  2. using Microsoft.Extensions.Logging;  
  3. using Prog12_Logging.Core;  
  4. using Prog12_Logging.IRepository;  
  5. using Prog12_Logging.Model;  
  6. using System;  
  7. using System.Collections.Generic;  
  8. using System.Linq;  
  9.   
  10. namespace Prog12_Logging.Controllers  
  11. {  
  12.   
  13.     [Route("api/[controller]")]  
  14.     public class TodoController : Controller  
  15.     {  
  16.         private readonly ITodoRepository _todoRepository;  
  17.         private readonly ILogger _logger;  
  18.   
  19.         public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)  
  20.         {  
  21.             _todoRepository = todoRepository;  
  22.             _logger = logger;  
  23.         }  
  24.   
  25.         [HttpGet]  
  26.         public IEnumerable<TodoItem> GetAll()  
  27.         {  
  28.             using (_logger.BeginScope("Message {HoleValue}", DateTime.Now))  
  29.             {  
  30.                 _logger.LogInformation(LoggingEvents.LIST_ITEMS, "Listing all items");  
  31.                 EnsureItems();  
  32.             }  
  33.             return _todoRepository.GetAll();  
  34.         }  
  35.   
  36.         [HttpGet("{id}", Name = "GetTodo")]  
  37.         public IActionResult GetById(string id)  
  38.         {  
  39.             _logger.LogInformation(LoggingEvents.GET_ITEM, "Getting item {ID}", id);  
  40.             var item = _todoRepository.Find(id);  
  41.             if (item == null)  
  42.             {  
  43.                 _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "GetById({ID}) NOT FOUND", id);  
  44.                 return NotFound();  
  45.             }  
  46.             return new ObjectResult(item);  
  47.         }  
  48.   
  49.         [HttpPost]  
  50.         public IActionResult Create([FromBody] TodoItem item)  
  51.         {  
  52.             if (item == null)  
  53.             {  
  54.                 return BadRequest();  
  55.             }  
  56.             _todoRepository.Add(item);  
  57.             _logger.LogInformation(LoggingEvents.INSERT_ITEM, "Item {0} Created", item.Key);  
  58.             return CreatedAtRoute("GetTodo"new { controller = "Todo", id = item.Key }, item);  
  59.         }  
  60.   
  61.         [HttpPut("{id}")]  
  62.         public IActionResult Update(string id, [FromBody] TodoItem item)  
  63.         {  
  64.             if (item == null || item.Key != id)  
  65.             {  
  66.                 return BadRequest();  
  67.             }  
  68.             var todo = _todoRepository.Find(id);  
  69.             if (todo == null)  
  70.             {  
  71.   
  72.                 _logger.LogWarning(LoggingEvents.GET_ITEM_NOTFOUND, "Update({0}) NOT FOUND", id);  
  73.                 return NotFound();  
  74.             }  
  75.   
  76.             _todoRepository.Update(item);  
  77.             _logger.LogInformation(LoggingEvents.UPDATE_ITEM, "Item {0} Updated", item.Key);  
  78.             return new NoContentResult();  
  79.         }  
  80.   
  81.         [HttpDelete("{id}")]  
  82.         public void Delete(string id)  
  83.         {  
  84.             _todoRepository.Remove(id);  
  85.             _logger.LogInformation(LoggingEvents.DELETE_ITEM, "Item {0} Deleted", id);  
  86.         }  
  87.   
  88.         private void EnsureItems()  
  89.         {  
  90.             if (!_todoRepository.GetAll().Any())  
  91.             {  
  92.                 _logger.LogInformation(LoggingEvents.GENERATE_ITEMS, "Generating sample items.");  
  93.                 for (int i = 1; i < 11; i++)  
  94.                 {  
  95.                     _todoRepository.Add(new TodoItem() { Name = "Item " + i });  
  96.                 }  
  97.             }  
  98.         }  
  99.     }  
  100. }  
LoggingEvents.cs
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5.   
  6. namespace Prog12_Logging.Core  
  7. {  
  8.     public class LoggingEvents  
  9.     {  
  10.         public const int GENERATE_ITEMS = 1000;  
  11.         public const int LIST_ITEMS = 1001;  
  12.         public const int GET_ITEM = 1002;  
  13.         public const int INSERT_ITEM = 1003;  
  14.         public const int UPDATE_ITEM = 1004;  
  15.         public const int DELETE_ITEM = 1005;  
  16.         public const int GET_ITEM_NOTFOUND = 4000;  
  17.         public const int UPDATE_ITEM_NOTFOUND = 4001;  
  18.     }  
  19. }  
ITodoRepository.cs
  1. using Prog12_Logging.Model;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.Linq;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace Prog12_Logging.IRepository  
  8. {  
  9.     public interface ITodoRepository  
  10.     {  
  11.         void Add(TodoItem item);  
  12.         IEnumerable<TodoItem> GetAll();  
  13.         TodoItem Find(string key);  
  14.         TodoItem Remove(string key);  
  15.         void Update(TodoItem item);  
  16.     }  
  17. }  
TodoItem.cs
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5.   
  6. namespace Prog12_Logging.Model  
  7. {  
  8.     public class TodoItem  
  9.     {  
  10.         public string Key { getset; }  
  11.         public string Name { getset; }  
  12.         public bool IsComplete { getset; }  
  13.     }  
  14. }  
TodoRepository.cs
  1. using Prog12_Logging.IRepository;  
  2. using Prog12_Logging.Model;  
  3. using System;  
  4. using System.Collections.Concurrent;  
  5. using System.Collections.Generic;  
  6. using System.Linq;  
  7. using System.Threading.Tasks;  
  8.   
  9. namespace Prog12_Logging.Repository  
  10. {  
  11.     public class TodoRepository : ITodoRepository  
  12.     {  
  13.         static ConcurrentDictionary<string, TodoItem> _todos = new ConcurrentDictionary<string, TodoItem>();  
  14.   
  15.         public IEnumerable<TodoItem> GetAll()  
  16.         {  
  17.             return _todos.Values;  
  18.         }  
  19.   
  20.         public void Add(TodoItem item)  
  21.         {  
  22.             item.Key = Guid.NewGuid().ToString();  
  23.             _todos[item.Key] = item;  
  24.         }  
  25.   
  26.         public TodoItem Find(string key)  
  27.         {  
  28.             TodoItem item;  
  29.             _todos.TryGetValue(key, out item);  
  30.             return item;  
  31.         }  
  32.   
  33.         public TodoItem Remove(string key)  
  34.         {  
  35.             TodoItem item;  
  36.             _todos.TryGetValue(key, out item);  
  37.             _todos.TryRemove(key, out item);  
  38.             return item;  
  39.         }  
  40.   
  41.         public void Update(TodoItem item)  
  42.         {  
  43.             _todos[item.Key] = item;  
  44.         }  
  45.     }  
  46. }  
Program.cs
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.IO;  
  4. using System.Linq;  
  5. using System.Threading.Tasks;  
  6. using Microsoft.AspNetCore.Hosting;  
  7.   
  8. namespace Prog12_Logging  
  9. {  
  10.     public class Program  
  11.     {  
  12.         public static void Main(string[] args)  
  13.         {  
  14.             var host = new WebHostBuilder()  
  15.                 .UseKestrel()  
  16.                 .UseContentRoot(Directory.GetCurrentDirectory())  
  17.                 .UseIISIntegration()  
  18.                 .UseStartup<Startup>()  
  19.                 .Build();  
  20.             host.Run();  
  21.   
  22.         }  
  23.   
  24.     }  
  25. }  
Startup.cs
  1. using Microsoft.AspNetCore.Builder;  
  2. using Microsoft.AspNetCore.Hosting;  
  3. using Microsoft.AspNetCore.Http;  
  4. using Microsoft.Extensions.DependencyInjection;  
  5. using Microsoft.Extensions.Logging;  
  6. using Prog12_Logging.IRepository;  
  7. using Prog12_Logging.Repository;  
  8.   
  9. namespace Prog12_Logging  
  10. {  
  11.   
  12.     public class Startup  
  13.     {  
  14.         public void ConfigureServices(IServiceCollection services)  
  15.         {  
  16.             services.AddMvc();  
  17.             services.AddScoped<ITodoRepository, TodoRepository>();  
  18.         }  
  19.   
  20.         public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)  
  21.         {  
  22.             loggerFactory  
  23.             .AddConsole()  
  24.             .AddDebug();  
  25.             app.UseStaticFiles();  
  26.             app.UseMvc();  
  27.   
  28.             app.Run(async (context) =>  
  29.             {  
  30.                 var logger = loggerFactory.CreateLogger("TodoApi.Startup");  
  31.                 logger.LogInformation("No endpoint found for request {path}", context.Request.Path);  
  32.                 await context.Response.WriteAsync("No endpoint found - try /api/todo.");  
  33.             });  
  34.         }  
  35.     }