iT邦幫忙

DAY 28
10

使用Asp.Net MVC打造Web Api系列 第 28

使用Asp.Net MVC打造Web Api (28) - 管理網站錯誤資料

在之前的文章中,有介紹過如何使用Elmah來記錄與收集網站的錯誤資訊,而且Elmah也提供了十分好用的錯誤瀏覽介面,可以讓我們觀察線上網站的錯誤記錄,然而在實際營運網站時比較尷尬的是,線上網站發生錯誤的頻率是相當高的,就算過濾掉404等還是相當可觀,我們不可能無時無刻都在觀察Elmah的網頁,但將錯誤通知打開通知訊息又會過多,相當令人困擾,然而我們也不可能放著這些錯誤訊息不修正,因此有一個有效的錯誤訊息管理方法也是很重要的,今天將向大家分享如何管理網站的錯誤訊息。
※建立每日錯誤統計報表
為了有效率的管理網站的錯誤訊息,我們可以每天統計各種錯誤發生的次數,再根據目前的專案進度或是工作內容,將發生頻率最高或影響最大的錯誤排入日常處理,而我們在之前將錯誤記錄存放在SQL Server中也是為了可以方便進行統計分析。

  1. 在Visual Studio的Cloud Service專案中,建立一個背景工作角色

  2. 新增一個背景工作角色

  3. 新增一個實體資料模型,用來存取Elmah的資料庫

  4. 設定連線字串

  5. 選擇所要加入的資料表

  6. 新增錯誤資料模型,用來存放及呈現錯誤資料
    ![](<br />
    public class ErrorReportModel
    {
    public string ApplicationName { get; set; }
    <br />
    public string ExceptionType { get; set; }
    <br />
    public string ExceptionMessage { get; set; }
    <br />
    public int ExceptionCount { get; set; }
    }
    )

  7. 新增報表範本,並且修改屬性為內嵌資源,我們在這邊使用RazorEngine這套Library來幫助我們完成Email的內容,它可以解析Razor格式的樣板並且將資料代入執行

        @foreach(var reports in Model)
        {
            <h2>@reports.Key</h2>
            <table border="1">
                <thead>
                    <tr>
                        <th>錯誤類型</th>
                        <th>錯誤訊息</th>
                        <th>錯誤次數</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach(var report in reports.Value)
                        {
                            <tr>
                                <td>@report.ExceptionType</td>
                                <td>@report.ExceptionMessage</td>
                                <td>@report.ExceptionCount</td>
                            </tr>
                    }
                </tbody>
            </table>
        }
    
  8. 新增一個排程工作,根據每天產生的錯誤進行分類,執行完成後送出報表 (使用Quartz.Net)

        public class DailyExceptionReportJob : IJob
        {
            public void Execute(IJobExecutionContext context)
            {
                // 決定開始和結束時間
                DateTime startTime = DateTime.Today.AddDays(-1).ToUniversalTime();
                DateTime endTime = DateTime.Today.ToUniversalTime();
    
                // 取得每日錯誤資料
                List<ErrorReportModel> result = this.GetErrorsByDatetimeRange(startTime, endTime);
    
                // 產生統計後資料
                Dictionary<string, List<ErrorReportModel>> reports = this.GetReportModels(result);
    
                // 產生HTML報表
                string content = this.GetReportHtml(reports);
    
                //寄送每日報表
                this.MailReport(content);            
            }
    
            private void MailReport(string content)
            {
                MailAddress from = new MailAddress("xxx@email.com", "xxxx", System.Text.Encoding.UTF8);
                MailMessage mail = new MailMessage(from, new MailAddress("xxx@email.com"));
    
                string subject = "Daily Error Report";
                mail.Subject = subject;
                mail.SubjectEncoding = System.Text.Encoding.UTF8;
    
                string body = content;
                mail.Body = body;
                mail.BodyEncoding = System.Text.Encoding.UTF8;
                mail.IsBodyHtml = true;
                mail.Priority = MailPriority.High;
    
                SmtpClient client = new SmtpClient();
                client.Host = "smtp.gmail.com";
                client.Port = 587;
                client.Credentials = new NetworkCredential("xxx@email.com", "xxxx");
                client.EnableSsl = true;
    
                client.Send(mail);
            }
    
            private string GetReportHtml(Dictionary<string, List<ErrorReportModel>> reports)
            {
                Assembly assembly = Assembly.GetExecutingAssembly();
                Stream str = assembly.GetManifestResourceStream("Ap iSample.BatchJob.BL.DailyException.ErrorReportTemplate.cshtml");
                StreamReader sr = new StreamReader(str, System.Text.Encoding.UTF8);
                var template = sr.ReadToEnd();
                string content = Razor.Parse(template, reports);
                return content;
            }
    
            private Dictionary<string, List<ErrorReportModel>> GetReportModels(List<ErrorReportModel> result)
            {
                Dictionary<string, List<ErrorReportModel>> reports = new Dictionary<string, List    <ErrorReportModel>>();
                foreach (var applicationErrors in result.GroupBy(i => i.ApplicationName))
                {
                    var report = applicationErrors.GroupBy(i => i.ExceptionType)
                                                  .Select(
                                                    i => new ErrorReportModel
                                                    {
                                                        ApplicationName = applicationErrors.Key,
                                                        ExceptionType = i.Key,
                                                        ExceptionMessage = i.First().ExceptionMessage,
                                                        ExceptionCount = i.Count()
                                                    })
                                                  .OrderByDescending(i => i.ExceptionCount)
                                                  .ToList();
    
                    reports.Add(applicationErrors.Key, report);
                }
                return reports;
            }
    
            private List<ErrorReportModel> GetErrorsByDatetimeRange(DateTime startTime, DateTime endTime)
            {
                List<ErrorReportModel> result;
                using (ExceptionDBContext dbContext = new ExceptionDBContext())
                {
                    result = dbContext.ELMAH_Error
                                      .Where(
                                          i => i.TimeUtc >= startTime &&
                                               i.TimeUtc < endTime)
                                      .Select(
                                          i => new ErrorReportModel()
                                          {
                                              ApplicationName = i.Application,
                                              ExceptionType = i.Type,
                                              ExceptionMessage = i.Message
                                          })
                                      .ToList();
                }
    
                return result;
            }
        }
    
  9. 在WorkerRole中設定工作排程,每天執行一次

        public class WorkerRole : RoleEntryPoint
        {
            private IScheduler scheduler;
    
            private ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
            public override void Run()
            {
                DateTimeOffset runTime = DateBuilder.EvenMinuteDate(DateTime.UtcNow);
                DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 10);
    
                var job = JobBuilder.Create<DailyExceptionReportJob>()
                    .WithIdentity("DailyExceptionJob", null)
                    .Build();
    
                ITrigger trigger = TriggerBuilder.Create()
                    .WithIdentity("default", null)
                    .StartAt(runTime)
                    .WithCronSchedule("* 3 * * * ?")
                    .Build();
    
                scheduler.ScheduleJob(job, trigger);
    
                this.CompletedEvent.WaitOne();
            }
    
            public override bool OnStart()
            {
                // construct a scheduler factory
                ISchedulerFactory factory = new StdSchedulerFactory();
    
                // get a scheduler
                this.scheduler = factory.GetScheduler();
                this.scheduler.Start();
    
                return base.OnStart();
            }
    
            public override void OnStop()
            {
                this.scheduler.Clear();
                this.CompletedEvent.Set();
                base.OnStop();
            }
        }
    
  10. 發行上雲端,每日執行後可以得到報表資料如下

    有了每日報表之後,就算我們沒有隨時的關切線上Elmah的錯誤清單,每天也可以收到當天網站的錯誤統計資料,這有助於我們針對網站的潛在問題及風險進行管理,也可以當作網站健康度指標的依據!

