C# 如何設計一個好用的紀錄檔庫?【架構篇】

2023-04-17 18:03:05

〇、前言

相信你在實際工作期間經常遇到或聽到這樣的說法:

  「我現在加一下紀錄檔,等會兒你再操作下。」

  「只有在程式出問題以後才會知道打一個好的紀錄檔有多麼重要。」

可見紀錄檔的記錄是日常開發的必備技能。

記錄紀錄檔的必要性:

  當業務比較複雜時,在關鍵程式碼附件新增合適的紀錄檔是非常重要的,這樣可以出現異常後,有章可循,較快速的在不停服的情況下,定位問題並解決。特別是在專案組中,人員較多,若沒有統一的紀錄檔記錄規範,查詢系統問題原因就更加費時費力。

記錄紀錄檔的三種實現:

  1. 當業務比較簡單,效能要求不高,只是單純的記錄程式的執行是否正常。此時就可以參考本文第一種實現,僅一種級別的文字記錄。
  2. 當業務複雜較複雜,對效能有一定要求時,可以根據實際情況,參考本文的第二、第三種實現。
  3. 當業務非常複雜,必然執行的效率就要求比較高,如何即讓程式穩定高效的執行,又能合理記錄程式執行狀態成為關鍵。高效的的紀錄檔操作可以參考本文的第三種實現。

一、紀錄檔的簡單記錄

如下,為簡單的記錄開發人員預輸出的文字內容,其內容為自定義,輸出的時間格式和固定標識需相同。

此方法的效能當然是最差的,針對同一個紀錄檔檔案,需要獨佔存取,當同時出現多個記錄需求時,會出現排隊的情況,導致系統出現卡頓。當然,可以採用多目標檔案的方式來提高效能表現,若業務較複雜,還是推薦使用後兩種方式。

紀錄檔內容測試結果:

public static string strlock = string.Empty;
static void Main(string[] args)
{
    lock(strlock) // 在同一個紀錄檔檔案操作範圍新增同一個鎖,避免多執行緒操作時因搶佔資源而報錯
    {
        WriteLogPublic.WriteLogFunStr("Program", "Main", "紀錄檔內容1");
        // 實際生成的路徑:C:\Logs\Program\Main\202304\log07.log
        // 記錄的內容:2023-04-07 11-21-31 --- 紀錄檔內容1
    }
}

 紀錄檔類內容:

public class WriteLogPublic
{
    /// <summary>
    /// 記錄紀錄檔
    /// </summary>
    /// <param name="projectname">專案名稱</param>
    /// <param name="controllername">控制器名稱</param>
    /// <param name="strlog">紀錄檔內容</param>
    public static void WriteLogFunStr(string projectname, string controllername, string strlog)
    {
        string sFilePath = $"C:\\Logs\\{projectname}\\{controllername}\\{DateTime.Now.ToString("yyyyMM")}"; // 根據專案名稱等建立資料夾
        string sFileName = $"log{DateTime.Now.ToString("dd")}.log";
        sFileName = sFilePath + "\\" + sFileName; // 檔案的絕對路徑
        if (!Directory.Exists(sFilePath)) // 驗證路徑是否存在
            Directory.CreateDirectory(sFilePath); // 不存在則建立
        FileStream fs;
        StreamWriter sw;
        if (File.Exists(sFileName)) // 驗證檔案是否存在,有則追加,無則建立
            fs = new FileStream(sFileName, FileMode.Append, FileAccess.Write);
        else
            fs = new FileStream(sFileName, FileMode.Create, FileAccess.Write);
        sw = new StreamWriter(fs);
        sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss") + " --- " + strlog);
        sw.Close();
        fs.Close();
    }
}

二、通過開源庫 HslCommunication 記錄不同級別的紀錄檔

此方式記錄紀錄檔,簡單高效,可以實現不同級別紀錄檔的輸出控制,紀錄檔選項的設定可以設定在程式的組態檔中,在程式啟動時載入即可。

若想實現實時載入,這隻能在每次寫紀錄檔前初始化紀錄檔物件,這樣估計就影響程式效能了。

紀錄檔內容測試結果:

