ASP.NET MVC NLog Tutorial

2022-02-02

說明如何使用 NLog 作為 ASP.NET MVC 的稽核與可歸責性解決方案,從安裝教學與 Config 編輯說明到使用檔案、資料庫與 Email Log 使用情境,並且回應資安法《附表十、資通系統防護基準修正規定》的控制措施,NLog 的強大絕對是每一位 .NET 開發人員都不能過錯過的 😎

logo

說明

環境設定

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

2.4.1 資通系統產生之日誌應包含事件類型、發生時間、發生位置及任何與事件相關之使用者身分識別等資訊,採用單一日誌機制,確保輸出格式之一致性,並應依資通安全政策及法規要求納入其他相關資訊
附表十、資通系統防護基準修正規定

為符合 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 系統的故障處理

2.4.1 資通系統於日誌處理失效時,應採取適當之行動
附表十、資通系統防護基準修正規定

預設上 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 等來判斷是否以電子郵件通知。

後續行動

2.1.3 應記錄資通系統管理者帳號所執行之各項功能
附表十、資通系統防護基準修正規定

參考資料

使用NLog - Advanced .NET Logging (2)

Log日誌功能(二)NLog使用及追蹤

Logging Troubleshooting

Internal Logging | NLog

Dtabase Target | NLog

相關連結

ASP.NET MVC 從無到有打造一個應用系統

Visual Studio 入門教學