Error Logging in Silverlight 2 with IIS and the Isolated Storage

Unfortunately software is not perfect, especially if is written by me:-), so it is very important to be able to capture errors in order to diagnose and fix bugs.
Silverlight applications run on the client so it is a bit harder to log errors compared to a classical ASP.NET solution. In this blog post I will explain a possible strategy/solution to log errors on the client but also on the server.

Why logging an error on the Server (report back to server)?

On the public internet you have no access to the client machine, so it is logical to report the error back to the server. This is of a paramount importance because consumers tend to simply close the browser than sending an email or call your site support. So you could have a non working app and don’t even know it. Furthermore with all the browsers/platforms that Silverlight support the chance that you have an issue in a specific OS/Browser urge for a report back to server solution. urge for a report back on the server side.

Why logging on the Client?

Sometime while testing an app or even in a more common intranet scenario, is quite useful to be able to access the last error generated by the app right there on the client. In addition to it is also much more convenient to get it on the client than looking into a server log. Also the server may be down and this could be the cause of the error.

I wrote a small sample project with a class library that you can use in your own applications. Enclosed to this post you can find the full source code. Following is the explanation of the solution.

Let’s start with the Server logging part or better “call back home and tell that something is wrong”:-).

In case of error logging I like to keep the code as minimal as possible to avoid further errors. You also want to consider the performance implication of potentially thousands of clients calling back the server to report an error. Last but not least maybe the issue is on the server itself like a DB not working anymore or an issue with ASP.NET, etc…

The easiest and most efficient way to log an error is to relay on what IIS is offering already to you.
In this case, this is what we plan to do:

- Do an HTTP GET to a non existing page with the .slerror extension. This makes it super easy to find the .slerror in the server log. We will not care about the 404 that IIS will return

- We will use the .xap name to identify the app: eg. SLGallery.xap.slerror

- We rely on IIS logging mechanism to store the error information on the log. IIS will store the GET query string where we will put the error message and the stack trace.

- We can now use any log analysis tool to get error statistics and filter out what you need

- Performance is not really affected because we are relying on highly efficient IIS logging mechanisms. We also avoid any issue with DB or ASP.NET

- We can even use FREB in IIS 7.0 to log separately all the Silverlight errors of a specific sub-site/app.

In the SilverlightErrorLogger project we have the actual call back home code. This code will be invoked in case of an unhandled exception within the Silverlight app in order to generate a server side log entry.

  private class ServerErrorLogger
         {
             public static void SendErrorToServer(string message, string stackTrace)
             {
                 //By default IIS is not accepting requests with more than 2048 charaters in the query string so we cut the stacktrace
                 if ((message.Length + stackTrace.Length) > 1800)
                    stackTrace = stackTrace.Remove(1800-message.Length);
  
                 try
                 {
                     Uri uri = new Uri(Application.Current.Host.Source.AbsolutePath + String.Format(".slerror?E={0}&S={1}", message, stackTrace),UriKind.Relative);
                     WebClient wClient = new WebClient();
                     wClient.DownloadStringAsync(uri);
                 }
                 catch
                 {
  
                 }
             }
         }

To note that IIS, for security reason, has a limit for query string length for avoiding DoS attacks. The default setting for RequestFiltering/RequestLimits/MaxQueryString is 2048 characters. So we simply cut the Stack Trace to avoid this issue.

Now we want also to add client side logging. The best solution is to write it on the IsolatedStorage.
In this case we will write a file that has the same name as the app plus the .txt extension e.g. SLGallery.xap.txt:

 private class IsolatedStorageErrorLogger
         {
  
  
             public static void WriteLogFile(string message, string stackTrace)
             {
  
                 using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication())
                 using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(getFileName(), FileMode.Create, isoStore))
                 {
                     using (StreamWriter writer = new StreamWriter(isoStream))
                     {
                         StringBuilder sb = new StringBuilder();
                         sb.AppendLine("DateTime:");
                         sb.AppendLine(DateTime.Now.ToString());
                         sb.AppendLine("Error message:");
                         sb.AppendLine(message);
                         sb.AppendLine("Stack trace:");
                         sb.AppendLine(stackTrace);
                         writer.Write(sb.ToString());
                     }
                 }
  
             }

We also want to be able to read the log in case we want to diagnose a problem right on the client. The idea is to be able to pass a parameter to Silverlight and tell to display the last logged error. Like in this example: https://localhost/SLErrorLogging.Web/SLErrorLoggingTestPage.aspx?LoggedSLError:

 public static string ReadLogFile()
             {
                 string fileName = getFileName();
  
  
  
                 using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication())
                 {
                     if (!isoStore.FileExists(fileName))
                         return null;
  
                     using (IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(fileName, FileMode.Open, isoStore))
                     {
                         using (StreamReader reader = new StreamReader(isoStream))
                         {
  
                             string message = reader.ReadToEnd();
                             return message;
                         }
  
                     }
                 }
  
             }