static void Main(string[] args)
{
    // 先初始化設定 HslCommunicationOper
    HslCommunicationOper.HslComLogCollection("Test.ConsoleApp", "Main", 5, HslCommunication.LogNet.GenerateMode.ByEveryHour);
    // HslCommunicationOper.HslComLog("Test.ConsoleApp", "Main"); // 單檔案
    // HslCommunicationOper.HslComLogSize("Test.ConsoleApp", "MainSize", 5); // 增加紀錄檔單檔案大小設定
    // HslCommunicationOper.HslComLogByDate("Test.ConsoleApp", "MainDate", TimeType.Day); // 按照日期分檔案儲存
    HslCommunicationOper.SetMessageDegree(MessageDegree.WARN);//紀錄檔級別
    
    // 記錄紀錄檔
    HslCommunicationOper.logNet.WriteDebug("偵錯資訊");
    HslCommunicationOper.logNet.WriteInfo("一般資訊"); 
    HslCommunicationOper.logNet.WriteWarn("警告資訊");
    HslCommunicationOper.logNet.WriteError("錯誤資訊");
    HslCommunicationOper.logNet.WriteFatal("致命資訊");

    HslCommunicationOper.logNet.WriteDebug("KeyWord偵錯資訊", "偵錯資訊");
    HslCommunicationOper.logNet.WriteInfo("KeyWord一般資訊", "一般資訊");
    HslCommunicationOper.logNet.WriteWarn("KeyWord警告資訊", "警告資訊");
    HslCommunicationOper.logNet.WriteError("KeyWord錯誤資訊", "錯誤資訊");
    HslCommunicationOper.logNet.WriteFatal("KeyWord致命資訊", "致命資訊");
    HslCommunicationOper.logNet.WriteException("KeyWord-WriteException", new IndexOutOfRangeException());

    HslCommunicationOper.logNet.WriteDebug("偵錯資訊");
    HslCommunicationOper.logNet.WriteInfo("一般資訊");
    HslCommunicationOper.logNet.WriteWarn("警告資訊");
    HslCommunicationOper.logNet.WriteError("錯誤資訊");
    HslCommunicationOper.logNet.WriteFatal("致命資訊");
}
// 紀錄檔輸出格式範例:
    [警告] 2023-04-07 18:22:03.565 Thread:[001] 警告資訊
    [錯誤] 2023-04-07 18:22:03.605 Thread:[001] 錯誤資訊
    [致命] 2023-04-07 18:22:03.605 Thread:[001] 致命資訊
    [警告] 2023-04-07 18:22:03.605 Thread:[001] KeyWord警告資訊 : 警告資訊
    [錯誤] 2023-04-07 18:22:03.605 Thread:[001] KeyWord錯誤資訊 : 錯誤資訊
    [致命] 2023-04-07 18:22:03.605 Thread:[001] KeyWord致命資訊 : 致命資訊
    [致命] 2023-04-07 18:22:03.676 Thread:[001] KeyWord-WriteException : 錯誤資訊:Index was outside the bounds of the array.
    錯誤源:
    錯誤堆疊:
    錯誤型別:System.IndexOutOfRangeException
    錯誤方法:
    /=================================================[    Exception    ]================================================/
    [警告] 2023-04-07 18:22:03.676 Thread:[001] 警告資訊
    [錯誤] 2023-04-07 18:22:03.676 Thread:[001] 錯誤資訊
    [致命] 2023-04-07 18:22:03.676 Thread:[001] 致命資訊

三個相關紀錄檔類:

  • HslCommunicationOper:操作類;
  • LogNetCollection:擴充套件類(提供紀錄檔檔案的大小、生成新檔案頻率的設定);
  • MessageDegree:訊息級別列舉。
public static class HslCommunicationOper
{
    public static ILogNet logNet = null;
    /// <summary>
    /// 紀錄檔檔案根目錄
    /// </summary>
    public static string rootpath = "C:\\Log";

    /// <summary>
    /// 單紀錄檔檔案儲存
    /// </summary>
    /// <param name="projectname"></param>
    /// <param name="opername">紀錄檔檔名</param>
    public static void HslComLog(string projectname, string opername)
    {
        logNet = new LogNetSingle($"{rootpath}\\{projectname}\\{opername}.txt");
        logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 預設儲存最低階別為 DEBUG
    }

    /// <summary>
    /// 限定紀錄檔檔案大小
    /// </summary>
    /// <param name="projectname"></param>
    /// <param name="opername">紀錄檔上級資料夾名</param>
    /// <param name="logfilesize">紀錄檔檔案大小(單位:M) 1~20,預設 5</param>
    public static void HslComLogSize(string projectname, string opername, int logfilesize = 5)
    {
        if (logfilesize < 1 || logfilesize > 20)
            logfilesize = 5;
        logNet = new LogNetFileSize($"{rootpath}\\{projectname}\\{opername}", logfilesize * 1024 * 1024); // 單位M(5M):5 * 1024 * 1024
        logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 預設儲存最低階別為 DEBUG
    }

    /// <summary>
    /// 按照日期儲存
    /// </summary>
    /// <param name="projectname"></param>
    /// <param name="opername">紀錄檔上級資料夾名</param>
    /// <param name="recodemode">傳入列舉型別(TimeType),值範圍:Minute、Hour、Day、Month、Season、Year</param>
    public static void HslComLogByDate(string projectname, string opername, GenerateMode generateMode = GenerateMode.ByEveryDay)
    {
        logNet = new LogNetDateTime($"{rootpath}\\{projectname}\\{opername}", generateMode); // 按每天
        logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 預設儲存最低階別為 DEBUG
    }

    /// <summary>
    /// 按照檔案或日期儲存
    /// </summary>
    /// <param name="projectname"></param>
    /// <param name="opername">紀錄檔上級資料夾名</param>
    /// <param name="generateMode">傳入列舉型別 GenerateMode</param>
    public static void HslComLogCollection(string projectname, string opername, int filesize, GenerateMode generateMode = GenerateMode.ByEveryDay)
    {
        logNet = new LogNetCollection($"{rootpath}\\{projectname}\\{opername}", filesize * 1024 * 1024, generateMode);
        logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 預設儲存最低階別為 DEBUG
    }

