目录
介绍
如何实现所需的接口
基础类和附件
FileLoggerProvider具体类及其附件
1. ConfigureLogging()
2. appsettings.json文件
介绍源代码可以在github上找到。
在可用的文档中,没有关于如何在ASP.NET Core中编写自定义日志记录提供程序的官方说明。因此,如果有人需要在ASP.NET Core中编写自定义日志记录提供程序,他必须研究该文档以及框架的相关源代码。
所需的部分是:
- 一个简单的日志选项类,即POCO
- ILogger接口的实现
- ILoggerProvider接口的实现
- 一些扩展方法,用于将记录器提供程序注册到框架。
让我们看看两个接口:
namespace Microsoft.Extensions.Logging
{
public interface ILogger
{
IDisposable BeginScope(TState state);
bool IsEnabled(LogLevel logLevel);
void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter);
}
}
namespace Microsoft.Extensions.Logging
{
public interface ILoggerProvider : IDisposable
{
ILogger CreateLogger(string categoryName);
}
}
如何实现所需的接口
ILoggerProvider唯一的目的是在ILogger框架询问时创建实例。
ILogger提供了Log()方法。调用Log()生成一个日志信息单元,一个日志条目。
这两个代码元素中的哪一个应该负责显示或持久化该日志条目?
通过学习ASP.NET核心代码的研究,可以明显看出这一责任属于ILogger实现,比如ConsoleLogger,DebugLogger和EventLogLogger类。我们也应该这样做吗?
如果答案是肯定的,那么我们需要一个对任何媒体的ILogger 和ILoggerProvider实现,比如文本文件,数据库或消息队列。
如果答案是否定的,那么我们只需要一个通用ILogger实现,并且只需要一个适用于任何不同介质的ILoggerProvider实现。
我们将遵循第二种方法。
一个生成日志信息单元的通用Logger类,将该信息打包到LogEntry类的实例中,然后将该实例传递给其创建者LoggerProvider以进行进一步处理。LoggerProvider将是一个基类,因此任何关于任何不同介质、文本文件、数据库等的专业化都会进入后代LoggerProvider类。
我们将应用上述想法并创建一个FileLoggerProvider类。
基础类和附件LogEntry表示日志条目的信息。在调用Logger的Log()方法时创建此类的实例,填充属性,然后将该信息传递给提供者调用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 StateProperties { get; set; }
public List Scopes { get; set; }
}
LogScopeInfo表示Scope有关LogEntry的信息。
public class LogScopeInfo
{
public LogScopeInfo()
{
}
public string Text { get; set; }
public Dictionary Properties { get; set; }
}
Logger表示处理日志信息的对象。此类不会将日志信息保存在媒体(如数据库)中。它的唯一责任是创造一个LogEntry。然后它填充该实例的属性,然后将其传递给关联的记录器(logger)提供程序以进行进一步处理。
internal class Logger : ILogger
{
public Logger(LoggerProvider Provider, string Category)
{
this.Provider = Provider;
this.Category = Category;
}
IDisposable ILogger.BeginScope(TState state)
{
return Provider.ScopeProvider.Push(state);
}
bool ILogger.IsEnabled(LogLevel logLevel)
{
return Provider.IsEnabled(logLevel);
}
void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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 takes 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, lets 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 Properties)
{
Info.StateProperties = new Dictionary();
foreach (KeyValuePair 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 Scope = new LogScopeInfo();
Info.Scopes.Add(Scope);
if (value is string)
{
Scope.Text = value.ToString();
}
else if (value is IEnumerable props)
{
if (Scope.Properties == null)
Scope.Properties = new Dictionary();
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是一个abstract基本记录器提供程序类。记录器提供程序实质上代表保存或显示日志信息的媒体。此类可以在编写文件或数据库记录器提供程序时充当基类。
public abstract class LoggerProvider : IDisposable, ILoggerProvider, ISupportExternalScope
{
ConcurrentDictionary loggers = new ConcurrentDictionary();
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; }
}
FileLoggerProvider具体类及其附件
FileLoggerOptions是Options文件记录器的类。
public class FileLoggerOptions
{
string fFolder;
int fMaxFileSizeInMB;
int fRetainPolicyFileCount;
public FileLoggerOptions()
{
}
public LogLevel LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information;
public string Folder
{
get { return !string.IsNullOrWhiteSpace(fFolder) ?
fFolder : System.IO.Path.GetDirectoryName(this.GetType().Assembly.Location); }
set { fFolder = value; }
}
public int MaxFileSizeInMB
{
get { return fMaxFileSizeInMB > 0 ? fMaxFileSizeInMB : 2; }
set { fMaxFileSizeInMB = value; }
}
public int RetainPolicyFileCount
{
get { return fRetainPolicyFileCount < 5 ? 5 : fRetainPolicyFileCount; }
set { fRetainPolicyFileCount = value; }
}
}
有两种配置文件记录器选项的方法:
- 在Program.cs中使用ConfigureLogging(),并通过调用AddFileLogger()的第二个版本,使用选项委托,或
- 使用appsettings.json文件。
.ConfigureLogging(logging =>
{
logging.ClearProviders();
// logging.AddFileLogger();
logging.AddFileLogger(options => {
options.MaxFileSizeInMB = 5;
});
})
2. appsettings.json文件
"Logging": {
"LogLevel": {
"Default": "Warning"
},
"File": {
"LogLevel": "Debug",
"MaxFileSizeInMB": 5
}
},
FileLoggerOptionsSetup通过对IConfiguration使用ConfigurationBinder.Bind()配置FileLoggerOptions实例。FileLoggerOptionsSetup类本质上是用appsettings.json文件中的节绑定FileLoggerOptions实例。这是一个至关重要的连接,特别是如果我们希望收到有关我们的记录器提供程序的appsettings.json更改通知。别担心,这只是管道。
internal class FileLoggerOptionsSetup : ConfigureFromConfigurationOptions
{
public FileLoggerOptionsSetup(ILoggerProviderConfiguration providerConfiguration)
: base(providerConfiguration.Configuration)
{
}
}
FileLoggerProvider是一个记录器提供程序,它将日志条目写入文本文件。“ File”是此提供程序的提供程序别名,可以在appsettings.json的Logging部分中使用,请参阅上文。
FileLoggerProvider 做一些有趣的事情。
它将Logger传递给它的每个LogEntry写入扩展名为*.log的文本文件,该文件位于FileLoggerOptions中指定的文件夹中(或在FileLoggerOptions读取的appsettings.json中)。
实际上,一个Logger调用abstract LoggerProvider.WriteLog(LogEntry Info)。重写FileLoggerProvider.WriteLog(LogEntry Info)不会阻止,因为它将传递的LogEntry推送到线程安全队列。稍后,线程检查该队列,弹出LogEntry并将其写入文本文件。这是一个异步操作。
FileLoggerProvider还关心它创建的日志文件的保留策略。它确实遵守了一些与保留策略相关的FileLoggerOptions设置。
FileLoggerProvider,感谢上面显示的FileLoggerOptionsSetup并且将IOptionsMonitor传递给它的构造函数,通知appsettings.json文件发生的变化并做出相应的响应。
[Microsoft.Extensions.Logging.ProviderAlias("File")]
public class FileLoggerProvider : LoggerProvider
{
bool Terminated;
int Counter = 0;
string FilePath;
Dictionary Lengths = new Dictionary();
ConcurrentQueue InfoQueue = new ConcurrentQueue();
void ApplyRetainPolicy()
{
FileInfo FI;
try
{
List FileList = new DirectoryInfo(Settings.Folder)
.GetFiles("*.log", SearchOption.TopDirectoryOnly)
.OrderBy(fi => fi.CreationTime)
.ToList();
while (FileList.Count >= Settings.RetainPolicyFileCount)
{
FI = FileList.First();
FI.Delete();
FileList.Remove(FI);
}
}
catch
{
}
}
void WriteLine(string Text)
{
// check the file size after any 100 writes
Counter++;
if (Counter % 100 == 0)
{
FileInfo FI = new FileInfo(FilePath);
if (FI.Length > (1024 * 1024 * Settings.MaxFileSizeInMB))
{
BeginFile();
}
}
File.AppendAllText(FilePath, Text);
}
string Pad(string Text, int MaxLength)
{
if (string.IsNullOrWhiteSpace(Text))
return "".PadRight(MaxLength);
if (Text.Length > MaxLength)
return Text.Substring(0, MaxLength);
return Text.PadRight(MaxLength);
}
void PrepareLengths()
{
// prepare the lengs table
Lengths["Time"] = 24;
Lengths["Host"] = 16;
Lengths["User"] = 16;
Lengths["Level"] = 14;
Lengths["EventId"] = 32;
Lengths["Category"] = 92;
Lengths["Scope"] = 64;
}
void BeginFile()
{
Directory.CreateDirectory(Settings.Folder);
FilePath = Path.Combine(Settings.Folder, LogEntry.StaticHostName + "-" + DateTime.Now.ToString("yyyyMMdd-HHmm") + ".log");
// titles
StringBuilder SB = new StringBuilder();
SB.Append(Pad("Time", Lengths["Time"]));
SB.Append(Pad("Host", Lengths["Host"]));
SB.Append(Pad("User", Lengths["User"]));
SB.Append(Pad("Level", Lengths["Level"]));
SB.Append(Pad("EventId", Lengths["EventId"]));
SB.Append(Pad("Category", Lengths["Category"]));
SB.Append(Pad("Scope", Lengths["Scope"]));
SB.AppendLine("Text");
File.WriteAllText(FilePath, SB.ToString());
ApplyRetainPolicy();
}
void WriteLogLine()
{
LogEntry Info = null;
if (InfoQueue.TryDequeue(out Info))
{
string S;
StringBuilder SB = new StringBuilder();
SB.Append(Pad(Info.TimeStampUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.ff"), Lengths["Time"]));
SB.Append(Pad(Info.HostName, Lengths["Host"]));
SB.Append(Pad(Info.UserName, Lengths["User"]));
SB.Append(Pad(Info.Level.ToString(), Lengths["Level"]));
SB.Append(Pad(Info.EventId != null ? Info.EventId.ToString() : "", Lengths["EventId"]));
SB.Append(Pad(Info.Category, Lengths["Category"]));
S = "";
if (Info.Scopes != null && Info.Scopes.Count > 0)
{
LogScopeInfo SI = Info.Scopes.Last();
if (!string.IsNullOrWhiteSpace(SI.Text))
{
S = SI.Text;
}
else
{
}
}
SB.Append(Pad(S, Lengths["Scope"]));
string Text = Info.Text;
/* writing properties is too much for a text file logger
if (Info.StateProperties != null && Info.StateProperties.Count > 0)
{
Text = Text + " Properties = " + Newtonsoft.Json.JsonConvert.SerializeObject(Info.StateProperties);
}
*/
if (!string.IsNullOrWhiteSpace(Text))
{
SB.Append(Text.Replace("\r\n", " ").Replace("\r", " ").Replace("\n", " "));
}
SB.AppendLine();
WriteLine(SB.ToString());
}
}
void ThreadProc()
{
Task.Run(() => {
while (!Terminated)
{
try
{
WriteLogLine();
System.Threading.Thread.Sleep(100);
}
catch // (Exception ex)
{
}
}
});
}
protected override void Dispose(bool disposing)
{
Terminated = true;
base.Dispose(disposing);
}
public FileLoggerProvider(IOptionsMonitor Settings)
: this(Settings.CurrentValue)
{
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens
SettingsChangeToken = Settings.OnChange(settings => {
this.Settings = settings;
});
}
public FileLoggerProvider(FileLoggerOptions Settings)
{
PrepareLengths();
this.Settings = Settings;
// create the first file
BeginFile();
ThreadProc();
}
public override bool IsEnabled(LogLevel logLevel)
{
bool Result = logLevel != LogLevel.None
&& this.Settings.LogLevel != LogLevel.None
&& Convert.ToInt32(logLevel) >= Convert.ToInt32(this.Settings.LogLevel);
return Result;
}
public override void WriteLog(LogEntry Info)
{
InfoQueue.Enqueue(Info);
}
internal FileLoggerOptions Settings { get; private set; }
}
FileLoggerExtensions包含将文件记录器提供程序(别名为'File')添加到可用服务中作为单例的方法,并将文件记录器选项类绑定到appsettings.json文件的'File'部分。
如您所见,没有ILoggerFactory扩展,只有扩展ILoggingBuilder。这意味着您应该在Program.cs中注册文件记录器提供程序,如上所示,而不是在Startup类中。检查AspNet.Core代码,关于类似的扩展方法,似乎注册记录器提供程序ILoggerFactory已经过时了。
static public class FileLoggerExtensions
{
static public ILoggingBuilder AddFileLogger(this ILoggingBuilder builder)
{
builder.AddConfiguration();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
return builder;
}
static public ILoggingBuilder AddFileLogger(this ILoggingBuilder builder, Action configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.AddFileLogger();
builder.Services.Configure(configure);
return builder;
}
}
原文地址:https://www.codeproject.com/Articles/1556475/How-to-Write-a-Custom-Logging-Provider-in-ASP-NET