做個極簡的文本日志收集
或許大家會疑問,已經有了強大的log4net,nlog等,為啥還要自己折騰寫日志呢,那是因為最近我有個需求,把所有的操作記錄到日志文件里,然后運維每天自動把這些日志同步到kibana做日志收集,然后分析處理。
其實一開始我是想直接讓他們做一個接口,然后我每次的操作都調用一次他們的接口,這樣也可以同步日志,但老大認為這種高頻低價值并且無需實時的數據沒必要動用接口,這樣其實是一種浪費,先寫日志,然后統一處理更高效。我想了一下,貌似確實是沒必要動用接口。
那么自己寫日志咋寫呢,一開始我是直接簡單粗暴每來一條日志,就寫一次文件:
/// <summary> /// 記錄推送日志 /// </summary> /// <param name="messageId">消息ID</param> /// <param name="status">推送狀態</param> /// <param name="brand">品牌</param> public static void AddPushLog(string messageId, PushStatus status, string brand) { if (string.IsNullOrEmpty(messageId)) { return; } var now = DateTime.Now; var fileName = $"{PassportConfig.Env.ContentRootPath}/pushlog/{now:yyyyMMdd}.csv"; File.AppendAllTextAsync(fileName, $"{messageId},{brand},{(int)status},{now.ToUnixTimestamp()}\n"); }
但是沒過幾天,我發現日志有點問題,會出現日志黏連現象,就是兩條日志黏在一起了,之所以這樣,是因為寫入太頻繁了,導致兩個寫入同時發生了,所以他們的寫入就可能黏在一起。
那么該如何避免這種并行事件呢,而且每來一條日志就寫一次日志確實性能也不佳。
我想了一下,那就1分鐘寫入一次吧,寫入先放在生產者列表,然后搞個后臺任務,每分鐘去查看一下生產者列表,發現有日志,則把生產者交給消費者,然后生產者清空后繼續生產,消費者就把這批次的日志批量寫入日志文件,代碼如下:
//日志生產者 private static List<string> _logsProducer = new List<string>(); //日志消費者 private static List<string> _logsConsumer; //日志臨時存放,用來交換生產者和消費者 private static List<string> _logsTemp = new List<string>(); /// <summary> /// 記錄推送日志 /// </summary> /// <param name="messageId">消息ID</param> /// <param name="status">推送狀態</param> /// <param name="brand">品牌</param> public static void AddPushLog(string messageId, PushStatus status, string brand) { if (!string.IsNullOrEmpty(messageId)) { _logsProducer.Add($"{messageId},{brand},{(int)status},{DateTime.Now.ToUnixTimestamp()}"); } } /// <summary> /// 清空push日志,寫入到push日志文件 /// </summary> public static void FlushPushLog() { //沒日志則不消費 if (_logsProducer.Count == 0) { return; } _logsConsumer = _logsProducer; _logsProducer = _logsTemp; var now = DateTime.Now; var fileName = $"{PassportConfig.Env.ContentRootPath}/pushlog/{now:yyyyMMdd}.csv"; File.AppendAllLines(fileName, _logsConsumer); _logsConsumer.Clear(); _logsTemp = _logsConsumer; }
代碼很簡潔,發現有日志時,
_logsConsumer = _logsProducer,這時候生產者和消費者指向了同一個列表,這時候
_logsProducer繼續增加日志的話,
_logsConsumer也會增加,因為他們指向了同一片內存,然后
_logsProducer = _logsTemp,因為
_logsTemp是空的,所以生產者相當于被清空了,這樣就完成了生產者和消費者交換數據的操作,然后消費者得到了生產者的數據后,就把日志通過
File.AppendAllLines一次性寫入到日志文件,我試過了,幾萬行幾十行日志寫入到文本也是一下子的事情,寫完日志后清空消費者日志列表,然后
_logsTemp = _logsConsumer就把消費者列表交還給臨時列表了,這樣操作就結束了。大家可以發現全程無鎖,全程只用到了兩個List,另一個List只是中轉用的,并沒有new出來的。
那么接下來只需要搞一個后臺任務去定時清空寫入日志即可,代碼如下:
/// <summary> /// 每分鐘寫一次push日志 /// </summary> public class PushLogService : BackgroundService { private readonly ILogger<PushLogService> _logger; /// <summary> /// 每分鐘寫一次push日志 /// </summary> private const int Sleep = 60000; public PushLogService(ILogger<PushLogService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { PushHelper.FlushPushLog(); } catch (Exception e) { _logger.LogError(e, "PushLogError"); } finally { await Task.Delay(Sleep); } } } }
這樣就完事了嗎,其實還有個問題,萬一網站關閉沒來得及清空日志咋辦,不怕,在網站關閉事件里清空一下日志就好了:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IHostApplicationLifetime applicationLifetime) { applicationLifetime.ApplicationStopping.Register(OnShutdown); } private void OnShutdown() { PushHelper.FlushPushLog(); }
這樣就可以在網站關閉前清空日志了。
浙公網安備 33010602011771號