關於“日誌”的一點心得

luckeryin發表於2009-06-08

這裡要講的“日誌”是指應用程式在執行過程中的事件記錄。

應用程式在跟使用者互動的過程,實質就是程式按照使用者的意願完成一件一件的事件操作的過程,然而,由於不同的互動指令,不同的環境等因素,同一事件往往會有不同的執行結果。為了監控程式的行動過程,便於發現問題中瞭解使用者的操作行為,我們往往在程式中設計一個“日誌”記錄的功能,用於記錄應用程式的行動狀況和使用者的操作過程,這樣做一方面有利於設計者處理程式異常,一方面有利於軟體的使用單位的管理人員瞭解其操作人員的操作行為,防範不良行為...

總之,為程式設計“日誌”功能是十分有用和必要的。

日誌的分類:

日誌要以分為系統日誌和操作日誌兩大類。系統日誌是應用程式執行過程中自身發生的事件的記錄,最常用的是記錄系統異常以及程式執行到某些關鍵節點上時的記錄狀態。這類日誌主要用於系統設計人員瞭解程式執行情況。而操作日誌則是對使用者操作行為的記錄。比如某位使用者的系統登入操作,使用者對某行記錄的修改、刪除點操作。這類日誌主要用於行為管理。由於這兩類日誌的監控物件和監控目的均不相同,因此,記錄的日誌的側重點也各不相同。前者側重於記錄事件發生時的系統狀況,如某些相關變數的值等,這樣便於以後分析產生這一事件的原因。而後者則側重於記錄使用者操作行為的具體內容,如某次記錄修改前後的資料值分別是多少。

那麼,一條日誌記錄應該記錄哪些內容呢?

本人認為:一條日誌記錄,就是一個事件的發生,那麼,我們應該能夠清楚準確的描述這一事件。通常事件包含以下幾個要素:人物,地點,時間以及事件內容。對於系統日誌一說,其中:人物就是當時的應用程式名稱,地點就是發生事件的具體程式碼位置,時間就是當時的系統時間,事件內容則隨需要而定,關鍵是要能充分為設計人員提供儘可能多的現場資訊。對於操作日誌一說,其中:人物就是當時的操作者,地點就是發生事件的操作模組,時間就是當時的系統時間,事件內容則使用者的具體操作內容,關鍵是要能充分反映此次操作前後的資料變化。

基於本人對“日誌”的上述理解,準備自己動手,為C#設計一個較為適用的日誌功能模組。

由於記錄日誌是在程式執行的各個時刻的事件的記錄,因此,記錄日誌的相關程式碼可能分佈在軟體設計的各個角落,因此該模組應該便於呼叫才行。

首先,對於系統日誌,可以用.Net框架中的EventLog類來完成。這個類可以向Windows事件檢視器中增加應用程式日誌記錄,可以很方便的滿足上述系統日誌的要求,因此我選擇用它來構建我的系統日誌部分。

其次,對於操作日誌,可以用檔案或資料庫表來記錄。用檔案記錄的好處是便於檢視和傳遞,缺點是不便於管理,易實修改或刪除,因此不適合用於記錄關鍵日誌。資料庫表的方法安全高效,便於查詢和管理,但不便於傳遞。基於這兩種方式各有優缺點,我準備同時實現這兩各方式,在實際呼叫時再決定用哪種方式。

