Using logging and telemetry for monitoring and supervision purposes

When you are developing your applications, you use one of the well-known integrated development environments such as Visual Studio 2019 or Visual Studio Code, as described in the initial chapters of the book. You do this every day, and most of the things you do become second nature and you perform them automatically after some time.

It is natural for you to be able to debug your applications and understand what is happening during runtime by using the advanced debugging features of Visual Studio 2019, for example. Looking up variable values, seeing what methods get called in what order, understanding what instances are injected, and capturing exceptions, are key to building applications that are robust and respond to business needs.

Then, when deploying your applications to production environments, you suddenly miss all of those features. Rarely will you find a production environment where Visual Studio is installed, but errors and unexpected behaviors will happen and you will need to be able to understand and fix them as fast as possible.

That is where logging and telemetry come into their own. By instrumenting your applications and logging when entering and leaving methods, as well as important variable values or any kind of information you consider important during runtime, you will be able to go to the application log and see what is happening in the production environment in the event of issues.

In this section, we go back to our Tic-Tac-Toe demo application, where we are going to show you how to use logging and exception handling to provide an industrialized solution to the problem where we only get an exception in production, without the finer details that can help you to debug the issue.

ASP.NET Core 3 provides built-in support for logging to the following targets:

  • Azure App Services
  • Console
  • Windows event source
  • Trace
  • Debugger output
  • Application Insights

However, files, databases, and logging services are not supported by default. If you want to send your logs to these targets, you need to use a third-party logger solution such as Log4net, Serilog, NLog, Apache, ELMAH, or Loggr.

You can also easily create your own provider by implementing the ILoggerProvider interface, as follows:

  1. Add a new Class Library (.NET Core) project to the solution and call it TicTacToe.Logging (delete the autogenerated Class1.cs file):

  1. Add the Microsoft.Extensions.Logging and Microsoft.Extensions.Logging.Configuration NuGet packages, via NuGet Package Manager:

  1. Add a project reference from the TicTacToe web application project so that we can use assets from the TicTacToe.Logging class library:

  1. Add a new POCO (short for Plain Old CLR Object) class called LogEntry to the TicTacToe.Logging project. This will contain log data, with an event id, the message for the actual log, the log level, the level (information, warning, or critical), and finally a timestamp when a log is created:
        public class LogEntry 
        { 
          public int EventId { get; internal set; } 
          public string Message { get; internal set; } 
          public string LogLevel { get; internal set; } 
          public DateTime CreatedTime { get; internal set; } 
        } 
  1. Add a new class called FileLoggerHelper, which will be used for file operations. Then we add field definitions and a constructor. The constructor makes sure that, every time FileLoggerHelper is instantiated, it is forced to accept a filename and make it available for use in internal methods such as InsertLog, as follows:
        public  class FileLoggerHelper 
        { 
          private string fileName; 
 
          public FileLoggerHelper(string fileName) 
          { 
            this.fileName = fileName; 
          } 
 
          static ReaderWriterLock locker = new ReaderWriterLock(); 
 
          //....
          } 
 
        }

Let's then add an InsertLog method to the FileLoggerHelper class. The method creates a file directory if it doesn't already exist, logs events to a file after acquiring a lock, and then releases them after use. InsertLog is implemented as follows:

public void InsertLog(LogEntry logEntry) 
{ 
  var directory = System.IO.Path.GetDirectoryName(fileName); 
 
  if (!System.IO.Directory.Exists(directory)) 
     System.IO.Directory.CreateDirectory(directory); 
 
  try 
  {   
    locker.AcquireWriterLock(int.MaxValue); 
    System.IO.File.AppendAllText(fileName,
$"{logEntry.CreatedTime} {logEntry.EventId} {logEntry.LogLevel}
{
logEntry.Message}" + Environment.NewLine); } finally { locker.ReleaseWriterLock(); }

Add a new class called FileLogger and implement the ILogger interface. The file logger concrete class will allow us to make use of the logging functionality that is available in the ILogger interface template provided by Microsoft in the .NET Core framework:

public sealed class FileLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return (_filter == null || _filter(_categoryName, logLevel));
}
public void Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception exception, Func<TState, Exception, string> formatter)
{
throw new NotImplementedException();
}
}

