2.NetDh框架之简单高效的日志操作类(附源码和示例代码)
前言
NetDh框架适用于C/S、B/S的服务端框架,可用于项目开发和学习。目前包含以下四个模块
1.数据库操作层封装Dapper,支持多种数据库类型、多库实例,简单强大;
此部分具体说明可参考博客: https://www.cnblogs.com/michaeldonghan/p/9317078.html
2.提供简单高效的日志操作类使用,支持日志写入Db和txt、支持任何数据库类型写入(包括传统sql数据库和nosql数据库等)、支持同步写入日志和后台独立线程异步处理日志队列;
此部分具体说明可参考博客: 本文以下章节内容。
3.提供简单缓存设计和使用;
此部分具体说明可参考博客: https://www.cnblogs.com/michaeldonghan/p/9321745.html
4.业务逻辑层服务简单设计,可方便支持二次开发模式。
此部分具体说明可参考博客: https://www.cnblogs.com/michaeldonghan/p/9321745.html
1.日志操作类LogHandle
NetDh.EasyLogger.LogHandle是一个轻便快捷的日志操作类。
1.支持日志写入数据库和txt文件;
2.支持所有数据库类型的写入日志,包括传统sql数据库和nosql数据库等(开放委托给调用方) ;
3.支持同步写入日志,也支持后台独立线程异步处理日志任务,后台线程数可通过构造函数配置。在构造函数中的asynThreadCount参数指定,asynThreadCount是异步队列处理日志的线程数,0表示同步处理;大于0表示后台开asynThreadCount个线程异步处理日志任务队列。普通日志量推荐默认的1,这样系统可异步处理日志,如果日志出错也是会记录到本地txt;如果日志量较多,再酌情设置大一些。
4.支持多个日志操作对象,比如想把用户操作日志和系统日志分开在不同表里记录,则可以再声明一个日志操作对象。
直接上源码(以源码中的注释作为说明):
1 using System; 2 using System.Collections.Concurrent; 3 using System.IO; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace NetDh.EasyLogger 9 { 10 /* 11 * 此LogHandle是一个轻便快捷的日志操作类。 12 * 1.支持日志写入数据库和txt文件; 13 * 2.支持所有数据库类型的写入日志,包括传统sql数据库和nosql数据库等,因为是开放"Db写入的委托"给调用方:) ; 14 * 3.支持同步写入日志,也支持后台独立线程异步处理日志任务。 15 * 说明: 16 * 此日志操作类可支持95%以上的场景。但不适用的场景是大并发超大量日志写入,这种情况需要考虑缓存队列、批次写入、故障处理等。 17 * 一般的,超大量的日志,有点失去了“日志”的意义,因为很难分析。 18 * 总之,不要用此类来做大并发超大量数据写入。 19 */ 20 21 /// <summary> 22 /// 轻便快捷的日志操作类 23 /// </summary> 24 public class LogHandle 25 { 26 #region 属性 27 /// <summary> 28 /// 日志记录者 29 /// </summary> 30 public string Recorder { get; set; } 31 /// <summary> 32 /// txt日志的目录;如果不需要记录到txt则为null 33 /// </summary> 34 public string DirectoryForTxt { get; set; } 35 /// <summary> 36 /// 定义写入日志到数据库的委托;如果不需要记录到数据库则为null 37 /// </summary> 38 public Action<string, TbLog> DoInsertLogToDb { get; set; } 39 /// <summary> 40 /// 异步队列处理日志的线程数。0表示同步处理;1表示后台开一个线程异步处理日志任务队列.. 41 /// (建议异步处理的线程不需要太多,按日志量:1到2个线程就好。) 42 /// </summary> 43 protected int AsynThreadCount { get; set; } 44 /// <summary> 45 /// 需要写入日志的队列。 46 /// (BlockingCollection多线程安全队列,可自动阻塞线程,默认是Queue结构) 47 /// </summary> 48 protected BlockingCollection<object> LogQueue = new BlockingCollection<object>(); 49 /// <summary> 50 /// 默认insert Sql语句。调用方可修改InsertLogSql,比如如果是oracle数据库,则要把InsertLogSql语句中的@改为: 51 /// (表名称可自定义。1 支持不同的表命名规则;2 支持实例化不同的表名称对象用于多表日志记录(比如分操作日志和系统后台日志等)) 52 /// </summary> 53 public string InsertLogSql = @" insert into {0}(Message,Recorder,LogLevel,LogCategory,CreateTime,Thread,LogUser,Ip) values (@Message,@Recorder,@LogLevel,@LogCategory,@CreateTime,@Thread,@LogUser,@Ip) "; 54 #endregion 55 56 #region 构造函数,配置日志 57 /// <summary> 58 /// 日志操作类,支持保存在数据库和本地txt 59 /// </summary> 60 /// <param name="recorder">日志记录者</param> 61 /// <param name="directoryForTxt">winform程式参考:Path.Combine(Environment.CurrentDirectory, "Logs"); 62 /// web程式参考:System.Web.Hosting.HostingEnvironment.MapPath("~/Logs")</param> 63 /// <param name="logToDbAction">日志写入数据库的委托。由调用方自动选择db日志写入方式,这样就可支持任何数据库类型写入日志</param> 64 /// <param name="asynThreadCount">异步队列处理日志的线程数。0表示同步处理;大于0表示后台开asynThreadCount个线程异步处理日志任务队列。普通日志量推荐默认的1,这样系统可异步处理日志,如果日志出错也是会记录到本地tx;如果日志量较多,可设置大一些。</param> 65 /// <param name="logTableName">日志表名,表名称默认是TbLog,可以自定义,比如TbLog等。1. 为了不同的表命名规则;2. 为了支持多表日志记录(比如分操作日志和系统后台日志等)。</param> 66 /// <param name="needStartLog">实例化日志对象时,是否记录一条start日志</param> 67 public LogHandle(string recorder, string directoryForTxt = "", Action<string, TbLog> logToDbAction = null, 68 int asynThreadCount = 1, string logTableName = "TbLog", bool needStartLog = true) 69 { 70 if (string.IsNullOrWhiteSpace(directoryForTxt) && logToDbAction == null) 71 { 72 throw new Exception("没有指定任何日志记录方式"); 73 } 74 Recorder = recorder; 75 DirectoryForTxt = directoryForTxt; 76 //初始化时确保日志文件夹存在,之后写入txt不用一直判断 77 if (!string.IsNullOrWhiteSpace(DirectoryForTxt) && !Directory.Exists(DirectoryForTxt)) 78 { 79 Directory.CreateDirectory(DirectoryForTxt); 80 } 81 DoInsertLogToDb = logToDbAction; 82 //指定日志表名 83 InsertLogSql = string.Format(InsertLogSql, logTableName); 84 AsynThreadCount = asynThreadCount; 85 //如果AsynThreadCount>=0,则异步处理日志写入;如果如果AsynThreadCount<=0,则是同步写入日志。 86 InitQueueConsume(); 87 if (needStartLog) 88 { 89 if (!string.IsNullOrWhiteSpace(DirectoryForTxt)) 90 { 91 LogToTxt(string.Format("init loghandle:{0}", Recorder), "start"); 92 } 93 if (DoInsertLogToDb != null) 94 { 95 LogToDb(string.Format("init loghandle:{0}", Recorder), "start"); 96 } 97 } 98 } 99 /// <summary> 100 /// 初始化异步处理队列 101 /// </summary> 102 protected virtual void InitQueueConsume() 103 { 104 for (int i = 0; i < AsynThreadCount; i++)//AsynThreadCount<=0的话,不会进入循环 105 { 106 Task.Factory.StartNew(() => 107 { 108 //GetConsumingEnumerable 如果队列中没有项,会自动阻塞等待Add。这个线程会一直在后台占用。 109 foreach (var item in LogQueue.GetConsumingEnumerable()) 110 { 111 try 112 { 113 if (item is string) 114 { 115 DoInsertLogToTxt(item.ToString()); 116 } 117 else 118 { 119 DoInsertLogToDb(InsertLogSql, (TbLog)item); 120 } 121 } 122 catch (Exception e) 123 {//如果在处理任务过程失败,需要捕获以继续处理下一个任务 124 } 125 } 126 }); 127 } 128 } 129 #endregion 130 131 #region Log、LogToDb、LogToTxt、LogToBoth 132 /// <summary> 133 /// 日志优先写入Db,当写入Db失败,才会写入txt。如果DoInsertLogToDb为null,则会自动选择写入txt。 134 /// (这也是最常用的模式,太多日志是不建议写入txt) 135 /// </summary> 136 /// <param name="msg">日志信息</param> 137 /// <param name="category">自定义类别</param> 138 /// <param name="level">日志等级:Info,Warn,Error,Fatal,Debug</param> 139 /// <param name="user"></param> 140 /// <param name="ip"></param> 141 public virtual void Log(string msg, string category = "", EnLogLevel level = EnLogLevel.Info, string user = "", string ip = "") 142 { 143 if (DoInsertLogToDb != null) 144 { 145 try 146 { 147 LogToDb(msg, category, level, user, ip); 148 } 149 catch (Exception e) 150 { 151 var exMsg = "-------------执行Log中的LogToDb时异常:" + LogHandle.GetExceptionDetailMsg(e); 152 if (!string.IsNullOrWhiteSpace(DirectoryForTxt))//如果写入数据库失败,则写入本地txt 153 { 154 LogToTxt(exMsg); 155 LogToTxt(msg, category, level, user, ip); 156 } 157 else 158 { 159 throw new Exception(exMsg); 160 } 161 } 162 } 163 else if (!string.IsNullOrWhiteSpace(DirectoryForTxt)) 164 { 165 LogToTxt(msg, category, level, user, ip); 166 } 167 } 168 /// <summary> 169 /// 日志记录到Db中。 170 /// </summary> 171 public virtual void LogToDb(string msg, string category = "", EnLogLevel level = EnLogLevel.Info, string user = "", string ip = "") 172 { 173 var sqlParams = new TbLog 174 { 175 Message = msg, 176 Recorder = Recorder, 177 LogLevel = level.ToString(), 178 LogCategory = category, 179 CreateTime = DateTime.Now, 180 Thread = Thread.CurrentThread.ManagedThreadId, 181 LogUser = user, 182 Ip = ip 183 }; 184 if (AsynThreadCount <= 0) 185 {//同步处理 186 DoInsertLogToDb(InsertLogSql, sqlParams); 187 } 188 else 189 {//异步处理 190 LogQueue.Add(sqlParams); 191 } 192 } 193 194 /// <summary> 195 /// 日志记录到txt中。 196 /// </summary> 197 /// <param name="msg">日志信息</param> 198 /// <param name="category">自定义类别</param> 199 /// <param name="level">日志等级:Info,Warn,Error,Fatal,Debug</param> 200 /// <param name="user"></param> 201 /// <param name="ip"></param> 202 public virtual void LogToTxt(string msg, string category = "", EnLogLevel level = EnLogLevel.Info, string user = "", string ip = "") 203 { 204 var threadId = Thread.CurrentThread.ManagedThreadId; 205 StringBuilder sb = new StringBuilder(); 206 sb.AppendFormat("[Thread]:{0} [Recorder]:{1} [Msg]:{2} ", threadId, Recorder, msg); 207 if (!string.IsNullOrWhiteSpace(category)) 208 { 209 sb.AppendFormat("[Category]:{0}", category); 210 } 211 if (level != EnLogLevel.Info) 212 { 213 sb.AppendFormat("[Level]:{0}", level.ToString()); 214 } 215 if (!string.IsNullOrWhiteSpace(user)) 216 { 217 sb.AppendFormat("[User]:{0}", user); 218 } 219 if (!string.IsNullOrWhiteSpace(ip)) 220 { 221 sb.AppendFormat("[Ip]:{0}", ip); 222 } 223 224 if (AsynThreadCount <= 0) 225 {//同步处理 226 DoInsertLogToTxt(sb.ToString()); 227 } 228 else 229 {//异步处理 230 LogQueue.Add(sb.ToString()); 231 } 232 } 233 private Object _lockWriteTxt = new object(); 234 /// <summary> 235 /// 日志记录到txt中。 236 /// (注意,此日志处理类,是为了支持普通量txt日志写入。如果是大并发写入txt,则要另外设计此场景的txt写入方式) 237 /// </summary> 238 /// <param name="strLog">需要记录的信息</param> 239 public virtual void DoInsertLogToTxt(string strLog) 240 { 241 strLog = string.Format("{0} {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), strLog); 242 //每天一个txt文件,如果需要可以改成每小时一个文件 243 string logPath = Path.Combine(DirectoryForTxt, string.Format(@"Log{0}.txt", DateTime.Now.ToString("yyyyMMdd"))); 244 lock (_lockWriteTxt) 245 { 246 //这边实现场景是一条一条日志记录。不适用大并发超大量txt写入,这种情况要另外设计此场景的txt写入方式,比如要考虑缓存队列、批次写入、故障处理等。 247 using (FileStream fs = new FileStream(logPath, FileMode.OpenOrCreate, FileAccess.Write)) 248 { 249 using (StreamWriter sw = new StreamWriter(fs)) 250 { 251 sw.BaseStream.Seek(0, SeekOrigin.End); 252 sw.WriteLine(strLog); 253 sw.Flush(); 254 } 255 } 256 } 257 } 258 /// <summary> 259 /// 日志写入Db和txt。 260 /// </summary> 261 /// <param name="msg">日志信息</param> 262 /// <param name="category">自定义类别</param> 263 /// <param name="level">日志等级:Info,Warn,Error,Fatal,Debug</param> 264 /// <param name="user"></param> 265 /// <param name="ip"></param> 266 public virtual void LogToBoth(string msg, string category = "", EnLogLevel level = EnLogLevel.Info, string user = "", string ip = "") 267 { 268 try 269 { 270 LogToDb(msg, category, level, user, ip); 271 } 272 catch (Exception e) 273 { 274 LogToTxt("-------------执行LogToBoth中的LogToDb时异常:" + e.Message); 275 LogToTxt(msg, category, level, user, ip); 276 return; 277 } 278 LogToTxt(msg, category, level, user, ip); 279 } 280 #endregion 281 282 /// <summary> 283 /// 生成自定义异常消息,包含异常的堆栈 284 /// </summary> 285 /// <param name="ex">异常对象</param> 286 /// <returns>异常字符串文本</returns> 287 public static string GetExceptionDetailMsg(Exception ex) 288 { 289 StringBuilder sb = new StringBuilder(); 290 sb.AppendFormat("异常时间:{0}", DateTime.Now); 291 sb.AppendFormat("异常信息:{0}", ex.Message); 292 sb.AppendLine(string.Empty); 293 sb.AppendFormat("异常堆栈:{0}", ex.StackTrace); 294 sb.AppendLine(string.Empty); 295 return sb.ToString(); 296 } 297 } 298 }
2.使用的示例代码
直接看代码和注释:
1 /// <summary> 2 /// NetDh模块使用示例代码 3 /// </summary> 4 public class NetDhExample 5 { 6 #region 用全局静态变量实现单例。 7 /// <summary> 8 /// 服务端使用数据库操作对象 9 /// </summary> 10 public static DbHandleBase DbHandle { get; set; } 11 /// <summary> 12 /// 日志操作对象 13 /// </summary> 14 public static LogHandle LogHandle { get; set; } 15 16 //说明:比如如果你想把用户操作日志和系统日志分开在不同表里记录,则可以再声明一个日志操作对象 17 public static LogHandle SysLogHandle { get; set; } 18 #endregion 19 /// <summary> 20 /// 静态构造函数,只会初始化一次 21 /// </summary> 22 static NetDhExample() 23 { 24 25 //初始化数据库操作对象 26 var connStr = "Data Source=.;Initial Catalog=Test;User Id=sa;Password=***;"; 27 DbHandle = new SqlServerHandle(connStr); 28 //如果有多库,可再new个对象 29 //ReadDbHandle = new SqlServerHandle(connStrForRead); 30 31 //初始化日志操作对象 32 //先定义日志写入数据库的委托 33 Action<string, TbLog> doInsert = (sql, model) => 34 { 35 DbHandle.ExecuteNonQuery(sql, model);//你想要用什么方式把日志写入Db,是可以自己指定。 36 //DbHandle.Insert(model); 37 //如果你的表结构和TbLog类一样,则可直接用:DbHandle.Insert(model);这样就不会用到InsertLogSql,也就不用管InsertLogSql的语法是否支持所有数据库. 38 }; 39 //其中的asynThreadCount参数默认是1,代表后台独立线程独立处理日志;我这边设置为0,代表同步处理日志。 40 LogHandle = new LogHandle("MyLocalTest.exe", Path.Combine(Environment.CurrentDirectory, "Logs"), doInsert, 0, "TbLog"); 41 //如果你想要有多个日志操作对象,则再new一个,把日志放不同目录不同数据表中 42 SysLogHandle = new LogHandle("MyLocalTest.exe", Path.Combine(Environment.CurrentDirectory, "SysLogs"), doInsert, 0, "TbSysLog"); 43 } 44 /// <summary> 45 /// 各模块使用的示例代码 46 /// </summary> 47 public static void TestMain() 48 { 49 #region 日志处理类 50 LogHandle.LogToTxt("日志写入txt"); 51 LogHandle.LogToTxt("日志写入txt", "logcategory1");//可用第二个参数来自定义分类日志 52 LogHandle.LogToDb("日志写入db", "logcategory2");//可用第二个参数来自定义分类日志 53 LogHandle.LogToBoth("日志同时写入txt和Db"); 54 //LogHandle.Log是最常用的函数,太多日志是不建议写入txt。 55 LogHandle.Log("日志优先写入Db,当写入Db失败,才会写入txt。如果LogHandle对象DoInsertLogToDb属性为null,则会自动选择写入txt。"); 56 #endregion 57 } 58 }
3.NetDh框架完整源码
国外有github,国内有码云,在国内使用码云速度非常快。NetDh框架源码放在码云上:
https://gitee.com/donghan/NetDh-Framework
异步队列处理日志的线程数。0表示同步处理;大于0表示后台开asynThreadCount个线程异步处理日志任务队列.普通日志量推荐默认的1,这样系统可异步处理日志,如果日志出错也是会记录到本地tx;,如果日志量较多,可设置大一些。