[NLog] 自訂 Event Context Layout 將額外錯誤資訊寫入 DB 獨立欄位

在透過 NLog 記錄 Exception 錯誤至 DB 時,當有其他「額外」資訊需要一併紀錄到「獨立」欄位時,可以利用自訂 Event Context Layout 來自行決定需輸出的欄位資訊,讓錯誤訊息清楚地呈現在每一筆 Log 資料的特定欄位中,對於錯誤釐清與各項資訊交叉比對都較為方便。

前言


NLog 在紀錄 Exception 時已提供不少 layout 參數供開發人員使用,但對維護人員能夠記錄下的錯誤資訊當然是越齊全越好,因此當有額外資訊需紀錄在個別 DB 欄位時 (例如發生錯誤的用戶編號、瀏覽網頁裝置 ... ),可以透過自訂 Event Context Layout 來自行決定輸出的欄位資訊,讓開發人員透過這些額外的資訊,理解該用戶當下的操作情境,讓錯誤得以重現並進行排除。以下介紹。

 

 

建立資料表


建立我們所需要的 ERROR_LOG 資料表,其中 ERROR_CREATOR 欄位為筆者自行定義的資訊,主要是紀錄問題發生當下的使用者為何;而其他欄位則是 NLog 預設傳入 Exception 就可提供的 layout 資訊。


CREATE TABLE [dbo].[ERROR_LOG](
	[ERROR_LOG_ID] [BIGINT] IDENTITY(1,1) NOT NULL,
	[ERROR_CREATE_DATE] [datetime] NOT NULL,
	[ERROR_CREATOR] [varchar](100) NULL,
	[ERROR_MACHINE_NAME] [varchar](255) NULL,
	[ERROR_LOCATION] [varchar](255) NULL,
	[ERROR_MESSAGE] [nvarchar](max) NULL,
	[ERROR_CALL_SITE] [varchar](1024) NULL,
	[ERROR_EXCEPTION] [text] NULL,
	[ERROR_STACK_TRACE] [varchar](1024) NULL,
	[ERROR_THREAD_ID] [int] NULL,
	[ERROR_LEVEL] [varchar](5) NULL,
	CONSTRAINT [PK_LOG_ERROR] PRIMARY KEY CLUSTERED 
	(
		[ERROR_LOG_ID] ASC
	)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

ALTER TABLE [dbo].[ERROR_LOG] ADD  CONSTRAINT [DF_LOG_ERROR_CREATE_DATE]  DEFAULT (getdate()) FOR [ERROR_CREATE_DATE]
GO


 

 

建立 NLog 設定擋


透過 NLog.config 設定來將錯誤記錄至 DB 目標,而絕大部分的 parameter layout 都是 NLog 所定義的,其中只有 @ERROR_CREATOR 這個 parameter 使用了自訂屬性,設定中的「紅色文字」表示自訂屬性名稱,後續可在寫入 Log 時以手動方式加入對應資料。

<parameter name="@ERROR_CREATOR" layout="${event-properties:item=ErrorCreator}"/>

 

完整 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">

  <!--[設定] 寫入目標-->
  <targets>

    <target name="db" xsi:type="Database"
          commandText="INSERT ERROR_LOG (ERROR_CREATOR, ERROR_MACHINE_NAME, ERROR_LOCATION, ERROR_MESSAGE, ERROR_CALL_SITE, ERROR_EXCEPTION, ERROR_STACK_TRACE, ERROR_THREAD_ID, ERROR_LEVEL)
	               VALUES (@ERROR_CREATOR, @ERROR_MACHINE_NAME, @ERROR_LOCATION, @ERROR_MESSAGE, @ERROR_CALL_SITE, @ERROR_EXCEPTION, @ERROR_STACK_TRACE, @ERROR_THREAD_ID, @ERROR_LEVEL);">
      <parameter name="@ERROR_CREATOR" layout="${event-properties:item=ErrorCreator}"/>
      <parameter name="@ERROR_MACHINE_NAME" layout="${machinename}"/>
      <parameter name="@ERROR_LOCATION" layout="${logger}"/>
      <parameter name="@ERROR_MESSAGE" layout="${message}"/>
      <parameter name="@ERROR_CALL_SITE" layout="${callsite}"/>
      <parameter name="@ERROR_EXCEPTION" layout="${exception}"/>
      <parameter name="@ERROR_STACK_TRACE" layout="${stacktrace}"/>
      <parameter name="@ERROR_THREAD_ID" layout="${threadid}"/>
      <parameter name="@ERROR_LEVEL" layout="${level}"/>
    </target>

  </targets>


  <!--[設定] 紀錄規則-->
  <rules>
    <logger name="*" levels="Trace,Debug,Info,Warn,Error,Fatal" writeTo="db" />
  </rules>
</nlog>

 

 

指定 DB 連線字串


通常會將連線字串定義在 web.config 中,因此先定義一個 LogConnStr 連線字串給 NLog 使用吧。

<configuration>
  ...
  <connectionStrings>
    <!--連線字串-->
    <add name="LogConnStr" connectionString="Data Source=OOO;Initial Catalog=OOO;Persist Security Info=True;User ID=OOO;Password=OOO" providerName="System.Data.SqlClient" />
  </connectionStrings>
  ...
</configuration>

 

接著就是要讓 NLog 知道連線字串,可以使用的方式有兩種:

 

方法一、於 NLog.config 設定

最簡單就是直接在 NLog.config 中 target 補上 connectionStringName 屬性即可,名稱就依照 web.config 中設定的連線字串 name 設定。

 

方法二、透過程式執行設定

有時連線字串可能自行加密過,會需要先在程式中轉換後才可使用,因此就需要透過程式來告訴 NLog 連線字串為何;以 Web API 為例,可於 Application_Start 應用程式啟動時做設定,取得 NLog.config 中名為 db 的 target 資料,並且直接給予正確 DB 連線字串即可。

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(WebApiConfig.Register);

        // 設定 db target 的資料庫連線字串
        var dbTarget = LogManager.Configuration.FindTargetByName("db") as DatabaseTarget;
        dbTarget.ConnectionString = WebConfigurationManager.ConnectionStrings["LogConnStr"].ConnectionString;
        LogManager.ReconfigExistingLoggers();
    }
}

 

 

