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
Written by Oliver Martell
Related protips
1 Response
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?