diff --git a/.gitignore b/.gitignore index 30ce4a7..156772a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,4 @@ output.calva-repl /.clj-kondo .envrc redis -localstack - +localstack \ No newline at end of file diff --git a/deps.edn b/deps.edn index e0bd505..1c4bc61 100644 --- a/deps.edn +++ b/deps.edn @@ -1,11 +1,11 @@ {:paths ["resources" "src"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} - org.clojure/core.async {:mvn/version "1.6.681"} - com.moclojer/moclojer {:mvn/version "0.3.2"} - com.moclojer/rq {:mvn/version "0.1.4"} + com.moclojer/moclojer {:mvn/version "0.3.4"} + com.moclojer/rq {:mvn/version "0.2.2"} clj-http/clj-http {:mvn/version "3.12.3"} com.zaxxer/HikariCP {:mvn/version "5.0.1"} com.stuartsierra/component {:mvn/version "1.1.0"} + org.clojure/core.async {:mvn/version "1.6.681"} com.taoensso/telemere {:mvn/version "1.0.0-beta25"} com.taoensso/slf4j-telemere {:mvn/version "1.0.0-beta21"} org.clojure/data.json {:mvn/version "2.5.0"} @@ -13,9 +13,9 @@ metosin/muuntaja {:mvn/version "0.6.8"} metosin/malli {:mvn/version "0.11.0"} metosin/reitit {:mvn/version "0.5.18"} - metosin/reitit-pedestal {:mvn/version "0.5.18"} metosin/reitit-swagger {:mvn/version "0.5.18"} metosin/reitit-swagger-ui {:mvn/version "0.5.18"} + ring/ring-jetty-adapter {:mvn/version "1.12.2"} io.sentry/sentry {:mvn/version "7.6.0"} io.sentry/sentry-clj {:mvn/version "7.6.215"} com.cognitect.aws/api {:mvn/version "0.8.561"} @@ -23,8 +23,6 @@ com.cognitect.aws/s3 {:mvn/version "822.2.1145.0"} org.postgresql/postgresql {:mvn/version "42.6.0"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.834"} - io.pedestal/pedestal.jetty {:mvn/version "0.5.10"} - io.pedestal/pedestal.service {:mvn/version "0.5.10"} commons-io/commons-io {:mvn/version "2.16.1"} commons-fileupload/commons-fileupload {:mvn/version "1.5"} migratus/migratus {:mvn/version "1.5.1"} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fe5f30e..23aa9cb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,7 +5,6 @@ services: - "6379:6379" volumes: - ./redis:/redis - # https://docs.localstack.cloud/user-guide/aws/s3/#s3-docker-image localstack: image: localstack/localstack:s3-latest diff --git a/src/com/moclojer/components/core.clj b/src/com/moclojer/components/core.clj index d1cfc3a..cf122f7 100644 --- a/src/com/moclojer/components/core.clj +++ b/src/com/moclojer/components/core.clj @@ -45,7 +45,7 @@ (defn new-router [routes] - (router/map->Router {:router (router/router routes)})) + (router/map->Router {:routes routes})) (defn new-sentry [] (sentry/map->Sentry {})) diff --git a/src/com/moclojer/components/moclojer.clj b/src/com/moclojer/components/moclojer.clj index 9be5745..99b1ab1 100644 --- a/src/com/moclojer/components/moclojer.clj +++ b/src/com/moclojer/components/moclojer.clj @@ -4,8 +4,7 @@ [com.moclojer.components.logs :as logs] [com.moclojer.io-utils :as m.io-utils] [com.moclojer.server :as server] - [com.stuartsierra.component :as component] - [io.pedestal.http :as http])) + [com.stuartsierra.component :as component])) (defn moclojer-server! [{:keys [config-path join?]}] (let [*router (m.adapters/generate-routes (m.io-utils/open-file config-path))] @@ -28,5 +27,5 @@ (stop [this] (let [stop-fn (-> this :moclojer :stop-future)] (stop-fn) - (http/stop (-> this :moclojer :server)) + (.stop (-> this :moclojer :server)) (assoc this :moclojer nil)))) diff --git a/src/com/moclojer/components/mq.clj b/src/com/moclojer/components/mq.clj index 7534d3c..52171e8 100644 --- a/src/com/moclojer/components/mq.clj +++ b/src/com/moclojer/components/mq.clj @@ -2,6 +2,7 @@ (:require [com.moclojer.components.logs :as logs] [com.moclojer.components.sentry :as sentry] + [com.moclojer.rq.queue] [com.moclojer.rq :as rq] [com.moclojer.rq.pubsub :as rq-pubsub] [com.stuartsierra.component :as component])) diff --git a/src/com/moclojer/components/router.clj b/src/com/moclojer/components/router.clj index a8f5f94..d9545d4 100644 --- a/src/com/moclojer/components/router.clj +++ b/src/com/moclojer/components/router.clj @@ -1,19 +1,16 @@ (ns com.moclojer.components.router (:require + [clojure.string :as str] [com.moclojer.components.logs :as logs] [com.stuartsierra.component :as component] [muuntaja.core :as m] [reitit.coercion.malli :as reitit.malli] [reitit.dev.pretty :as pretty] - [reitit.http :as http] - [reitit.http.coercion :as coercion] - [reitit.http.interceptors.exception :as exception] - [reitit.http.interceptors.multipart :as multipart] - [reitit.http.interceptors.muuntaja :as muuntaja] - [reitit.http.interceptors.parameters :as parameters] - [reitit.pedestal :as pedestal] [reitit.ring :as ring] - [com.moclojer.components.sentry :as sentry] + [reitit.ring.coercion :as coercion] + [reitit.ring.middleware.multipart :as multipart] + [reitit.ring.middleware.muuntaja :as muuntaja] + [reitit.ring.middleware.parameters :as parameters] [reitit.swagger :as swagger] [reitit.swagger-ui :as swagger-ui])) @@ -26,74 +23,80 @@ :error "failed to send sentry event" {:event data-ex} sentry-ex))))) -(defn- coercion-error-handler [status] - (fn [exception request] - (logs/log :error "failed to coerce req/resp" - (logs/log :error "Failed to coerce request/response" - {:uri (:uri request) - :method (:request-method request)} - nil - exception)) - (send-sentry-evt-from-req! request exception) - {:status status - :body (if (= 400 status) - (str "Invalid path or request parameters, with the following errors: " - (:errors (ex-data exception))) - "Error checking path or request parameters.")})) +(defn exception-middleware + [handler-fn] + (fn [request] + (try + (handler-fn request) + (catch Exception e + (logs/log :error (.getMessage e) + :ctx {:exception e}) + (send-sentry-evt-from-req! request e) + {:status 500 + :body (.getMessage e)})))) + +(defn log-request-middleware + [handler-fn] + (fn [request] + (let [method (str/upper-case (name (:request-method request)))] + (logs/log :info (str method " " (:uri request)) + :ctx {:method method + :host (:server-name request) + :uri (:uri request) + :query-string (:query-string request)}) + (handler-fn request)))) -(defn- exception-info-handler [exception request] - (logs/log :error "server exception" - nil nil exception) - (send-sentry-evt-from-req! request exception) - {:status 500 - :body "Internal error."}) +(defn cid-middleware + "Extends incoming request with CID if not given already" + [handler-fn] + (fn [request] + (->> {:cid (get-in request [:headers "cid"] + (:cid (logs/gen-ctx-with-cid)))} + (assoc request :ctx) + (handler-fn)))) -(def router-settings - {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs - ;;:validate spec/validate ;; enable spec validation for route data - ;;:reitit.spec/wrap spell/closed ;; strict top-level validation - :exception pretty/exception +(defn build-components-middleware + [components] + (fn [handler-fn] + (fn [request] + (handler-fn (assoc request :components components))))) + +(defn build-router-settings + [components] + {:exception pretty/exception :data {:coercion reitit.malli/coercion :muuntaja (m/create (-> m/default-options - (assoc-in [:formats "application/json" :decoder-opts :bigdecimals] true))) - :interceptors [;; swagger feature - swagger/swagger-feature - ;; query-params & form-params - (parameters/parameters-interceptor) - ;; content-negotiation - (muuntaja/format-negotiate-interceptor) - ;; encoding response body - (muuntaja/format-response-interceptor) - ;; exception handling - (exception/exception-interceptor - (merge - exception/default-handlers - {:reitit.coercion/request-coercion (coercion-error-handler 400) - :reitit.coercion/response-coercion (coercion-error-handler 500) - clojure.lang.ExceptionInfo exception-info-handler})) - ;; decoding request body - (muuntaja/format-request-interceptor) - ;; coercing response bodys - (coercion/coerce-response-interceptor) - ;; coercing request parameters - (coercion/coerce-request-interceptor) - ;; multipart - (multipart/multipart-interceptor)]}}) - -(defn router [routes] - (pedestal/routing-interceptor - (http/router routes router-settings) - ;; optional default ring handler (if no routes have matched) - (ring/routes - (swagger-ui/create-swagger-ui-handler - {:path "/" - :config {:validatorUrl nil - :operationsSorter "alpha"}}) - (ring/create-resource-handler) - (ring/create-default-handler)))) + (assoc-in [:formats "application/json" + :decoder-opts :bigdecimals] + true))) + :middleware [(build-components-middleware components) + log-request-middleware + cid-middleware + exception-middleware + swagger/swagger-feature + parameters/parameters-middleware + muuntaja/format-negotiate-middleware + muuntaja/format-response-middleware + muuntaja/format-request-middleware + coercion/coerce-response-middleware + coercion/coerce-request-middleware + multipart/multipart-middleware]}}) -(defrecord Router [router] +(defrecord Router [routes] component/Lifecycle - (start [this] this) - (stop [this] this)) + (start [this] + (assoc this :router + (ring/ring-handler + (ring/router + routes + (build-router-settings this)) + (ring/routes + (swagger-ui/create-swagger-ui-handler + {:path "/" + :config {:validatorUrl nil + :operationsSorter "alpha"}}) + (ring/create-resource-handler) + (ring/create-default-handler))))) + (stop [this] + (dissoc this :router))) diff --git a/src/com/moclojer/components/webserver.clj b/src/com/moclojer/components/webserver.clj index ef0f104..e7d2a8c 100644 --- a/src/com/moclojer/components/webserver.clj +++ b/src/com/moclojer/components/webserver.clj @@ -1,99 +1,23 @@ (ns com.moclojer.components.webserver (:require - [clojure.pprint :refer [pprint]] - [clojure.string :as str] + [ring.adapter.jetty :as jetty] [com.moclojer.components.logs :as logs] - [com.stuartsierra.component :as component] - [io.pedestal.http :as server] - [io.pedestal.interceptor.helpers :refer [before on-request]] - [reitit.pedestal :as pedestal])) - -(defn- add-system [service] - (before (fn [context] (assoc-in context [:request :components] service)))) - -(defn system-interceptors - "Extend to service's interceptors to include one to inject the components - into the request object" - [service-map service] - (update-in service-map - [::server/interceptors] - #(vec (->> % (cons (add-system service)))))) - -(defn cid-interceptor - "Extends incoming request with CID if not given already" - [service-map] - (update-in service-map - [::server/interceptors] - #(conj % (before - (fn [ctx] - (assoc-in ctx [:request :ctx] - {:cid (get-in ctx [:request :headers "cid"] - (:cid (logs/gen-ctx-with-cid)))})))))) - -(def req-logger - (on-request - ::log-request - (fn [req] - (logs/log - :info (str (str/lower-case (name (:request-method req))) " " - (:uri req)) - (apply dissoc req [:body :components :servlet :servlet-request - :servlet-response])) - req))) - -(defn base-service [port] - {::server/port port - ::server/type :jetty - ::server/host "0.0.0.0" - ;; dev false to not lock repl/thread - ::server/join? true - ;; no pedestal routes - ::server/request-logger req-logger - ::server/routes [] - ;; allow serving the swagger-ui styles & scripts from self - ::server/secure-headers {:content-security-policy-settings - {:default-src "'self'" - :style-src "'self' 'unsafe-inline'" - :script-src "'self' 'unsafe-inline'" - :img-src "'self' 'unsafe-inline' data: https://validator.swagger.io"}}}) - -(defn dev-init [service-map router] - (-> service-map - (merge {:env :dev - ;; do not block thread that starts web server - ::server/join? false - ::server/allowed-origins {:creds true :allowed-origins (constantly true)} - ;; Content Security Policy (CSP) is mostly turned off in dev mode - ::server/secure-headers {:content-security-policy-settings {:object-src "none"}}}) - ;; Wire up interceptor chains - (server/default-interceptors) - (pedestal/replace-last-interceptor router) - (server/dev-interceptors))) - -(defn prod-init [service-map router] - (-> service-map - (merge {:env :prod}) - (server/default-interceptors) - (pedestal/replace-last-interceptor router))) + [com.stuartsierra.component :as component])) (defrecord WebServer [config router] component/Lifecycle (start [this] (let [{:webserver/keys [port] :keys [env]} (:config config) - init-fn (if (= env :dev) dev-init prod-init)] - (logs/log :info "starting webserver config" - {:env env :port port}) - (assoc this :webserver - (-> (base-service port) - (init-fn (:router router)) - (cid-interceptor) - (system-interceptors this) - (server/create-server) - (server/start))))) + cfg {:port port + :host "0.0.0.0" + :join? true + :env env}] + (logs/log :info "starting webserver config" {:env env :port port}) + (assoc this :webserver (jetty/run-jetty (:router router) cfg)))) (stop [this] (logs/log :info "stopping webserver") - (server/stop (:webserver this)) - (dissoc this :webserver) - this)) + (when-let [srv (:webserver this)] + (.stop srv)) + (dissoc this :webserver)))