Before we implement the Log method, let's create our constructor and field definitions. We make sure that the category name, log level, and filename are all supplied, and we also create a new instance of FileLoggerHelper as follows:

          private string _categoryName; 
private Func<string, LogLevel, bool> _filter;
private string _fileName;
private FileLoggerHelper _helper;

public FileLogger(string categoryName, Func<string,
LogLevel,
bool> filter, string fileName)
{
_categoryName = categoryName;
_filter = filter;
_fileName = fileName;
_helper = new FileLoggerHelper(fileName);
}

And then there is our main Log method, now implemented as follows:

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,     Func<TState, Exception, string> formatter) 
{
if (!IsEnabled(logLevel)) return;
if (formatter == null) throw new
ArgumentNullException(nameof(formatter));
var message = formatter(state, exception);
if (string.IsNullOrEmpty(message)) return;
if (exception != null) message += " " + exception.ToString();
var logEntry = new LogEntry
{
Message = message,
EventId = eventId.Id,
LogLevel = logLevel.ToString(),
CreatedTime = DateTime.UtcNow
};
_helper.InsertLog(logEntry);
}
  1. Add a new class called FileLoggerProvider and implement the ILoggerProvider interface. This is used to supply FileLogger instances of ILogger when required by ASP.NET Core, and will be injected later:
        public class FileLoggerProvider : ILoggerProvider 
        { 
          private readonly Func<string, LogLevel, bool> _filter; 
          private string _fileName; 
 
          public FileLoggerProvider(Func<string, LogLevel, bool> 
filter, string fileName) { _filter = filter; _fileName = fileName; } public ILogger CreateLogger(string categoryName) { return new FileLogger(categoryName, _filter, _fileName); } public void Dispose() { } }

  1. To simplify calling the file logging provider from the web application, we need to add a static class called FileLoggerExtensions (with the configuration section, filename, and log verbosity level as parameters):
        public static class FileLoggerExtensions 
        { 
          const long DefaultFileSizeLimitBytes = 1024 * 1024 *
1024; const int DefaultRetainedFileCountLimit = 31; }

Our FileLoggerExtensions class will have three different overloads on the AddFile method. Now, let's add our first implementation of the AddFile method:

public static ILoggingBuilder AddFile(this ILoggingBuilder loggerBuilder, IConfigurationSection configuration)
{
if (loggerBuilder == null) throw new
ArgumentNullException(nameof(loggerBuilder))
if (configuration == null) throw new
ArgumentNullException(nameof(configuration));
var minimumLevel = LogLevel.Information;
var levelSection = configuration["Logging:LogLevel"];
if (!string.IsNullOrWhiteSpace(levelSection))
{
if (!Enum.TryParse(levelSection, out minimumLevel))
{
System.Diagnostics.Debug.WriteLine("The minimum level setting
`{0}` is invalid", levelSection);
minimumLevel = LogLevel.Information;
}
}
return loggerBuilder.AddFile(configuration[
"Logging:FilePath"], (category, logLevel) => (logLevel >=
minimumLevel), minimumLevel);
}

And then there is the second overload for the AddFile method:

public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath, Func<string, LogLevel,
bool> filter, LogLevel minimumLevel =
LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));

var fileInfo = new System.IO.FileInfo(filePath);

if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();

loggerBuilder.AddProvider(new FileLoggerProvider
(filter, filePath));

return loggerBuilder;
}

Then, there is the third overload implementation for the AddFile method:

public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath, LogLevel minimumLevel =
LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));

var fileInfo = new System.IO.FileInfo(filePath);

if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();

loggerBuilder.AddProvider(new FileLoggerProvider
((category,
logLevel) => (logLevel >= minimumLevel), filePath));

return loggerBuilder;
}
  1. In the TicTacToe web project, add two new options, called LoggingProviderOption and LoggingOptionsto the Options folder:
        public class LoggingProviderOption 
        { 
          public string Name { get; set; } 
          public string Parameters { get; set; } 
          public int LogLevel { get; set; } 
        } 
