ASP.NET MVC NLog Tutorial
2022-02-02
說明如何使用 NLog 作為 ASP.NET MVC 的稽核與可歸責性解決方案,從安裝教學與 Config 編輯說明到使用檔案、資料庫與 Email Log 使用情境,並且回應資安法《附表十、資通系統防護基準修正規定》的控制措施,NLog 的強大絕對是每一位 .NET 開發人員都不能過錯過的 😎
說明
環境設定
Nuget 安裝 NLog.Config,會一併自動安裝 NLog 以及 NLog.Schema。
基礎使用方式
首先於 NLog.config 設定 targets
以及 rules
。
Section | Usage |
---|---|
Targets | 設定 Logs 的寫入位置,包含 Text File, CSV, Json, DB, Email 等。 |
Rules | 設定各種等級的以及各稱的 Logger 如何對應到 Target |
NLog.config
基礎範例是將 Log 寫入應用程式的 App_Data/Logs 資料夾當中,並以日期作為檔案命名。其中寫入的資料為 3 欄,分別是 Log 日期時間 (longdata)、Log 事件等級 (levle) 以及可以客製化的訊息 (Message)。
<targets>
<target xsi:type="File" name="text"
fileName="${basedir}/App_Data/Logs/${shortdate}/${logger}.txt"
layout="${longdate} ${uppercase:${level}} ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="text" />
</rules>
HomeController.cs
在 Controller 中取得屬於目前 Class 名稱的 Logger,並且可以 Info 事件等級的方式將資料寫入 Log 檔案當中。
public class HomeController : Controller
{
private NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
public ActionResult Index()
{
var userName = User.Identity.Name;
var actionName = ControllerContext.RouteData.Values["action"];
logger
.Info($"{userName} Into {actionName} Page");
return View();
}
}
深入 NLog
NLog 事件等級
事件等級分為 6 類,分別如下:
- Trace
- 詳盡的資訊內容,包含通訊協定之間的 payload 等,通常用於開發期間的除錯。
- Debug
- 除錯情境使用的資訊,資訊量較 Trace 少。
- Info
- 一般通知訊息,在正式環境上使用的事件等級。
- Warning
- 輕微或短暫性的錯誤,主要用於警示。
- Error
- 觸發例外情形的事件等級,應由人為介入處理。
- Fatal
- 非常嚴重的錯誤,必須立刻處理。
Targets
Text
<target xsi:type="File" name="text"
fileName="${basedir}/App_Data/Logs/${shortdate}/${logger}.txt"
layout="${longdate} ${uppercase:${level}} ${message}" />
CSV
<target xsi:type="File" name="csv"
fileName="${basedir}/App_Data/Logs/${shortdate}/${logger}.csv">
<layout xsi:type="CSVLayout">
<column name="time" layout="${longdate}" />
<column name="message" layout="${message}" />
<column name="level" layout="${level}"/>
</layout>
</target>
Json
<target xsi:type="File" name="json"
fileName="${basedir}/App_Data/Logs/${shortdate}/${logger}.json" >
<layout xsi:type="JsonLayout">
<attribute name="time" layout="${longdate}" />
<attribute name="level" layout="${level:upperCase=true}"/>
<attribute name="message" layout="${message}" />
</layout>
</target>
Database
<target name="database" xsi:type="Database">
<connectionString>
server=.;Database=NLogDB;user id=LogWriter;password=********
</connectionString>
<commandText>
insert into dbo.Log
(
MachineName, Logged, Level, Message, Exception, UserName, IP
)
values (
@MachineName, @Logged, @Level, @Message, @Exception, @UserName, @IP
);
</commandText>
<parameter name="@MachineName" layout="${machinename}" />
<parameter name="@Logged" layout="${date}" />
<parameter name="@Level" layout="${level}" />
<parameter name="@Message" layout="${message}" />
<parameter name="@Exception" layout="${exception:tostring}" />
<parameter name="@UserName" layout="${event-properties:Property1}" />
<parameter name="@IP" layout="${event-properties:Property2}" />
</target>
除了明確指定 ConnectionString 也可以使用 web.config
當中已經存在的 Connection String:
<targets>
<target name="database" xsi:type="Database" connectionStringName="NorthwindEntities">
<commandText>
...
</commandText>
</target>
</targets>
注意如果要使用已經存在的 Connection String,要輸入「connectionStringName」否則會出現「初始化字串的格式從索引 0 處開始即與規格不符。」讓人摸不著意思的錯誤訊息😥
另外要先在資料庫建立資料表:
CREATE TABLE [dbo].[Log](
[Id] [int] IDENTITY(1,1) NOT NULL,
[MachineName] [nvarchar](50) NOT NULL,
[Logged] [datetime] NOT NULL,
[Level] [nvarchar](50) NOT NULL,
[Message] [nvarchar](max) NOT NULL,
[Logger] [nvarchar](250) NULL,
[Callsite] [nvarchar](max) NULL,
[Exception] [nvarchar](max) NULL,
[UserName] [nvarchar](250) NULL,
[IP] [nvarchar](128) NULL,
[RequestUrl] [nvarchar](200) NULL,
CONSTRAINT [PK_dbo.Log] PRIMARY KEY CLUSTERED
(
[Id] ASC
)
)
Custom Log Properties
為符合 SSDLC 需求,除了 Message 外,可以使用 Properties 增加客製 Log 欄位,包含使用者身分識別 (UserName) 以及裝置來源 (IP Address)。
logger 使用 WithProperty
將資訊送入 Target 當中。
LogFilter.cs / namespace NLogFilters.Filters
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var userName = controller.HttpContext.User.Identity.Name;
var ip = controller.HttpContext.Request.UserHostAddress;
var actionName = controller.RouteData.Values["action"];
logger
.WithProperty("Property1", userName)
.WithProperty("Property2", ip)
.Info($"{userName} Request { actionName } Page");
}
搭配在 NLog.config 的 Target 設定,指令如何處理傳入的 Property 儲存到資料庫當中,本例 Property1、Property2 分別代表傳入 UserName 以及 IP,Target 是資料庫,使用 Insert 的方式傳入資料庫當中。
<target name="database" xsi:type="Database">
<commandText>
insert into dbo.Log
(
MachineName, Logged, Level, Message, Exception, UserName, IP
)
values (
@MachineName, @Logged, @Level, @Message, @Exception, @UserName, @IP
);
</commandText>
<parameter name="@MachineName" layout="${machinename}" />
<parameter name="@Logged" layout="${date}" />
<parameter name="@Level" layout="${level}" />
<parameter name="@Message" layout="${message}" />
<parameter name="@Exception" layout="${exception:tostring}" />
<parameter name="@UserName" layout="${event-properties:Property1}" />
<parameter name="@IP" layout="${event-properties:Property2}" />
</target>
Fitler Register
將 Logger 以 ActionFilter 可以容易的重複使用 Logger:
filters/LogFilter.cs
public class LogFilter : ActionFilterAttribute
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller.ControllerContext;
var userName = controller.HttpContext.User.Identity.Name;
var ip = controller.HttpContext.Request.UserHostAddress;
var actionName = controller.RouteData.Values["action"];
logger
.WithProperty("Property1", userName)
.WithProperty("Property2", ip)
.Info($"{userName} Into { actionName } Page");
base.OnActionExecuting(filterContext);
}
}
Action Level
註冊在單一 Action 上
public class HomeController : Controller
{
[LogFilter]
public ActionResult Index()
{
...
}
}
Controller Level
註冊在 Controller 上,所有 Controller 下的 Action 都會受影響
[LogFilter]
public class HomeController : Controller
{
public ActionResult Index()
{
...
}
}
Global Level
註冊在 Global.asax,所有的 Controller 都會受到影響
Global.asax
protected void Application_Start()
{
GlobalFilters.Filters.Add(new NLogFilters.Filters.LogFilter());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
NLog 系統的故障處理
預設上 NLog 無法正常運作時是保持沉默的,不干擾應用程式的進行。但可以在 NLog.config 中進行設定:
如果將 throwExceptions
調整為 true,則 NLog 會產生 Exception 讓應用程式無法運作,但這個方式對於應用程式有巨大的影響,業務使用可能因此受到打斷。
<nlog throwExceptions="true"
internalLogLevel="Error" internalLogFile="c:\temp\nlog-internal.log">
另可以調整 throwConfigExceptions
確認 Config 是否有錯誤 (例如 DB 連線資訊錯誤)
<nlog throwConfigExceptions="true">
或是利用 Internal Logging 的方式,確保稽核本身的錯誤資訊能夠被留下紀錄:
<nlog internalLogLevel="Error"
internalLogFile="c:\temp\nlog-internal.log">
但更理想的方式是如果稽核發生問題,應該使用電子郵件通知到系統管理人員,但目前這個機制尚未實現。可能可以從確認 NLog 的 config 等來判斷是否以電子郵件通知。
後續行動
參考資料
使用NLog - Advanced .NET Logging (2)