    /// <summary>
    /// 單獨設定紀錄檔級別
    /// </summary>
    /// <param name="messageDegree">預設 DEBUG</param>
    public static void SetMessageDegree(MessageDegree messageDegree = MessageDegree.DEBUG)
    {
        switch (messageDegree)
        {
            case MessageDegree.DEBUG:
                logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 所有等級儲存
                break;
            case MessageDegree.INFO:
                logNet.SetMessageDegree(HslMessageDegree.INFO); // 除 DEBUG 外,都儲存
                break;
            case MessageDegree.WARN:
                logNet.SetMessageDegree(HslMessageDegree.WARN); // 除 DEBUG 和 INFO 外,都儲存
                break;
            case MessageDegree.ERROR:
                logNet.SetMessageDegree(HslMessageDegree.ERROR); // 只儲存 ERROR 和 FATAL
                break;
            case MessageDegree.FATAL:
                logNet.SetMessageDegree(HslMessageDegree.FATAL); // 只儲存 FATAL
                break;
            case MessageDegree.None:
                logNet.SetMessageDegree(HslMessageDegree.None); // 不儲存任何等級
                break;
        }
    }
}
public class LogNetCollection : LogPathBase, ILogNet, IDisposable
{
    private int fileMaxSize = 10485760; // 預設 10M
    private int currentFileSize = 0;
    private GenerateMode generateMode = GenerateMode.ByEveryYear;

    public LogNetCollection(string filePath, int fileMaxSize = 10485760, GenerateMode generateMode = GenerateMode.ByEveryDay, int fileQuantity = -1)
    {
        base.filePath = filePath;
        this.fileMaxSize = fileMaxSize;
        this.generateMode = generateMode;
        controlFileQuantity = fileQuantity;
        base.LogSaveMode = LogSaveMode.FileFixedSize;
        if (!string.IsNullOrEmpty(filePath) && !Directory.Exists(filePath))
        {
            Directory.CreateDirectory(filePath);
        }
    }

    protected override string GetFileSaveName()
    {
        if (string.IsNullOrEmpty(filePath))
        {
            return string.Empty;
        }

        if (string.IsNullOrEmpty(fileName))
        {
            fileName = GetLastAccessFileName();
        }

        if (File.Exists(fileName))
        {
            FileInfo fileInfo = new FileInfo(fileName);
            if (fileInfo.Length > fileMaxSize)
            {
                fileName = GetDefaultFileName();
            }
            else
            {
                currentFileSize = (int)fileInfo.Length;
            }
        }

        return fileName;
    }

    private string GetLastAccessFileName()
    {
        string[] existLogFileNames = GetExistLogFileNames();
        foreach (string result in existLogFileNames)
        {
            FileInfo fileInfo = new FileInfo(result);
            if (fileInfo.Length < fileMaxSize) // 判斷已建立的紀錄檔檔案是否達到最大記憶體
            {
                currentFileSize = (int)fileInfo.Length;
                return result;
            }
        }

        return GetDefaultFileName(); // 若未建立過,通過指定方式建立
    }

    private string GetDefaultFileName()
    {
        switch (generateMode)
        {
            case GenerateMode.ByEveryMinute:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".txt");
            case GenerateMode.ByEveryHour:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd_HH") + ".txt");
            case GenerateMode.ByEveryDay:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd") + ".txt");
            case GenerateMode.ByEveryWeek:
                {
                    GregorianCalendar gregorianCalendar = new GregorianCalendar();
                    int weekOfYear = gregorianCalendar.GetWeekOfYear(DateTime.Now, CalendarWeekRule.FirstDay, DayOfWeek.Monday);
                    return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + "_W" + weekOfYear + ".txt");
                }
            case GenerateMode.ByEveryMonth:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyy_MM") + ".txt");
            case GenerateMode.ByEverySeason:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + "_Q" + (DateTime.Now.Month / 3 + 1) + ".txt");
            case GenerateMode.ByEveryYear:
                return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + ".txt");
            default:
                return string.Empty;
        }
    }

    public override string ToString()
    {
        return $"LogNetFileSize[{fileMaxSize}];LogNetDateTime[{generateMode}]";
    }
}
/// <summary>
/// 訊息級別
/// </summary>
public enum MessageDegree
{
    DEBUG = 1,
    INFO = 2,
    WARN = 3,
    ERROR = 4,
    FATAL = 5,
    None = 9
}

 參考:C# 紀錄檔記錄分級功能使用 按照日期,大小,或是單檔案儲存

三、通過開源庫 NLog 實現通過組態檔設定紀錄檔選項

 NLog 是一個基於 .net 平臺編寫的紀錄檔記錄類庫,我們可以使用 NLog 在應用程式中新增極為完善的跟蹤偵錯程式碼。

 本文將通過紀錄檔框架 Nlog 和 ConcurrentQueue 佇列,實現一個高效能的紀錄檔庫。

 首先,為什麼相中了 Nlog ?

  • NLog 是適用於各個 .net 平臺的靈活且免費的紀錄檔記錄平臺。通過 NLog, 可以輕鬆地寫入多個目標(例如:資料庫、檔案、控制檯等), 並可動態更改紀錄檔記錄設定資訊。
  • NLog 支援結構化和傳統紀錄檔記錄。
  • NLog 的特點: 高效能、易於使用、易於擴充套件和靈活設定。

ConcurrentQueue:表示執行緒安全的先進先出(FIFO)集合。所有公共成員和受保護成員 ConcurrentQueue<T> 都是執行緒安全的,可以從多個執行緒並行使用。

