網頁

ASP.NET MVC - 凡走過,必留下痕跡之Log記錄

Log記錄 - 前言

我們之前已經寫過數篇討論檔案上傳下載的文章,如「檔案上傳下載筆記」,不過還有加強的空間,例如今天的主題「Log記錄」。

Log問題,在以前,可大可小,例如,我們寫網頁,可以什麼都不做,IIS還是會幫我們留一些記錄,我們還可以使用超強大Log流量分析軟體 AWStats來分析產出美美的報表。

但今日,個人資料處理法已經快要正式立法完成,我們必須讓走過我們的系統的使用者都留下Log記錄,以防日後舉證之需要。但不經整理的IIS Log記錄,法官應該是不可能接受,一是太大,二是太亂。類似 AWStats 的Log流量分析軟體,整理後的資訊,對於法律實際上效用有限,所以我們必須除了IIS Log記錄外,最好還能提供整個使用者的Log記錄。

Log記錄的方式

要留下Log記錄,基本方向可以有:
  1. System Log記錄
  2. File Log記錄
  3. Database Log記錄
  4. Log Server
System Log記錄指的是像IIS Log記錄就是System Log記錄。或撰寫程式碼,向Windows本身的事件檢視器來發送Log記錄。

File Log記錄指的是透過 System.IO 寫一支程式,將你要的Log記錄到一個純文字檔,例如, FileLogs.txt,因為程式自己寫,你能控制什麼是你要,什麼是你不要,與System Log記錄相比,可以免除太大、太亂的問題。檔案太大還是有可能,這部分可能要寫程式自動處理,或是手動處理。

Database Log記錄,即將Log記錄直接記錄在Database之中,與System Log記錄與File Log記錄相較,Database本來就是儲存與管理資料所在地,而且與撰寫發送Windows Log記錄事件檢視器程式碼和File Log記錄程式碼來看,資料庫 Log記錄程式碼簡單多了。。

Log Server一般而言,是當伺服器群成長到一定數量,會架一台專門處理Log記錄的硬體伺服器,例如,Linux平台的Syslog,Windows下的Kiwi Syslog Server來架設,或是買專門機架硬體式Log Server,來統一收集、分析、管理這些Server Log記錄。

真是可大可小。

以下,我會使用 ASP.NET MVC來實作一個Database Log記錄範例。

Database Log記錄 - Schema準備

針對我們檔案管理系統部分,我們會有兩張 Table,一張是記錄有權限的人員做了什麼動作,一張是記錄使用者下載了那些資料。

KKBruceFileLog Schema

這是檔案下載的Log記錄表,要注意的只有 HiterIP 長度為 50,為什訂那麼長?我們事先把 IPv6 考慮進去。這樣,不管我們的 .NET Framwork 程式是抓到 IPv4 或 IPv6 ,記錄進資料庫都不會有問題。

-- 請切換至你的資料庫 --
USE [KKBruce]
GO

/****** Object:  Table [dbo].[KKBruceFileLog]    Script Date: 11/08/2011 14:31:52 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[KKBruceFileLog](
 [FilesLogId] [int] IDENTITY(1,1) NOT NULL,
 [FileId] [int] NOT NULL,
 [HiterIP] [nvarchar](50) NOT NULL,
 [CreateTime] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_FilesLogs] PRIMARY KEY CLUSTERED 
(
 [FilesLogId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

/*
我的資料表裡有外來鍵及其他設定,
你可以依需求設定,單純測試不需要。
*/

/*
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'檔案編號' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'KKBruceFileLog', @level2type=N'COLUMN',@level2name=N'FileId'
GO

EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'檔案下載Log表' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'KKBruceFileLog'
GO

ALTER TABLE [dbo].[KKBruceFileLog]  WITH CHECK ADD  CONSTRAINT [FK_FilesLogs_Files] FOREIGN KEY([FileId])
REFERENCES [dbo].[ProductFile] ([FileId])
GO

ALTER TABLE [dbo].[KKBruceFileLog] CHECK CONSTRAINT [FK_FilesLogs_Files]
GO

ALTER TABLE [dbo].[KKBruceFileLog] ADD  CONSTRAINT [DF_FilesLogs_HitIPv4]  DEFAULT (N'下載者IPv4') FOR [HiterIP]
GO
*/

KKBruceEmployeeLog Schema

人員Log記錄表,記錄人員操作的任何動作,我們記錄登入人員的Id、動作、說明、IP、時間等資訊。另外補充,為了與 Entity Framework 有比較配合度,日期時間建議使用 datetime2 格式

