C#创建日志记录自定义插件

概述

程序开发与运行过程需要对各类信息进行记录,具体记录的内容应用实际需要进行设定,例如:程序运行Debug记录、程序运行错误、一般运行信息、运行警告、接口运行警告、接口运行错误、接口一般运行信息、接口运行警告、sql执行记录、LuckTrace等。根据日志记录发生频率需要可以预置天、时、分、不需要记录 等。

日志记录操作理解为“信息队列”,信息队列其实质为多线程的键值集合,记录日志就是从信息队列中获取信息,再写入文件。

创建项目

基于上述要求,创建一个基础类来实现日志记录功能,创建解决方案NetDBLogger,向解决方案添加用于日志记录的DBLoggerBase项目,向解决方案添加应用项目DBLoggerSite。C#创建日志记录自定义插件

基础类定义

日志频率

定义枚举,在项目DBLoggerBase中创建LogRule.cs类文件,代码:

namespace DBLoggerBase
{
    public enum LogRule
    {
        Day,
        Hour,
        Minute,
        No
    }
}

日志级别

定义日志级别枚举,在项目DBLoggerBase中创建LogLevel.cs类文件,代码:

namespace DBLoggerBase
{
    public enum LogLevel
    {
        /// <summary>
        /// 程序运行记录,Debug模式
        /// </summary>
        AppTrace,

        /// <summary>
        /// 程序运行错误
        /// </summary>
        AppException,

        /// <summary>
        /// 一般运行信息,主要日志方式
        /// </summary>
        AppInfo,

        /// <summary>
        /// 运行警告,由开发人员记录
        /// </summary>
        AppWarn,

        /// <summary>
        /// 接口运行记录,Debug模式
        /// </summary>
        ApiTrace,

        /// <summary>
        /// 接口运行错误
        /// </summary>
        ApiException,

        /// <summary>
        /// 接口一般运行信息,主要日志方式
        /// </summary>
        ApiInfo,

        /// <summary>
        /// 接口运行警告,由开发人员记录
        /// </summary>
        ApiWarn,
        /// <summary>
        /// sql执行记录
        /// </summary>
        SqlTrace,
        /// <summary>
        /// 抽奖统计记录
        /// </summary>
        LuckTrace
    }
}

日志写入

程序调试或运行的写入的日志进入键值集合对象,再从集合中获取信息,写入设定目录的文件,文件路径由应用配置中设置的LogPath确定,创建类文件Logger.cs,代码:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Timers;

namespace DBLoggerBase
{
    public class Logger
    {
        /// <summary>
        /// 日志队列
        /// </summary>
        private static ConcurrentQueue<KeyValuePair<string, string>> _logQueue;

        /// <summary>
        /// 日志文件夹
        /// </summary>
        private string _logPath = System.Configuration.ConfigurationManager.AppSettings["LogPath"].ToString();

        private int _size;

        private Timer _watcher;

        #region Instance

        private static readonly object _lockObject = new object();
        private static volatile Logger _instance = null;

        /// <summary>
        /// 写日志,通过队列容量或定时触发写入操作
        /// </summary>
        /// <param name="capacity">记录数,默认为100</param>
        /// <param name="seconds">毫秒数,默认60秒</param>
        public Logger(int size = 100, int milliseconds = 60000)
        {
            //如果目录不存在则创建
            if (!Directory.Exists(this._logPath))
                Directory.CreateDirectory(this._logPath);

            _logQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
            _size = size;

            _watcher = new Timer(milliseconds);
            _watcher.Elapsed += (o, e) =>
            {
                Submit();
            };
            _watcher.Start();
        }

        public static Logger Instance()
        {
            if (_instance == null)
            {
                lock (_lockObject)
                {
                    if (_instance == null)
                    {
#if DEBUG
                        _instance = new Logger(100, 3000);
#else
                        _instance = new Logger();
#endif
                    }
                }
            }
            return _instance;
        }

