新的一年開始,也該開始學習一些新的東西,首先就是將去年寫的C#伺服器重新構思一下。
今天的目標:日誌系統
需求:
1.顯示日誌
2.寫入到本地檔案
3.錯誤或異常日誌需要通過郵件通知
4.可以自定日誌類型(可有可無)
日誌:
我個人理解,就是系統在運行過程中所產生的提示資訊。比如,Debug程式的時候,除了進行斷點調試,大多都是進行文本輸出,來觀察這一段程式書寫是否正確。其次,就是我們經常碰到的錯誤或者捕獲的異常資訊等。通過觀察日誌,可以快速的協助我們找到想要東西。
在該日誌系統中,我們使用兩個類:Debug和LogInFile,LogInFile主要是用來進行日誌寫入功能得。
1.顯示日誌
C# 輸出文字的語句:
Console.WriteLine(info, param);
在現實日誌上,我們只是對上面語句進行了一層封裝而已。
實現:
/// <summary> /// 資訊 /// </summary> /// <param name="info"></param> /// <param name="param"></param> public static void LogInfo(string info, params object[] param) { LogInfo("", info.ToString(), param); } /// <summary> /// 資訊 /// </summary> /// <param name="info"></param> public static void LogInfo(object info) { LogInfo("", info.ToString(), null); } /// <summary> /// 自訂資訊 /// </summary> /// <param name="genre"></param> /// <param name="info"></param> /// <param name="param"></param> public static void LogInfo(string genre, string info, params object[] param) { SetColor(genre, ColorConfig.InfoColor, info,param); LogInFileInfo lifi = WriteLine(genre, LogGenreEnum.Info, info, param); if(lifi!=null) SendEamil(genre, LogGenreEnum.Info, string.Format("{0} - Info:{1}", DateTime.Now, string.Format(info, param)), lifi.GetMessage); } /// <summary> /// 錯誤 /// </summary> /// <param name="info"></param> /// <param name="param"></param> public static void LogError(string info, params object[] param) { LogError("", info.ToString(), param); } /// <summary> /// 錯誤 /// </summary> /// <param name="info"></param> public static void LogError(object info) { LogError("", info.ToString(), null); } /// <summary> /// 錯誤 /// </summary> /// <param name="genre"></param> /// <param name="info"></param> /// <param name="param"></param> public static void LogError(string genre, string info, params object[] param) { SetColor(genre, ColorConfig.ErrorColor, info,param); LogInFileInfo lifi = WriteLine(genre, LogGenreEnum.Error, info, param); if(lifi!=null) SendEamil(genre, LogGenreEnum.Warring, string.Format("{0} - Error:{1}", DateTime.Now, string.Format(info, param)), lifi.GetMessage); } /// <summary> /// 警告 /// </summary> /// <param name="info"></param> /// <param name="param"></param> public static void LogWarring(string info, params object[] param) { LogWarring("", info, param); } /// <summary> /// 警告 /// </summary> /// <param name="info"></param> public static void LogWarring(object info) { LogWarring("", info.ToString(), null); } /// <summary> /// 警告 /// </summary> /// <param name="genre"></param> /// <param name="info"></param> /// <param name="param"></param> public static void LogWarring(string genre, string info, params object[] param) { SetColor(genre, ColorConfig.WarringColor, info,param); LogInFileInfo lifi = WriteLine(genre, LogGenreEnum.Warring, info, param); //發送郵件 if (lifi != null) SendEamil(genre, LogGenreEnum.Warring, string.Format("{0} - Warring:{1}", DateTime.Now, string.Format(info, param)), lifi.GetMessage); } /// <summary> /// 異常資訊 /// </summary> /// <param name="ex"></param> /// <param name="tag"></param> public static void LogException(Exception ex, string tag = "") { LogException("", ex, tag); } /// <summary> /// 輸出異常 /// </summary> /// <param name="ex"></param> /// <param name="tag"></param> public static void LogException(string genre, Exception ex, string tag = "") { string info = string.Empty; info += string.Format("異常資訊 {0} - {1} " + Environment.NewLine, tag, ex.Message); info += string.Format("\t異常對象:{0}" + Environment.NewLine, ex.Source); info += "\t異常堆棧:" + Environment.NewLine + "\t" + ex.StackTrace.Trim() + Environment.NewLine; info += string.Format("\t觸發方法:{0}" + Environment.NewLine + Environment.NewLine, ex.TargetSite); SetColor(genre, ColorConfig.ExcepitonColor, info); LogInFileInfo lifi = WriteLine(genre,LogGenreEnum.Exception, info); if (lifi != null) SendEamil(genre, LogGenreEnum.Exception, string.Format("{0} - Exception:{1}", DateTime.Now, ex.Message), lifi.GetMessage); }
上面只是進行一層封裝沒什麼好說的。
從上面可以看出,我將程式裡的資訊分為四類:
public enum LogGenreEnum : int { /// <summary> /// 資訊 /// </summary> Info = 2, /// <summary> /// 錯誤 /// </summary> Error = 4, /// <summary> /// 警告 /// </summary> Warring = 8, /// <summary> /// 異常 /// </summary> Exception = 16 }
感覺這4類已經足夠用了。
2.寫入本地檔案
我的想法是:
1.建立一個佇列儲存體日誌資訊
2.開啟別的線程進行檔案寫入,線程數量可以進行設定,預設為3條
3.每個線程每次可以寫入N條日誌,日誌資料可以設定,預設100條
4.當CPU的使用率低於多少數值時,才進行寫入,使用率可以設定,預設30%(我的想法是,在電腦閒置時候進行寫入,但是我不知道怎麼才能判斷電腦是否在空閑狀態,所以用cpu的使用率。這裡有一個問題就是我在擷取CPU使用率的時候感覺很慢,不知道為什麼,大家有什麼辦法嗎。)
5.記錄檔每天一個,如果檔案大小大於50M則自動建立新的檔案(剛想起來,還沒寫,不過就是判斷檔案大小,然後建立一個就行了)
6.線程每隔多少秒進行一次寫入,時間也是可以進行配置的。
完整的LogInFile類
using System;using System.Collections.Generic;using System.Diagnostics;using System.Linq;using System.Text;using System.Threading;using System.Threading.Tasks;using System.IO;namespace SvEngine.Log{ public class LogInFile { PerformanceCounter cpuRate = new PerformanceCounter("Processor", "% Processor Time", "_Total"); public List<LogInFileInfo> MessageStack = new List<LogInFileInfo>(); public void AddMessage(LogInFileInfo message) { lock (MessageStack) { MessageStack.Add(message); } } /// <summary> /// 開始寫入檔案 /// </summary> public void StartInFile() { while (true) { lock (MessageStack) { if (MessageStack.Count < 1) { Thread.Sleep(Debug.InFileConfig.LogInFileWriteInterval); continue; } if (cpuRate.NextValue() > Debug.InFileConfig.CpuRate) { Thread.Sleep(Debug.InFileConfig.LogInFileWriteInterval); continue; } List<LogInFileInfo> writeList = new List<LogInFileInfo>(); if (MessageStack.Count > Debug.InFileConfig.LogInFileWriteNum) { writeList.AddRange(MessageStack.GetRange(0, Debug.InFileConfig.LogInFileWriteNum)); MessageStack.RemoveRange(0, Debug.InFileConfig.LogInFileWriteNum); } else { writeList.AddRange(MessageStack); MessageStack.Clear(); } string infoMessage = string.Empty; string errorMessage = string.Empty; string waringMessage = string.Empty; //寫入檔案 for (int i = 0; i < writeList.Count; i++) { LogInFileInfo curMes = writeList[i]; switch (curMes.Genre) { case Debug.LogGenreEnum.Info: infoMessage += curMes.GetMessage; break; case Debug.LogGenreEnum.Error: case Debug.LogGenreEnum.Exception: errorMessage += curMes.GetMessage; break; case Debug.LogGenreEnum.Warring: waringMessage += curMes.GetMessage; break; } curMes.Dispose(); } WriteText(Debug.LogGenreEnum.Info,infoMessage); WriteText(Debug.LogGenreEnum.Error, errorMessage); WriteText(Debug.LogGenreEnum.Warring, waringMessage); writeList = null; } //等待執行 Thread.Sleep(Debug.InFileConfig.LogInFileWriteInterval); } } private void WriteText(Debug.LogGenreEnum genre,string text) { //檔案寫入 string filePath = string.Format(@"{0}\{1}\", Debug.InFileConfig.LogPath, genre.ToString()); string fileName = string.Format("{0}.txt", CurDate); if (!Directory.Exists(filePath)) Directory.CreateDirectory(filePath); if (!File.Exists(filePath + fileName)) { FileStream fs = File.Create(filePath + fileName); fs.Close(); } using (FileStream fs = new FileStream(filePath + fileName, FileMode.Append, FileAccess.Write, FileShare.Write)) { Byte[] info = new UTF8Encoding(true).GetBytes(text); fs.Write(info, 0, info.Length); } } private string CurDate { get { return string.Format("{0}-{1}-{2}", DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); } } } /// <summary> /// 日誌資料 /// </summary> public class LogInFileInfo:IDisposable { /// <summary> /// Log類型 /// </summary> public Debug.LogGenreEnum Genre; /// <summary> /// 建立時間 /// </summary> public DateTime CreateTime; /// <summary> /// 內容 /// </summary> public string MessageConnect; /// <summary> /// 擷取內容 /// </summary> public string GetMessage { get { return string.Format("{0} - {1}"+ System.Environment.NewLine , CreateTime, MessageConnect); } } public void Dispose() { } }}
PerformanceCounter cpuRate = new PerformanceCounter("Processor", "% Processor Time", "_Total");
這條語句就是擷取當前CPU的使用率,一定要注意,這個條語句最好不要放在方法中,第一次擷取的值為0,如果在方法中的每一次調用,總是會擷取的值為0的。
if (cpuRate.NextValue() > Debug.CpuRate) { Thread.Sleep(Debug.LogInFileWriteInterval); continue; }
這裡判斷了一下CPU的使用率。
case Debug.LogGenreEnum.Error: case Debug.LogGenreEnum.Exception: errorMessage += curMes.GetMessage;
我將Error類型的日誌和異常類型的日誌,放在同一個檔案中。
if (!File.Exists(filePath + fileName)){ FileStream fs = File.Create(filePath + fileName); fs.Close(); }
這裡檔案建立完畢後,一定要關閉一下,否則下面會報檔案被其他進程佔用的異常。
在InFileConfig類中,則存放了日誌寫入相關的配置和方法:
/// <summary> /// 日誌寫入配置 /// </summary> public class InFileConfig { private LogInFile _logInFile; private Thread[] _logInFileThread; private bool _isInFile = false; /// <summary> /// 是否寫入檔案 /// </summary> public bool IsInFile { get { return _isInFile; } set { _isInFile = value; if (_isInFile) { _logInFile = new LogInFile(); _logInFileThread = new Thread[_logInFileThreadNum]; StartThread(); } else { CloseThread(); _logInFile = null; } } } private int _inFileGenre = (int)LogGenreEnum.Info | (int)LogGenreEnum.Error | (int)LogGenreEnum.Exception | (int)LogGenreEnum.Warring; /// <summary>