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
:dependencies [[com.bugsnag/bugsnag "1.2.2"]]
Then we need to make the Bugsnag Java classes available to our namespace with
(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
(defn wrap-bugsnag [handler] (fn [req] (try (handler req) (catch Throwable t (notify t (metadata req)) (throw t)))))
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.
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)))
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"]))))
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
(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
(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.
Bugsnag Java Library