ASP.NET Core 源碼學習之 Logging[4]:FileProvider

来源:http://www.cnblogs.com/RainingNight/archive/2017/08/11/asp-net-core-logging-file.html
-Advertisement-
Play Games

前面幾章介紹了 ASP.NET Core Logging 系統的配置和使用,而對於 Provider ,微軟也提供了 Console, Debug, EventSource, TraceSource 等,但是沒有我們最常用的 FilePrivider,而比較流行的 Log4Net , NLog 等也 ...


前面幾章介紹了 ASP.NET Core Logging 系統的配置和使用,而對於 Provider ,微軟也提供了 Console, Debug, EventSource, TraceSource 等,但是沒有我們最常用的 FilePrivider,而比較流行的 Log4Net , NLog 等也對 ASP.NET Core 的 Logging 系統提供了擴展,但是太過於複雜,而且他們本身就是一個完整的日誌系統,功能上會有較多的重合,所以我們不妨自己動手,寫一個輕量級的完全基於 ASP.NET Core Logging 系統的 FileProvider

IMessageWriter

首先定義一個日誌寫入介面:

public interface IMessageWriter : IDisposable
{
    Task WriteMessagesAsync(string message, CancellationToken cancellationToken = default(CancellationToken));
}

只有一個非同步的寫日誌方法,用來將日誌寫入到文件或者隊列中。

FileWriter

IMessageWriter 最核心的實現,將日誌寫入到文件中。

public class FileWriter : IMessageWriter, IDisposable
{
    ...

    public FileWriter(string path, long? fileSizeLimit = null)
    {
        ...
        _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
        _output = new StreamWriter(_underlyingStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
    }

    public async Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        if (_maxFileSize > 0 && _underlyingStream.Length > _maxFileSize)
        {
            return;
        }
        await _output.WriteAsync(message);
        FlushToDisk();
    }

    ...
}

其實現很簡單,就是使用最基本的文件 Stream 來寫入文件 ,並立即刷新到磁碟。

BatchingWriter

上面 FileWriter 最大的弊端就是每次寫日誌都要進行一次文件IO操作,效率較低,可以使用定時器,來定時刷新到磁碟,來提高性能。不過,在 Logging.AzureAppServices 中發現了更好的實現方式,即使用批量提交:

public class BatchingWriter : IMessageWriter, IDisposable
{
    ...

    public BatchingWriter(IMessageWriter writer, TimeSpan interval, int? batchSize, int? queueSize)
    {
        ...
        Start();
    }

    private void Start()
    {
        _messageQueue = _queueSize == null ?
            new BlockingCollection<string>(new ConcurrentQueue<string>()) :
            new BlockingCollection<string>(new ConcurrentQueue<string>(), _queueSize.Value);

        _cancellationTokenSource = new CancellationTokenSource();
        _outputTask = Task.Factory.StartNew<Task>(
            ProcessLogQueue,
            null,
            TaskCreationOptions.LongRunning);
    }

    private async Task ProcessLogQueue(object state)
    {
        StringBuilder currentBatch = new StringBuilder();
        while (!_cancellationTokenSource.IsCancellationRequested)
        {
            var limit = _batchSize ?? int.MaxValue;
            while (limit > 0 && _messageQueue.TryTake(out var message))
            {
                currentBatch.Append(message);
                limit--;
            }
            if (currentBatch.Length > 0)
            {
                try
                {
                    await _writer.WriteMessagesAsync(currentBatch.ToString(), _cancellationTokenSource.Token);
                }
                catch
                {
                    // ignored
                }
            }
            await IntervalAsync(_interval, _cancellationTokenSource.Token);
        }
    }

    protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
    {
        return Task.Delay(interval, cancellationToken);
    }

    private void Stop()
    {
        ...
    }

    public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        if (!_messageQueue.IsAddingCompleted)
        {
            try
            {
                _messageQueue.Add(message, _cancellationTokenSource.Token);
            }
            catch
            {
                //cancellation token canceled or CompleteAdding called
            }
        }
        return Task.CompletedTask;
    }

    ...
}

首先定義了一個併發隊列,每次寫入只需要將日誌保存到隊列當中,通過配置獲取執行周期來定期從隊列中取出日誌,再使用上面的 FileWriter 來持久化到磁碟。

RollingFileWriter

使用上面兩個類,已滿足了最基本的寫日誌功能,但是在 Log4Net 等日誌框架中,我們經常會按一定的頻度滾動日誌記錄文件,也就是 RollingFile 功能,可實現將每天或每小時的日誌保存到一個文件中,按文件大小進行滾動等功能。

首先是定義了一個 RollingFrequency 類,用來根據配置的文件名,來獲取滾動頻率,比如我們指定日誌文件名為 Logs\my-{Date}.log,則表示每天滾動一次。

public class RollingFrequency
{
    public static readonly RollingFrequency Date = new RollingFrequency("Date", "yyyyMMdd", TimeSpan.FromDays(1));
    public static readonly RollingFrequency Hour = new RollingFrequency("Hour", "yyyyMMddHH", TimeSpan.FromHours(1));

