Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 68 additions & 38 deletions src/circleci/rollcage/core.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
(ns circleci.rollcage.core
(:require
[circleci.rollcage.http :as http]
[clojure.string :as string]
[cheshire.core :as json]
[schema.core :as s]
[clj-http.client :refer (post)]
[clj-stacktrace.core :refer (parse-trace-elem)]
[clj-stacktrace.repl :refer (method-str)])
(:import
Expand All @@ -12,7 +12,22 @@

(def endpoint "https://api.rollbar.com/api/1/item/")

(def Person {:id String
:username (s/maybe String)
:email (s/maybe String)})

(def Request {(s/optional-key :url) String
(s/optional-key :method) String
(s/optional-key :headers) {s/Any s/Any}
(s/optional-key :params) {s/Any s/Any}
(s/optional-key :GET) {s/Any s/Any}
(s/optional-key :POST) {s/Any s/Any}
(s/optional-key :user_ip) String
(s/optional-key :query_string) String
(s/optional-key :body) String})

(def Client {:access-token String
:http-client (s/protocol http/HttpClient)
:data {:environment (s/maybe String)
:platform String
:language String
Expand All @@ -22,6 +37,12 @@
:root String
:code_version (s/maybe String)}}})

(def DataFromParams {(s/optional-key :custom) {s/Any s/Any}
(s/optional-key :request) Request
(s/optional-key :person) Person
(s/optional-key :context) String
(s/optional-key :framework) String})

(defn- deep-merge
"Like merge, but merges maps recursively."
[& maps]
Expand All @@ -32,9 +53,10 @@
:level String
:timestamp s/Int
:uuid UUID
:custom s/Any ;; TODO verify custom
:request {:url (s/maybe String)}}}))

(s/optional-key :custom) s/Any ;; TODO verify custom
(s/optional-key :person) Person
(s/optional-key :context) String
(s/optional-key :request) Request}}))

(defn- guess-os []
(System/getProperty "os.name"))
Expand Down Expand Up @@ -106,62 +128,70 @@
(defn- ^UUID uuid []
(UUID/randomUUID))

(s/defn ^:private params->data :- DataFromParams
"Extract data for the Rollbar API from params"
[params :- (s/maybe s/Any)]
(let [param-keys [:request :person :context :framework]
custom (apply dissoc params param-keys)]
(cond-> (select-keys params param-keys)
(not-empty custom) (assoc :custom custom))))

(s/defn make-rollbar :- Item
"Build a map that matches the Rollbar API"
[client :- Client
level :- String
exception :- Throwable
url :- (s/maybe String)
params :- (s/maybe s/Any)]
;; TODO: Pass request parameters through to here
;; TODO: add person here
(-> client
(assoc-in [:data :body :trace_chain] (build-trace exception))
(assoc-in [:data :level] level)
(assoc-in [:data :timestamp] (timestamp))
(assoc-in [:data :uuid] (uuid))
(assoc-in [:data :custom] params)
(assoc-in [:data :request :url] url)))
params :- (s/maybe {s/Any s/Any})]
(let [data (cond-> {:body {:trace_chain (build-trace exception)}
:level level
:timestamp (timestamp)
:uuid (uuid)}
true (merge (params->data params))
url (assoc-in [:request :url] url))]
(update-in client [:data] merge data)))

(defn snake-case [kw]
(string/replace (name kw) "-" "_"))

(defn send-item
"Send a Rollbar item using the HTTP REST API.
Return the result JSON parsed as a Map"
[endpoint item]
(let [result (post endpoint {:body (json/generate-string item {:key-fn snake-case})
:content-type :json})]
(json/parse-string (:body result) true)))

(s/defn ^:private client* :- Client
[access-token :- String
{:keys [os hostname environment code-version file-root]
:or {environment "production"}}]
(let [os (or os (guess-os))
hostname (or hostname (guess-hostname))
file-root (or file-root (guess-file-root))]
{:access-token access-token
:data {:environment (name environment)
:platform (name os)
:language "Clojure"
:framework "Ring"
:notifier {:name "Rollcage"}
:server {:host hostname
:root file-root
:code_version code-version}}}))
{:keys [os hostname environment code-version framework file-root http-client]
:or {environment "production"
framework "Ring"
os (guess-os)
file-root (guess-file-root)
hostname (guess-hostname)
http-client (http/make-default-http-client)}}]
{:access-token access-token
:http-client http-client
:data {:environment (name environment)
:platform (name os)
:language "Clojure"
:framework framework
:notifier {:name "Rollcage"}
:server {:host hostname
:root file-root
:code_version code-version}}})

