diff --git a/backend/resources/error-report.tmpl b/backend/resources/error-report.tmpl
new file mode 100644
index 000000000..32f796d6d
--- /dev/null
+++ b/backend/resources/error-report.tmpl
@@ -0,0 +1,135 @@
+
+
+
+
+
+ penpot - error report {{id}}
+
+
+
+
+
+
+
+
+ {% if type %}
+
+ {% endif %}
+ {% if code %}
+
+ {% endif %}
+
+
+
+
+
PATH:
+
{{method|upper}} {{path}}
+
+
+ {% if params %}
+
+ {% endif %}
+
+ {% if explain %}
+
+ {% endif %}
+
+ {% if data %}
+
+ {% endif %}
+
+
+
+
+
+
+
diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj
index 08c569622..3417b1b90 100644
--- a/backend/src/app/config.clj
+++ b/backend/src/app/config.clj
@@ -26,7 +26,7 @@
:secret-key "default"
:asserts-enabled true
- :public-uri "http://localhost:3449/"
+ :public-uri "http://localhost:3449"
:redis-uri "redis://localhost/0"
:storage-backend :fs
diff --git a/backend/src/app/error_reporter.clj b/backend/src/app/error_reporter.clj
index e9fd0a5cc..7b9ddead8 100644
--- a/backend/src/app/error_reporter.clj
+++ b/backend/src/app/error_reporter.clj
@@ -12,67 +12,94 @@
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
+ [app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.tasks :as tasks]
[app.worker :as wrk]
[app.util.json :as json]
[app.util.http :as http]
+ [app.util.template :as tmpl]
+ [clojure.pprint :refer [pprint]]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
+ [clojure.java.io :as io]
[cuerdas.core :as str]
[integrant.core :as ig]
- [promesa.exec :as px]))
+ [promesa.exec :as px])
+ (:import
+ org.apache.logging.log4j.core.LogEvent
+ org.apache.logging.log4j.util.ReadOnlyStringMap))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Error Reporting
+;; Error Listener
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(declare send-notification!)
-(defonce queue-fn identity)
+(declare handle-event)
+
+(defonce queue (a/chan (a/sliding-buffer 64)))
+(defonce queue-fn (fn [event] (a/>!! queue event)))
(s/def ::uri ::us/string)
-(defmethod ig/pre-init-spec ::instance [_]
- (s/keys :req-un [::wrk/executor]
+
+(defmethod ig/pre-init-spec ::reporter [_]
+ (s/keys :req-un [::wrk/executor ::db/pool]
:opt-un [::uri]))
-(defmethod ig/init-key ::instance
+(defmethod ig/init-key ::reporter
[_ {:keys [executor uri] :as cfg}]
- (let [out (a/chan (a/sliding-buffer 64))]
- (log/info "Intializing error reporter.")
- (if uri
- (do
- (alter-var-root #'queue-fn (constantly (fn [x] (a/>!! out (str x)))))
- (a/go-loop []
- (let [val (a/ (io/resource "error-report.tmpl")
+ (tmpl/render content)))]
+
+
+ (fn [request]
+ (let [result (some-> (parse-id request)
+ (retrieve-report)
+ (render-template))]
+ (if result
+ {:status 200
+ :headers {"content-type" "text/html; charset=utf-8"}
+ :body result}
+ {:status 404
+ :body "not found"})))))
diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj
index 19fb909da..aeffb8020 100644
--- a/backend/src/app/http.clj
+++ b/backend/src/app/http.clj
@@ -9,17 +9,18 @@
(ns app.http
(:require
- [app.common.spec :as us]
[app.common.data :as d]
+ [app.common.spec :as us]
+ [app.common.uuid :as uuid]
[app.config :as cfg]
+ [app.http.assets :as assets]
[app.http.auth :as auth]
[app.http.errors :as errors]
[app.http.middleware :as middleware]
- [app.http.assets :as assets]
[app.metrics :as mtx]
+ [clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]
- [clojure.spec.alpha :as s]
[reitit.ring :as rr]
[ring.adapter.jetty9 :as jetty])
(:import
@@ -101,12 +102,11 @@
(try
(handler request)
(catch Exception e
- (log/errorf e
- (str "Unhandled exception: " (ex-message e) "\n"
- "=| uri: " (pr-str (:uri request)) "\n"
- "=| method: " (pr-str (:request-method request)) "\n"))
- {:status 500
- :body "internal server error"})))))
+ (let [cdata (errors/get-error-context request e)]
+ (errors/update-thread-context! cdata)
+ (log/errorf e "Unhandled exception: %s (id: %s)" (ex-message e) (str (:id cdata)))
+ {:status 500
+ :body "internal server error"}))))))
(defn- create-router
[{:keys [session rpc google-auth gitlab-auth github-auth metrics ldap-auth storage svgparse] :as cfg}]
@@ -119,12 +119,15 @@
["/by-file-media-id/:id" {:get #(assets/file-objects-handler storage %)}]
["/by-file-media-id/:id/thumbnail" {:get #(assets/file-thumbnails-handler storage %)}]]
+ ["/dbg"
+ ["/error-by-id/:id" {:get (:error-reporter-handler cfg)}]]
+
["/api" {:middleware [[middleware/format-response-body]
[middleware/parse-request-body]
- [middleware/errors errors/handle]
[middleware/params]
[middleware/multipart-params]
[middleware/keyword-params]
+ [middleware/errors errors/handle]
[middleware/cookies]]}
["/svg" {:post svgparse}]
diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj
index febc707de..7c16e965e 100644
--- a/backend/src/app/http/errors.clj
+++ b/backend/src/app/http/errors.clj
@@ -10,27 +10,50 @@
(ns app.http.errors
"A errors handling for the http server."
(:require
- [clojure.pprint :refer [pprint]]
[app.common.exceptions :as ex]
+ [app.common.uuid :as uuid]
+ [app.config :as cfg]
+ [clojure.pprint :refer [pprint]]
[clojure.tools.logging :as log]
[cuerdas.core :as str]
- [expound.alpha :as expound]))
+ [expound.alpha :as expound])
+ (:import
+ org.apache.logging.log4j.ThreadContext))
-(defn get-context-string
- [request edata]
- (str "=| uri: " (pr-str (:uri request)) "\n"
- "=| method: " (pr-str (:request-method request)) "\n"
- "=| params: \n"
- (with-out-str
- (pprint (:params request)))
- "\n"
+(defn update-thread-context!
+ [data]
+ (run! (fn [[key val]]
+ (ThreadContext/put
+ (name key)
+ (cond
+ (coll? val) (with-out-str (pprint val))
+ (instance? clojure.lang.Named val) (name val)
+ :else (str val))))
+ data))
- (when (map? edata)
- (str "=| ex-data: \n"
- (with-out-str
- (pprint edata))))
+(defn- explain-error
+ [error]
+ (with-out-str
+ (expound/printer (:data error))))
- "\n"))
+(defn get-error-context
+ [request error]
+ (let [edata (ex-data error)]
+ (merge
+ {:id (uuid/next)
+ :path (:uri request)
+ :method (:request-method request)
+ :params (:params request)
+ :version (:full cfg/version)
+ :host (:host cfg/config)
+ :class (.getCanonicalName ^java.lang.Class (class error))
+ :hint (ex-message error)}
+
+ (when (map? edata)
+ edata)
+
+ (when (and (map? edata) (:data edata))
+ {:explain (explain-error edata)}))))
(defmulti handle-exception
(fn [err & _rest]
@@ -42,11 +65,6 @@
[err _]
{:status 401 :body (ex-data err)})
-(defn- explain-error
- [error]
- (with-out-str
- (expound/printer (:data error))))
-
(defmethod handle-exception :validation
[err req]
(let [header (get-in req [:headers "accept"])
@@ -66,11 +84,10 @@
(defmethod handle-exception :assertion
[error request]
- (let [edata (ex-data error)]
- (log/error error
- (str "Internal error: assertion\n"
- (get-context-string request edata)
- (explain-error edata)))
+ (let [edata (ex-data error)
+ cdata (get-error-context request error)]
+ (update-thread-context! cdata)
+ (log/errorf error "Internal error: assertion (id: %s)" (str (:id cdata)))
{:status 500
:body {:type :server-error
:data (-> edata
@@ -83,15 +100,15 @@
(defmethod handle-exception :default
[error request]
- (let [edata (ex-data error)]
- (log/error error
- (str "Internal Error: "
- (ex-message error)
- (get-context-string request edata)))
+ (let [cdata (get-error-context request error)]
+ (update-thread-context! cdata)
+ (log/errorf error "Internal error: %s (id: %s)"
+ (ex-message error)
+ (str (:id cdata)))
{:status 500
:body {:type :server-error
:hint (ex-message error)
- :data edata}}))
+ :data (ex-data error)}}))
(defn handle
[error req]
diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj
index 3caa6f858..4aab02774 100644
--- a/backend/src/app/http/middleware.clj
+++ b/backend/src/app/http/middleware.clj
@@ -14,7 +14,6 @@
[app.metrics :as mtx]
[app.util.transit :as t]
[app.util.json :as json]
- ;; [clojure.data.json :as json]
[clojure.java.io :as io]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj
index 18911e2dc..411a54525 100644
--- a/backend/src/app/main.clj
+++ b/backend/src/app/main.clj
@@ -84,6 +84,9 @@
:github-auth (ig/ref :app.http.auth/github)
:ldap-auth (ig/ref :app.http.auth/ldap)
:svgparse (ig/ref :app.svgparse/handler)
+
+ :error-reporter-handler (ig/ref :app.error-reporter/handler)
+
:storage (ig/ref :app.storage/storage)}
:app.svgparse/svgc
@@ -240,10 +243,14 @@
:app.srepl/server
{:port 6062}
- :app.error-reporter/instance
- {:uri (:error-report-webhook cfg/config)
+ :app.error-reporter/reporter
+ {:uri (:error-report-webhook cfg/config)
+ :pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
+ :app.error-reporter/handler
+ {:pool (ig/ref :app.db/pool)}
+
:app.storage/storage
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index f74c9f2d8..769b01434 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -131,6 +131,9 @@
{:name "0039-fix-some-on-delete-triggers"
:fn (mg/resource "app/migrations/sql/0039-fix-some-on-delete-triggers.sql")}
+
+ {:name "0040-add-error-report-tables"
+ :fn (mg/resource "app/migrations/sql/0040-add-error-report-tables.sql")}
])
diff --git a/backend/src/app/migrations/sql/0040-add-error-report-tables.sql b/backend/src/app/migrations/sql/0040-add-error-report-tables.sql
new file mode 100644
index 000000000..a5271a9c6
--- /dev/null
+++ b/backend/src/app/migrations/sql/0040-add-error-report-tables.sql
@@ -0,0 +1,10 @@
+CREATE TABLE server_error_report (
+ id uuid NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
+ content jsonb,
+
+ PRIMARY KEY (id, created_at)
+);
+
+ALTER TABLE server_error_report
+ ALTER COLUMN content SET STORAGE external;
diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf
index 78a50b949..2227dc05a 100644
--- a/docker/devenv/files/nginx.conf
+++ b/docker/devenv/files/nginx.conf
@@ -99,6 +99,10 @@ http {
proxy_pass http://127.0.0.1:6060/api;
}
+ location /dbg {
+ proxy_pass http://127.0.0.1:6060/dbg;
+ }
+
location /export {
proxy_pass http://127.0.0.1:6061;
}