        #endregion

        #region 写日志

        /// <summary>
        /// 写日志 是一个入队操作
        /// </summary>
        /// <param name="str"></param>
        public void Write(string s, string prefix, LogRule rule = LogRule.Day)
        {
            if (string.IsNullOrWhiteSpace(s))
                return;

            DateTime dt = DateTime.Now;
            string val = string.Concat(dt, "\r\n", s);
            string key = string.Concat(prefix, GetKey(dt, rule));
            Write(key, val);
        }

        public void Write(string key, string val)
        {
            _logQueue.Enqueue(new KeyValuePair<string, string>(key, val));
            if (_logQueue.Count() >= _size)
                Submit();
        }

        /// <summary>
        /// 写日志 是一个入队操作
        /// </summary>
        /// <param name="str"></param>
        public void Write(string s, LogLevel level = LogLevel.AppInfo, LogRule rule = LogRule.Day, long ms = 0)
        {
            string prefix = level.ToString();
            if (ms > 0)
                ms = ms / 10 * 10;
            if (ms > 0)
                prefix = string.Concat("_", prefix, ms, "_");
            Write(s, prefix, rule);
        }
        #endregion

        #region 文本记录日志

        /// <summary>
        /// 写入文本记录
        /// </summary>
        public void Submit()
        {
            //独占方式,因为文件只能由一个进程写入.
            StreamWriter writer = null;
            var dict = GetLogText();
            string filename;
            FileInfo file;
            try
            {
                lock (_lockObject)
                {
                    foreach (var kv in dict)
                    {
                        filename = string.Concat(kv.Key, ".txt");
                        file = new FileInfo(this._logPath + "/" + filename);
                        //文件不存在就创建,true表示追加
                        writer = new StreamWriter(file.FullName, true, Encoding.UTF8);
                        writer.WriteLine(kv.Value);
                        writer.Close();
                    }
                }
            }
            finally
            {
                if (writer != null)
                    writer.Close();
            }
        }

        private string GetKey(DateTime dt, LogRule rule)
        {
            string key;
            switch (rule)
            {
                case LogRule.Minute:
                    key = dt.ToString("yyyyMMddHHmm");
                    break;
                case LogRule.No:
                    key = "";
                    break;
                case LogRule.Day:
                    key = dt.ToString("yyyyMMdd");
                    break;
                case LogRule.Hour:
                default:
                    key = dt.ToString("yyyyMMddHH");
                    break;
            }
            return key;
        }

        /// <summary>
        /// 得到日志文本 一个出队操作
        /// 将主键相同的数据拼接到一起,减少写入的io操作
        /// </summary>
        /// <returns></returns>
        private ConcurrentDictionary<string, string> GetLogText()
        {
            ConcurrentDictionary<string, string> dict = new ConcurrentDictionary<string, string>();
            string key, val;
            do
            {
                KeyValuePair<string, string> kv;

                if (_logQueue.TryDequeue(out kv))
                {
                    key = kv.Key;
                    val = string.Concat(kv.Value, "\r\n----------------------------------------\r\n");
                    dict.AddOrUpdate(key, val, (k, v) => string.Concat(v + "\r\n" + val));
                }
            } while (_logQueue.Count > 0);
            return dict;
        }
        #endregion
    }
}

日志方法

提供一些可供应用操作的方法,用以写入日志,创建类文件DbLogger.cs,代码:

using System;
using System.Data;
using System.Text;

namespace DBLoggerBase
{
    #region Logger

    public static class DbLogger
    {
        //初始化接口
        public static void LogWrite(long userId,int teacherId,string techName, string createTime, string valiTime)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(string.Concat("执行状态: 成功!"));
            sb.AppendLine(string.Concat("模板标题:预售结束通知"));
            sb.AppendLine(string.Concat("用户ID:" + userId));
            sb.AppendLine(string.Concat("分析师ID:" + teacherId));
            sb.AppendLine(string.Concat("拜讯时间:" + createTime));
            sb.AppendLine(string.Concat("结束时间:" + valiTime));

