├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── src └── inquest │ └── core.clj └── test └── inquest └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /doc 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein test-all 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inquest 2 | 3 | [![Build Status](https://travis-ci.org/weavejester/inquest.svg?branch=master)](https://travis-ci.org/weavejester/inquest) 4 | 5 | Inquest is a library for non-invasive logging and monitoring in 6 | Clojure. 7 | 8 | Logging or monitoring code is typically added inline to an 9 | application's source code. Inquest takes the view that logging should 10 | be more akin to an external debugger. 11 | 12 | Rather than adding logging code inline, you instead tell Inquest which 13 | vars you wish to monitor, and then pass it a reporter function that is 14 | called whenever the var is used. 15 | 16 | 17 | ## Installation 18 | 19 | To install, add the following to your project `:dependencies`: 20 | 21 | [inquest "0.1.0"] 22 | 23 | 24 | ## Documentation 25 | 26 | * [API Docs](https://weavejester.github.io/inquest/inquest.core.html) 27 | 28 | 29 | ## Basic Usage 30 | 31 | Let's say you have some functions you wish to monitor: 32 | 33 | ```clojure 34 | (defn foo [x] (+ x 1)) 35 | (defn bar [x] (- x 1)) 36 | ``` 37 | 38 | The easiest way to monitor these functions is to create an *inquest*: 39 | 40 | ```clojure 41 | (require '[inquest.core :as inq]) 42 | 43 | (def stop-inquest (inq/inquest [#'foo #'bar] prn)) 44 | ``` 45 | 46 | In this case the inquest is monitoring both `foo` and `bar`, and the 47 | reporter it uses is just `prn`. 48 | 49 | If we run one of the monitored functions, we can see the reports being 50 | generated: 51 | 52 | ```clojure 53 | user=> (foo 1) 54 | {:time 296453512642817, :thread 51, :state :enter, :var #'user/foo, :args (1)} 55 | {:time 296453512979429, :thread 51, :state :exit, :var #'user/foo, :return 2} 56 | 2 57 | ``` 58 | 59 | Any exceptions that occur are also reported: 60 | 61 | ```clojure 62 | user=> (foo "1") 63 | {:time 296509361190517, :thread 52, :state :enter, :var #'user/foo, :args ("1")} 64 | {:time 296509361928724, :thread 52, :state :throw, :var #'user/foo, :exception #error {...}} 65 | ``` 66 | 67 | When we want to stop the inquest, we call the function it returned 68 | earlier: 69 | 70 | ```clojure 71 | user=> (stop-inquest) 72 | ``` 73 | 74 | 75 | ### Advanced Usage 76 | 77 | Often you'll want a more sophisticated reporter than `prn`. One 78 | solution is to define a multimethod: 79 | 80 | ```clojure 81 | (defmulti console-reporter 82 | (juxt :state :var)) 83 | 84 | (defmethod console-reporter :default [_] 85 | ; do nothing 86 | ) 87 | 88 | (defmethod console-reporter [:enter #'foo] [{[x] :args}] 89 | (println "Running foo with" x)) 90 | 91 | (defmethod console-reporter [:enter #'bar] [{[x] :args}] 92 | (println "Running bar with" x)) 93 | ``` 94 | 95 | We can then use this multimethod to generate an inquest: 96 | 97 | ```clojure 98 | (def stop-inquest 99 | (let [vars (map second (keys (methods console-reporter)))] 100 | (inq/inquest vars console-reporter))) 101 | ``` 102 | 103 | Note that rather than declare the vars we want twice, we can use the 104 | `methods` function to ask the multimethod which vars it can handle. In 105 | order for this to work, the inquest must be started after the 106 | multimethod is fully defined. 107 | 108 | 109 | ## Caveats 110 | 111 | Inquest uses the `alter-var-root` function to add monitoring to 112 | vars. This function doesn't currently work with protocol methods, so 113 | neither does Inquest. 114 | 115 | 116 | ## License 117 | 118 | Copyright © 2015 James Reeves 119 | 120 | Distributed under the Eclipse Public License either version 1.0 or (at 121 | your option) any later version. 122 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject inquest "0.1.0" 2 | :description "Non-invasive monitoring library" 3 | :url "https://github.com/weavejester/inquest" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [medley "0.6.0"]] 8 | :plugins [[codox "0.8.12"]] 9 | :aliases {"test-all" ["do" 10 | ["test"] 11 | ["with-profile" "+1.6" "test"] 12 | ["with-profile" "+1.7" "test"]]} 13 | :profiles 14 | {:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} 15 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 16 | :dev {:dependencies [[criterium "0.4.3"]]}}) 17 | -------------------------------------------------------------------------------- /src/inquest/core.clj: -------------------------------------------------------------------------------- 1 | (ns inquest.core 2 | "Functions to add monitoring to existing vars." 3 | (:require [medley.core :refer [dissoc-in]])) 4 | 5 | (def ^:private reporters (atom {})) 6 | 7 | (defn- make-report [key map] 8 | (into {:time (System/nanoTime) 9 | :thread (.getId (Thread/currentThread)) 10 | :key key} 11 | map)) 12 | 13 | (defn- dispatch! [reporters report] 14 | (doseq [r reporters] (r report))) 15 | 16 | (defn monitor 17 | "Takes a function and a unique key, and returns a new function that will 18 | report on its operation. A report is a map that contains the following 19 | keys: 20 | 21 | :time - the system time in nanoseconds 22 | :thread - the thread ID 23 | :target - the target being monitored 24 | :state - one of :enter, :exit or :throw 25 | 26 | Additionally there may be the following optional keys: 27 | 28 | :args - the arguments passed to the var (only in the :enter state) 29 | :return - the return value from the var (only in the :exit state) 30 | :exception - the thrown exception (only in the :throw state) 31 | 32 | Reports are sent to reporters that can be registered with the monitor key 33 | using add-reporter." 34 | [func key] 35 | (with-meta 36 | (fn [& args] 37 | (if-let [rs (vals (@reporters key))] 38 | (do (dispatch! rs (make-report key {:state :enter, :args args})) 39 | (try 40 | (let [ret (apply func args)] 41 | (dispatch! rs (make-report key {:state :exit, :return ret})) 42 | ret) 43 | (catch Throwable th 44 | (dispatch! rs (make-report key {:state :throw, :exception th})) 45 | (throw th)))) 46 | (apply func args))) 47 | {::original func 48 | ::key key})) 49 | 50 | (defn unmonitor 51 | "Remove monitoring from a function." 52 | [func] 53 | (-> func meta ::original (or func))) 54 | 55 | (defn add-reporter 56 | "Add a reporter function to the function associated with monitor-key. The 57 | reporter-key should be unique to the reporter." 58 | [monitor-key reporter-key reporter] 59 | (swap! reporters assoc-in [monitor-key reporter-key] reporter)) 60 | 61 | (defn remove-reporter 62 | "Remove a reporter function identified by reporter-key, from the function 63 | associated with monitor-key." 64 | [monitor-key reporter-key] 65 | (swap! reporters dissoc-in [monitor-key reporter-key])) 66 | 67 | (defn inquest 68 | "A convenience function for monitoring many vars with the same reporter 69 | function. The return value is a zero-argument function that, when called, 70 | will remove all of the monitoring added for the inquest. 71 | 72 | See the monitor function for an explanation of the reporter function." 73 | [vars reporter] 74 | (let [key (gensym "inquest-")] 75 | (doseq [v vars] 76 | (alter-var-root v monitor v) 77 | (add-reporter v key reporter)) 78 | (fn [] 79 | (swap! reporters dissoc key) 80 | (doseq [v vars] 81 | (alter-var-root v unmonitor))))) 82 | -------------------------------------------------------------------------------- /test/inquest/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns inquest.core-test 2 | (:require [clojure.test :refer :all] 3 | [inquest.core :refer :all])) 4 | 5 | (defn foo [x] (+ x 1)) 6 | (defn bar [x] (- x 1)) 7 | 8 | (deftest test-monitor 9 | (let [reports (atom []) 10 | foo' (monitor foo #'foo)] 11 | (add-reporter #'foo ::test #(swap! reports conj %)) 12 | (is (= (foo' 1) 2)) 13 | (is (thrown? ClassCastException (foo' "1"))) 14 | (is (= (count @reports) 4)) 15 | (let [r (@reports 0)] 16 | (is (= (:key r) #'foo)) 17 | (is (= (:state r) :enter)) 18 | (is (= (:args r) '(1)))) 19 | (let [r (@reports 1)] 20 | (is (= (:key r) #'foo)) 21 | (is (= (:state r) :exit)) 22 | (is (= (:return r) 2))) 23 | (let [r (@reports 2)] 24 | (is (= (:key r) #'foo)) 25 | (is (= (:state r) :enter)) 26 | (is (= (:args r) '("1")))) 27 | (let [r (@reports 3)] 28 | (is (= (:key r) #'foo)) 29 | (is (= (:state r) :throw)) 30 | (is (= (type (:exception r)) ClassCastException))))) 31 | 32 | (deftest test-unmonitor 33 | (is (= (unmonitor (monitor foo #'foo)) foo))) 34 | 35 | (deftest test-inquest 36 | (let [reports (atom []) 37 | stop (inquest [#'foo #'bar] #(swap! reports conj %))] 38 | (testing "started" 39 | (try 40 | (is (= (foo 1) 2)) 41 | (is (= (bar 2) 1)) 42 | (is (= (count @reports) 4)) 43 | (let [r (@reports 0)] 44 | (is (= (:key r) #'foo)) 45 | (is (= (:state r) :enter)) 46 | (is (= (:args r) '(1)))) 47 | (let [r (@reports 1)] 48 | (is (= (:key r) #'foo)) 49 | (is (= (:state r) :exit)) 50 | (is (= (:return r) 2))) 51 | (let [r (@reports 2)] 52 | (is (= (:key r) #'bar)) 53 | (is (= (:state r) :enter)) 54 | (is (= (:args r) '(2)))) 55 | (let [r (@reports 3)] 56 | (is (= (:key r) #'bar)) 57 | (is (= (:state r) :exit)) 58 | (is (= (:return r) 1))) 59 | (finally 60 | (stop)))) 61 | (testing "stopped" 62 | (is (= (foo 1) 2)) 63 | (is (= (bar 2) 1)) 64 | (is (= (count @reports) 4))))) 65 | --------------------------------------------------------------------------------