Error Logging
One of the very first things I do, for a new ASP.NET application, is to setup a decent error logging. I want to know when and why my application crashes and I don’t want to relay on users accurately reporting every error. It’s quite easy to do this using HttpApplication.Error event:
1 using System;
2 using System.Configuration;
3 using System.Data;
4 using System.Data.SqlClient;
5 using System.Security.Cryptography;
6 using System.Text;
7 using System.Web;
8
9
10 public class Application : HttpApplication {
11
12 private String Md5Sum(String message) {
13 var bytes = Encoding.UTF8.GetBytes(message);
14 var digest = (new MD5CryptoServiceProvider()).ComputeHash(bytes);
15 var checksum = new StringBuilder();
16 foreach (var b in digest) {
17 checksum.Append(b.ToString("X2"));
18 }
19 return checksum.ToString();
20 }
21
22 private void LogError(String checksum, Int32 httpCode, String type, String message, String stack, String source, String url, String user, String machine, String os, String version) {
23 var connectionString = ConfigurationManager.ConnectionStrings["DB"].ConnectionString;
24 using (var connection = new SqlConnection(connectionString)) {
25 connection.Open();
26 var command = connection.CreateCommand();
27 command.CommandText = @"
28 UPDATE errors
29 SET
30 last_occurrence = current_timestamp,
31 occurrences = occurrences + 1,
32 http_code = @http_code,
33 type = @type,
34 message = @message,
35 source = @source,
36 url = @url,
37 [user] = @user,
38 machine = @machine,
39 os = @os,
40 version = @version
41 WHERE
42 checksum = @checksum;
43 ";
44 command.Parameters.Add("http_code", SqlDbType.SmallInt).Value = httpCode;
45 command.Parameters.Add("type", SqlDbType.VarChar, 250).Value = type;
46 command.Parameters.Add("message", SqlDbType.VarChar, 1000).Value = message;
47 command.Parameters.Add("source", SqlDbType.VarChar, 100).Value = source;
48 command.Parameters.Add("url", SqlDbType.VarChar, 150).Value = url;
49 command.Parameters.Add("user", SqlDbType.VarChar, 50).Value = user;
50 command.Parameters.Add("machine", SqlDbType.VarChar, 50).Value = machine;
51 command.Parameters.Add("os", SqlDbType.VarChar, 100).Value = os;
52 command.Parameters.Add("version", SqlDbType.VarChar, 100).Value = version;
53 command.Parameters.Add("checksum", SqlDbType.VarChar, 32).Value = checksum;
54 if (command.ExecuteNonQuery() == 0) {
55 command.CommandText = @"
56 INSERT INTO errors (checksum, http_code, type, message, stack, source, url, [user], machine, os, version)
57 VALUES (@checksum, @http_code, @type, @message, @stack, @source, @url, @user, @machine, @os, @version);
58 ";
59 command.Parameters.Add("stack", SqlDbType.Text).Value = stack;
60 command.ExecuteNonQuery();
61 }
62 }
63 }
64
65 public void Application_Error(Object sender, EventArgs e) {
66 Server.ClearError();
67 Response.Clear();
68 var exception = Server.GetLastError().GetBaseException();
69 var stack = exception.StackTrace;
70 var checksum = Md5Sum(stack);
71 var httpCode = ((HttpException)Server.GetLastError()).GetHttpCode();
72 var httpMessage = Response.StatusDescription;
73 var type = exception.GetType().ToString();
74 var message = exception.Message;
75 var source = exception.Source;
76 var url = Request.Path;
77 var user = Environment.UserName;
78 //var user = Request.ServerVariables["AUTH_USER"];
79 var machine = Environment.MachineName;
80 var os = Environment.OSVersion.ToString();
81 var version = Environment.Version.ToString();
82 var dateTime = DateTime.UtcNow.ToString("o");
83 var details = "";
84 var developers = ConfigurationManager.AppSettings["Developers"].Split(';');
85 //if (Array.IndexOf(developers, Request.ServerVariables["REMOTE_ADDR"]) > -1) {
86 if (Array.IndexOf(developers, user) > -1) {
87 details = String.Format(@"
88 <p>[<b>{0}</b>: {1}]</p>
89 <pre><code>{2}</code></pre>
90 <hr/>
91 <p>{3} ({4}), {5}<i> -- {6} @ {7} ({8}/{9})</i></p>
92 ", type, message, stack, source, url, dateTime, user, machine, os, version);
93 }
94 var page = String.Format(@"
95 <?xml version=""1.0"" encoding=""UTF-8""?>
96 <!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.1//EN"" ""http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"">
97 <html xmlns=""http://www.w3.org/1999/xhtml"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:schemaLocation=""http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd"" xml:lang=""en"">
98 <head>
99 <title>{0} - {1}</title>
100 </head>
101 <body style=""font-size:smaller;"">
102 <p style=""text-align:center;color:white;background:orangered;""><b>{2}</b></p>
103 {3}
104 </body>
105 </html>
106 ", httpCode, httpMessage, checksum, details);
107 Response.Write(page);
108 Response.StatusCode = httpCode;
109 LogError(checksum, httpCode, type, message, stack, source, url, user, machine, os, version);
110 }
111
112 }
You could put these methods directly into global.asax, but I prefer to compile and drop the DLL into /bin, or to save everything as a file with .cs extension into /app_code. Two later cases requires this one line in global.asax:
1 <%@ Inherits="Application" %>
It’s probably not a good idea to display error details to every user, just a checksum should be enough, so we’ll put users that need to see full errors into web.config:
1 <?xml version="1.0" encoding="utf-8"?>
2 <configuration>
3 <appSettings>
4 <add key="Developers" value="Alice;Bob" />
5 </appSettings>
6 </configuration>
One tricky thing about all this, is exceptions thrown from Application_Error() method. It would make sense, if these exceptions would show up as regular ASP.NET errors, but that’s not how it works in my experience. To show our own error page, we need to clear the error that resulted in Application_Error() being called, if we don’t call ClearError(), the original error will get displayed instead of our own, this also means that, if an exception gets thrown before ClearError(), you won’t see it. Situation with exceptions after ClearError() is a bit mysterious, on .NET I get a blank page, on Mono I get a regular ASP.NET error. Just know that, if something isn’t working as expected, first thing to try is to wrap everything in try-catch, to see if there’s any hidden exceptions.
Here’s the table definition, if you decide to use something similar:
1 CREATE TABLE errors (
2 id smallint IDENTITY NOT NULL,
3 checksum char(32) NOT NULL UNIQUE,
4 last_occurrence smalldatetime NOT NULL DEFAULT current_timestamp,
5 occurrences int NOT NULL DEFAULT 1,
6 http_code smallint NOT NULL,
7 type varchar(250) NOT NULL,
8 message varchar(1000) NOT NULL,
9 stack text NOT NULL,
10 source varchar(100) NOT NULL,
11 url varchar(150) NOT NULL,
12 [user] varchar(50) NOT NULL,
13 machine varchar(50) NOT NULL,
14 os varchar(100) NOT NULL,
15 version varchar(100) NOT NULL,
16 CONSTRAINT PK_errors PRIMARY KEY CLUSTERED (id)
17 );