正式開始設計了:我將名稱空間定義為:Lucker.LogManager,將類名定義為LogManager,並將它獨立成一個專案,將來可以編譯成一個單獨的Dll檔案,便於在其它工程中引用該Dll檔案。LogManager類有分別三個構造方法:
public LogManager(string LogName),用於使用檔案記錄日誌
public LogManager(string LogName),用於使用Windows事件檢視器管理日誌
public LogManager(SqlConnection con,string User),用於使用資料庫表記錄日誌
當決定採用某種方式時,就宣告相應的構造方法。如:
LogManager lm1 = new LogManager();
LogManager lm2 = new LogManager(Application.ProductName);
LogManager lm3 = new LogManager(SqlCon,"Admin");
其中,具體引數的意義見下文。LogManager類為每一種方式暴露了一個記錄日誌的方法,它們分別是WriteFileLog,WriteWindowsLog,WriteDBLog。每一個方法又分別有若干種過載形式,方便在不同的情況下呼叫。這樣,就可以很方便的呼叫它們進行日誌記錄了:
lm1.WriteFileLog("log1");
lm1.WriteFileLog("type1", "log2");
...
lm2.WriteWindowsLog("log3");
lm2.WriteWindowsLog("E", "Log4");
lm2.WriteWindowsLog("source", "log5", EventLogEntryType.Error, 21, 10);
...
lm3.WriteDBLog("Test1", "Obj", "999", "99", "Just a test!");
lm3.WriteDBLog("Test2", "Obj","Just a test!");

三種方式記錄日誌的效果圖如下:
使用Windows事件檢視器管理記錄日誌:

image
使用檔案記錄日誌,採用記事本開啟:
image 
使用資料庫log表記錄日誌:
image

