🎉 Add better error reporting.

This commit is contained in:
Andrey Antukh 2020-12-04 20:38:38 +01:00 committed by Alonso Torres
parent a881d86637
commit 4d7a34a998
14 changed files with 238 additions and 72 deletions

View file

@ -34,6 +34,7 @@
org.postgresql/postgresql {:mvn/version "42.2.16"} org.postgresql/postgresql {:mvn/version "42.2.16"}
com.zaxxer/HikariCP {:mvn/version "3.4.5"} com.zaxxer/HikariCP {:mvn/version "3.4.5"}
funcool/log4j2-clojure {:mvn/version "2020.11.23-1"}
funcool/datoteka {:mvn/version "1.2.0"} funcool/datoteka {:mvn/version "1.2.0"}
funcool/promesa {:mvn/version "5.1.0"} funcool/promesa {:mvn/version "5.1.0"}
funcool/cuerdas {:mvn/version "2020.03.26-3"} funcool/cuerdas {:mvn/version "2020.03.26-3"}

View file

@ -12,6 +12,10 @@
</Policies> </Policies>
<DefaultRolloverStrategy max="9"/> <DefaultRolloverStrategy max="9"/>
</RollingFile> </RollingFile>
<CljFn name="error-reporter" ns="app.error-reporter" fn="enqueue">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
</CljFn>
</Appenders> </Appenders>
<Loggers> <Loggers>
@ -23,13 +27,17 @@
<AppenderRef ref="console"/> <AppenderRef ref="console"/>
</Logger> </Logger>
<Logger name="app.error-reporter" level="debug" additivity="false">
<AppenderRef ref="console"/>
</Logger>
<Logger name="app" level="debug" additivity="false"> <Logger name="app" level="debug" additivity="false">
<AppenderRef ref="main" level="debug" /> <AppenderRef ref="main" level="debug" />
<AppenderRef ref="error-reporter" level="error" />
</Logger> </Logger>
<Root level="info"> <Root level="info">
<AppenderRef ref="main" /> <AppenderRef ref="main" />
<!-- <AppenderRef ref="console" /> -->
</Root> </Root>
</Loggers> </Loggers>
</Configuration> </Configuration>

View file

@ -40,6 +40,8 @@
:smtp-default-reply-to "no-reply@example.com" :smtp-default-reply-to "no-reply@example.com"
:smtp-default-from "no-reply@example.com" :smtp-default-from "no-reply@example.com"
:host "devenv"
:allow-demo-users true :allow-demo-users true
:registration-enabled true :registration-enabled true
:registration-domain-whitelist "" :registration-domain-whitelist ""
@ -78,6 +80,9 @@
(s/def ::media-uri ::us/string) (s/def ::media-uri ::us/string)
(s/def ::media-directory ::us/string) (s/def ::media-directory ::us/string)
(s/def ::secret-key ::us/string) (s/def ::secret-key ::us/string)
(s/def ::host ::us/string)
(s/def ::error-report-webhook ::us/string)
(s/def ::smtp-enabled ::us/boolean) (s/def ::smtp-enabled ::us/boolean)
(s/def ::smtp-default-reply-to ::us/email) (s/def ::smtp-default-reply-to ::us/email)
(s/def ::smtp-default-from ::us/email) (s/def ::smtp-default-from ::us/email)
@ -135,6 +140,7 @@
::assets-uri ::assets-uri
::media-directory ::media-directory
::media-uri ::media-uri
::error-report-webhook
::secret-key ::secret-key
::smtp-default-from ::smtp-default-from
::smtp-default-reply-to ::smtp-default-reply-to
@ -145,6 +151,7 @@
::smtp-password ::smtp-password
::smtp-tls ::smtp-tls
::smtp-ssl ::smtp-ssl
::host
::file-trimming-threshold ::file-trimming-threshold
::debug-humanize-transit ::debug-humanize-transit
::allow-demo-users ::allow-demo-users

View file

@ -0,0 +1,83 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
(ns app.error-reporter
"A mattermost integration for error reporting."
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.db :as db]
[app.tasks :as tasks]
[app.util.async :as aa]
[app.worker :as wrk]
[app.util.http :as http]
[clojure.core.async :as a]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[cuerdas.core :as str]
[mount.core :as mount :refer [defstate]]
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Public API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce enqueue identity)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- send-to-mattermost!
[log-event]
(try
(let [text (str/fmt "Unhandled exception: `host='%s'`, `version=%s`.\n@channel ⇊\n```%s\n```"
(:host cfg/config)
(:full @cfg/version)
(str log-event))
rsp (http/send! {:uri (:error-reporter-webhook cfg/config)
:method :post
:headers {"content-type" "application/json"}
:body (json/write-str {:text text})})]
(when (not= (:status rsp) 200)
(log/warnf "Error reporting webhook replying with unexpected status: %s\n%s"
(:status rsp)
(pr-str rsp))))
(catch Exception e
(log/warnf e "Unexpected exception on error reporter."))))
(defn- send!
[val]
(aa/thread-call wrk/executor (partial send-to-mattermost! val)))
(defn- start
[]
(let [qch (a/chan (a/sliding-buffer 128))]
(log/info "Starting error reporter loop.")
;; Only enable when a valid URL is provided.
(when (:error-reporter-webhook cfg/config)
(alter-var-root #'enqueue (constantly #(a/>!! qch %)))
(a/go-loop []
(let [val (a/<! qch)]
(if (nil? val)
(do
(log/info "Closing error reporting loop.")
(alter-var-root #'enqueue (constantly identity)))
(do
(a/<! (send! val))
(recur))))))
qch))
(defstate reporter
:start (start)
:stop (a/close! reporter))

View file

@ -30,8 +30,8 @@
(rring/router (rring/router
[["/metrics" {:get mtx/dump}] [["/metrics" {:get mtx/dump}]
["/api" {:middleware [[middleware/format-response-body] ["/api" {:middleware [[middleware/format-response-body]
[middleware/errors errors/handle]
[middleware/parse-request-body] [middleware/parse-request-body]
[middleware/errors errors/handle]
[middleware/params] [middleware/params]
[middleware/multipart-params] [middleware/multipart-params]
[middleware/keyword-params] [middleware/keyword-params]

View file

@ -10,13 +10,16 @@
(ns app.http.errors (ns app.http.errors
"A errors handling for the http server." "A errors handling for the http server."
(:require (:require
[app.common.exceptions :as ex]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[cuerdas.core :as str])) [cuerdas.core :as str]
[expound.alpha :as expound]))
(defmulti handle-exception (defmulti handle-exception
(fn [err & _rest] (fn [err & _rest]
(:type (ex-data err)))) (let [edata (ex-data err)]
(or (:type edata)
(class err)))))
(defmethod handle-exception :authorization (defmethod handle-exception :authorization
[err _] [err _]
@ -26,17 +29,19 @@
(defmethod handle-exception :validation (defmethod handle-exception :validation
[err req] [err req]
(let [header (get-in req [:headers "accept"]) (let [header (get-in req [:headers "accept"])
response (ex-data err)] edata (ex-data err)]
(cond (cond
(and (str/starts-with? header "text/html") (and (str/starts-with? header "text/html")
(= :spec-validation (:code response))) (= :spec-validation (:code edata)))
{:status 400 {:status 400
:headers {"content-type" "text/html"} :headers {"content-type" "text/html"}
:body (str "<pre style='font-size:16px'>" (:explain response) "</pre>\n")} :body (str "<pre style='font-size:16px'>"
(with-out-str
(:data edata))
"</pre>\n")}
:else :else
{:status 400 {:status 400
:body response}))) :body edata})))
(defmethod handle-exception :ratelimit (defmethod handle-exception :ratelimit
[_ _] [_ _]
@ -60,11 +65,38 @@
:body {:type :parse :body {:type :parse
:message (ex-message err)}}) :message (ex-message err)}})
(defn get-context-string
[err request]
(str
"=| uri: " (pr-str (:uri request)) "\n"
"=| method: " (pr-str (:request-method request)) "\n"
"=| path-params: " (pr-str (:path-params request)) "\n"
"=| query-params: " (pr-str (:query-params request)) "\n"
(when-let [bparams (:body-params request)]
(str "=| body-params: " (pr-str bparams) "\n"))
(when (ex/ex-info? err)
(str "=| ex-data: " (pr-str (ex-data err)) "\n"))
"\n"))
(defmethod handle-exception :assertion
[err request]
(let [{:keys [data] :as edata} (ex-data err)]
(log/errorf err
(str "Assertion error\n"
(get-context-string err request)
(with-out-str (expound/printer data))))
{:status 500
:body {:type :internal-error
:message "Assertion error"
:data (ex-data err)}}))
(defmethod handle-exception :default (defmethod handle-exception :default
[err req] [err request]
(log/error "Unhandled exception on request:" (:path req) "\n" (log/errorf err (str "Internal Error\n" (get-context-string err request)))
(with-out-str
(.printStackTrace ^Throwable err (java.io.PrintWriter. *out*))))
{:status 500 {:status 500
:body {:type :internal-error :body {:type :internal-error
:message (ex-message err) :message (ex-message err)

View file

@ -9,6 +9,10 @@
(ns app.http.handlers (ns app.http.handlers
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.emails :as emails]
[app.http.session :as session]
[app.services.init] [app.services.init]
[app.services.mutations :as sm] [app.services.mutations :as sm]
[app.services.queries :as sq])) [app.services.queries :as sq]))
@ -25,36 +29,40 @@
:login}) :login})
(defn query-handler (defn query-handler
[req] [{:keys [profile-id] :as request}]
(let [type (keyword (get-in req [:path-params :type])) (let [type (keyword (get-in request [:path-params :type]))
data (merge (:params req) data (assoc (:params request) ::sq/type type)
{::sq/type type}) data (if profile-id
data (cond-> data (assoc data :profile-id profile-id)
(:profile-id req) (assoc :profile-id (:profile-id req)))] (dissoc data :profile-id))]
(if (or (:profile-id req) (contains? unauthorized-services type))
(if (or (uuid? profile-id)
(contains? unauthorized-services type))
{:status 200 {:status 200
:body (sq/handle (with-meta data {:req req}))} :body (sq/handle (with-meta data {:req request}))}
{:status 403 {:status 403
:body {:type :authentication :body {:type :authentication
:code :unauthorized}}))) :code :unauthorized}})))
(defn mutation-handler (defn mutation-handler
[req] [{:keys [profile-id] :as request}]
(let [type (keyword (get-in req [:path-params :type])) (let [type (keyword (get-in request [:path-params :type]))
data (merge (:params req) data (d/merge (:params request)
(:body-params req) (:body-params request)
(:uploads req) (:uploads request)
{::sm/type type}) {::sm/type type})
data (cond-> data data (if profile-id
(:profile-id req) (assoc :profile-id (:profile-id req)))] (assoc data :profile-id profile-id)
(if (or (:profile-id req) (contains? unauthorized-services type)) (dissoc data :profile-id))]
(let [result (sm/handle (with-meta data {:req req}))
(if (or (uuid? profile-id)
(contains? unauthorized-services type))
(let [result (sm/handle (with-meta data {:req request}))
mdata (meta result) mdata (meta result)
resp {:status (if (nil? (seq result)) 204 200) resp {:status (if (nil? (seq result)) 204 200)
:body result}] :body result}]
(cond->> resp (cond->> resp
(:transform-response mdata) ((:transform-response mdata) req))) (:transform-response mdata) ((:transform-response mdata) request)))
{:status 403 {:status 403
:body {:type :authentication :body {:type :authentication
:code :unauthorized}}))) :code :unauthorized}})))

View file

@ -0,0 +1,43 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services
"A initialization of services."
(:require
[app.services.middleware :as middleware]
[app.util.dispatcher :as uds]
[mount.core :as mount :refer [defstate]]))
;; --- Initialization
(defn- load-query-services
[]
(require 'app.services.queries.projects)
(require 'app.services.queries.files)
(require 'app.services.queries.comments)
(require 'app.services.queries.profile)
(require 'app.services.queries.recent-files)
(require 'app.services.queries.viewer))
(defn- load-mutation-services
[]
(require 'app.services.mutations.demo)
(require 'app.services.mutations.media)
(require 'app.services.mutations.projects)
(require 'app.services.mutations.files)
(require 'app.services.mutations.comments)
(require 'app.services.mutations.profile)
(require 'app.services.mutations.viewer)
(require 'app.services.mutations.verify-token))
(defstate query-services
:start (load-query-services))
(defstate mutation-services
:start (load-mutation-services))

View file

@ -7,33 +7,4 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.init (ns app.services.init)
"A initialization of services."
(:require
[mount.core :as mount :refer [defstate]]))
(defn- load-query-services
[]
(require 'app.services.queries.projects)
(require 'app.services.queries.files)
(require 'app.services.queries.comments)
(require 'app.services.queries.profile)
(require 'app.services.queries.recent-files)
(require 'app.services.queries.viewer))
(defn- load-mutation-services
[]
(require 'app.services.mutations.demo)
(require 'app.services.mutations.media)
(require 'app.services.mutations.projects)
(require 'app.services.mutations.files)
(require 'app.services.mutations.comments)
(require 'app.services.mutations.profile)
(require 'app.services.mutations.viewer)
(require 'app.services.mutations.verify-token))
(defstate query-services
:start (load-query-services))
(defstate mutation-services
:start (load-mutation-services))

View file

@ -380,7 +380,7 @@
(defn thread-pool (defn thread-pool
([] (thread-pool {})) ([] (thread-pool {}))
([{:keys [min-threads max-threads name] ([{:keys [min-threads max-threads name]
:or {min-threads 0 max-threads 128}}] :or {min-threads 0 max-threads 256}}]
(let [executor (QueuedThreadPool. max-threads min-threads)] (let [executor (QueuedThreadPool. max-threads min-threads)]
(.setName executor (or name "default-tp")) (.setName executor (or name "default-tp"))
(.start executor) (.start executor)

View file

@ -17,7 +17,7 @@
[mount.core :as mount] [mount.core :as mount]
[environ.core :refer [env]] [environ.core :refer [env]]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.services.init] [app.services]
[app.services.mutations.profile :as profile] [app.services.mutations.profile :as profile]
[app.services.mutations.projects :as projects] [app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams] [app.services.mutations.teams :as teams]
@ -50,8 +50,8 @@
#'app.redis/client #'app.redis/client
#'app.redis/conn #'app.redis/conn
#'app.media/semaphore #'app.media/semaphore
#'app.services.init/query-services #'app.services/query-services
#'app.services.init/mutation-services #'app.services/mutation-services
#'app.migrations/migrations #'app.migrations/migrations
#'app.media-storage/assets-storage #'app.media-storage/assets-storage
#'app.media-storage/media-storage}) #'app.media-storage/media-storage})

View file

@ -6,7 +6,7 @@
(ns app.common.data (ns app.common.data
"Data manipulation and query helper functions." "Data manipulation and query helper functions."
(:refer-clojure :exclude [concat read-string hash-map]) (:refer-clojure :exclude [concat read-string hash-map merge])
#?(:cljs #?(:cljs
(:require-macros [app.common.data])) (:require-macros [app.common.data]))
(:require (:require
@ -210,6 +210,17 @@
(assoc m key v) (assoc m key v)
m))) m)))
(defn merge
"A faster merge."
[& maps]
(loop [res (transient (first maps))
maps (next maps)]
(if (nil? maps)
(persistent! res)
(recur (reduce-kv assoc! res (first maps))
(next maps)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion ;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -48,3 +48,7 @@
(defmacro try (defmacro try
[& exprs] [& exprs]
`(try* (^:once fn* [] ~@exprs) identity)) `(try* (^:once fn* [] ~@exprs) identity))
(defn ex-info?
[v]
(instance? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) v))

View file

@ -189,9 +189,7 @@
(let [edata (s/explain-data spec data)] (let [edata (s/explain-data spec data)]
(throw (ex/error :type :validation (throw (ex/error :type :validation
:code :spec-validation :code :spec-validation
:explain (with-out-str :data data))))
(expound/printer edata))
:data (::s/problems edata)))))
result)) result))
(defmacro instrument! (defmacro instrument!