1. 組態檔

對於 ASP.NET 應用程式,存在嵌入程式組態檔和單獨組態檔兩種方式,程式在啟動時,會在應用程式主目錄下依次查詢:web.config(*.exe.config、*.web.config)、web.nlog(*.exe.nlog)、NLog.config

個人推薦單獨檔案設定,便於修改和迭代使用。

第一種方式:單獨組態檔

  常用名稱為 NLog.config。此時需要在根節點 nlog 加上智慧感知(Intellisense)的屬性設定,詳見下文組態檔 XML 程式碼。

  1/5 targets(必須有) - 定義紀錄檔目標/輸出

  • name:是指的輸出地方的一個名詞(給 rules 呼叫的);
  • xsi:type:輸出檔案的型別,File 指的是檔案,Console 控制檯輸出;
  • fileName:輸出到目標檔案的地址,使用的相對路徑,可以自行設定輸出的地點。
  • layout:在最簡單的形式中,佈局是帶有嵌入標記的文字,這些嵌入標記樣子例如:${xxxx};
  • archiveFileName:表示捲動紀錄檔存放路徑;
  • archiveAboveSize:單次紀錄檔的儲存大小(單位是KB),超過設定,會 archiveFileName 中建立新的紀錄檔;
  • archiveNumbering:Sequence(排序),Rolling(捲動);
  • concurrentWrites:支援多個並行一起寫檔案,提高檔案寫入效能;
  • keepFileOpen:為了提高檔案寫入效能,避免每次寫入檔案都開關檔案;
  • autoFlush:為了提高紀錄檔寫入效能,不必每次寫入紀錄檔都直接寫入到硬碟;
  • createDirs:若設定的紀錄檔資料夾不存在,則自動建立資料夾,true:建立;false:不建立。

  其中,layout 屬性的標記變數(${xxx})解析可以參考以下程式碼:

點選展開 檢視標記釋義
${cached} - 將快取應用於另一個佈局輸出。
${db-null} - 為資料庫呈現 DbNull
${exception} - 通過呼叫記錄器方法之一提供的異常資訊
${level} - 紀錄檔級別(例如錯誤、偵錯)或級別序號(數位)
${literal} - 字串 literal。(文字) - 用於跳脫括號
${logger} - 記錄器名稱。GetLogger, GetCurrentClassLogger 等
${message} - (格式化的)紀錄檔訊息。
${newline} - 換行符文字。
${object-path} - 呈現物件的(巢狀)屬性
${onexception} - 僅在為紀錄檔訊息定義了異常時才輸出內部佈局。
${onhasproperties} - 僅當事件屬性包含在紀錄檔事件中時才輸出內部佈局。
${var} - 渲染變數

// 呼叫站點和堆疊跟蹤
${callite} - 呼叫站點(類名、方法名和源資訊)
${callite-filename} - 呼叫站點原始檔名。
${callsite-linenumber} - 呼叫站點源行編號。
${stacktrace} - Render the Stack trace

// 條件
${when} - 僅在滿足指定條件時輸出內部佈局。
${whenempty} - 當內部佈局生成空結果時輸出備用佈局。

// 上下文資訊
${activity} - 從 System.Diagnostics.Activity.Current NLog.DiagnosticSource External 捕獲跟蹤上下文
${activityid} - 將 System.Diagnostics 跟蹤關聯 ID 放入紀錄檔中。
${all-event-properties} - 記錄所有事件上下文資料。
${event-context} - 記錄事件屬性資料 - 替換為 ${事件屬性}
${event-properties} - 記錄事件屬性資料 - 重新命名 ${事件-上下文}
${gdc} - 全域性診斷上下文項。用於儲存每個應用程式範例值的字典結構。
${install-context} - 安裝引數(傳遞給 InstallNLogConfig)。
${mdc} - 對映的診斷上下文 - 執行緒本地結構。
${mdlc} - 非同步對映診斷上下文 - 作用域內上下文的執行緒本地結構。MDC 的非同步版本。
${ndc} - 巢狀診斷上下文 - 執行緒本地結構。
${ndlc} - 非同步巢狀診斷上下文 - 執行緒本地結構。

// 計數器
${counter} - 計數器值(在每次佈局呈現時增加)
${guid} - 全域性唯一識別符號(GUID)。
${sequenceid} - 紀錄檔序列 ID

// 日期和時間
${date} - 當前日期和時間。
${longdate} - 日期和時間採用長而可排序的格式"yyyy-MM-dd HH:mm:ss.ffff"。
${qpc} - 高精度計時器,基於從 QueryPerformanceCounter 返回的值。
${shortdate} - 可排序格式為 yyyy-MM-dd 的短日期。
${ticks} - 當前日期和時間的分筆報價值。
${time} - 24 小時可排序格式的時間 HH:mm:ss.mmm。

