C# 簡單的日誌系統(服務端)
新的一年開始,也該開始學習一些新的東西,首先就是將去年寫的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>
/// 開啟日誌寫入的型別
/// </summary>
public int InFileGenre
{
get { return _inFileGenre; }
set { _inFileGenre = value; }
}
private string _logPath = "";
/// <summary>
/// 日誌儲存位置
/// </summary>
public string LogPath
{
get { return _logPath.Equals("") ? Environment.CurrentDirectory + @"\Log" : _logPath; }
set { _logPath = value; }
}
private int _logInFileWriteInterval = 2000;
/// <summary>
/// 日誌寫入間隔時間
/// </summary>
public int LogInFileWriteInterval
{
get { return _logInFileWriteInterval; }
set { _logInFileWriteInterval = value; }
}
private int _logInFileWriteNum = 99;
/// <summary>
/// 每次日誌寫入量
/// </summary>
public int LogInFileWriteNum
{
get { return _logInFileWriteNum; }
set { _logInFileWriteNum = value; }
}
private int _logInFileThreadNum = 3;
/// <summary>
/// 日誌寫入執行緒數量
/// </summary>
public int LogInFileThreadNum
{
get { return _logInFileThreadNum; }
set
{
_logInFileThreadNum = value;
if (_logInFileThread != null)
CloseThread();
_logInFileThread = new Thread[_logInFileThreadNum];
}
}
private float _cpuRate = 30;
/// <summary>
/// Cpu 使用率小於多少開始寫入檔案
/// </summary>
public float CpuRate
{
get { return _cpuRate; }
set { _cpuRate = value; }
}
/// <summary>
/// 新增訊息
/// </summary>
/// <param name="message"></param>
public void AddMessage(LogInFileInfo message)
{
_logInFile.AddMessage(message);
}
/// <summary>
/// 開啟執行緒
/// </summary>
private void StartThread()
{
for (int i = 0; i < _logInFileThread.Length; i++)
{
_logInFileThread[i] = new Thread(new ThreadStart(_logInFile.StartInFile));
_logInFileThread[i].IsBackground = true;
_logInFileThread[i].Start();
}
}
/// <summary>
/// 關閉執行緒
/// </summary>
private void CloseThread()
{
for (int i = 0; i < _logInFileThread.Length; i++)
{
if (_logInFileThread[i].IsAlive)
_logInFileThread[i].Abort();
}
_logInFileThread = null;
}
}
以上這些屬性對寫入日誌進行相關配置。
StartThread()
和CloseThread()
定義了執行緒開啟和關閉。
InFileGenre
屬性則定義了那些日誌可以及進行檔案寫入
private int _inFileGenre = (int)LogGenreEnum.Info | (int)LogGenreEnum.Error | (int)LogGenreEnum.Exception | (int)LogGenreEnum.Warring;
預設下4中型別都可以進行寫入。
日誌寫入大概就是這些內容。
3.郵件通知
郵件部分我也是參考其他作者的部落格寫的,下面放出地址:
參考地址 https://www.cnblogs.com/xiezunxu/articles/7421322.html
/// <summary>
/// 傳送郵件,參考地址 https://www.cnblogs.com/xiezunxu/articles/7421322.html
/// </summary>
public static void SendEmail(string title,string body)
{
if (!EmailConfig.IsEmail //不傳送郵件
|| EmailConfig.SenderEmail.Equals(string.Empty) //傳送人為空
|| EmailConfig.ReceiverEamil.Count < 1 //接收人為空
|| EmailConfig.AuthorizationCode.Equals(string.Empty) //授權碼為空
|| EmailConfig.SmtpHost.Equals(string.Empty)) return; //主機名為空
MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress(EmailConfig.SenderEmail);
for(int i = 0;i<EmailConfig.ReceiverEamil.Count;i++)
mailMessage.To.Add(new MailAddress(EmailConfig.ReceiverEamil[i]));
mailMessage.Subject = title;
mailMessage.Body = body;
SmtpClient client = new SmtpClient();
client.Host =EmailConfig.SmtpHost;
client.EnableSsl = true;
client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(EmailConfig.SenderEmail, EmailConfig.AuthorizationCode);
client.Send(mailMessage);
}
}
上面這個方法就是郵件傳送的主要方法。
4.自定義級別:
使用比較簡單的方式:
private static Dictionary<string, CustomGenre> _customGenre = new Dictionary<string, CustomGenre>();
/// <summary>
/// 自定義型別
/// </summary>
/// <param name="genre"></param>
public static void CustomGenre(Dictionary<string, CustomGenre> genre)
{
_customGenre = genre;
}
把自定義型別傳進來,在呼叫其他輸出方法是,設定引數string genre
即可,會先去匹配_customGenre 中是否有該型別。
/// <summary>
/// 顏色配置
/// </summary>
public class ColorConfig
{
/// <summary>
/// 字型顏色
/// </summary>
public ConsoleColor FontColor
{
get { return Console.ForegroundColor; }
set
{
Console.ForegroundColor = value;
}
}
/// <summary>
/// 預設顏色
/// </summary>
public ConsoleColor DefaultColor = ConsoleColor.White;
/// <summary>
/// 資訊顏色
/// </summary>
public ConsoleColor InfoColor = ConsoleColor.White;
/// <summary>
/// 錯誤顏色
/// </summary>
public ConsoleColor ErrorColor = ConsoleColor.Red;
/// <summary>
/// 警告顏色
/// </summary>
public ConsoleColor WarringColor = ConsoleColor.Yellow;
/// <summary>
/// 異常顏色
/// </summary>
public ConsoleColor ExcepitonColor = ConsoleColor.Cyan;
}
ColorConfig
這個型別進行了一些顏色屬性的配置。
測試:
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Environment.CurrentDirectory + @"\Log");
Debug.InFileConfig.IsInFile = true;
Debug.IsDebug = true;
Debug.EmailConfig.SenderEmail = "***@qq.com";
Debug.EmailConfig.ReceiverEamil.Add("***@qq.com");
Debug.EmailConfig.SmtpHost = "smtp.qq.com";
//此處在你郵箱中獲取
Debug.EmailConfig.AuthorizationCode = "******";
Debug.EmailConfig.IsEmail = true;
try
{
throw new Exception("這是一個測試異常");
}
catch (Exception ex)
{
Debug.LogException(ex);
}
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ParameterizedThreadStart(Input));
t.Start(i);
}
Console.ReadKey();
}
static void Input(object data)
{
int idnex = (int)data;
for (int i = 0; i < 100; i++)
{
Debug.LogError("{0} - {1} 日誌測試", idnex, i);
}
}
}
成功收到郵件:
日誌寫入成功:
注意CPU的使用率大於設定的使用率是不會進行寫入的,等到條件達成後才會進行寫入。
完整的Debug類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Text;
using System.Threading;
using static SvEngine.Log.Debug;
namespace SvEngine.Log
{
public class Debug
{
public enum LogGenreEnum : int
{
/// <summary>
/// 資訊
/// </summary>
Info = 2,
/// <summary>
/// 錯誤
/// </summary>
Error = 4,
/// <summary>
/// 警告
/// </summary>
Warring = 8,
/// <summary>
/// 異常
/// </summary>
Exception = 16
}
private static EmailConfig _emailConfig = new EmailConfig();
/// <summary>
/// 郵件配置
/// </summary>
public static EmailConfig EmailConfig
{
get {
return _emailConfig;
}
}
private static InFileConfig _inFileConfig = new InFileConfig();
/// <summary>
/// 寫入日誌配置
/// </summary>
public static InFileConfig InFileConfig {
get {
return _inFileConfig;
}
}
private static ColorConfig _colorConfig = new ColorConfig();
/// <summary>
/// 顏色配置
/// </summary>
public static ColorConfig ColorConfig
{
get { return _colorConfig; }
}
private static bool _isDebug = true;
/// <summary>
/// 是否顯示
/// </summary>
public static bool IsDebug
{
get { return _isDebug; }
set { _isDebug = value; }
}
private static bool _isEnable = true;
/// <summary>
/// 是否開啟
/// </summary>
public static bool IsEnable
{
get
{
return _isEnable;
}
set { _isEnable = value; }
}
private static Dictionary<string, CustomGenre> _customGenre = new Dictionary<string, CustomGenre>();
/// <summary>
/// 自定義型別
/// </summary>
/// <param name="genre"></param>
public static void CustomGenre(Dictionary<string, CustomGenre> genre)
{
_customGenre = genre;
}
/// <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