前言:好久沒動筆了,都有點生疏,12月都要接近尾聲,可是這月連一篇的產出都沒有,不能壞了“規矩”,今天還是來寫一篇。最近個把月確實很忙,不過每天早上還是會抽空來園子里逛逛。一如既往,園子里每年這個時候都有大把的年終總結、回憶過去展望未來之類的文章。博主是沒時間寫總結了,要學的東西太多。關於Vue的系列一定要抽時間補上。最近剛用了一個日志組件Elmah,比較適合開發階段異常信息的快速定位與追溯,有興趣的跟着博主一起來看看吧。
本文原創地址:http://www.cnblogs.com/landeanfen/p/6221403.html
一、組件介紹
ELMAH的全稱是The Error Logging Modules And Handlers,翻譯過來是錯誤日志模塊和處理。顧名思義,就是一個日志的攔截和處理組件,說到.net的日志組件,大家的第一反應該是Log4Net、NLog等這些東西,關於Log4Net和NLog,可以說是.net日志組件里面使用最為廣泛的組件了,它們功能強大、使用方便。相比它們:
1、ELMAH的使用更加簡單,它甚至不用寫一句代碼,只需要引入dll,然后在Web.config里面配置相應的節點即可;
2、按照網上的說法,ELMAH是一種“可拔插式”的組件,即在一個運行的項目里面我們可以隨意輕松加入日志功能,或者移除日志功能;
3、ELMAH組件自帶界面,不用寫任何代碼,即可查看異常日志的界面,輕松找到當前異常的詳細信息;和web的結合更加緊密;
4、組件提供了一個用於集中記錄和通知錯誤日志的機制,通過郵件的機制通知錯誤信息給相關人員。
二、組件安裝使用
1、安裝組件
Elmah的安裝使用同樣也很簡單,我們萬能的Nuget幫我們一鍵搞定。

安裝如上圖的兩個組件即可在MVC項目里面應用起來。注意這里有一個依賴關系Elmah.MVC依賴Elmah.corelibrary組件。
安裝成功后,項目中會添加如下兩個dll的引用。

