How to Write a Custom Logging Provider in ASP.NET Core
Theo Bebekis - 15/Apr/2019
Theo Bebekis - 15/Apr/2019
[SHOWTOGROUPS=4,20]
How to write a custom logging provider in ASP.NET Core
Introduction
Source code can be found on github.
There are no official instructions on how to write a custom logging provider in ASP.NET Core in the available documentation yet. So if someone is in need of writing a custom logging provider in ASP.NET Core, he has to study that documentation and the related source code of the framework.
The required parts are:
How to Implement the Required Interfaces
The sole purpose of ILoggerProvider is in creating ILogger instances when asked by the framework.
ILogger provides the Log() method. A call to Log() produces a unit of log information, a log entry.
Which of those two code elements should be responsible for displaying or persisting that log entry?
By studying the ASP.NET Core code, it becomes obvious that this responsibility goes to ILogger implementations, such as ConsoleLogger, DebugLogger and EventLogLogger classes. Should we do that too?
If the answer is yes, then we need an ILogger and an ILoggerProvider implementation for any medium, say text file, database or a message queue.
If the answer is no, then we need just a universal ILogger implementation and just an ILoggerProvider implementation for any different medium.
We are going to follow that second approach.
A single universal Logger class that produces a unit of log information, packs that information into an instance of a LogEntry class, and then passes that instance to its creator LoggerProvider for further processing. That LoggerProvider is going to be a base class, so any specialization regarding any different medium, text file, database, etc. goes to descendant LoggerProvider classes.
We are going to apply the above idea and create a FileLoggerProvider class.
Base Classes and Accessories
LogEntry represents the information of a log entry. The Logger creates an instance of this class when its Log() method is called, fills the properties and then passes that information to the provider calling WriteLog().
Logger represents an object that handles log information. This class does not save log information in a medium. Its sole responsibility is to create a LogEntry. It then fills the properties of that instance, and then passes it to the associated logger provider for further processing.
LoggerProvider is an abstract base logger provider class. A logger provider essentially represents the medium where log information is saved or displayed. This class may serve as base class in writing a file or database logger provider.
[/SHOWTOGROUPS]
How to write a custom logging provider in ASP.NET Core
Introduction
Source code can be found on github.
There are no official instructions on how to write a custom logging provider in ASP.NET Core in the available documentation yet. So if someone is in need of writing a custom logging provider in ASP.NET Core, he has to study that documentation and the related source code of the framework.
The required parts are:
- a simple class for the log options, that is a POCO
- an implementation of the ILogger interface
- an implementation of the ILoggerProvider interface
- a few extension methods for registering the logger provider to the framework
Код:
namespace Microsoft.Extensions.Logging
{
public interface ILogger
{
IDisposable BeginScope<TState>(TState state);
bool IsEnabled(LogLevel logLevel);
void Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception exception, Func<TState, Exception, string> formatter);
}
}
namespace Microsoft.Extensions.Logging
{
public interface ILoggerProvider : IDisposable
{
ILogger CreateLogger(string categoryName);
}
}
How to Implement the Required Interfaces
The sole purpose of ILoggerProvider is in creating ILogger instances when asked by the framework.
ILogger provides the Log() method. A call to Log() produces a unit of log information, a log entry.
Which of those two code elements should be responsible for displaying or persisting that log entry?
By studying the ASP.NET Core code, it becomes obvious that this responsibility goes to ILogger implementations, such as ConsoleLogger, DebugLogger and EventLogLogger classes. Should we do that too?
If the answer is yes, then we need an ILogger and an ILoggerProvider implementation for any medium, say text file, database or a message queue.
If the answer is no, then we need just a universal ILogger implementation and just an ILoggerProvider implementation for any different medium.
We are going to follow that second approach.
A single universal Logger class that produces a unit of log information, packs that information into an instance of a LogEntry class, and then passes that instance to its creator LoggerProvider for further processing. That LoggerProvider is going to be a base class, so any specialization regarding any different medium, text file, database, etc. goes to descendant LoggerProvider classes.
We are going to apply the above idea and create a FileLoggerProvider class.
Base Classes and Accessories
LogEntry represents the information of a log entry. The Logger creates an instance of this class when its Log() method is called, fills the properties and then passes that information to the provider calling WriteLog().
Код:
public class LogEntry
{
public LogEntry()
{
TimeStampUtc = DateTime.UtcNow;
UserName = Environment.UserName;
}
static public readonly string StaticHostName = System.Net.Dns.GetHostName();
public string UserName { get; private set; }
public string HostName { get { return StaticHostName; } }
public DateTime TimeStampUtc { get; private set; }
public string Category { get; set; }
public LogLevel Level { get; set; }
public string Text { get; set; }
public Exception Exception { get; set; }
public EventId EventId { get; set; }
public object State { get; set; }
public string StateText { get; set; }
public Dictionary<string, object> StateProperties { get; set; }
public List<LogScopeInfo> Scopes { get; set; }
}
LogScopeInfo represents Scope information regarding a LogEntry.
Hide Copy Code
public class LogScopeInfo
{
public LogScopeInfo()
{
}
public string Text { get; set; }
public Dictionary<string, object> Properties { get; set; }
}
Logger represents an object that handles log information. This class does not save log information in a medium. Its sole responsibility is to create a LogEntry. It then fills the properties of that instance, and then passes it to the associated logger provider for further processing.
Код:
internal class Logger : ILogger
{
public Logger(LoggerProvider Provider, string Category)
{
this.Provider = Provider;
this.Category = Category;
}
IDisposable ILogger.BeginScope<TState>(TState state)
{
return Provider.ScopeProvider.Push(state);
}
bool ILogger.IsEnabled(LogLevel logLevel)
{
return Provider.IsEnabled(logLevel);
}
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if ((this as ILogger).IsEnabled(logLevel))
{
LogEntry Info = new LogEntry();
Info.Category = this.Category;
Info.Level = logLevel;
// well, the passed default formatter function
// does not take the exception into account
// SEE: https://github.com/aspnet/Extensions/blob/master/src/
Logging/Logging.Abstractions/src/LoggerExtensions.cs
Info.Text = exception?.Message ?? state.ToString(); // formatter(state, exception)
Info.Exception = exception;
Info.EventId = eventId;
Info.State = state;
// well, you never know what it really is
if (state is string)
{
Info.StateText = state.ToString();
}
// in case we have to do with a message template,
// let's get the keys and values (for Structured Logging providers)
// SEE: https://docs.microsoft.com/en-us/aspnet/core/
// fundamentals/logging#log-message-template
// SEE: https://softwareengineering.stackexchange.com/
// questions/312197/benefits-of-structured-logging-vs-basic-logging
else if (state is IEnumerable<KeyValuePair<string, object>> Properties)
{
Info.StateProperties = new Dictionary<string, object>();
foreach (KeyValuePair<string, object> item in Properties)
{
Info.StateProperties[item.Key] = item.Value;
}
}
// gather info about scope(s), if any
if (Provider.ScopeProvider != null)
{
Provider.ScopeProvider.ForEachScope((value, loggingProps) =>
{
if (Info.Scopes == null)
Info.Scopes = new List<LogScopeInfo>();
LogScopeInfo Scope = new LogScopeInfo();
Info.Scopes.Add(Scope);
if (value is string)
{
Scope.Text = value.ToString();
}
else if (value is IEnumerable<KeyValuePair<string, object>> props)
{
if (Scope.Properties == null)
Scope.Properties = new Dictionary<string, object>();
foreach (var pair in props)
{
Scope.Properties[pair.Key] = pair.Value;
}
}
},
state);
}
Provider.WriteLog(Info);
}
}
public LoggerProvider Provider { get; private set; }
public string Category { get; private set; }
}
LoggerProvider is an abstract base logger provider class. A logger provider essentially represents the medium where log information is saved or displayed. This class may serve as base class in writing a file or database logger provider.
Код:
public abstract class LoggerProvider : IDisposable, ILoggerProvider, ISupportExternalScope
{
ConcurrentDictionary<string, Logger> loggers = new ConcurrentDictionary<string, Logger>();
IExternalScopeProvider fScopeProvider;
protected IDisposable SettingsChangeToken;
void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider)
{
fScopeProvider = scopeProvider;
}
ILogger ILoggerProvider.CreateLogger(string Category)
{
return loggers.GetOrAdd(Category,
(category) => {
return new Logger(this, category);
});
}
void IDisposable.Dispose()
{
if (!this.IsDisposed)
{
try
{
Dispose(true);
}
catch
{
}
this.IsDisposed = true;
GC.SuppressFinalize(this); // instructs GC not bother to call the destructor
}
}
protected virtual void Dispose(bool disposing)
{
if (SettingsChangeToken != null)
{
SettingsChangeToken.Dispose();
SettingsChangeToken = null;
}
}
public LoggerProvider()
{
}
~LoggerProvider()
{
if (!this.IsDisposed)
{
Dispose(false);
}
}
public abstract bool IsEnabled(LogLevel logLevel);
public abstract void WriteLog(LogEntry Info);
internal IExternalScopeProvider ScopeProvider
{
get
{
if (fScopeProvider == null)
fScopeProvider = new LoggerExternalScopeProvider();
return fScopeProvider;
}
}
public bool IsDisposed { get; protected set; }
}
[/SHOWTOGROUPS]