如何在ASP.NET Core中编写自定义日志记录提供程序

1095 篇文章 54 订阅
622 篇文章 16 订阅

目录

介绍

如何实现所需的接口

基础类和附件

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>(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);
    }
}

如何实现所需的接口

ILoggerProvider唯一的目的是在ILogger框架询问时创建实例。

ILogger提供了Log()方法。调用Log()生成一个日志信息单元,一个日志条目。

这两个代码元素中的哪一个应该负责显示或持久化该日志条目?

通过学习ASP.NET核心代码的研究,可以明显看出这一责任属于ILogger实现,比如ConsoleLoggerDebugLoggerEventLogLogger类。我们也应该这样做吗?

如果答案是肯定的,那么我们需要一个对任何媒体的ILogger ILoggerProvider实现,比如文本文件,数据库或消息队列。

如果答案是否定的,那么我们只需要一个通用ILogger实现,并且只需要一个适用于任何不同介质的ILoggerProvider实现。

我们将遵循第二种方法。

一个生成日志信息单元的通用Logger类,将该信息打包到LogEntry类的实例中,然后将该实例传递给其创建者LoggerProvider以进行进一步处理。LoggerProvider将是一个基类,因此任何关于任何不同介质、文本文件、数据库等的专业化都会进入后代LoggerProvider类。

我们将应用上述想法并创建一个FileLoggerProvider类。

基础类和附件

LogEntry表示日志条目的信息。在调用LoggerLog()方法时创建此类的实例,填充属性,然后将该信息传递给提供者调用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表示Scope有关LogEntry的信息。

public class LogScopeInfo
{ 
    public LogScopeInfo()
    {
    }
 
    public string Text { get; set; }
    public Dictionary<string, object> 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>(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 takes the exception into account
            // SEE:  <a href="https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging.Abstractions/src/LoggerExtensions.cs">https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging.Abstractions/src/LoggerExtensions.cs</a>
            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: <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging#log-message-template">https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging#log-message-template</a>
            // SEE: <a href="https://softwareengineering.stackexchange.com/questions/312197/benefits-of-structured-logging-vs-basic-logging">https://softwareengineering.stackexchange.com/questions/312197/benefits-of-structured-logging-vs-basic-logging</a>
            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是一个abstract基本记录器提供程序类。记录器提供程序实质上代表保存或显示日志信息的媒体。此类可以在编写文件或数据库记录器提供程序时充当基类。

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; }
}

FileLoggerProvider具体类及其附件

FileLoggerOptionsOptions文件记录器的类。

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; }
    }
}

有两种配置文件记录器选项的方法:

  1. Program.cs使用ConfigureLogging()并通过调用AddFileLogger()的第二个版本,使用选项委托,或
  2. 使用appsettings.json文件。

1. ConfigureLogging()

.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<FileLoggerOptions>
{
    public FileLoggerOptionsSetup(ILoggerProviderConfiguration<FileLoggerProvider> providerConfiguration)
        : base(providerConfiguration.Configuration)
    {
    }
}

FileLoggerProvider是一个记录器提供程序,它将日志条目写入文本文件。 File是此提供程序的提供程序别名,可以在appsettings.jsonLogging部分中使用,请参阅上文。

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<string, int> Lengths = new Dictionary<string, int>();
    
    ConcurrentQueue<LogEntry> InfoQueue = new ConcurrentQueue<LogEntry>();
 
    void ApplyRetainPolicy()
    {
        FileInfo FI;
        try
        {
            List<FileInfo> 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<FileLoggerOptions> Settings)
        : this(Settings.CurrentValue)
    {  
        // <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens">https://docs.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens</a>
        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<ILoggerProvider, FileLoggerProvider>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<FileLoggerOptions>, FileLoggerOptionsSetup>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<FileLoggerOptions>, LoggerProviderOptionsChangeTokenSource<FileLoggerOptions, FileLoggerProvider>>());
        return builder;
    }
 
    static public ILoggingBuilder AddFileLogger(this ILoggingBuilder builder, Action<FileLoggerOptions> 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值