做个极简的文本日志收集
或许大家会疑问,已经有了强大的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(); }
这样就可以在网站关闭前清空日志了。