-- 請切換至你的資料庫 --
USE [KKBruce]
GO

/****** Object:  Table [dbo].[KKBruceEmployeeLog]    Script Date: 11/08/2011 14:44:35 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[KKBruceEmployeeLog](
 [EmployeeLogID] [int] IDENTITY(1,1) NOT NULL,
 [EmployeeId] [int] NOT NULL,
 [ActionName] [nvarchar](250) NOT NULL,
 [Description] [nvarchar](max) NOT NULL,
 [ClientIP] [nvarchar](50) NOT NULL,
 [CreateTime] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_KKBruceEmployeesLogs] PRIMARY KEY CLUSTERED 
(
 [EmployeeLogID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

/*
我的資料表裡有外來鍵及其他設定,
你可以依需求設定,單純測試不需要。
*/

/*
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'人員Log記錄檔' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'KKBruceEmployeeLog'
GO

ALTER TABLE [dbo].[KKBruceEmployeeLog]  WITH CHECK ADD  CONSTRAINT [FK_EmployeesLogs_Employees] FOREIGN KEY([EmployeeId])
REFERENCES [dbo].[KKBruceEmployee] ([EmployeeId])
GO

ALTER TABLE [dbo].[KKBruceEmployeeLog] CHECK CONSTRAINT [FK_EmployeesLogs_Employees]
GO
*/

ASP.NET MVC實作Log記錄

如果拿ASP.NET與ASP.NET MVC比較,要在 ASP.NET MVC裡實作Log記錄,老實說,沒有太大難度。因為ASP.NET MVC架構上非常容易實作與執行這一類程式碼。ASP.NET麻煩了些。

什麼「這一類程式碼」?即需要「大量且重覆使用的程式碼」。在ASP.NET MVC裡有個機制稱 ActionFilter,就是專門在處理這種事,透過 ActionFilter 機制,我們可以很容易且簡單的去重覆使用我們的過濾器。因為我們要寫Log記錄,我們可以稱我們寫的為 Log ActionFilter、Log過濾器。

實作人員Log記錄


  • 新增一目錄「ActionFilters」→ 新增一類別「EmployeeLogAttribute.vb」→ 繼承「ActionFilterAttribute 屬性類別」→ 覆寫「OnActionExecuting 副程式」。

EmployeeLogAttribute.vb


Public Class EmployeeLogAttribute
    Inherits ActionFilterAttribute

    ' 由開發人員設定傳入相關Action的說明
    Public Property Description() As String

    Public Sub New()
    End Sub

    Public Overrides Sub OnActionExecuting(filterContext As ActionExecutingContext)
        ' 有使用者登入訊息
        If filterContext.HttpContext.User IsNot Nothing Then
            Using db As New KKBruceEntities

                ' 設定Log相關資訊
                Dim log As New KKBruceEmployeeLog() With
                {
                    .EmployeeId = (From e In db.KKBruceEmployee
                                   Where e.Account = filterContext.HttpContext.User.Identity.Name
                                   Select e.EmployeeId).First(),
                    .ActionName = filterContext.RouteData.Values("controller") & "/" & _
                                  filterContext.RouteData.Values("action"),
                    .Description = Me.Description,
                    .ClientIP = filterContext.HttpContext.Request.UserHostAddress,
                    .CreateTime = DateTime.Now.ToString()
                }

                ' 寫入資料庫
                db.KKBruceEmployeeLog.Add(log)
                db.SaveChanges()

                ' 邀免Log成長太大,保留六個月的Log

                ' --- 補充 ---
                ' 這一段會造成大量的Scan,Log越大,效能影響越明顯,
                ' 原因在每個人員的每一個操作,我們就會執行一次這段程式,
                ' 可在 Database 設定 Index、減少保留日期 … 來處理,
    ' 或改使用排程來週期性的刪除。
                ' EF 效能考量,可參考MSDN:
                ' http://msdn.microsoft.com/zh-tw/library/cc853327.aspx

                Dim DeleteLogs = From logs In db.KKBruceEmployeeLog
                                 Where logs.CreateTime <= DateTime.Now.AddMonths(-6)
                                 Select logs

                ' EF批次相關資料:
                ' 1. http://geeks.ms/blogs/unai/archive/2008/07/17/multiple-entity-updates-with-entity-framework-ef-fetch-updates.aspx
                ' 2. http://www.dotblogs.com.tw/wadehuang36/archive/2010/08/20/entityframeworkmultipleupdateanddelete.aspx

                For Each DelLog In DeleteLogs
                    db.Entry(DelLog).State = EntityState.Deleted
                Next
                db.SaveChanges()
            End Using
        End If
    End Sub
