• 重复造轮子,编写一个轻量级的异步写日志的实用工具类(LogAsyncWriter)


    一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net、NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用法及配置文件,这对于有些小工具、小程序、小网站来说,有点“杀鸡焉俺用牛刀”的感觉,而且如果对这些日志框架不了解,可能输出来的日志性能或效果未毕是与自己所想的,鉴于这几个原因,我自己重复造轮子,编写了一个轻量级的异步写日志的实用工具类(LogAsyncWriter),这个类还是比较简单的,实现思路也很简单,就是把消息日志先入内存队列,然后由异步监听线程从队列中取出日志并批量输出到本地文件中,同时参照各大日志框架,对单个文件过大采取分割生成多个日志文件。

    经测试发现性能非常不错,先看示例使用代码:(采取并发多线程同时写入1000000万条日志)

                Task.Factory.StartNew(() =>
                {
                    DateTime startTime = DateTime.Now;
                    int logCount = 1000000;
                    Parallel.For(1, logCount, (i) =>
                    {
                        if (i % 2 == 0)
                        {
                            LogAsyncWriter.Default.Error("测试并发写错误日志-" + i.ToString(), "TestClass.TestLog", i.ToString());
                        }
                        else
                        {
                            LogAsyncWriter.Default.Info("测试并发写普通日志-" + i.ToString(), "TestClass.TestLog", i.ToString());
                        }
                    });
    
                    this.Invoke(new MethodInvoker(() =>
                    {
                        MessageBox.Show(DateTime.Now.ToString() + "," + logCount + "条日志写完了!,耗时:" + (DateTime.Now - startTime).TotalMilliseconds + "ms");
                    }));
                });
    
                MessageBox.Show(DateTime.Now.ToString() + ",同步方法已结束");
            }
    

     执行效果如下图示:

    因为采用异步,故方法先走到结尾,输出了同步的MsgBox,随后弹出的是100W日志输出到文件后的耗时MsgBox,从截图可以看出,不足1S(当然这里的1S不是真实的输出到本地方件,而是把所有的日志推到了Queue中而矣,但不影响不阻塞业务处理),而本地日志文件的大小达到了263MB(设置最大值的MaxSizeBackup,使其不滚动备份),由此看性能是不错的;

    因为是异步延迟输出到本地日志文件,故大家在代码中任意地方,比如:循环中都可以使用它,不用担心会影响你的正常的业务逻辑的执行效率;

    如果采取滚动备份(设置MaxSizeBackup为一个合理值,默认为10MB),则生成的日志文件形式如下:(超过最大容量则生成同名文件+2位序号),超过一个月则会自动清除

    打开日志文件会看到所有的日志内容:

    有些人可能要问,输出格式是固定的吗?能否自定义布局,告诉你是可以的,我考虑到每个人对日志输出的格式都有严格的要求,故可以通过设置:LineLayoutRenderFormat属性来设置每条日志输出的格式内容

     好了,介绍了使用功能及效果、性能,下面贴出源代码:

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Text.RegularExpressions;
    
    namespace Zuowj.Common
    {
        /// <summary>
        /// 日志异步生成器
        /// Author:Zuowenjun(http://www.zuowenjun.cn)
        /// Date:2018-6-14
        /// </summary>
        public class LogAsyncWriter
        {
            public const string InfoLevel = "INFO";
            public const string WarnLevel = "WARN";
            public const string ErrorLevel = "ERROR";
    
            private readonly ConcurrentQueue<string[]> logMsgQueue = new ConcurrentQueue<string[]>();
            private readonly CancellationTokenSource cts = null;
            private string lineLayoutRenderFormat = "[{0:yyyy-MM-dd HH:mm:ss}]	{1}	{2}	{3}:{4},Trace:{5};Other1:{6},Other2:{7},Other3:{8}";
            private long maxSizeBackup = 10485760L;//默认10MB
            private string todayLogName = null;
    
            private static readonly LogAsyncWriter instance = new LogAsyncWriter();
    
    
            private LogAsyncWriter()
            {
                cts = new CancellationTokenSource();
                ListenSaveLogAsync(cts.Token);
            }
    
            private void ListenSaveLogAsync(CancellationToken cancellationToken)
            {
                Task.Factory.StartNew(() =>
                {
                    DateTime lastSaveLogTime = DateTime.Now;
                    while (!cancellationToken.IsCancellationRequested)//如果没有取消线程,则一直监听执行写LOG
                    {
                        if (logMsgQueue.Count >= 10 || (logMsgQueue.Count > 0 && (DateTime.Now - lastSaveLogTime).TotalSeconds > 30))//如是待写日志消息累计>=10条或上一次距离现在写日志时间超过30s则需要批量提交日志
                        {
                            List<string[]> logMsgList = new List<string[]>();
                            string[] logMsgItems = null;
    
                            while (logMsgList.Count < 10 && logMsgQueue.TryDequeue(out logMsgItems))
                            {
                                logMsgList.Add(logMsgItems);
                            }
    
                            WriteLog(logMsgList);
    
                            lastSaveLogTime = DateTime.Now;
                        }
                        else
                        {
                            SpinWait.SpinUntil(() => logMsgQueue.Count >= 10, 5000);//自旋等待直到日志队列有>=10的记录或超时5S后再进入下一轮的判断
                        }
                    }
                }, cancellationToken);
            }
    
            private string GetLogFilePath()
            {
                string logFileDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
                if (!Directory.Exists(logFileDir))
                {
                    Directory.CreateDirectory(logFileDir);
                }
    
                string logDateStr = DateTime.Now.ToString("yyyyMMdd");
                string logName = logDateStr;
                if (!string.IsNullOrEmpty(todayLogName) && todayLogName.StartsWith(logName))
                {
                    logName = todayLogName;
                }
                else
                {
                    todayLogName = logName;
                }
    
                string logFilePath = Path.Combine(logFileDir, logName + ".log");
    
                if (File.Exists(logFilePath))
                {
                    File.SetAttributes(logFilePath, FileAttributes.Normal);
                    if (File.GetLastWriteTime(logFilePath).Month != DateTime.Today.Month) //30天滚动(删除旧的文件),防止日志文件过多
                    {
                        File.Delete(logFilePath);
                        string[] oldLogFiles = Directory.GetFiles(logFileDir, string.Format("{0}-##.log", logDateStr), SearchOption.TopDirectoryOnly);
                        foreach (string fileName in oldLogFiles)
                        {
                            File.SetAttributes(fileName, FileAttributes.Normal);
                            File.Delete(fileName);
                        }
                    }
                    else if (new FileInfo(logFilePath).Length > MaxSizeBackup)
                    {
                        Regex rgx = new Regex(@"^d{8}-(?<fnum>d{2})$");
                        int fnum = 2;
                        if (rgx.IsMatch(logName))
                        {
                            fnum = int.Parse(rgx.Match(logName).Groups["fnum"].Value) + 1;
                        }
    
                        logName = string.Format("{0}-{1:D2}", logDateStr, fnum);
                        todayLogName = logName;
                        logFilePath = Path.Combine(logFileDir, logName + ".log");
                    }
                }
    
                return logFilePath;
            }
    
            private void WriteLog(IEnumerable<string[]> logMsgs)
            {
                try
                {
                    List<string> logMsgLines = new List<string>();
                    foreach (var logMsgItems in logMsgs)
                    {
                        var logMsgLineFields = (new object[] { DateTime.Now }).Concat(logMsgItems).ToArray();
                        string logMsgLineText = string.Format(LineLayoutRenderFormat, logMsgLineFields);
                        logMsgLines.Add(logMsgLineText);
                    }
    
                    string logFilePath = GetLogFilePath();
                    File.AppendAllLines(logFilePath, logMsgLines);
                }
                catch
                { }
            }
    
    
    
            public static LogAsyncWriter Default
            {
                get
                {
                    return instance;
                }
            }
    
            public string LineLayoutRenderFormat
            {
                get { return lineLayoutRenderFormat; }
                set
                {
                    if (string.IsNullOrWhiteSpace(value))
                    {
                        throw new ArgumentException("无效的LineLayoutRenderFormat属性值");
                    }
    
                    lineLayoutRenderFormat = value;
                }
            }
    
            public long MaxSizeBackup
            {
                get { return maxSizeBackup; }
                set
                {
                    if (value <= 0)
                    {
                        throw new ArgumentException("无效的MaxSizeBackup属性值");
                    }
    
                    maxSizeBackup = value;
                }
            }
    
            public void SaveLog(string logLevel, string msg, string source, string detailTrace = null, string other1 = null, string other2 = null, string other3 = null)
            {
                logMsgQueue.Enqueue(new[] { logLevel, Thread.CurrentThread.ManagedThreadId.ToString(), source, msg, detailTrace ?? string.Empty, other1 ?? string.Empty, other2 ?? string.Empty, other3 ?? string.Empty });
            }
    
            public void Info(string msg, string source, string detailTrace = null, string other1 = null, string other2 = null, string other3 = null)
            {
                SaveLog(InfoLevel, msg, source, detailTrace, other1, other2, other3);
            }
    
            public void Warn(string msg, string source, string detailTrace = null, string other1 = null, string other2 = null, string other3 = null)
            {
                SaveLog(WarnLevel, msg, source, detailTrace, other1, other2, other3);
            }
    
            public void Error(string msg, string source, string detailTrace = null, string other1 = null, string other2 = null, string other3 = null)
            {
                SaveLog(ErrorLevel, msg, source, detailTrace, other1, other2, other3);
            }
    
            public void Error(Exception ex, string source, string other1 = null, string other2 = null, string other3 = null)
            {
                SaveLog(ErrorLevel, ex.Message, source, ex.StackTrace, other1, other2, other3);
            }
    
            ~LogAsyncWriter()
            {
                cts.Cancel();
            }
    
        }
    }
    

     代码重点说明:

    1.各种日志方法入参解释:

    i. Msg:日志消息(一般是指简要消息)
    ii. Source:日志产生源(一般是指该日志是由哪个位置产生的,可以定义为:类.方法名)
    iii. detailTrace:日志详情(一般是指堆栈信息或日志更具体的信息)
    iv. other1, other2, other3:备用日志字段,可根据实际情况记录相关信息,比如:入参、返参,执行耗时等;
    v. log Level:日志消息级别一般有很多级别,但常用的只有3类,即:Info=普通日志消息,Warn=警告日志消息,Error=错误日志消息;

    2.核心异步批量写日志的方法:(采用后台线程监听日志消息队列,当达到10条日志消息或写日志的时间间隔超过1分钟,则会批量提交1次日志,解决了普通的同步写日志方法造成写压力过大,且存在阻塞业务逻辑的情况)

    3.采用单例模式,当第一次使用该类时,则会启动异步监听线程,当该类释放时(一般指应用进程关闭时,会发送通知线程取消监听,避免一切可能的线程驻留问题)

    好了就介绍到这里,大家若想试用或想改造,可以直接复制上述代码,不足之处可以指出,谢谢!

  • 相关阅读:
    Android实现通过浏览器点击链接打开本地应用(APP)并拿到浏览器传递的数据(转)
    保存图片文件到本地
    android ScrollView中嵌套GridView,ListView只显示一行的解决办法
    蒙版提示页(添加新功能后的一种提示)
    C和指针 第三章--数据
    *(ptr++) += 123
    优先级队列-堆
    单链表相关(一)
    字符间的距离-动态规划
    和最大的连续子数组
  • 原文地址:https://www.cnblogs.com/zuowj/p/9182554.html
Copyright © 2020-2023  润新知