1. 程式人生 > >C# 簡單的日誌系統(服務端)

C# 簡單的日誌系統(服務端)

新的一年開始,也該開始學習一些新的東西,首先就是將去年寫的C#伺服器重新構思一下。
今天的目標:日誌系統
需求:
1.顯示日誌
2.寫入到本地檔案
3.錯誤或異常日誌需要通過郵件通知
4.可以自定日誌型別(可有可無)

日誌:
我個人理解,就是系統在執行過程中所產生的提示資訊。比如,Debug程式的時候,除了進行斷點除錯,大多都是進行文字輸出,來觀察這一段程式書寫是否正確。其次,就是我們經常碰到的錯誤或者捕獲的異常資訊等。通過觀察日誌,可以快速的幫助我們找到想要東西。

在該日誌系統中,我們使用兩個類:DebugLogInFileLogInFile主要是用來進行日誌寫入功能得。

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