End Class

如果你想測試這一段程式碼,你還必須建立會員登入相關資料表及程式碼才會運作。你可以先
  1. 註解第12 ( filterContext.HttpContext.User )、18 ( EmployeeId )、55 ( End If )行;
  2. 還有 KKBruceEmployeeLog Schema 裡的 EmployeeId 也必須把 NOT NULL 關鍵字移除。
先不進行會員相關資料的取得及寫入,即可測試。

使用Log ActionFilter

針對我們寫好的Log ActionFilter,使用有非常簡單,開啟你要設定的 Controller,然後在Action上設定 Log ActionFilter屬性。

上傳檔案Log ActionFilter設定


#Region "上傳"
 <HttpPost()>
 <EmployeeLog(Description:="上傳檔案")>
 Function Create(upfile As HttpPostedFileBase) As ActionResult
  ' 略,請參考之前文章
  Return View()
 End Function
 
 <HttpPost()>
 <EmployeeLog(Description:="上傳多個檔案")>
 Function CreateMultiFile(form As FormCollection) As ActionResult
  ' 略,請參考之前文章
  Return View()
 End Function
#End Region

有進到 HttpPost 代表有Submit,我們只需一一為相關Action設定好EmployeeLog及說明,然後測試一下相關動作,再到資料庫或另寫一頁顯示Log記錄的頁面查詢,應該就能看到如下Log記錄:
EmployeeLogID EmployeeId ActionName Description ClientIP CreateTime
1 1 Account/LogOff 登出系統 ::1 2011-06-30 11:22:49.0000000
2 1 Account/Index 查看員工資料 ::1 2011-06-30 11:23:56.0000000
3 1 Account/Edit 修改人員資料 ::1 2011-06-30 11:24:16.0000000
4 1 Account/Index 查看員工資料 ::1 2011-06-30 11:24:16.0000000
5 1 Account/Edit 修改人員資料 ::1 2011-06-30 11:24:28.0000000

檔案下載的Log ActionFilter

一般檔案下載是匿名的,當我們把 EmployeeLogAttribute.vb 裡取得會員資料相關程式碼註解後,就差不多是檔案下載的 Log ActionFilter,把「設定Log相關資訊」及Entity Framework對應的「資料表名稱」改一改即可。然後,設定到檔案下載相關 Controller/Action 去,即可。

就算是採會員制,上面那個Log ActionFilter一樣寫好了,就拿去用吧!

一樣的東西,留給你去做。^_^

Log記錄結論

System Log或IIS Log說真的,要看也沒幾個人看得懂,就算看得懂,要從數萬、數十萬、數百萬行裡找出問題點,那本身就是一件高難度任務。透過ASP.NET MVC裡的ActionFilter幫忙,讓我們很簡單且快速就完成有效的Log記錄,不論是稽核、分析、統計、舉證…都更有力,且存放於Database,不論是透過 SSMS 或網頁,我們都能快速的查詢我們所需的資訊。

4 則留言:

  1. 您好,謝謝您的文章對我有所啟發。
    請教一下,我想用log來記錄網站後台操作過程,例如:查詢、新增、修改、刪除等。
    為了日後查詢,我決定使用Database Log記錄的方式。
    不過,這些記錄可能一天之內就會有上千筆,這樣一來資料成長的速度相當的可怕。
    不知道依您的經驗,如何處理比較適當?

    我本來想在網站的資料庫裡直接建立一個log資料表來記錄
    但是,想到記錄的成長速度,我怕資料庫肥大後,會影響網站的其他資料讀取速度

    所以,我想到的方式是用SQLite來建立Log資料庫
    然後每個月建立一個新的資料表來記錄該月的Log
    這樣會不會太瑣碎了?還是一個資料表到底,反正用SQL語法來查詢過濾即可得到資料?

    回覆刪除
    回覆
    1. 一天“上千筆”,這對MS SQL Server是小菜中小菜吧 XD

      如果可以,可以把Log存放至獨立資料庫中,這樣就不會影響正式資料庫。 :D

      刪除
  2. Bruce大您好:
    想請教您如果要做的功能是紀錄『每篇文章的瀏覽量』並依照瀏覽量大小做排序,您會建議使用MSSQL存取資料嗎? 目前的想法是記錄IP若是在2小時內有瀏覽過文章,就不寫入資料庫,反之則寫入,想請教您會如何處理呢?
    目前是擔心一個頁面要呈現多筆文章,若是每篇文章都有上百或上千筆瀏覽數,不知道效能上要如何做處理比較好呢?

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。