fmk-3q
Last Updated: February 25, 2016
·
1.787K
· omartell

Using a Clojure Ring Middleware to Send Exceptions to Bugsnag

I was recently implementing an API web endpoint with Ring and as part of the task I needed to send any exceptions raised by the handler to Bugsnag
(an error tracking system).

Using Bugsnag within Clojure is fairly simple, since we can use their Java library. While looking at their documentation I noticed their library hooks into Thread.UncaughtExceptionHandler and after creating/configuring an instance of their client I was expecting to automatically receive notifications for exceptions in my dashboard, but that didn't work when raising exceptions from within the route handler.

I decided to implement a Ring middleware that would catch any exception raised by the web handler, use the client to send the notification and then rethrow the exception. This turned out to be a good approach since I could also include the request information, which is not included by default when using the Java client but can help a lot when trying to find what the actual issue is in a production environment.

After doing some research on Ring middleware I was quite surprised to see how easy it is to hook into Ring request-response lifecycle. A Ring middleware is basically just a function that takes a handler and returns another function that accepts the request map as an argument. Within the inner function you can transform the request map, the response map or do some other logic before/after calling the original handler.

(defn a-not-very-useful-middleware
  [handler]
  (fn [req] 
    (handler req)))

The previous handler doesn't do much, but you get the idea. You could, for example, automatically parse a JSON request body into a Clojure map if you know you're always expecting a JSON payload. Then, in your handlers you wouldn't have to do any parsing logic. This is a very elegant approach to only get the functionality you need in your web app and to make sure different middlewares compose together nicely.

Before getting into the middleware, we need to add the Bugsnag dependency to our project. The latest version at the moment is 1.2.2.

:dependencies [[com.bugsnag/bugsnag "1.2.2"]]

Then we need to make the Bugsnag Java classes available to our namespace with import.

(ns your-app.ring-bugsnag
  (:import
    [com.bugsnag Client MetaData]))

I'm calling my middleware wrap-bugsnag, since that seems to be the naming convention used for middlewares. This is the actual code for the wrap-bugsnag function:

(defn wrap-bugsnag
  [handler]
  (fn [req]
    (try
      (handler req)
      (catch Throwable t
        (notify t (metadata req))
        (throw t)))))

Here the wrap-bugsnag handler is calling the original handler within the try function and catching all exceptions that could be raised with Throwable, which is the base class for all things that can be thrown in Java. Then, we call the notify function with the actual exception and the request metadata. At the end we have to rethrow the exception to avoid silently swallowing the error.

The notify function uses the Bugsnag client to send the notification. Since we're calling the method notify on the client object we need to prefix the function name with a ..

(defn notify
  ([error metadata]
   (.notify client error metadata)))

Inside the client function we create an instance of com.bugsnag.Client and set some configuration settings, such as your api key.

(def client
  (doto (Client. "your api key goes here")
      (.setReleaseStage "development")
      (.setNotifyReleaseStages (into-array String ["staging" "production"]))))

The doto macro makes it very easy to call a series of Java methods on an object and then return the object itself. It is similar to Ruby's tap . Note that client is not a function like all the others.

The metadata function is the last piece of code I had to implement. For this, we need to instantiate com.bugsnag.Metadata and add key value pairs to the Request section by calling the method .addToTab. The key value pairs come from the actual request map, which has things like :json-params, :headers, :request-method and :query-params.

(defn metadata
  [req]
  (let [metadata (MetaData.)]
    (doseq [[k v] req]
      (.addToTab metadata "Request" (str k) (str v)))
    metadata))

Here we're using doseq to destructure the request and then call the .addToTab method for each key value pair of the request map.

Finally, you can use the middleware in your handler.clj. This assumes that you would put all the previous functions inside the ring-bugsnag namespace.

(def app
  (-> app-routes ring-bugsnag/wrap-bugsnag))

In my application I placed the wrap-bugsnag call after app-routes and before all the other params middlewares. This means that my middleware would get the transformations done by the other middlewares, like :json-params, but it would not catch the exceptions raised by them.

(def app
  (-> app-routes
      ring-bugsnag/wrap-bugsnag
      ring.middleware.keyword-params/wrap-keyword-params
      ring.middleware.params/wrap-params
      ring.middleware.json/wrap-json-params
      ring.middleware.nested-params/wrap-nested-params))

I hope this is useful for you. It certainly was for me as a way to learn more about Java interop. In the future I hope to make this a more solid wrapper and release it as a library. You can get all the code shown here in this gist.

Links:
Bugsnag Java Library

1 Response
Add your response

17831

I came across this post because I also have a Ring app which seems to fail to hook Bugsnag onto Thread.UncaughtExceptionHandler, even after initializing the Client. Have you discovered why that's the case?

over 1 year ago ·