(defn client
([access-token]
(client access-token {}))
([access-token options]
(client* access-token options)))

(defn send-item
[http-client endpoint item]
(let [body (json/generate-string item {:key-fn snake-case})
result (http/post http-client endpoint body)]
(json/parse-string result true)))

(defn notify
([level client exception]
(notify level client exception {}))
([level client exception {:keys [url params]}]
(send-item endpoint
([level {:keys [http-client] :as client} exception {:keys [url params]}]
(send-item http-client
endpoint
(make-rollbar client level exception url params))))

(def critical (partial notify "critical"))
Expand Down
16 changes: 16 additions & 0 deletions src/circleci/rollcage/http.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(ns circleci.rollcage.http
(:require [clj-http.client :as http]))

(defprotocol HttpClient
(post [this url json]
"Makes a post request to Rollbar API and returns response body"))

(defrecord CljHttpClient []
HttpClient
(post [this url json]
(-> (http/post url {:body json :content-type :json}) :body)))

(defn make-default-http-client
"Makes clj-http client"
[]
(->CljHttpClient))
48 changes: 46 additions & 2 deletions test/circleci/rollcage/test_core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
[schema.test :refer (validate-schemas)]
[clojure.test :refer :all]
[clojure.string :as string]
[clojure.set :refer [subset?]]
[circleci.rollcage.core :as client]
[circleci.rollcage.http :as http]
[clojure.test.check.clojure-test :as ct :refer (defspec)]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop])
Expand All @@ -19,6 +21,11 @@
(let [tail (subvec (vec a) (- (count a) (count b)))]
(= tail (vec b))))

(defrecord TestHttpClient []
http/HttpClient
(post [this ex params]
"{\"test\":\"ok\"}"))

(deftest ends-with-works?
(is (ends-with? "foobar" "bar"))
(is (not (ends-with? "foobaz" "bar"))))
Expand Down Expand Up @@ -108,15 +115,52 @@
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"Output of client\* does not match schema"
(client/client "e" {:hostname 1}))))

(deftest it-can-use-custom-http-clients
(let [http-client (->TestHttpClient)
c (client/client "access-token" {:http-client http-client})]
(is (= http-client (:http-client c)))))

(deftest environments-can-be-kw-or-string
(letfn [(env [e] (-> (client/client "token" {:environment e}) :data :environment))]
(is (= "test" (env :test)))
(is (= "dev" (env 'dev)))
(is (= "staging" (env "staging")))))

(deftest it-can-make-items
(let [c (client/client "access-token" {})]
(client/make-rollbar c "error" (Exception.) nil nil) ) )
(let [c (client/client "access-token" {})
make-item (partial client/make-rollbar c "error" (Exception.))]
(let [item (make-item nil nil)]
(is (apply = (map #(select-keys % [:access-token :http-client])
[c item]))))

(let [item (make-item "http://example.com" nil)]
(is (= "http://example.com" (get-in item [:data :request :url]))))

(let [req {:url "http://example.com"
:params {:param-1 1 :param-2 2}
:headers {"Content-Type" "text/plain"}}
item (make-item nil {:request req})]
(is (= req (get-in item [:data :request]))))

(let [item (make-item nil {:context "project#context"})]
(is (= "project#context" (get-in item [:data :context]))))

(let [person {:email "email@example.com" :id "123" :username "some-user"}
item (make-item nil {:person person})]
(is (= person (get-in item [:data :person]))))

(let [custom {:some-key "custom"}
item (make-item nil custom)]
(is (= custom (get-in item [:data :custom]))))

(let [item (make-item "http://url1.com" {:request {:url "http://url2.com"}})]
(is (= "http://url1.com" (get-in item [:data :request :url]))))))

(deftest it-can-send-items-via-custom-http-client
(let [http-client (->TestHttpClient)
c (client/client "access-token" {:http-client http-client})
r (client/notify "error" c (Exception.))]
(is (= {:test "ok"} r))))

(deftest ^:integration test-environment-is-setup
(is (not (string/blank? (System/getenv "ROLLBAR_ACCESS_TOKEN")))
Expand Down