// 編碼和字串轉換
${json-encode} - 使用 JSON 規則跳脫另一個佈局的輸出。
${left} - 文字的剩餘部分
${lowercase} - 將另一個佈局輸出的結果轉換為小寫。
${norawvalue} - 防止將另一個佈局呈現器的輸出視為原始值
${pad} - 將填充應用於另一個佈局輸出。
${replace} - 將另一個佈局輸出中的字串替換為另一個字串。使用正規表示式可選
${replace-newlines} - 將換行符替換為另一個字串。
${right} - 文字的右側部分
${rot13} - 使用 ROT-13 解碼"加密"的文字。
${substring} - 文字的子字串
${trim-whitespace} - 從另一個佈局呈現器的結果中修剪空格。
${uppercase} - 將另一個佈局輸出的結果轉換為大寫。
${url-encode} - 對另一個佈局輸出的結果進行編碼,以便與 URL 一起使用。
${wrapline} - 以指定的行長度換行另一個佈局輸出的結果。
${xml-encode} - 將另一個佈局輸出的結果轉換為符合 XML 標準。

// 環境和組態檔
${appsetting} - 來自 .config 檔案 NLog.Extended 的應用程式設定設定
${configsetting} - 來自 appsettings.json 的值或 ASP.NET Core & .NET Core NLog.Extensions.LoggingNLog.Extensions.HostingNLog.Web.AspNetCore
${environment} - 環境變數。(例如 PATH、OSVersion)
${environment-user} - 使用者標識資訊(使用者名稱)。
${registry} - 來自 Windows 登入檔的值。

// 檔案和目錄
${basedir} - 當前應用程式域的基目錄。
${currentdir} - 應用程式的當前工作目錄。
${dir-separator} - 作業系統相關目錄分隔符。
${file-contents} - 呈現指定檔案的內容。
${filesystem-normalize} - 通過將檔名中不允許使用的字元替換為安全字元來篩選它們。
${nlogdir} - NLog.dll所在的目錄。
${processdir} - 應用程式的可執行程序目錄。
${specialfolder} - 系統特殊資料夾路徑(包括"我的檔案"、"我的音樂"、"程式檔案"、"桌面"等)。
${tempdir} - 一個臨時目錄。

// 身份
${identity} - 執行緒標識資訊(名稱和身份驗證資訊)。
${windows-identity} - Thread Windows identity information (username)
${windows-identity} - Thread Windows identity information (username) Nlog.WindowsIdentity

// 整合
${gelf} - 將 LogEvents 轉換為 GELF 格式以傳送到 Graylog NLog.GelfLayout External
${log4jxmlevent} - XML 事件描述與 log4j、Chainsaw 和 NLogViewer 相容。

// 程序、執行緒和程式集
${appdomain} - 當前應用域。
${assembly-version} - 預設應用程式域中可執行檔案的版本。
${gc} - 有關垃圾回收器的資訊。
${hostname} - 執行程序的計算機的主機名。
${local-ip} - 來自網路介面的本地 IP 地址。
${machinename} - 執行程序的計算機名稱。
${performancecounter} - 效能計數器。
${processid} - 當前程序的識別符號。
${processinfo} - 有關正在執行的程序的資訊,例如 StartTime、PagedMemorySize
${processname} - 當前程序的名稱。
${processtime} - 格式為 HH:mm:ss.mmm 的處理時間。
${threadid} - 當前執行緒的識別符號。
${threadname} - 當前執行緒的名稱。

// 銀光
${document-uri} - 承載當前 Silverlight 應用程式的 HTML 頁面的 URI。
${sl-appinfo} - 有關 Silverlight 應用程式的資訊。

// 網路、ASP.NET 和 ASP.NET 核心
${aspnet-appbasepath} - ASP.NET Application base path (Content Root) NLog.WebNLog.Web.AspNetCore
${aspnet-application} - ASP.NET Application variable. NLog.Web
${aspnet-environment} - ASP.NET Environment name NLog.Web.AspNetCore
${aspnet-item} - ASP.NET 'HttpContext' item variable. NLog.WebNLog.Web.AspNetCore
${aspnet-mvc-action} - ASP.NET MVC Action Name from routing parameters NLog.WebNLog.Web.AspNetCore
${aspnet-mvc-controller} - ASP.NET MVC Controller Name from routing parameters NLog.WebNLog.Web.AspNetCore
${aspnet-request} - ASP.NET Request variable. NLog.WebNLog.Web.AspNetCore
${aspnet-request-contenttype} - ASP.NET Content-Type header (Ex. application/json) NLog.Web.AspNetCore
${aspnet-request-cookie} - ASP.NET Request cookie content. NLog.WebNLog.Web.AspNetCore
${aspnet-request-form} - ASP.NET Request form content. NLog.WebNLog.Web.AspNetCore
${aspnet-request-headers} - ASP.NET Header key/value pairs. NLog.Web.Web.AspNetCore
${aspnet-request-host} - ASP.NET Request host. NLog.WebNLog.Web.AspNetCore
${aspnet-request-ip} - Client IP. NLog.WebNLog.Web.AspNetCore
${aspnet-request-method} - ASP.NET Request method (GET, POST etc). NLog.WebNLog.Web.AspNetCore
${aspnet-request-posted-body} - ASP.NET posted body / payload NLog.WebNLog.Web.AspNetCore
${aspnet-request-querystring} - ASP.NET Request querystring. NLog.WebNLog.Web.AspNetCore
${aspnet-request-referrer} - ASP.NET Request referrer. NLog.WebNLog.Web.AspNetCore
${aspnet-request-routeparameters} - ASP.NET Request route parameters. NLog.WebNLog.Web.AspNetCore
${aspnet-request-url} - ASP.NET Request URL. NLog.WebNLog.Web.AspNetCore
${aspnet-request-useragent} - ASP.NET Request useragent. NLog.WebNLog.Web.AspNetCore
${aspnet-response-statuscode} - ASP.NET Response status code content. NLog.WebNLog.Web.AspNetCore
${aspnet-session} - ASP.NET Session variable. NLog.WebNLog.Web.AspNetCore
${aspnet-sessionid} - ASP.NET Session ID variable. NLog.WebNLog.Web.AspNetCore
${aspnet-traceidentifier} - ASP.NET trace identifier NLog.WebNLog.Web.AspNetCore
${aspnet-user-authtype} - ASP.NET User auth. NLog.WebNLog.Web.AspNetCore
${aspnet-user-claim} - ASP.NET User Claims 授權值 NLog.Web.AspNetCore
${aspnet-user-identity} - ASP.NET User variable. NLog.WebNLog.Web.AspNetCore
${aspnet-user-isauthenticated} - ASP.NET User authenticated? NLog.WebNLog.Web.AspNetCore
${aspnet-webrootpath} - ASP.NET Web root path (wwwroot) NLog.WebNLog.Web.AspNetCore
${iis-site-name} - IIS site name. NLog.WebNLog.Web.AspNetCore