public class LoggingOptions { public LoggingProviderOption[] Providers { get; set; } }

  1. In the TicTacToe web project, add a new extension called ConfigureLoggingExtension to the Extensions folder:
        public static class ConfigureLoggingExtension 
        { 
          public static ILoggingBuilder AddLoggingConfiguration(this 
ILoggingBuilder loggingBuilder, IConfiguration
configuration) { var loggingOptions = new LoggingOptions(); configuration.GetSection("Logging").
Bind(loggingOptions); foreach (var provider in loggingOptions.Providers) { switch (provider.Name.ToLower()) { case "console": { loggingBuilder.AddConsole();
break;
} case "file": { string filePath = System.IO.Path.Combine(
System.IO.Directory.GetCurrentDirectory(),
"logs",
$"TicTacToe_{System.DateTime.Now.ToString(
"ddMMyyHHmm")}.log"); loggingBuilder.AddFile(filePath,
(LogLevel)provider.LogLevel); break; } default: { break; } } } return loggingBuilder; } }
  1. Go to the Program class of the TicTacToe web application project, update the BuildWebHost method, and call the extension:
        public static IHostBuilder CreateHostBuilder(string[] args) 
=>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.CaptureStartupErrors(true);
webBuilder.PreferHostingUrls(true);
webBuilder.UseUrls("http://localhost:5000");
webBuilder.ConfigureLogging((hostingcontext,
logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
});
});
Don't forget to add the following using statement at the beginning of the class:
using TicTacToe.Extensions;.
  1. Add a new section called Logging to the appsettings.json file:
        "Logging": { 
          "Providers": [ 
            { 
              "Name": "Console", 
              "LogLevel": "1" 
            }, 
            { 
              "Name": "File", 
              "LogLevel": "2" 
            } 
          ], 
          "MinimumLevel": 1 
        }
  1. Start the application and verify that a new log file has been created in a folder called logs within the application folder:

This is the first step and is straightforward and quick to complete. You now have a log file to which you can write your logs. You will see that it is just as easy to use the integrated logging functionalities to create logs from anywhere within your ASP.NET Core 3 applications (Controllers, Services, and more).

Let's quickly add some logs to the Tic-Tac-Toe application:

  1. Update the UserRegistrationController constructor implementation so that we supply a logger instance for the entire controller:
       readonly IUserService _userService; 
       readonly IEmailService _emailService; 
       readonly ILogger<UserRegistrationController> _logger; 
       public UserRegistrationController(IUserService userService,
IEmailService emailService,
ILogger<UserRegistrationController>
logger) { _userService = userService; _emailService = emailService; _logger = logger; }
  1. Update the EmailConfirmation method in UserRegistrationController and add a log at the start of the method:
        _logger.LogInformation($"##Start## Email confirmation 
process for {email}");
  1. Update the EmailService implementation, and add a logger to its constructor so that it is available to the email service:
public class EmailService : IEmailService 
        { 
          private EmailServiceOptions _emailServiceOptions; 
          readonly ILogger<EmailService> _logger; 
          public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger) { _emailServiceOptions = emailServiceOptions.Value; _logger = logger; } }

And then replace the SendMail method in EmailService with the following:

        
          public Task SendEmail(string emailTo, string subject,  
string message) { try { _logger.LogInformation($"##Start sendEmail## Start
sending Email to {emailTo}"); using (var client = new
SmtpClient(_emailServiceOptions.MailServer,
int.Parse(_emailServiceOptions.MailPort))) { if (bool.Parse(_emailServiceOptions.UseSSL)
== true)
client.EnableSsl = true; if (!string.IsNullOrEmpty
(_emailServiceOptions.UserId)) client.Credentials = new NetworkCredential
(_emailServiceOptions.UserId,
_emailServiceOptions.Password); client.Send(new MailMessage
("[email protected]", emailTo, subject,
message)); } } catch (Exception ex) { _logger.LogError($"Cannot
send email {ex}"); } return Task.CompletedTask; }
  1. Then, open the generated log file and analyze its contents, after running the application and registering a new user:

You will notice that the start of the email confirmation process and the start of sending an email have all been duly recorded in the logs. The failure to send an email itself has also been logged as an exception with its stack trace.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.128.113