在透過 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 時以手動方式加入對應資料。
完整 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,以此紀錄錯誤發生當下的使用者資訊。
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,這樣就可透過拋出的錯誤來排除問題囉!
參考資訊
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !