天天看點

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.業務邏輯層服務簡單設計,可友善支援二次開發模式。

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;,如果日志量較多,可設定大一些。

分享、互相交流學習