We also want to be able to choose which form of logging we want for our application:

 public class ErrorLogger
     {
         public enum LoggingType
         {
             Server,
             Client,
             ClientAndServer,
         }
  
         static public void LogError(string message, string stackTrace, LoggingType loggingType)
         {
             switch (loggingType)
             { 
                 case LoggingType.Client:
                     IsolatedStorageErrorLogger.WriteLogFile(message, stackTrace);
                     break;
                 
                 case LoggingType.Server:
                     ServerErrorLogger.SendErrorToServer(message, stackTrace);
                     break;
                 
                 case LoggingType.ClientAndServer:
                     ServerErrorLogger.SendErrorToServer(message, stackTrace);
                     IsolatedStorageErrorLogger.WriteLogFile(message, stackTrace);
                     break;
             }
         }
  
         static public string GetLastLoggedError()
         {
             return IsolatedStorageErrorLogger.ReadLogFile();
         }

Now we are ready to add it to a sample app. After referencing the SilverlightErrorLogger project you need to add the code to the App.cs file where a global error handling event is wired up:

  private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
         {
  
  
             ErrorLogger.LogError(e.ExceptionObject.Message, e.ExceptionObject.StackTrace, ErrorLogger.LoggingType.ClientAndServer);
  
             // If the app is running outside of the debugger then report the exception using
             // the browser's exception mechanism. On IE this will display it a yellow alert 
             // icon in the status bar and Firefox will display a script error.
             if (!System.Diagnostics.Debugger.IsAttached)
             {
  
                 // NOTE: This will allow the application to continue running after an exception has been thrown
                 // but not handled. 
                 // For production applications this error handling should be replaced with something that will 
                 // report the error to the website and stop the application.
                 e.Handled = true;
                 Deployment.Current.Dispatcher.BeginInvoke(delegate { ReportErrorToDOM(e); });
             }
         }

Remember to remove the code part as described in the comment for production deployment.

Last and optional is the possibility to enable the display of the last logged error:

 private void Application_Startup(object sender, StartupEventArgs e)
         {
  
             if (HtmlPage.Document.DocumentUri.Query.Contains("LoggedSLError"))
             {
                 string message = ErrorLogger.GetLastLoggedError();
                 if(message!=null)
                     MessageBox.Show(message);
             }
                      
             this.RootVisual = new Page();
         }

Now we have to specify to run the app in IIS and not on Visual Studio Web Server and we are set to go.
In order to do that , go to the project property of your web application and set it to host it IIS:

 image

If you run the app and click the button it will throw an Out of Memory Exception and it will be reported in the IIS log. The IIS log files can be found under \inetpub\logs\LogFiles:

2008-12-03 14:01:22 ::1 GET /SLErrorLogging.Web/ClientBin/SLErrorLogging.xap.slerror E=Memoria%20insufficiente%20per%20continuare%20l'esecuzione%20del%20programma.&S=%20%20%20at%20SLErrorLogging.Page.Button_Click(Object%20sender,%20RoutedEventArgs%20e)%20%20%20at%20System.Windows.Controls.Primitives.ButtonBase.OnClick()%20%20%20at%20System.Windows.Controls.Button.OnClick()%20%20%20at%20System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs%20e)%20%20%20at%20System.Windows.Controls.Control.OnMouseLeftButtonUp(Control%20ctrl,%20EventArgs%20e)%20%20%20at%20MS.Internal.JoltHelper.FireEvent(IntPtr%20unmanagedObj,%20IntPtr%20unmanagedObjArgs,%20Int32%20argsTypeIndex,%20String%20eventName) 80 - ::1 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.0;+SLCC1;+.NET+CLR+2.0.50727;+Media+Center+PC+5.0;+InfoPath.2;+.NET+CLR+3.5.21022;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30618) 404 0 2 1

Here you also get for free tons of info like date time, ip  address and platform information.
To note that you get the error message on the language of the client in my case Italian.

Now you can try to add ?LoggedSLError to the url e.g https://localhost/SLErrorLogging.Web/SLErrorLoggingTestPage.aspx and you will see that a Message Box will pop up with the last logged error:

image

Now you can even enable FREB in IIS 7 to log separately all the .slerror for a specific sub site.
First enable FREB for the web site:

image

 image

Then create a FREB rule for the specific portion of the site:

image

image

image

image

Now if you try again generating the error you will see an .xml file with the log in this directory \inetpub\logs\FailedReqLogFiles if you open the .xml with IE you get this output:

image

As you have seen, it is very simple and straight forward to implement an error logging system and it is absolutely vital to have one in your application.

Thanks to Ken Casada for validating the idea and to Benedikt Unold from Comparis.ch for pointing out the query string restriction of IIS.

Have fun

Ronnie Saurenmann

SilverlightErrorLogger.zip