c# 编写一个轻量级的异步写日志的实用工具类(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}]\t{1}\t{2}\t{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.采用单例模式,当第一次使用该类时,则会启动异步监听线程,当该类释放时(一般指应用进程关闭时,会发送通知线程取消监听,避免一切可能的线程驻留问题)

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

以上就是c# 编写一个轻量级的异步写日志的实用工具类(LogAsyncWriter)的详细内容,更多关于c# 编写异步写日志工具类的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#异步方法返回void与Task的区别详解

    C#异步方法返回void和Task的区别 如果异步(async关键字)方法有返回值,返回类型为T时,返回类型必然是 Task<T>. 但是如果没有返回值,异步方法的返回类型有2种,一个是返回 Task, 一个是返回 void: public async Task CountDownAsync(int count) { for (int i = count; i >= 0; i--) { await Task.Delay(1000); } } public async void Count

  • C#打印日志的方法总结

    在我们对程序进行操作过程中,一般都需要有一个操作流程的记录显示.用C#进行编程时可以很容易实现这个功能.本经验提供案例仅供参考 下面小编就来介绍一下如何使用textbox控件实现日志功能. 打开Visual Studio 2010,建立一个新的C#程序.在工具箱中双击[textbox]控件. 在界面上放置一个[textbox]控件作为日志显示,同时添加一个按钮控件. 添加日志显示的方法[displaylog],在textbox控件中输入信息. 在load方法中调用[displaylog]方法,输

  • c# winform异步不卡界面的实现方法

    快速阅读 如何在winform程序中,让界面不再卡死. 关于委托和AsyncCallback的使用. 界面卡死的原因是因为耗时任务的计算占用了主线程,导致主界面没有办法进行其它操作,比如拖动.造成界面卡死的现象.我们只需要把耗时任务放在子线程中执行就可以了. 子线程的计算结果 要更新到界面中,怎么更新呢,因为不能操作主线程 ,所以要用委托来实现 . 我们来看个例子. 场景 界面上一个按钮加一人richbox , 点击以后获得当前所在年份 代码实现 定义一个委托实现子线程更新主线程 public

  • c#中Winform实现多线程异步更新UI(进度及状态信息)

    引言 在进行Winform程序开发需要进行大量的数据的读写操作的时候,往往会需要一定的时间,然在这个时间段里面,界面ui得不到更新,导致在用户看来界面处于假死的状态,造成了不好的用户体验.所以在大量数据操作的应用上,需要使用多线程来处理这种情况.在c#中使用多线程很方便只需要使用System.Threading.Thread的一个实例的Start方法就行了,但是如何实现多线程之间的交互就不是那么简单.本文实现了用子线程去处理数据,并实时更新主线程的ui状态了.下面就开始一步步的去实现异步线程更新

  • C#中一个高性能异步socket封装库的实现思路分享

    前言 socket是软件之间通讯最常用的一种方式.c#实现socket通讯有很多中方法,其中效率最高就是异步通讯. 异步通讯实际是利用windows完成端口(IOCP)来处理的,关于完成端口实现原理,大家可以参考网上文章. 我这里想强调的是采用完成端口机制的异步通讯是windows下效率最高的通讯方式,没有之一! 异步通讯比同步通讯处理要难很多,代码编写中会遇到许多"坑".如果没有经验,很难完成. 我搜集了大量资料,完成了对异步socket的封装.此库已用稳定高效的运行几个月. 纵观网

  • 深入分析C#中的异步和多线程

    许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识.在这里,你将了解这两个概念之间的区别,并使用c#实现它们. 我:"服务员,这是我第一次来这家餐厅.通常需要4个小时才能拿到食物吗?" 服务员:"哦,是的,先生.这家餐厅的厨房里只有一个厨师." 我:"--只有一个厨师吗?" 服务员:"是的,先生,我们有好几个厨师,但每次只有一个在厨房工作." 我:"所以其他10个穿着厨师服站在厨房里的人--什么

  • c# 用Dictionary实现日志数据批量插入

    背景 最近再做一个需求,就是对站点的一些事件进行埋点,说白了就是记录用户的访问行为.那么这些数据怎么保存呢,人家点一下保存一下?显然不合适,肯定是需要批量保存,提高效率. 问题窥探 首先,我想到的是Dictionary,对于C#中的Dictionary类相信大家都不陌生,这是一个Collection(集合)类型,可以通过Key/Value(键值对的形式来存放数据:该类最大的优点就是它查找元素的时间复杂度接近O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率.Dictionary是非线

  • c#快速写本地日志方法

    很多人的程序在本地运行是好的,但是发布在服务器上后就会有各种各样的问题,但是服务器上又不能直接调试,所以直接读写本地日志成为解决问题的关键,我这个方法,会在发布网站的根目录自动创建 log.txt,并且会自动拼接日志信息. 日志可在如下找到: 代码如下: 1.引用 using System; using System.IO; using System.Text; 2.具体方法: public static void Writelog(string msg) { StreamWriter stre

  • 基于c# Task自己动手写个异步IO函数

    前言 对于服务端,达到高性能.高扩展离不开异步.对于客户端,函数执行时间是1毫秒还是100毫秒差别不大,没必要为这一点点时间煞费苦心.对于异步,好多人还有误解,如: 异步就是多线程:异步就是如何利用好线程池.异步不是这么简单,否则微软没必要在异步上花费这么多心思.本文就介绍异步最新的实现方式:Task,并自己动手写一个异步IO函数.只有了解了异步函数内部实现方式,才能更好的利用它. 对于c#,异步处理经过了多个阶段,但是对于现阶段异步就是Task,微软用Task来抽象异步操作.以后的异步函数,处

  • c# 用ELMAH日志组件处理异常

    背景 ELMAH就是一个日志的拦截和处理组件,说到.net的日志组件,大家的第一反应该是Log4Net.NLog等这些东西,关于Log4Net和NLog,可以说是.net日志组件里面使用最为广泛的组件了,它们功能强大.使用方便. 优点 相比它们: 1.ELMAH的使用更加简单,它甚至不用写一句代码: 2.ELMAH是一种"可拔插式"的组件,即在一个运行的项目里面我们可以随意轻松加入日志功能,或者移除日志功能: 3.ELMAH组件自带界面,不用写任何代码,即可查看异常日志的界面: 4.组

  • C#实现异步编程的方法

    最近在我参与的几个.Net项目中都有用到异步编程,作为一名.Net小白,很有必要好好地学习一下C#异步编程. 什么是异步 异步指的就是不用阻塞当前线程来等待任务的完成,而是将任务扔到线程池中去执行,当前线程可以继续向下执行,直至其它线程将任务完成,并回调通知当前线程.整个任务从开始到结束都是异步完成的,不会阻塞当前线程.因此,异步很重要的一点就是,不会阻塞当前线程. 实现异步编程 在C#语言中,主要是通过委托来实现异步编程的,在委托类型中定义了两个方法BeginInvoke()和EndInvok

  • C#使用SqlServer作为日志数据库的设计与实现

    前言 做一个简单的日志数据库 功能不需要特别繁琐 主要就是记录普通日志和错误日志(INFO,ERROR) 用数据库作为日志有好处也有坏处 相比于文本来说 更加容易操作 后期查看日志可以根据时间筛选 当然要求也多了点 没那么灵活了 首先你的PC上还要安装一个SqlServer 本来是想用log4net配置去实现的 发现配置很繁琐 决定自己设计一个 肯定有不少不足之处 分为以下几个步骤 1.建立日志数据表 都用一个表来存放,那么字段就要多设置一个 用来区分不同的日志类型 具体怎么设置 也很简单 字段

  • C#用委托BeginInvoke做异步线程

    一个应用场景,浏览器上传一个文件,此文件后台调用文件转换,需要耗费相当长的时间,这样,如果是一个线程同步式的做下去,那么用户在浏览器上感觉就是卡住了,卡卡卡卡,这里我们利用委托的BeginInvoke和EndInvoke方法操作线程,BeginInvoke方法可以使用线程异步地执行委托所指向的方法.然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用,说白了就是相当于开个多线程,你用户文件保存了之后,响应返回,这个Be

  • C#中四步轻松使用log4net记录本地日志的方法

    在这里,记录我在项目中使用log4net记录本地日志的步骤.在不会之前感觉很难,很神秘,一旦会了之后其实没那么难.其实所有的事情都是一样的,下面我就分享一下我使用log4Net的经验. 第一步:首先从Visual Studio中的Nuget包管理中搜索下载 Log4Net dll文件 如下图: 选择安装的项目(哪个类库中需要记录日志就勾选上) 第二步:打开配置文件 WinFrom就是 App.config Web就是 web.config 将以下配置信息加入 <configSections>

  • C# 开发日志本地化工具

    程序员讨厌写文档, 讨厌写注释, 而我还讨厌写日志, 输出一个  "Id=5, 姓名=王大锤, 性别=男, 生日=2020年1月1日"  总归会用到字符串的填充 var log = $"Id={person.Id}, 姓名={person.Name}, 性别={(person.Sex == SexType.Man ? "男性" : "女性")}, 生日={person.Birthday}"; Json序列化工具多好啊, 可是输

随机推荐