愛流浪的小風

技術隨手寫

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

| Comments

在之前的文章中,有介紹過如何使用Elmah來記錄與收集網站的錯誤資訊,而且Elmah也提供了十分好用的錯誤瀏覽介面,可以讓我們觀察線上網站的錯誤記錄,然而在實際營運網站時比較尷尬的是,線上網站發生錯誤的頻率是相當高的,就算過濾掉404等還是相當可觀,我們不可能無時無刻都在觀察Elmah的網頁,但將錯誤通知打開通知訊息又會過多,相當令人困擾,然而我們也不可能放著這些錯誤訊息不修正,因此有一個有效的錯誤訊息管理方法也是很重要的,今天將向大家分享如何管理網站的錯誤訊息。

建立每日錯誤統計報表

為了有效率的管理網站的錯誤訊息,我們可以每天統計各種錯誤發生的次數,再根據目前的專案進度或是工作內容,將發生頻率最高或影響最大的錯誤排入日常處理,而我們在之前將錯誤記錄存放在SQL Server中也是為了可以方便進行統計分析。

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

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

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

  4. 設定連線字串

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

  6. 新增錯誤資料模型,用來存放及呈現錯誤資料

    public class ErrorReportModel
    {
        public string ApplicationName { get; set; }
    
        public string ExceptionType { get; set; }
    
        public string ExceptionMessage { get; set; }
    
        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的錯誤清單,每天也可以收到當天網站的錯誤統計資料,這有助於我們針對網站的潛在問題及風險進行管理,也可以當作網站健康度指標的依據!

延伸閱讀:

定期清除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. 發行至雲端,設定好排程工作的執行,這麼一來我們就可以只保留需要範圍的錯誤記錄了!

本日小結

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

Comments

comments powered by Disqus