Global 錯誤捕捉 & 記錄


錯誤記錄在於捕捉那些不預期產生的錯誤,藉由這些被捕捉的資訊來反思推敲程式面是否有漏洞;以 Web API 為例,通常都會建立一個全域錯誤捕捉的 Filter 來獲得所有不預期拋出的 Exception 錯誤,所以先建立一個名為 FatalFilterAttribute 的 Exception Filter Attribute 類別。

覆寫 OnExceptoin 方法讓錯誤發生時傳遞 Exception 資訊給 NLog,並且直接透過 LogEventInfo.Properties 物件給予對應 ErrorCreator layout 的 value,以此紀錄錯誤發生當下的使用者資訊。

自訂名稱為 ErrorCreator 的 layout 可透過 LogEventInfo.Properties["ErrorCreator"] 給值。
public class FatalFilterAttribute : ExceptionFilterAttribute
{
    private static Logger logger = LogManager.GetCurrentClassLogger();

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {

        var errorCreator = "PoolUserId"; // might get request user from http request header
        var controllerName = string.Format("{0}/{1}",
                actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName,
                actionExecutedContext.ActionContext.ActionDescriptor.ActionName);

        // 建立 Log 事件資訊
        var logEvenInfo = new LogEventInfo()
        {
            LoggerName = controllerName,
            Level = LogLevel.Fatal,
            Message = actionExecutedContext.Exception.ToString(),
            Exception = actionExecutedContext.Exception
        };

        // 設定自定義的屬性值 (名稱需對應至 NLog.config 參數)
        // <parameter name="@ERROR_CREATOR" layout="${event-properties:item=ErrorCreator}"/>
        logEvenInfo.Properties["ErrorCreator"] = errorCreator;

        // 執行紀錄
        logger.Log(logEvenInfo);
    }
}

 

將剛定義的 FatalFilterAttribute 套用至整個應用程式,捕捉所有應用程式拋出的錯誤。

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configure(WebApiConfig.Register);

        // 將自定義的 Exceptoin Filter 套用至整個應用程式
        GlobalConfiguration.Configuration.Filters.Add(new FatalFilterAttribute());

    }
}

 

 

產生 Exception 來驗證


簡單來個低級錯誤,就是除法分母為 0 的情境。

public class HomeController : ApiController
{
    public IHttpActionResult Get(int menuSerilNo = -1)
    {
        var i = 0;
        var j = 1;
        var result = j / i;

        return Ok("done");
    }
}

 

執行後馬上就拋出錯誤,並且順利被 FatalFilterAttribute捕捉且記錄相關資訊至 DB 中。

筆者自行定義的 ERROR_CREATOR 資訊也順利寫入了,這下就可得知苦主是 PoolUserId 啦!我們可以好好跟他探探錯誤發生當下的操作情境,看是否能夠幫助開發人員重現問題。

 

 

什麼? 資料沒寫入!


當資料沒有順利記錄到 DB 時,很有可能就是 NLog 發生錯誤了;但 NLog 預設是不會將錯誤給拋出,所以必須將 throwExceptions 屬性設定為 true,這樣就可透過拋出的錯誤來排除問題囉!

 

 

參考資訊


Event Context Layout Renderer

 

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !