Usando um middleware Clojure Ring para enviar exceções para o Bugsnag

Recentemente, estava implementando um endpoint da Web de API com Ring e, como parte da tarefa, precisava enviar qualquer exceção levantada pelo manipulador para o Bugsnag
(um sistema de rastreamento de erros).

Usar o Bugsnag no Clojure é bastante simples, já que podemos usar sua biblioteca Java. Enquanto olhava a documentação, percebi que a biblioteca engatava Thread.UncaughtExceptionHandlere, depois de criar / configurar uma instância do cliente, esperava receber automaticamente notificações de exceções em meu painel, mas isso não funcionou ao levantar exceções de dentro do gerenciador de rota.

Decidi implementar um middleware Ring que capturasse qualquer exceção levantada pelo manipulador da web, usaria o cliente para enviar a notificação e então relançaria a exceção. Isso acabou sendo uma boa abordagem, pois também poderia incluir as informações da solicitação, que não são incluídas por padrão ao usar o cliente Java, mas podem ajudar muito ao tentar descobrir qual é o problema real em um ambiente de produção.

Depois de fazer algumas pesquisas sobre o middleware do Ring, fiquei bastante surpreso ao ver como é fácil se conectar ao ciclo de vida de solicitação-resposta do Ring. Um middleware Ring é basicamente apenas uma função que pega um manipulador e retorna outra função que aceita o mapa de solicitação como um argumento. Dentro da função interna, você pode transformar o mapa de solicitação, o mapa de resposta ou fazer alguma outra lógica antes / depois de chamar o manipulador original.

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

O manipulador anterior não faz muito, mas essa é a ideia. Você poderia, por exemplo, analisar automaticamente um corpo de solicitação JSON em um mapa Clojure se souber que está sempre esperando uma carga útil JSON. Então, em seus manipuladores, você não teria que fazer nenhuma lógica de análise. Esta é uma abordagem muito elegante para obter apenas a funcionalidade de que você precisa em seu aplicativo da web e para garantir que diferentes middlewares sejam compostos bem juntos.

Antes de entrar no middleware, precisamos adicionar a dependência Bugsnag ao nosso projeto. A versão mais recente no momento é .1.2.2

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

Em seguida, precisamos disponibilizar as classes Bugsnag Java para nosso namespace com import.

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

Estou chamando meu middleware , já que essa parece ser a convenção de nomenclatura usada para middlewares. Este é o código real para a função:wrap-bugsnagwrap-bugsnag

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

Aqui, o manipulador está chamando o manipulador original dentro da função e capturando todas as exceções que podem ser levantadas com , que é a classe base para todas as coisas que podem ser lançadas em Java. Em seguida, chamamos a função com a exceção real e os metadados da solicitação. No final, temos que relançar a exceção para evitar engolir silenciosamente o erro.wrap-bugsnagtryThrowablenotify

A notifyfunção usa o cliente Bugsnag para enviar a notificação. Como estamos chamando o método notifyno objeto cliente, precisamos prefixar o nome da função com um ..

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

Dentro da clientfunção, criamos uma instância de e definimos algumas configurações, como sua chave de API.com.bugsnag.Client

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

A dotomacro torna muito fácil chamar uma série de métodos Java em um objeto e retornar o próprio objeto. É semelhante ao de Ruby tap. Observe que cliente não é uma função como todas as outras.

A função de metadados é a última parte do código que tive de implementar. Para isso, precisamos instanciar e adicionar pares de valores-chave à seção chamando o método . Os pares de chaves de valor vem do mapa pedido real, que tem coisas como , , e .com.bugsnag.MetadataRequest.addToTab:json-params:headers:request-method:query-params

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

Aqui, estamos usando doseqpara desestruturar a solicitação e, em seguida, chamar o método para cada par de valores-chave do mapa de solicitação..addToTab

Finalmente, você pode usar o middleware em seu . Isso pressupõe que você colocaria todas as funções anteriores dentro do namespace.handler.cljring-bugsnag

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

Em meu aplicativo, coloquei a chamada wrap-bugsnag após app-routes e antes de todos os outros middlewares de parâmetros. Isso significa que meu middleware obteria as transformações feitas pelos outros middlewares, como , mas não pegaria as exceções levantadas por eles.:json-params

(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))

Espero que isso seja útil para você. Certamente foi para mim uma forma de aprender mais sobre a interoperabilidade Java. No futuro, espero tornar este um invólucro mais sólido e lançá-lo como uma biblioteca. Você pode obter todo o código mostrado aqui nesta essência .

Links:
Biblioteca Bugsnag Java