2、配置組件
組件安裝成功之后,會自動在我們MVC項目的Web.config里面添加如下節點:
<sectionGroup name="elmah"> <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" /> <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" /> <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" /> <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" /> </sectionGroup> ...... <appSettings> <add key="elmah.mvc.disableHandler" value="false" /> <add key="elmah.mvc.disableHandleErrorFilter" value="false" /> <add key="elmah.mvc.requiresAuthentication" value="false" /> <add key="elmah.mvc.IgnoreDefaultRoute" value="false" /> <add key="elmah.mvc.allowedRoles" value="*" /> <add key="elmah.mvc.allowedUsers" value="*" /> <add key="elmah.mvc.route" value="elmah" /> <add key="elmah.mvc.UserAuthCaseSensitive" value="true" />
</appSettings> ...... <httpModules> <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" /> <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" /> <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" /> </httpModules> ...... <system.webServer> <modules> <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" /> <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" /> <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" /> </modules> </system.webServer> ...... <elmah> </elmah>
除此之外,還需要手動在Web.config里面加入如下配置節點:
<elmah> <security allowRemoteAccess="false" /> <!--三種存儲方式:內存、xml、數據庫。存儲到xml里面格式如下行--> <!--<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Static/Log/" />--> <!--<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="ElmahConn" />--> </elmah> <location path="elmah.axd" inheritInChildApplications="false"> <system.web> <httpHandlers> <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" /> </httpHandlers> </system.web> <system.webServer> <handlers> <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" /> </handlers> </system.webServer> </location>
需要說明的是在elmah節點里面可以配置組件支持的三種日志存儲方式:
- 如果elmah節點不配置任何任何東西,表示默認組件是將日志信息保存到內存中,這種方式的弊端在於一旦系統重啟,所有的異常信息都會丟失;
- <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Static/Log/" /> 表示日志保存到xml文件里面,第二個屬性配置保存xml的路徑;
- <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="ElmahConn" /> 表示日志保存到數據庫里面,第二個參數對應數據庫的鏈接字符串的name,也就是說如果配置保存到數據庫,則必須要在web.config里面配置連接字符串,這里的connectionStringName就對應我們配置的連接字符串的name,並且這個時候還需要使用腳本在數據庫里面新建需要的表和存儲過程,這個放在后面說。
3、測試效果
通過以上安裝和配置,我們即可記錄異常。首先我們采用內存的方式來記錄異常,先來看看效果:
public class HomeController : Controller { public ActionResult Index() { return View(); } //測試異常 public JsonResult Get() { var a = 0; var b = 5; var c = b / a; return Json(a, JsonRequestBehavior.AllowGet); } }
然后我們前端ajax來調用
<html> <head> <title>Index</title> <script src="~/Scripts/jquery-1.9.1.min.js"></script> <script type="text/javascript"> $(function () { $("#btn").click(function () { $.ajax({ type: 'get', url: '/Home/Get', data: {}, }); }); }); </script> </head> <body> <h1>首頁</h1> <div> <button type="button" id="btn">測試異常</button> </div> </body> </html>
點擊按鈕出現異常,然后我們通過地址http://localhost:51230/elmah.axd來查看異常信息。

點擊展開詳細信息如下

到這一步,我們的組件就可以生效了。但是看到這個elmah.axd這個路徑太惡心了,我們想要改一下,用我們自己的路徑,呵呵,這個確實是可以配置的。我們再來看看web.config里面的location節點,在location節點里面其實就配置一個東西——HttpHandler,雖然有 system.web 和 system.webServer 兩個節點,如果你仔細觀察,其實它們是一個東西,只不過是為了兼容不同的IIS版本而寫了兩個配置,這一點和我們HttpHandler的配置是相同的,對於這個配置不熟悉的,可以看看博主之前的文章http://www.cnblogs.com/landeanfen/p/6000978.html。我們將location的節點改成這樣:
<location path="log.axd" inheritInChildApplications="false"> <!--<system.web> <httpHandlers> <add verb="POST,GET,HEAD" path="log.axd" type="Elmah.ErrorLogPageFactory, Elmah" /> </httpHandlers> </system.web>--> <system.webServer> <handlers> <add name="ELMAH" verb="POST,GET,HEAD" path="log.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" /> </handlers> </system.webServer> </location>
然后我們通過http://localhost:51230/log.axd這個地址來訪問,能達到同樣的效果。博主本地使用的是IIS經典模式,所以使用的是system.webServer里面的配置,注釋掉或者刪掉system.web節點都不會有任何問題。
三、功能介紹
1、將日志信息保存到數據庫
上述使用在內存中保存日志信息的方式,在實際項目中基本上很少會用到。除此之外,還有保存到xml和數據庫兩種方式。保存到xml文件這個沒什么好說的,就是配置一下保存路徑即可。下面就以保存到數據庫的方式來看看想想介紹下。
首先我們在web.config里面加入連接字符串節點。
<connectionStrings> <add name="ElmahConn" connectionString="Data source=127.0.0.1;Initial Catalog=PowerMangent;Persist Security Info=True;User ID=sa;Password=123456" providerName="System.Data.EntityClient" /> <add name="ElmahContainer" connectionString="metadata=res://*/Elmah.csdl|res://*/Elmah.ssdl|res://*/Elmah.msl;provider=System.Data.SqlClient;provider connection string="data source=127.0.0.1;initial catalog=PowerMangent;user id=sa;password=123456;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" /> </connectionStrings>
然后配置elmah節點
<elmah> <security allowRemoteAccess="false" /> <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="ElmahConn" /> </elmah>
最后就是在數據庫創建需要的表和存儲過程,官方提供了腳本,這個不用我們擔心,腳本如下
CREATE TABLE dbo.ELMAH_Error ( ErrorId UNIQUEIDENTIFIER NOT NULL, Application NVARCHAR(60) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, Host NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, Type NVARCHAR(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, Source NVARCHAR(60) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, Message NVARCHAR(500) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, [User] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, StatusCode INT NOT NULL, TimeUtc DATETIME NOT NULL, Sequence INT IDENTITY (1, 1) NOT NULL, AllXml NTEXT COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE dbo.ELMAH_Error WITH NOCHECK ADD CONSTRAINT PK_ELMAH_Error PRIMARY KEY NONCLUSTERED ( ErrorId ) ON [PRIMARY] GO ALTER TABLE dbo.ELMAH_Error ADD CONSTRAINT DF_ELMAH_Error_ErrorId DEFAULT (newid()) FOR [ErrorId] GO CREATE NONCLUSTERED INDEX IX_ELMAH_Error_App_Time_Seq ON dbo.ELMAH_Error ( [Application] ASC, [TimeUtc] DESC, [Sequence] DESC ) ON [PRIMARY] GO SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS ON GO CREATE PROCEDURE dbo.ELMAH_GetErrorXml ( @Application NVARCHAR(60), @ErrorId UNIQUEIDENTIFIER ) AS SET NOCOUNT ON SELECT AllXml FROM ELMAH_Error WHERE ErrorId = @ErrorId AND Application = @Application GO SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS ON GO CREATE PROCEDURE dbo.ELMAH_GetErrorsXml ( @Application NVARCHAR(60), @PageIndex INT = 0, @PageSize INT = 15, @TotalCount INT OUTPUT ) AS SET NOCOUNT ON DECLARE @FirstTimeUTC DateTime DECLARE @FirstSequence int DECLARE @StartRow int DECLARE @StartRowIndex int -- Get the ID of the first error for the requested page SET @StartRowIndex = @PageIndex * @PageSize + 1 SET ROWCOUNT @StartRowIndex SELECT @FirstTimeUTC = TimeUTC, @FirstSequence = Sequence FROM ELMAH_Error WHERE Application = @Application ORDER BY TimeUTC DESC, Sequence DESC -- Now set the row count to the requested page size and get -- all records below it for the pertaining application. SET ROWCOUNT @PageSize SELECT @TotalCount = COUNT(1) FROM ELMAH_Error WHERE Application = @Application SELECT errorId, application, host, type, source, message, [user], statusCode, CONVERT(VARCHAR(50), TimeUtc, 126) + 'Z' time FROM ELMAH_Error error WHERE Application = @Application AND TimeUTC <= @FirstTimeUTC AND Sequence <= @FirstSequence ORDER BY TimeUTC DESC, Sequence DESC FOR XML AUTO GO SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS ON GO CREATE PROCEDURE dbo.ELMAH_LogError ( @ErrorId UNIQUEIDENTIFIER, @Application NVARCHAR(60), @Host NVARCHAR(30), @Type NVARCHAR(100), @Source NVARCHAR(60), @Message NVARCHAR(500), @User NVARCHAR(50), @AllXml NTEXT, @StatusCode INT, @TimeUtc DATETIME ) AS SET NOCOUNT ON INSERT INTO ELMAH_Error ( ErrorId, Application, Host, Type, Source, Message, [User], AllXml, StatusCode, TimeUtc ) VALUES ( @ErrorId, @Application, @Host, @Type, @Source, @Message, @User, @AllXml, @StatusCode, @TimeUtc ) GO SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS ON GO
做了以上三步之后,程序里面的異常就能記錄到數據庫里面,程序再次啟動的時候會自動從數據庫里面去取對應的信息。
2、程序“吃掉”異常
如果你反編譯elmah組件,你會它的原理其實就是通過配置的方式通過HttpModule注冊應用程序的Error事件,然后統一處理記錄。既然是注冊的Application_Error事件,那么肯定就存在某些情況會吃掉異常。
(1)在程序里面try...catch...
這種情況很好理解,如果你再代碼里面顯示的聲明了try...catch那么異常肯定不會進到Application_Error事件里面去,組件也不會記錄異常。
(2)在異常捕獲器里面處理了異常
除了使用try吃掉異常之外,很多系統里面都會使用異常捕獲器去統一捕獲異常,如果再異常捕獲器里面設置過異常已經處理,組件也不會記錄異常。比如:
protected override void OnException(ExceptionContext filterContext) { //如果加了這一句,表示異常已經處理,不會盡到應用程序的Error事件里面去 filterContext.ExceptionHandled = true; //........ base.OnException(filterContext); }
這就是異常捕獲器統一處理異常,既然這里標識了異常已經處理過,那么組件肯定不會再次處理。可是有些情況下,我們需要處理某些自定義異常,而對於系統異常我們還是希望組件能夠記錄,這種情況下怎么辦呢?其實很簡單,這里只需要判斷一下,如果是自定義異常信息,這里就加上 filterContext.ExceptionHandled = true; 這一句,而對於其他系統異常,則統一加上這一句即可。這里還是做一個簡單的演示供需要的園友參考。
public class MyException : Exception { public MyException(string message) : base(message) { } }
滿足一定條件則拋出自定義異常
public JsonResult Get() { if (DateTime.Now > Convert.ToDateTime("2016-12-15 10:00:00")) { //如果滿足某些條件則拋出異常 throw new MyException("當前時間已過期"); } return Json("OK", JsonRequestBehavior.AllowGet); }
然后再全局異常處理里面
protected override void OnException(ExceptionContext filterContext) { if (filterContext.Exception is MyException) { //如果加了這一句,表示異常已經處理,不會進到應用程序的Error事件里面去 filterContext.ExceptionHandled = true; } base.OnException(filterContext); }
這樣就能達到我們系統異常記錄,自定義異常不記錄的目的了。
3、組件權限問題
關於elmah組件,被人詬病的一個重要原因就是其安全問題,如果控制不好很容易招到他人入侵。大神湯姆大叔有篇文章記錄這個,有興趣可以看看。關於我們/elmah.axd路徑,我們肯定是需要做一些限制,不能允許每個人都去查看,下面從以下幾個方面來完善。
(1)拒絕遠程訪問
在web.config里面有一個節點配置不允許遠程訪問。
<elmah> <security allowRemoteAccess="false" /> </elmah>
(2)拒絕匿名用戶訪問
對於沒有登錄到系統的用戶,拒絕訪問。這個可以在IIS上面配置,當然,我們在web.config里面也要做相應的配置
<location path="log.axd" inheritInChildApplications="false"> <system.web> <authorization> <deny users="?"/> </authorization> </system.web> </location>
(3)指定角色或用戶訪問
可以在應用程序全局配置文件Global.asax里面的認證事件里面去做判斷。
protected void Application_AuthenticateRequest(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; // 處理日志權限問題 if (Request.Url.ToString().Contains("elmah.axd")) { var user = app.Context.User; if (user == null) { Response.Write("無權限訪問"); Response.End(); } var userData = (UserInfo)user; //管理員角色才能查看 if (!userData.UserData.IsAdmin) { Response.Write("無權限訪問"); Response.End(); } //或者指定用戶才能訪問 //if (userData.UserData.UserName!="administrator") //{ // Response.Write("無權限訪問"); //Response.End(); //} } }
四、總結
以上總結了組件Elmah組件的使用和一些常見問題的處理。有興趣的可以看看。歡迎推薦!
本文原創出處:http://www.cnblogs.com/landeanfen/
歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利
