diff --git a/project.clj b/project.clj index e993152..cb1de50 100644 --- a/project.clj +++ b/project.clj @@ -1,5 +1,16 @@ (defproject clj-librato "0.0.4-SHAPSHOT" :description "Clojure interface to the Librato service" :dependencies [[org.clojure/clojure "1.5.1"] - [clj-http "0.7.5"] - [cheshire "5.2.0"]]) + [org.clojure/tools.logging "0.2.6"] + [clj-http "0.7.5" + :exclusions [commons-logging]] + [cheshire "5.2.0"]] + :profiles + {:dev {:dependencies [[log4j/log4j "1.2.16" + :exclusions [javax.mail/mail + javax.jms/jms + com.sun.jdmk/jmxtools + com.sun.jmx/jmxri]] + [org.slf4j/slf4j-log4j12 "1.7.5"] + [org.slf4j/jcl-over-slf4j "1.7.5"]]} + :test {:resource-paths ["resources" "test-resources"]}}) diff --git a/src/clj_librato/metrics.clj b/src/clj_librato/metrics.clj index ae63666..a6ed799 100644 --- a/src/clj_librato/metrics.clj +++ b/src/clj_librato/metrics.clj @@ -2,7 +2,9 @@ (:use [slingshot.slingshot :only [try+]]) (:require [cheshire.core :as json] [clj-http.client :as client] + [clj-http.conn-mgr :as conn-mgr] [clojure.string :as string] + [clojure.tools.logging :as logging] clj-http.util)) (def uri-base "https://metrics-api.librato.com/v1/") @@ -10,8 +12,8 @@ (defn uri "The full URI of a particular resource, by path fragments." [& path-fragments] - (apply str uri-base - (interpose "/" (map (comp clj-http.util/url-encode str) + (apply str uri-base + (interpose "/" (map (comp clj-http.util/url-encode str) path-fragments)))) (defn unparse-kw @@ -19,9 +21,9 @@ Recursive." [m] (cond - (map? m) (into {} (map (fn [[k v]] - [(string/replace (name k) "-" "_") - (unparse-kw v)]) + (map? m) (into {} (map (fn [[k v]] + [(string/replace (name k) "-" "_") + (unparse-kw v)]) m)) (coll? m) (map unparse-kw m) true m)) @@ -32,40 +34,58 @@ [m] (into {} (map (fn [[k v]] [(keyword (string/replace k "_" "-")) v]) m))) +(defn connection-manager + "Return a connection manager that can be passed as :connection-manager in + a request." + [{:keys [timeout threads] :or {timeout 10 threads 2} :as options}] + (conn-mgr/make-reusable-conn-manager + (merge {:timeout timeout :threads threads :default-per-route threads} + options))) + (defn request - "Constructs the HTTP client request map." + "Constructs the HTTP client request map. + options will be merged verbatim into the request map." ([user api-key params] {:basic-auth [user api-key] :content-type :json :accept :json :throw-entire-message? true :query-params (unparse-kw params)}) - ([user api-key params body] (assoc (request user api-key params) - :body (json/generate-string (unparse-kw body))))) + :body (json/generate-string (unparse-kw body))))) -(defn collate [user api-key gauges counters] - "Posts a set of gauges and counters." - (assert (every? :name gauges)) - (assert (every? :name counters)) - (assert (every? :value gauges)) - (assert (every? :value counters)) - (client/post (uri "metrics") - (request user api-key {} {:gauges gauges :counters counters}))) +(defn collate + "Posts a set of gauges and counters. options is a map of clj-http options." + ([user api-key gauges counters] + (collate user api-key gauges counters nil)) + ([user api-key gauges counters options] + (assert (every? :name gauges)) + (assert (every? :name counters)) + (assert (every? :value gauges)) + (assert (every? :value counters)) + (client/post (uri "metrics") + (merge + options + (request user api-key {} + {:gauges gauges :counters counters}))))) (defn metric "Gets a metric by name. - + See http://dev.librato.com/v1/get/metrics" ([user api-key name] - (metric user api-key name {})) - + (metric user api-key name {} nil)) ([user api-key name params] + (metric user api-key name params nil)) + + ([user api-key name params options] (assert name) (try+ (let [body (-> (client/get (uri "metrics" name) - (request user api-key params)) + (merge + options + (request user api-key params))) :body json/parse-string parse-kw)] (assoc body :measurements (into {} (map (fn [[source measurements]] @@ -77,44 +97,70 @@ (defn create-annotation "Creates a new annotation, and returns the created annotation as a map. - + http://dev.librato.com/v1/post/annotations/:name" - [user api-key name annotation] - (assert name) - (-> (client/post (uri "annotations" name) - (request user api-key {} annotation)) - :body - json/parse-string - parse-kw)) + ([user api-key name annotation] + (create-annotation user api-key name annotation)) + ([user api-key name annotation options] + {:pre [(or (nil? options)(map? options))]} + (assert name) + (-> (client/post (uri "annotations" name) + (merge + options + (request user api-key {} annotation))) + :body + json/parse-string + parse-kw))) (defn update-annotation "Updates an annotation. - + http://dev.librato.com/v1/put/annotations/:name/events/:id" - [user api-key name id annotation] - (assert name) - (assert id) - (client/put (uri "annotations" name id) - (request user api-key {} annotation))) - -(defn annotate - "Creates or updates an annotation. If id is given, updates. If id is - missing, creates a new annotation." - ([user api-key name annotation] - (create-annotation user api-key name annotation)) ([user api-key name id annotation] - (update-annotation user api-key name id annotation))) + (update-annotation user api-key name id annotation nil)) + ([user api-key name id annotation options] + (assert name) + (assert id) + (client/put (uri "annotations" name id) + (merge + options + (request user api-key {} annotation))))) + +(let [warn-on-deprecate (atom true)] + ;; Deprecated due to argument ambiguity. + ;; A future version could rename create-annotation as annotate. + (defn annotate + "Creates or updates an annotation. If id is given, updates. If id is + missing, creates a new annotation." + ([user api-key name annotation] + (create-annotation user api-key name annotation nil)) + ([user api-key name annotation options] + (if (map? annotation) + (create-annotation user api-key name annotation options) + (do + ;; user api-key name id annotation + (update-annotation user api-key name annotation options) + (when @warn-on-deprecate + (reset! warn-on-deprecate false) + (logging/warn + (str "`annotate` called for annotation update is deprecated. " + "Please use update-annotation.")))))))) (defn annotation "Find a particular annotation event. See http://dev.librato.com/v1/get/annotations/:name/events/:id" - [user api-key name id] - (assert name) - (assert id) - (try+ - (-> (client/get (uri "annotations" name id) (request user api-key {})) - :body - json/parse-string - parse-kw) - (catch [:status 404] _ nil))) + ([user api-key name id] + (annotation user api-key name id nil)) + ([user api-key name id options] + (assert name) + (assert id) + (try+ + (-> (client/get (uri "annotations" name id) + (merge + options + (request user api-key {}))) + :body + json/parse-string + parse-kw) + (catch [:status 404] _ nil)))) diff --git a/test/clj_librato/metrics_test.clj b/test/clj_librato/metrics_test.clj index 802316e..8bb2863 100644 --- a/test/clj_librato/metrics_test.clj +++ b/test/clj_librato/metrics_test.clj @@ -59,7 +59,61 @@ (is (= (:measure-time m) (:measure-time gauge))) (is (= (:value m) (double (:value gauge)))) (is (= (:count m) 1))) - ))) + ))) + +(defn test-value + [] + {:name "test.gauge" + :source "clj-librato" + :value (/ (rand-int 1000) (rand-int 1000)) + :measure-time (now)}) + +(defn test-collate-and-guage + [options] + (testing "gauge" + (let [gauge (test-value)] + ;; Submit gauge + (is (collate user apikey [gauge] [] options) "is created") + + (testing "can be queried" + (let [metric (metric user apikey (:name gauge) + {:end-time (:measure-time gauge) + :count 1 + :resolution 1} + options)] + (is (= (:type metric) "gauge")) + (is (= (:name metric) (:name gauge))) + (testing "has a last metric value" + (let [m (-> metric + :measurements + (get (:source gauge)) + (first))] + (is m) + (is (= (:measure-time m) (:measure-time gauge))) + (is (= (:value m) (double (:value gauge)))) + (is (= (:count m) 1))))))))) + +(deftest collate-with-http-options-test + (testing "reject nil names" + (is (thrown? java.lang.AssertionError + (collate user apikey [{:name nil}] [] {}))) + (is (thrown? java.lang.AssertionError + (collate user apikey [] [{:name nil}] {})))) + + (testing "reject nil values" + (is (thrown? java.lang.AssertionError + (collate user apikey [{:name "foo" + :value nil}] [] {}))) + (is (thrown? java.lang.AssertionError + (collate user apikey [] [{:name "foo" + :value nil}] {})))) + + (testing "with no http options" + (test-collate-and-guage nil)) + (testing "with persistent http options" + (let [cm (connection-manager {})] + (is cm "connection manager created") + (test-collate-and-guage {:connection-manager cm})))) (deftest annotate-test (let [name "test.annotations" @@ -82,14 +136,64 @@ (is (= (:end-time e) (:end-time event))) ; Update annotation - (annotate user apikey name (:id res) + (annotate user apikey name (:id res) {:end-time (inc (:end-time event))}) ; Verify update was applied. (is (= (inc (:end-time event)) (:end-time (annotation user apikey name (:id res))))))) +(defn test-annotation + [] + {:title (str "A test event: " (rand 10000000)) + :source "clj-librato" + :description "Testing clj-librato annotations" + :start-time (now) + :end-time (+ 10 (now))}) + +(defn annotation= + [e1 e2] + (let [ks [:title :description :source :start-time :end-time]] + (= (select-keys e1 ks) (select-keys e2 ks)))) + +(defn test-annotate + [options] + (testing "a test annotation" + (let [name "test.annotations" + annot-map (test-annotation)] + + (testing "can be created" + (let [res (annotate user apikey name annot-map options)] + (is res "is created without error") + + (testing "and is queryable" + (let [a (annotation user apikey name (:id res) options)] + (is a "can be queried by returned id") + (is (= res a) "is as returned by annotate") + (is (annotation= a annot-map) "has matching attributes"))) + + (testing "and can be updated" + (update-annotation user apikey name (:id res) + {:end-time (inc (:end-time annot-map))} + options) + (is (= (inc (:end-time annot-map)) + (:end-time (annotation user apikey name (:id res) options))) + "updated attribute matches"))))))) + +(deftest annotate-with-http-options-test + (testing "with no http options" + (test-annotate nil)) + (testing "with persistent http options" + (let [cm (connection-manager {})] + (is cm "connection manager created") + (test-annotate {:connection-manager cm})))) + (deftest annotation-test ; 404s return nil (is (nil? (annotation user apikey "asdilhugflsdbfg" 1234))) (is (nil? (annotation user apikey name 2345235624534)))) + +(deftest annotation-with-http-options-test + ; 404s return nil + (is (nil? (annotation user apikey "asdilhugflsdbfg" 1234 {}))) + (is (nil? (annotation user apikey name 2345235624534 {}))))