A real-time notification system is often useful, if not necessary, to handle unexpected events in order to make humans or their automated counterparts aware of what's going on while things happen. An unhandled exception could well be one of those scenarios where such a system could become interesting. In this recipe, we'll see how we can set up a sample application so that it can intercept unhandled exceptions in a single central place and post them to a monitoring system that will then use SignalR to broadcast the details about the errors it receives to its users.
We will not need many features from SignalR; actually, the only feature that we'll really need is the capability to invoke a client-side callback from outside any hub's method, thanks to the hub context.
Our sample will consist of the following two different projects:
Recipe52
: This is a simple and traditional sample web application with just one page that raises an exception. In this application, we'll add a global exception handler, and from there, we'll collect information about the error to post to the Recipe52.Errors
web application. We'll need to create a new empty web application to build this one.Recipe52.Errors
: A generic SignalR-based web application that we can use to publish errors that come from monitored applications. Also, in this case, we'll start by creating an empty web application.Let's describe the two projects in the same order in which we listed them previously.
Recipe52
is just a sample application that generates errors; let's quickly illustrate the steps that are needed to build it:
index.cshtml
, which will always throw an exception:@using System @{ throw new Exception("Something went wrong!"); }
We use a
Razor Web Page (.cshtml
) in order to easily trigger a server-side exception; therefore, we need to enable Web Pages support by adding the following configuration key to our web.config
file:
<appSettings> <add key="webpages:Version" value="2.0" /> </appSettings>
global.asax
file, removing all the plumbing code added by the Visual Studio template except for the Application_Error()
method, which will be triggered every time an unhandled exception will be thrown on the application: protected void Application_Error(
object sender, EventArgs e)
{
HttpContext.Current.Error.Post(
"Recipe52",
new Uri(
"http://localhost:15852/PostError.axd"));
}
We are invoking an extension method called Post()
on the Error
member of HttpContext.Current
, which contains the last exception thrown on the current request's thread. The goal of this method is to provide a way to post information about the error to an external application. The method is supplied with a string that contains the name of the throwing application and a target URL to which the details of the error will be posted.
Post()
method we just used does not exist yet, so let's define it:public static class ExceptionExtensions { public static void Post(this Exception error, string application, Uri destination) { using (var wc = new WebClient()) { var descriptor = new NameValueCollection { { "Application", application }, { "Error", error.Message } }; try { wc.UploadValues(destination, descriptor); } finally { } } } }
Its implementation prepares a collection of key-value pairs that contain the name of the application received as a parameter and the Message
property of the exception the method is invoked on. The collection is then posted to the destination URL using the WebClient
type and its
UploadValues()
method.
The source of errors is ready. Of course, in a real-world scenario, the Post()
method would probably belong to an external and reusable library, and maybe, it would post errors in an asynchronous way, but the way we did this is enough to illustrate the concept.
Let's now move on to Recipe52.Errors
, the actual monitoring application where, of course, SignalR is a necessary component. As usual, we add it through the Microsoft.AspNet.SignalR
NuGet package before starting to add the components of the application. Perform the following steps to do so:
ErrorsConnection
, whose content will be extremely simple, as shown:public class ErrorsConnection : PersistentConnection { }
The client page of our monitoring application will connect here, and then it will have the receive()
method of the connection
object called whenever the server will send data to them. We'll be using ErrorsConnection
on the server side shortly.
We could have used a simple Hub
and, if the monitoring application would need more features, it would've been the most sensible option. However, in this case, the implementation is so simple that we decided to use a persistent connection to illustrate its usage in a real-world sample.
Startup
class that is generated using the corresponding template and whose implementation is a simple bootstrap sequence for persistent connection (exposed on the "/errors"
endpoint), as shown in the following code snippet:public void Configuration(IAppBuilder app) { app.MapSignalR<ErrorsConnection>("/errors"); }
Errors
, which we are going to add to the project using the template Visual Studio provides for its creation, as shown in the following screenshot:This handler can be targeted by any external application to post information about their errors to the monitoring system. We just need to expose it by registering it in the web.config
file:
<system.webServer> <handlers> <add name="PostError" verb="POST" path="PostError.axd" type="Recipe52.Errors.Errors" /> </handlers> </system.webServer>
This new entry in the handlers
section declares that an instance of our Errors
type will be invoked any time a client reaches an endpoint called PostError.axd
with an HTTP POST
request.
public class Errors : IHttpHandler { public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { var post = context.Request.Params; var application = post["Application"]; var error = post["Error"]; var connection = GlobalHost.ConnectionManager .GetConnectionContext<ErrorsConnection>() .Connection; connection.Send( new ConnectionMessage( connection.DefaultSignal, new { application, error })); } }
Just like any other ASP.NET Handler, Errors
implements IHttpHandler
; therefore, it has to provide a property called IsReusable
, which, in our case, can simply return true
and a method called ProcessRequest
, which will be invoked every time an HTTP request reaches it. The content of ProcessRequest
is quite simple and is as follows:
POST
payload from any client that contacts these handlers: Application
and Error
.ConnectionManager
from GlobalHost
, and from there, it retrieves a connection context related to our ErrorsConnection
type, which is eventually used to send data to every connected client in the form of an anonymous type instance that contains the application name and the error message that comes from the posting application.We're almost done; now, we just need a simple client page that will receive all incoming errors posted to our monitoring application in real time.
We create a page called index.html
and add the following content:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script src="Scripts/jquery-2.1.0.js"></script>
<script src="Scripts/jquery.signalR-2.0.2.js"></script>
<script>
$(function() {
$.connection('errors')
.received(
function (e) {
var message =
e.application +
' failed with error: ' +
e.error;
$('#errors')
.prepend($('<li/>').text(
message));
})
.start();
});
</script>
</head>
<body>
<ul id="errors"></ul>
</body>
</html>
The code is very simple; it just contains the necessary JavaScript references, an unordered list in the body
section of the page, and a piece of script to open a SignalR connection after having registered a receive
callback that will be invoked for every error descriptor broadcasted by the server-side Errors
handler. The callback will build a simple string with some detail about the error, and it will prepend it to the unordered list on the page.
In order to test the monitoring application we have to build the Recipe52.Errors
project, launch it, and then navigate to its index.html
page. While we keep that browser instance around, we build and launch Recipe52
, we navigate to its index.cshtml
page, and we observe the Yellow Screen Of Death (YSOD) that appears because of the exception it raises. At the same time, on the browser window we opened earlier pointing at the Recipe52.Errors
application, we should see a message describing the same error and displayed at the same time the error happens on Recipe52
.
With this sample, you should have a general idea about how to build such a monitoring system to notify errors in real time, but it's also true that there are a lot of details to take care of if you want to provide a full-fledged solution to the problem. In case you are interested, a similar solution already exists and is basically built on the same ideas that we just saw. It's called
ElmahR (http://elmahr.apphb.com/), and it's a real-time error-monitoring dashboard that can be configured to display live information about errors that happen on multiple monitored applications. The potential sources of errors just have to use
ELMAH (https://code.google.com/p/elmah/), a very well known library to intercept unhandled exceptions and plug into it a module exposed by
ElmahR.Elmah, which belongs to the ElmahR project. ELMAH and ElmahR.Elmah are both available on NuGet, and together, they play the same role of both the Error
handler and the Post
extension method from Recipe52
, while Recipe52.Errors
corresponds to the ElmahR dashboard. For more details, you can check its source code at https://bitbucket.org/wasp/elmahr/wiki/Home.