//參考: https://www.cnblogs.com/zmy2020/p/15936886.html

  2/5 rules(必須有) - 定義紀錄檔路由規則

  rules 下只有一種節點 logger(可同時設定多個),其屬性釋義如下:

  • name:logger 名稱,若為 * 則表示適用於所有紀錄檔,?:匹配單個字元;
  • minlevel:表示記錄的最低紀錄檔級別,只有大於等於該紀錄檔級別才會被記錄;
  • maxlevel:記錄的最高階別;
  • level:單極記錄,只記錄一個級別紀錄檔;
  • levels:同時記錄多個級別的紀錄檔,用逗號分隔;
  • writeTo:和 target 節點的 name 屬性值匹配,一個 rules 對應一個 target;
  • enabled:通過值為 false 禁用規則,而不用刪除;
  • ruleName:規則識別符號,允許使用 Configuration.FindRuleByName 和進行規則查詢 Configuration.RemoveRuleByName,在 NLog 4.6.4 中引入。

  3/5 variables - 宣告變數的值

  variable 元素定義了組態檔中需要用到的變數,一般用來表示複雜或者重複的表示式(例如檔名)。變數需要先定義後使用,否則組態檔將初始化失敗。

  • name:變數名;
  • value:變數值。

  定義變數之後,可以通過 ${my_name} 語法來使用。

 4/5 extensions - 定義要載入的 NLog 擴充套件項 *.dll 檔案

  extensions 節點可以新增額外的 NLog 元包或自定義功能,assembly 屬性指定的被包含程式集不帶字尾 .dll 。範例如下:

<nlog>
    <extensions> 
        <add assembly="MyAssembly" />
    </extensions>
    <targets>
        <target name="a1" type="MyFirst" host="localhost" />
    </targets>
    <rules>
        <logger name="*" minLevel="Info" appendTo="a1" />
    </rules>
</nlog>

  NLog 4.0 之後,與 NLog.dll 同目錄下名如 NLog*.dll 的程式集(如:NLog.CustomTarget.dll)會被自動載入。

  5/5 includes - 指定當前組態檔包含多個子組態檔

  通過 ${} 語法可以使用環境變數,下例展示包含一個名為當前機器名的組態檔。

<nlog>
    ...
    <include file="${machinename}.config" />
    ...
</nlog>

  NLog 4.4.2 之後可以使用萬用字元 * 指定多個檔案。例如:<include file="nlog-*.config" />

範例設定:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
        autoReload="true"
        throwExceptions="false"
        internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
    <variable name="appName" value="ConsoleAppDemo"/>
    <targets>
    <target name="logconsole" xsi:type="Console"
				layout="${longdate} [${uppercase:${level}}] ${callsite}(${callsite-filename:includeSourcePath=False}:${callsite-linenumber}) - ${message} ${exception:format=ToString}"
		/>
    <target name="logfile"
				xsi:type="File"
				fileName="${basedir}/logs/${appName}-${shortdate}.log"
				layout="${longdate} [${uppercase:${level}}] ${callsite}(${callsite-filename:includeSourcePath=False}:${callsite-linenumber}) - ${message} ${exception:format=ToString}"
				maxArchiveFiles="999"
				archiveFileName="${basedir}/logs/${appName}-${shortdate}-${###}.log"
				createDirs="true"
				archiveAboveSize="102400"
				archiveEvery="Day"
				encoding="UTF-8"
		/>
    </targets>
    <rules>
    <logger name="*" minlevel="Debug" writeTo="logfile" />
    </rules>
</nlog>

  參考:完善 .Net Core 專案 — NLog入門 (紀錄檔元件)

第二種方式:嵌入程式組態檔

  NLog 設定資訊可以嵌入在 .net 應用程式自身的組態檔中,例如 *.exe.config 或者 *.web.config 中,需要使用 configSections 節點設定,如下 XML 程式碼,再將其他設定填入 nlog 節點即可。

  nlog 節點內的內容,參考前邊‘第一種方式’。

