前言
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;,如果日志量較多,可設定大一些。
分享、互相交流學習