    public string Name { get; }
    public string Format { get; }
    public TimeSpan Interval { get; }

    RollingFrequency(string name, string format, TimeSpan interval)
    {
        if (name == null) throw new ArgumentNullException(nameof(name));
        Format = format ?? throw new ArgumentNullException(nameof(format));
        Name = "{" + name + "}";
        Interval = interval;
    }

    public DateTime GetCurrentCheckpoint(DateTime instant)
    {
        if (this == Hour)
        {
            return instant.Date.AddHours(instant.Hour);
        }
        return instant.Date;
    }

    public DateTime GetNextCheckpoint(DateTime instant) => GetCurrentCheckpoint(instant).Add(Interval);

    public static bool TryGetRollingFrequency(string pathTemplate, out RollingFrequency specifier)
    {
        if (pathTemplate == null) throw new ArgumentNullException(nameof(pathTemplate));
        var frequencies = new[] { Date, Hour }.Where(s => pathTemplate.Contains(s.Name)).ToArray();
        specifier = frequencies.LastOrDefault();
        return specifier != null;
    }
}

再看一下 RollingFileWriter

public class RollingFileWriter : IMessageWriter, IDisposable
{
    ...

    public RollingFileWriter(string pathFormat, long? fileSizeLimitBytes = null, int? retainedFileCountLimit = null)
    {
        ...
    }

    public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        AlignFileWriter();
        return _currentFileWriter.WriteMessagesAsync(message, cancellationToken);
    }

    private void AlignFileWriter()
    {
        DateTime now = DateTime.Now;
        if (!_nextCheckpoint.HasValue)
        {
            OpenFileWriter(now);
        }
        else if (now >= _nextCheckpoint.Value)
        {
            CloseFileWriter();
            OpenFileWriter(now);
        }
    }

    private void OpenFileWriter(DateTime now)
    {
        var currentCheckpoint = _roller.GetCurrentCheckpoint(now);
        _nextCheckpoint = _roller.GetNextCheckpoint(now);

        var existingFiles = Enumerable.Empty<string>();
        try
        {
            existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern).Select(Path.GetFileName);
        }
        catch (DirectoryNotFoundException) { }

        var latestForThisCheckpoint = _roller
            .SelectMatches(existingFiles)
            .Where(m => m.DateTime == currentCheckpoint)
            .OrderByDescending(m => m.SequenceNumber)
            .FirstOrDefault();

        var sequence = latestForThisCheckpoint != null ? latestForThisCheckpoint.SequenceNumber : 0;

        const int maxAttempts = 3;
        for (var attempt = 0; attempt < maxAttempts; attempt++)
        {
            string path = _roller.GetLogFilePath(now, sequence);
            try
            {
                _currentFileWriter = new FileWriter(path, _maxfileSizeLimit);
            }
            catch (IOException)
            {
                sequence++;
                continue;
            }
            RollFiles(path);
            return;
        }
    }

    // 刪除超出保留文件數的日誌文件
    private void RollFiles(string currentFilePath)
    {
        if (_maxRetainedFiles > 0)
        {
            var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern)
                .Select(Path.GetFileName);
            var moveFiles = _roller
                .SelectMatches(potentialMatches)
                .OrderByDescending(m => m.DateTime)
                .ThenByDescending(m => m.SequenceNumber)
                .Skip(_maxRetainedFiles.Value)
                .Select(m => m.Filename);
            foreach (var obsolete in moveFiles)
            {
                System.IO.File.Delete(Path.Combine(_roller.LogFileDirectory, obsolete));
            }
        }
    }

    ...
}

根據滾動頻率指定應該創建的文件名,然後調用 FileWriter 進行寫入,具體代碼可以去看文末貼的 GitHub 地址。

FileLogger

FileLogger 則是由上一章講到的 Logger 來調用的,而在這裡,它的作用是首先對日誌進行過濾,然後將日誌組裝成字元串,再調用我們前面定義的 IMessageWriter 進行日誌的寫入:

public class FileLogger : ILogger, IDisposable
{
    ...

    public FileLogger(IMessageWriter writer, string category, Func<string, LogLevel, bool> filter)
    {
        ...
    }

    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 builder = new StringBuilder();
        builder.Append(DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
        builder.Append(" [");
        builder.Append(GetLogLevelString(logLevel));
        builder.Append("] ");
        builder.Append(_category);
        builder.Append("[");
        builder.Append(eventId);
        builder.Append("]");
        builder.Append(": ");
        builder.AppendLine(formatter(state, exception));
        if (exception != null)
        {
            builder.AppendLine(exception.ToString());
        }
        _writer.WriteMessagesAsync(builder.ToString()).Wait();
    }

    ...
}

在這裡,日誌的拼裝是寫死的,後續可以提供一個可配置的日誌渲染器,來自定義輸出格式。

ConsoleLoggerProvider

FileLoggerProvider 的唯一職責就是創建 FileLogger

[ProviderAlias("File")]
public class FileLoggerProvider : ILoggerProvider
{
    ...