<configuration>
  <configSections>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
  </configSections>
  <nlog>
      ......
  </nlog>
</configuration>

2. 測試程式碼

static void Main(string[] args)
{
    try
    {
        LoggerHelper._.Info($"完成");
        LoggerHelper._.Debug($"Debug完成");
        LoggerHelper._.Error($"Error完成");
        throw (new Exception());
    }
    catch (Exception ex)
    {
        LoggerHelper._.Error(ex.Message);
    }
}
// 輸出紀錄檔
2023-04-04 17:14:45.6651 [INFO] YOKAVerse.Net.Log.LoggerHelper.Info(Logger.cs:40) - 完成 
2023-04-04 17:14:46.7303 [DEBUG] YOKAVerse.Net.Log.LoggerHelper.Debug(Logger.cs:28) - Debug完成 
2023-04-04 17:14:47.2924 [ERROR] YOKAVerse.Net.Log.LoggerHelper.Error(Logger.cs:76) - Error完成 
2023-04-04 17:14:49.5869 [ERROR] YOKAVerse.Net.Log.LoggerHelper.Error(Logger.cs:76) - Exception of type 'System.Exception' was thrown. 

3. 紀錄檔記錄類

以下程式碼對 NLog 進行了封裝,將紀錄檔記錄先存線上程安全的佇列裡,以避免呼叫寫入檔案時 I/O 的耗時操作拖垮應用程式

佇列有兩個,一個是操作佇列-concurrentQueue_operation,一個是助手佇列-concurrentQueue_assistant,程式中的紀錄檔記錄需求直接寫入助手佇列,避免影響程式頻繁寫入造成的系統等待。當操作佇列中的記錄處理完成後,再將助手佇列的記錄轉至操作佇列,繼續進行比較耗時的寫入操作。

當然這種方法在提高系統響應速度的同時,也存在一個弊端,就是在程式崩潰而異常退出時,可能造成積壓在佇列中的紀錄檔記錄未全部完成落地,導致紀錄檔內容丟失。所以使用時還請權衡利弊,慎重使用。

public class LoggerHelper
{
    /// <summary>
    /// 範例化nLog,即為獲取組態檔相關資訊(獲取以當前正在初始化的類命名的記錄器)
    /// </summary>
    private readonly NLog.Logger logger = LogManager.GetCurrentClassLogger();
    private static LoggerHelper _obj;
    /// <summary>
    /// 輔助佇列
    /// </summary>
    private static ConcurrentQueue<LogModel> concurrentQueue_assistant = new ConcurrentQueue<LogModel>();
    /// <summary>
    /// 操作佇列
    /// </summary>
    private static ConcurrentQueue<LogModel> concurrentQueue_operation = new ConcurrentQueue<LogModel>();
    private static string lockobj_assistant = string.Empty;
    private static string lockobj_operation = string.Empty;

    public static LoggerHelper LHR
    {
        get => _obj ?? (_obj = new LoggerHelper());
        set => _obj = value;
    }

    public LoggerHelper()
    {
        InitializeTask();
    }

    private static LogModel logModel_init = null;
    /// <summary>
    /// 初始化後臺執行緒
    /// </summary>
    private void InitializeTask()
    {
        if (logModel_init == null)
        {
            logModel_init = new LogModel();
            Thread t = new Thread(new ThreadStart(LogOperation));
            t.IsBackground = false;
            t.Start();
        }
    }

    /// <summary>
    /// 記錄紀錄檔
    /// </summary>
    private void LogOperation()
    {
        while (true) // 執行緒持續處理
        {
            if (concurrentQueue_assistant.Count > 0 && concurrentQueue_operation.Count == 0)
            {
                lock (lockobj_assistant)
                {
                    concurrentQueue_operation = concurrentQueue_assistant; // 將資料轉至操作佇列
                    concurrentQueue_assistant = new ConcurrentQueue<LogModel>(); // 注意此處不可用 .Clear() 因為 ConcurrentQueue<T> 為參照型別
                }
                LogModel logModel;
                // 取出佇列 concurrentQueue_operation 中待寫入的紀錄檔記錄,直至全部記錄完成
                while (concurrentQueue_operation.Count > 0 && concurrentQueue_operation.TryDequeue(out logModel))
                {
                    switch (logModel.type) // 紀錄檔型別分流
                    {
                        case NLogLevel.Trace:
                            if (logModel.exobj != null)
                                logger.Trace(logModel.content);
                            else
                                logger.Trace(logModel.content, logModel.exobj);
                            break;
                        case NLogLevel.Debug:
                            if (logModel.exobj != null)
                                logger.Debug(logModel.content);
                            else
                                logger.Debug(logModel.content, logModel.exobj);
                            break;
                        case NLogLevel.Info:
                            if (logModel.exobj != null)
                                logger.Info(logModel.content, logModel.exobj);
                            else
                                logger.Info(logModel.content);
                            break;
                        case NLogLevel.Error:
                            if (logModel.exobj != null)
                                logger.Error(logModel.content, logModel.exobj);
                            else
                                logger.Error(logModel.content);
                            break;
                        case NLogLevel.Warn:
                            if (logModel.exobj != null)
                                logger.Warn(logModel.content, logModel.exobj);
                            else
                                logger.Warn(logModel.content);
                            break;
                        case NLogLevel.Fatal:
                            if (logModel.exobj != null)
                                logger.Fatal(logModel.content, logModel.exobj);
                            else
                                logger.Fatal(logModel.content);
                            break;
                        default:
                            break;
                    }
                }
            }
            else
                Thread.Sleep(1000);
        }
    }