            Logger.Instance().Write(sb.ToString(), LogLevel.AppInfo);
        }

        public static void LogWrite(string message,string userId)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(string.Concat("消息内容:" + message + "  用户ID:" + userId));

            Logger.Instance().Write(sb.ToString(), LogLevel.AppInfo);
        }

        public static void LogWriteMessage(string message)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(string.Concat("时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "  消息内容:") + message);
            Logger.Instance().Write(sb.ToString(), LogLevel.AppInfo);
        }

        public static void LogException(Exception ex, string lastCommand = null)
        {
            StringBuilder sb = new StringBuilder();

            if (!string.IsNullOrWhiteSpace(lastCommand))
                sb.AppendLine(string.Concat("LastCommand:", lastCommand));

            if (ex.InnerException != null)
                ex = ex.InnerException;

            sb.AppendLine(string.Concat("异常信息: " + ex.Message));
            sb.AppendLine(string.Concat("错误源:" + ex.Source));
            sb.AppendLine(string.Concat("堆栈信息:\r\n" + ex.StackTrace));

            Logger.Instance().Write(sb.ToString(), LogLevel.AppInfo);
        }

        public static void LogTrace(IDbCommand cmd, long elapsedMilliseconds = 0)
        {
            StringBuilder sb = new StringBuilder();

            if (elapsedMilliseconds > 0)
                sb.AppendLine(string.Format("执行时长: {0} ms", elapsedMilliseconds));

            sb.AppendLine(string.Format("CommandText: {0}", cmd.CommandText));
            sb.AppendLine(string.Format("CommandType: {0}", cmd.CommandType));
            sb.AppendLine(string.Format("Parameters: {0}", cmd.Parameters.Count));
            foreach (IDbDataParameter m in cmd.Parameters)
            {
                sb.Append(string.Format("\tDirection: {0}", m.Direction));
                sb.Append(string.Format("\tParameterName: {0}", m.ParameterName));
                sb.Append(string.Format("\tDbType: {0}", m.DbType));
                sb.AppendLine(string.Format("\tDbValue: {0}", m.Value));
            }
            Logger.Instance().Write(sb.ToString(), LogLevel.SqlTrace, LogRule.Hour, elapsedMilliseconds);
        }
        public static void LogLuckTrace(string message,long elapsedMilliseconds = 0)
        {
            Logger.Instance().Write(message.ToString(), LogLevel.LuckTrace, LogRule.Day, elapsedMilliseconds);
        }
    }
    #endregion
}

至此,日志记录功能所需要的类创建完成,下面在应用中调用。

应用访问

在DBLoggerSite项目中创建Web页Index.aspx,此处以记录一般信息、运行异常为例,代码:

using DBLoggerBase;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DBLoggerSite
{
    public partial class Index : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            try
            {
                var x = 10000;
                var y = decimal.Zero;
                DbLogger.LogWriteMessage("Index页面调用了日志记录功能!");
                var i = x / y;
            }
            catch (Exception ex)
            {
                DbLogger.LogException(ex, "除数为零的异常");
            }
        }
    }
}

打开D:\Logs\DBLogger目录下的日志文件,内容如下:

2019/3/16 20:56:45
时间:2019-03-16 20:56:45  消息内容:Index页面调用了日志记录功能!

----------------------------------------

2019/3/16 20:56:45
LastCommand:除数为零的异常
异常信息: 尝试除以零。
错误源:mscorlib
堆栈信息:
   在 System.Decimal.FCallDivide(Decimal& d1, Decimal& d2)
   在 System.Decimal.op_Division(Decimal d1, Decimal d2)
   在 DBLoggerSite.Index.Page_Load(Object sender, EventArgs e) 位置 D:\NetDBLogger\DBLoggerSite\Index.aspx.cs:行号 20

----------------------------------------