延伸閱讀:
* 用Razor語法寫範本-RazorEngine組件介紹

※定期清除Elmah錯誤記錄
隨著時間的累積,Elmah的錯誤記錄也會隨之增加,然而由於網站持續的在改善並更新程式碼,我們不可能經常的花時間清查過久以前的Log,而為了避免錯誤資料過於龐大,我們習慣會設一個排程清除Elmah的錯誤資料,只保留比較近的日期。

  1. 在ExceptionDB新增StoredProcedure,為了避免一次刪除大量資料影響資料庫效能,我們這邊判斷過期資料後,一次只刪除小量資料

                USE [ExceptionDB]
    	GO
    
    	SET ANSI_NULLS ON
    	GO
    
    	SET QUOTED_IDENTIFIER ON
    	GO
    
    	CREATE PROCEDURE [dbo].[ELMAH_ClearErrorsLogs] 
    	AS
    	BEGIN
    		SET NOCOUNT ON;
    
    		DECLARE @clearDate char(8) = format(dateadd(d,-30,getdate()),'yyyyMMdd');	
    
    		--Remove temp table
    		IF (OBJECT_ID('tempdb..##tmpOverdateErrorId') IS NOT NULL)
    			drop table #tmpOverdateErrorId;
    
    	    --Move overdate error id to temp table
    		SELECT [ErrorId]
    		into #tmpOverdateErrorId
    		FROM [dbo].[ELMAH_Error]
    		WHERE [TimeUtc] < @clearDate;	
    
    		--Remove elmah errors
    		WHILE 1 = 1 
    		BEGIN
    			WAITFOR DELAY '00:00:01';
    
    			DELETE TOP(5) [dbo].[ELMAH_Error] 
    				FROM [dbo].[ELMAH_Error] t1
    				INNER JOIN #tmpOverdateErrorId t2 ON t1.[ErrorId] = t2.[ErrorId];
    
    			IF @@ROWCOUNT = 0
    				BREAK;
    		END;
    	END
    
    	GO
    
  2. 更新EntityFramework的DbContext

  3. 新增Elmah_ClearErrorsLog

  4. 新增一個清除ErrorLog的工作

        public class ClearErrorLogsJob : IJob
        {
            public void Execute(IJobExecutionContext context)
            {
                using (ExceptionDBContext dbContext = new ExceptionDBContext())
                {
                    var count = dbContext.ELMAH_ClearErrorsLogs();
    
                    Trace.WriteLine("Clear error log count: " + count);
                }
            }
        }
    
  5. 在Worker Role增加一個排程

        var job = JobBuilder.Create<ClearErrorLogsJob>()
                            .WithIdentity("ClearErrorLogsJob", null)
                            .Build();
    
        ITrigger trigger = TriggerBuilder.Create()
                                         .WithIdentity("default", null)
                                         .StartAt(runTime)
                                         .WithCronSchedule("* 3 * * * ?")
                                         .Build();
    
        scheduler.ScheduleJob(job, trigger);
    
  6. 發行至雲端,設定好排程工作的執行,這麼一來我們就可以只保留需要範圍的錯誤記錄了!

※本日小結
藉由排程工作的幫助,我們可以將系統的數據得到實際的報表,這麼一來就可以很方便的了解網站的實際狀況,並且安排對應的工作進行處理,也可以避免線上網站有大量的錯誤卻沒有被發現,關於今天的內容,歡迎大家一起討論喔^_^


上一篇
使用Asp.Net MVC打造Web Api (27) - 在Azure上執行排程工作
下一篇
使用Asp.Net MVC打造Web Api (29) - 使用HipChat整合系統通知
系列文
使用Asp.Net MVC打造Web Api30

尚未有邦友留言

立即登入留言