以下到了設計LogManager類具體實現的時候了:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Lucker.LogManager
{
    public class LogManager
    {
        FileStream fs;//日誌檔案物件流
        string filepath = string.Empty;//檔案儲存的路徑
        string file = string.Empty;//檔案全名(含路徑)

        EventLog el;//EventLog物件

        SqlConnection sqlcon;//日誌存放的資料庫連線物件
        SqlCommand sqlcom=new SqlCommand();//執行日誌寫入操作的命令物件
        string MachineName = Environment.MachineName;//日誌發生的客戶端機器名
        string user = string.Empty;//操作使用者名稱
        const string CreateTable = @"
            CREATE TABLE [dbo].[log](
                [ID] [numeric](10, 0) IDENTITY(1,1) NOT NULL,
                [User] [nvarchar](20) NOT NULL,
                [WorkStation] [nvarchar](50) NULL,
                [DateTime] [datetime] NULL,
                [LogType] [nvarchar](20) NULL,
                [Object] [nvarchar](20) NULL,
                [OriginalValue] [nvarchar](200) NULL,
                [NewValue] [nvarchar](200) NULL,
                [Remark] [nvarchar](200) NULL,
             CONSTRAINT [PK_log] PRIMARY KEY CLUSTERED
            (
                [ID] ASC
            )) ON [PRIMARY]
        ";//建立日誌記錄表的SQL語句

        #region 三個構造方法
        public LogManager(string LogName)
        {
            el = new EventLog(LogName, ".", "Default Event");//例項化一個EventLog物件,預設源為“Default Event”
        }
        public LogManager()
        {
            //檔案儲存在應用程式目錄下的LogFiles資料夾下
            filepath = System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "//LogFiles//";
            if (!Directory.Exists(filepath))
            {
                Directory.CreateDirectory(filepath);
            }
            //檔名為當前日期,確定每天一個日期檔案,防止單個檔案過大。
            string filename = string.Format("{0:0000}{1:00}{2:00}.log", DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
            file = filepath + filename;
        }
        public LogManager(SqlConnection con,string User)
        {
            sqlcon = con;
            sqlcom.Connection = sqlcon;
            //新增並初始化日誌表中各個引數
            sqlcom.Parameters.Add("User", SqlDbType.NVarChar, 20);
            sqlcom.Parameters.Add("WorkStation", SqlDbType.NVarChar, 50);
            sqlcom.Parameters.Add("DateTime", SqlDbType.DateTime);
            sqlcom.Parameters.Add("LogType", SqlDbType.NVarChar, 20);
            sqlcom.Parameters.Add("Object", SqlDbType.NVarChar, 20);
            sqlcom.Parameters.Add("OriginalValue", SqlDbType.NVarChar, 200);
            sqlcom.Parameters.Add("NewValue", SqlDbType.NVarChar, 200);
            sqlcom.Parameters.Add("Remark", SqlDbType.NVarChar, 200);
            user = User;
            IniParameters();
        }
        #endregion

        #region Windows log
        //當指定的日誌源不存在時,需要建立它
        private static void CreateEventSource(string EventSourceName,string LogName)
        {           
            if (!EventLog.SourceExists(EventSourceName))
            {               
                try
                {
                    EventLog.CreateEventSource(EventSourceName, LogName);
                }
                catch (Exception)
                {
                    throw new Exception("Create event source failed:" + EventSourceName);
                }
            } 
        }
        public void Close()
        {
            el.Close(); //退出程式時需要關閉物件
        }
        //以下是幾種寫Windows日誌的過載方法:
        public void WriteWindowsLog(string LogString)
        {           
            el.WriteEntry(LogString);
        }
        public void WriteWindowsLog(string EventSourceName, string LogString)
        {
            CreateEventSource(EventSourceName,el.Log);
            EventLog.WriteEntry(EventSourceName, LogString);
        }
        public void WriteWindowsLog(string EventSourceName, string LogString, EventLogEntryType LogType)
        {
            CreateEventSource(EventSourceName, el.Log);
            EventLog.WriteEntry(EventSourceName, LogString, LogType);
        }
        public void WriteWindowsLog(string LogString, EventLogEntryType LogType)
        {
            el.WriteEntry(LogString, LogType);
        }
        public void WriteWindowsLog(string LogString, EventLogEntryType LogType, Int32 EventID)
        {
            el.WriteEntry(LogString, LogType,EventID);
        }
        public void WriteWindowsLog(string LogString, EventLogEntryType LogType, int EventID, short Category)
        {
            el.WriteEntry(LogString, LogType, EventID,Category);
        }
        public void WriteWindowsLog(string EventSourceName, string LogString, EventLogEntryType LogType, int EventID, short Category)
        {
            CreateEventSource(EventSourceName, el.Log);
            EventLog.WriteEntry(EventSourceName, LogString, LogType, EventID, Category);
        }
        #endregion

        #region File log
        //以下是幾種寫檔案日誌的方法:
        public void WriteFileLog(string msg)
        {
            try
            {               
                fs = System.IO.File.Open(file, System.IO.FileMode.Append, System.IO.FileAccess.Write);
                Byte[] info = new UTF8Encoding(true).GetBytes(string.Format("{0:yyyy-MM-dd HH:mm:ss}",DateTime.Now) + ":/r/n/t" + msg + "/r/n");
                fs.Write(info, 0, info.Length);
                fs.Close();
            }
            catch(Exception e)
            {
                throw new Exception("Write file log failed:" + e.Message);               
            }
        }
        public void WriteFileLog(string LogType,string msg)
        {
            try
            {
                fs = System.IO.File.Open(file, System.IO.FileMode.Append, System.IO.FileAccess.Write);
                Byte[] info = new UTF8Encoding(true).GetBytes(string.Format("{0:yyyy-MM-dd HH:mm:ss}",DateTime.Now) + ":/t" + LogType + "/r/n/t" + msg + "/r/n");
                fs.Write(info, 0, info.Length);
                fs.Close();
            }
            catch (Exception e)
            {
                throw new Exception("Write file log failed:" + e.Message);
            }
        }
        #endregion

        #region Database log
        //以下是幾種寫資料庫日誌的方法:
        public void WriteDBLog(string LogType, string Object, string OriginalValue, string NewValue, string Remark)
        {
            sqlcom.Parameters["LogType"].Value = LogType;
            sqlcom.Parameters["Object"].Value = Object;
            sqlcom.Parameters["OriginalValue"].Value = OriginalValue;
            sqlcom.Parameters["NewValue"].Value = NewValue;
            sqlcom.Parameters["Remark"].Value = Remark;
            WriteDBLog();
        }
        //修改資料日誌:
        public void WriteDBLog(string LogType, string Object, string OriginalValue, string NewValue)
        {
            sqlcom.Parameters["LogType"].Value = LogType;
            sqlcom.Parameters["Object"].Value = Object;
            sqlcom.Parameters["OriginalValue"].Value = OriginalValue;
            sqlcom.Parameters["NewValue"].Value = NewValue;
            WriteDBLog();
        }
        //物件操作日誌:
        public void WriteDBLog(string LogType, string Object,string Remark)
        {
            sqlcom.Parameters["LogType"].Value = LogType;
            sqlcom.Parameters["Object"].Value = Object;
            sqlcom.Parameters["Remark"].Value = Remark;
            WriteDBLog();
        }
        //無物件操作日誌:
        public void WriteDBLog(string LogType, string Remark)
        {
            sqlcom.Parameters["LogType"].Value = LogType;
            sqlcom.Parameters["Remark"].Value = Remark;
            WriteDBLog();           
        }
        //初始化日誌表中各個引數
        private void IniParameters()
        {
            sqlcom.Parameters["User"].Value = user;
            sqlcom.Parameters["WorkStation"].Value = MachineName;
            sqlcom.Parameters["DateTime"].Value = DateTime.Now;
            sqlcom.Parameters["LogType"].Value = "Unknow";
            sqlcom.Parameters["Object"].Value = "Unknow";
            sqlcom.Parameters["OriginalValue"].Value = "Unknow";
            sqlcom.Parameters["NewValue"].Value = "Unknow";
            sqlcom.Parameters["Remark"].Value = "none";
        }
        private void WriteDBLog()
        {
            GetConnection();//檢查資料庫是否連線,未連線時先連線
            CheckTable();//檢查日誌表是否存在,不存在時先建立
            try
            {
                sqlcom.CommandText = "insert into [log]([User],[WorkStation],[DateTime],[LogType],[Object],[OriginalValue],[NewValue],[Remark]) values(@User,@WorkStation,@DateTime,@LogType,@Object,@OriginalValue,@NewValue,@Remark)";
                sqlcom.ExecuteNonQuery();
                IniParameters();//寫入資料庫後要將各引數復位
            }
            catch (Exception e)
            {
                throw new Exception("Write database log failed:" + e.Message);
            }
        }
        private void GetConnection()
        {
            if(sqlcon==null)
                throw new Exception("There is no SqlConnection!");
            try
            {
                if (sqlcon.State != ConnectionState.Open)
                    sqlcon.Open();
            }
            catch (SqlException)
            {
                throw new Exception("Open SqlConnection failed!");
            }
        }
        private void CheckTable()
        {
            try
            {
                SqlCommand cmd = new SqlCommand();
                cmd.Connection = sqlcon;
                cmd.CommandText = "select count(*) from sysobjects where name = 'log'";
                if (Convert.ToInt32(cmd.ExecuteScalar()) == 1)//log表存在
                    return;
                else//log表不存在
                {
                    cmd.CommandText = CreateTable;
                    cmd.ExecuteNonQuery();
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Table log create failed:"+ex.Message);
            }
        }
        #endregion
    }
}

由於該類考慮到了當檔案路徑,日誌源或資料庫表不存在時的情況並對它們作了相應的自動處理,因此不需要呼叫前作任務設定。有了該類,只需要簡單的在程式的開頭宣告並例項化一個全域性的LogManager型別的物件lm,然後就可以四處呼叫它了:lm.Writer**Log(××);確定十分方便易用。而且該類還實現了三個不同的方式記錄日誌,可以滿足不同的需要。

當然,LogManager類還是很多可以進一步完善的地方,比如,對於用資料庫記錄日誌的情況,還可以設計一個日誌檢視、查詢的視窗介面,用XML檔案來代替普通檔案記錄日誌等。希望廣大網友多多指教。

相關文章