    /// <summary>
    /// 加入佇列前,根據紀錄檔級別統一驗證
    /// </summary>
    /// <param name="logModel"></param>
    public void EnqueueLogModel(LogModel logModel)
    {
        if ((logModel.type == NLogLevel.Trace && logger.IsTraceEnabled) || (logModel.type == NLogLevel.Debug && logger.IsDebugEnabled)
            || (logModel.type == NLogLevel.Info && logger.IsInfoEnabled) || (logModel.type == NLogLevel.Warn && logger.IsWarnEnabled)
            || (logModel.type == NLogLevel.Error && logger.IsErrorEnabled) || (logModel.type == NLogLevel.Fatal && logger.IsFatalEnabled))
        {
            lock (lockobj_assistant)
            {
                concurrentQueue_assistant.Enqueue(logModel);
            }
        }
    }

    /// <summary>
    /// Trace,追蹤,非常詳細的紀錄檔,該紀錄檔等級通常僅在開發過程中被使用
    /// </summary>
    /// <param name="msg"></param>
    public void Trace(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Trace, content = logcontent });
    }
    public void Trace(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Trace, content = logcontent, exobj = exception });
    }

    /// <summary>
    /// Debug,偵錯,詳盡資訊次於 Trace,在生產環境中通常不啟用
    /// </summary>
    /// <param name="msg"></param>
    public void Debug(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Debug, content = logcontent });
    }
    public void Debug(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Debug, content = logcontent, exobj = exception });
    }

    /// <summary>
    /// Info,資訊,通常在生產環境中通常啟用
    /// </summary>
    /// <param name="msg"></param>
    public void Info(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Info, content = logcontent });
    }
    public void Info(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Info, content = logcontent, exobj = exception });
    }

    /// <summary>
    /// Warn,警告,通常用於非關鍵問題,這些問題可以恢復,或者是暫時的故障
    /// </summary>
    /// <param name="msg"></param>
    public void Warn(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Warn, content = logcontent });
    }
    public void Warn(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Warn, content = logcontent, exobj = exception });
    }

    /// <summary>
    /// Error,錯誤,多數情況下記錄Exceptions(異常)資訊
    /// </summary>
    /// <param name="msg"></param>
    public void Error(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Error, content = logcontent });
    }
    public void Error(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Error, content = logcontent, exobj = exception });
    }

    /// <summary>
    /// Fatal,致命錯誤,非常嚴重的錯誤
    /// </summary>
    /// <param name="msg"></param>
    public void Fatal(string logcontent)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Fatal, content = logcontent });
    }
    public void Fatal(string logcontent, Exception exception)
    {
        EnqueueLogModel(new LogModel() { type = NLogLevel.Fatal, content = logcontent, exobj = exception });
    }
}
public class LogModel
{
    public NLogLevel type { get; set; }
    public string content { get; set; }
    public Exception exobj { get; set; }
}
/// <summary>
/// NLog 紀錄檔等級
/// </summary>
public enum NLogLevel
{
    Trace,
    Debug,
    Info,
    Warn,
    Error,
    Fatal
}

  參考:C# 超高速高效能寫紀錄檔 程式碼開源       .net core 中的那些常用的紀錄檔框架(NLog篇)

四、紀錄檔檢視器

作為一名研發人員,高效率的紀錄檔分析是必須的,當然好的工具也是前提條件。

要想高效分析紀錄檔,有幾個問題需要解決:

  • 快速定位,在海量紀錄檔資訊中快速定位目標行;
  • 高亮顯示,以不同顏色顯示目標行,以便分類提高辨識度;
  • 只顯示有用的行。

在日常開發使用最多的莫過於 NotePad++ 了,儘管其可以通過 「搜尋-標記/標記所有-使用格式1/2/3/4/5」的操作來實現以上的前兩點,但是操作較繁瑣,當紀錄檔行數比較多時,也無法僅顯示標記行,從而造成效率低下。

當然,對於普通的業務量不太高的紀錄檔記錄,NotePad++ 足以滿足使用。

下面介紹一個非常簡單實用的開源紀錄檔檢視工具 TextAnalysisTool.NET。

1. 下載應用程式包

下載完成後,如下圖開啟最新版的應用程式:

  

2. 分析的紀錄檔檔案

按照「File -> Open」選擇要開啟的紀錄檔檔案。

雙擊任意行,便會跳出「Add Filter」視窗:(Text 預設為滑鼠焦點行的內容)

  

可以通過修改「Text Color」和「Background」來指定查詢結果的文字和行底色,達到高亮顯示目的。

其他選項:Description:描述;Excluding:排除,不包含;Case-sensitive:大小寫敏感;Regular-expression:按照正規表示式查詢。

如下圖範例,查詢三個語句,標誌為不同的行底色效果:

  

若想只顯示查詢目標所在的行,可以如下圖滑鼠操作,也可使用快捷鍵 Ctrl+H,取消時重複操作即可。

  

  參考:使用TextAnalysisTool來快速提高你分析文字紀錄檔的效率