    public FileLoggerProvider(IOptionsMonitor<FileLoggerOptions> options)
    {
        _optionsChangeToken = options.OnChange(UpdateOptions);
        UpdateOptions(options.CurrentValue);
    }

    private void UpdateOptions(FileLoggerOptions options)
    {
        if (RollingFrequency.TryGetRollingFrequency(options.Path, out var r))
        {
            _msgWriter = new RollingFileWriter(options.Path, options.FileSizeLimit, options.RetainedFileCountLimit);
        }
        else
        {
            _msgWriter = new FileWriter(options.Path, options.FileSizeLimit);
        }
        if (options.IsEnabledBatching)
        {
            _msgWriter = new BatchingWriter(_msgWriter, options.FlushPeriod, options.BatchSize, options.BackgroundQueueSize);
        }
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new FileLogger(_msgWriter, categoryName, _filter);
    }

    ...
}

首先是 ProviderAlias 特性,為 Provider 指定一個別名,這樣,我們在配置文件中指定 Provider 時,使用別名即可,然後使用了 IOptionsMonitor 模式,監控配置的變化,併進行更新,而不用去重啟Web伺服器。

FileLoggerFactoryExtensions

最後便是提供擴展方法,方便我們在 Program 中對日誌系統進行配置。而擴展方法的實現只是很簡單的將我們定義的 FileProvider 註入進去:

public static class FileLoggerFactoryExtensions
{
    ...

    public static ILoggingBuilder AddFile(this ILoggingBuilder builder, IConfiguration configuration)
    {
        builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration));
        builder.Services.Configure<FileLoggerOptions>(configuration);
        builder.Services.AddSingleton<IConfigureOptions<FileLoggerOptions>>(new FileLoggerConfigureOptions(configuration));
        builder.AddFile();
        return builder;
    }

    ...
}

只提供了 ILoggingBuilder 的擴展,而不再提供 ILoggerFactory 的擴展方法,全力擁抱 .NET Core 2.0

總結

通過對網上各種流行的開源日誌框架學習借鑒,寫了一個 ASP.NET Core 的 Logging 系統的文件擴展,還有很多不足之處,但更多的是一種探索,學習,藉此也對 Logging 系統更加瞭解。而後續會再研究一下分散式日記系統。

最後附上本文所示代碼地址:zero-logging


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原文發表於cu:2016-05-05 參考文檔: 一.環境 Server:CentOS-7-x86_64-1511 Client:Win7 x86_64, Chrome/Firefox瀏覽器 二.準備 1. iptables CentOS7預設自帶firewall,無iptables。 2. SEL ...
  • 如果你的伺服器的總是報告記憶體不足,並且時常因為記憶體不足而引發服務被強制kill的話,在不增加物理記憶體的情況下,啟用swap交換區作為虛擬記憶體是一個不錯的選擇,如果是SSD硬碟,正常讀寫速度都在300MB/s以上,啟用swap後性能提高了不少,特別是在處理消耗大記憶體的腳本方面。 ...
  • 0x00. Command Notation[Name] The name of the command.[Format] The code sequence. [ ]k indicates the contents of the [ ] s... ...
  • 聲明:本文為轉載的文章;並非由本人創作;發博文只是為了整理、記錄。 推薦的比較完全,比較清晰的文章(含圖):http://blog.csdn.net/sunny04/article/details/40627311 轉載時請註明出處和作者聯繫方式:http://blog.csdn.net/absur ...
  • 本文目錄: 1.8.1 匿名管道"|" 1.8.2 重定向 1.8.2.1 重定向基礎 1.8.2.2 cat和重定向配合 1.8.2.3 tee雙重定向 1.8.2.4 <<和<<< 1.8.1 匿名管道"|" 管道符號意如其名,類似管道一樣將管道入口的數據通過管道傳遞給管道出口。 管道是為瞭解決 ...
  • 由於業務的關係我們用的是阿裡雲的ECS主機,需要對業務進程需要監控,查看後發現阿裡雲提供自定義監控SDK,這有助於我們定製化的根據自身業務來做監控,下麵我就根據業務需求來介紹一個簡單的自定義監控配置 備註:我用的shell腳本用來部署的 業務需求:我們需要監控ECS伺服器中某個進程是否存在,如果小於 ...
  • DHCP前身是BOOTP,在Linux的網卡配置中也能看到顯示的是BOOTP,DHCP引進一個bootp沒有的概念:租約。bootp分配的地址是永久的,而dhcp分配的地址是可以有期限的。 DHCP可以自動分配IP、子網掩碼、網關、DNS。 DHCP客戶端使用的埠68,服務端使用埠67,使用的U ...
  • 在nginx中配置偽靜態,也就是常說的url重寫功能,只需在nginx.conf配置文件中寫入重寫規則即可。 當然,這個規則是需要熟悉正則表達式,只掌握nginx自身的正則匹配模式即可,對正則不瞭解的朋友,建議補一下這方面的知識。 下麵,收集了幾篇關於nginx rewrite重寫的教程文章,感興趣 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...