diff --git a/README.md b/README.md index 703c9a6..0f8c61c 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,19 @@ Mbean search and execution util. ## Installation -Install `fzf` first: `brew install --HEAD fzf` - -`lein uberjar` the project - -`cd mbeanz && pip install -r requirements.txt` - -## Usage - -### Run the server - -Configure: +latest stable: ``` -export MBEANZ_OBJECT_PATTERN="MyBeanz:*" -export MBEANZ_JMX_REMOTE_HOST="localhost" -export MBEANZ_JMX_REMOTE_PORT=11080 +brew install ojung/mbeanz/mbeanz ``` -Start the server -`java -jar target/uberjar/mbeanz-0.1.0-SNAPSHOT-standalone.jar` +adjust the config to your needs: +``` +vim /usr/local/etc/mbeanz.edn +``` -### Use the client +## Usage -Either put `mbeanz/mbeanz` in your path or use it from the project root. +`mbeanz ` where profile is one of the profiles defined in `/usr/local/etc/mbeanz.edn` ## License diff --git a/mbeanz/mbeanz b/mbeanz/mbeanz index 3509bdd..7980415 100755 --- a/mbeanz/mbeanz +++ b/mbeanz/mbeanz @@ -19,13 +19,13 @@ class colors: ENDC = '\033[0m' BOLD = '\033[1m' -def get_mbeans(): - response = requests.get(API_URL + '/list') +def get_mbeans(profile): + response = requests.get(API_URL + '/' + profile + '/list') mbeans = response.json() return [mbean['bean'] + SEPARATOR + mbean['operation'] for mbean in mbeans] -def select_mbean(): - mbeans = str.join('\n', get_mbeans()) +def select_mbean(profile): + mbeans = str.join('\n', get_mbeans(profile)) fzf = subprocess.Popen('fzf', stdin = subprocess.PIPE, stdout = subprocess.PIPE) mbean_operation = fzf.communicate(mbeans)[0].rstrip('\n').split(SEPARATOR) return tuple(mbean_operation) @@ -43,8 +43,9 @@ def choose_signature(descriptions): chosen = descriptions[int(index)] return tuple([chosen['description'], chosen['signature']]) -def describe_mbean(mbean, operation): - mbean_infos = requests.get(API_URL + '/describe/' + operation, params = {'bean': mbean}).json() +def describe_mbean(profile, mbean, operation): + mbean_infos = requests.get(API_URL + '/' + profile + '/describe/' + operation, + params = {'bean': mbean}).json() length = len(mbean_infos) if (length == 1): only_operation = mbean_infos[0] @@ -60,19 +61,21 @@ def get_arguments(signature): arguments.append(tuple([parameter['type'], user_input])) return arguments -def invoke_operation(mbean, operation, arguments): +def invoke_operation(profile, mbean, operation, arguments): types = [arg[0] for arg in arguments] values = [arg[1] for arg in arguments] query_parameters = {'bean': mbean, 'args': values, 'types': types} - response = requests.get(API_URL + '/invoke/' + operation, params = query_parameters) + response = requests.get(API_URL + '/' + profile + '/invoke/' + operation, + params = query_parameters) return response.json() if __name__ == "__main__": - mbean, operation = select_mbean() - description, signature = describe_mbean(mbean, operation) + profile = sys.argv[1] + mbean, operation = select_mbean(profile) + description, signature = describe_mbean(profile, mbean, operation) print(colors.BOLD + '\n' + description + colors.ENDC, end = '\n\n') arguments = get_arguments(signature) - result = invoke_operation(mbean, operation, arguments) + result = invoke_operation(profile, mbean, operation, arguments) if (result): if ('error' in result): print() diff --git a/src/mbeanz/handler.clj b/src/mbeanz/handler.clj index 55c89df..c17b2c8 100644 --- a/src/mbeanz/handler.clj +++ b/src/mbeanz/handler.clj @@ -15,63 +15,69 @@ (defonce server (atom nil)) -(defonce object-pattern (atom "java.lang:*")) +(defonce config (atom nil)) -(defonce jmx-remote-host (atom "localhost")) +(defn- get-error-response [exception] + (let [{:keys [message class]} (parse-exception exception)] + {:error {:class (str class) :message message}})) -(defonce jmx-remote-port (atom 1080)) +(defn get-connection-map [config-name] + (if-let [{:keys [jmx-remote-host jmx-remote-port]} (config-name @config)] + {:host jmx-remote-host :port jmx-remote-port} + (throw (RuntimeException. (str "no config entry for " config-name))))) -(defn- handle-describe [operation] +(defn get-object-pattern [config-name] + (:object-pattern (config-name @config))) + +(defn- handle-describe [config-name operation] (fn [request] - (jmx/with-connection {:host @jmx-remote-host :port @jmx-remote-port} + (jmx/with-connection (get-connection-map config-name) (let [mbean (get-in request [:params :bean]) op (keyword operation)] (doall (describe mbean op)))))) (defn- try-invoke [mbean operation args types] - (try - (if (string? args) - {:result (invoke mbean (keyword operation) (list types args))} - {:result (apply invoke mbean (keyword operation) (map list types args))}) - (catch Exception exception - (let [{:keys [message class]} (parse-exception exception)] - {:error {:class (str class) :message message}})))) + (if (string? args) + {:result (invoke mbean (keyword operation) (list types args))} + {:result (apply invoke mbean (keyword operation) (map list types args))})) -(defn- handle-invoke [operation] +(defn- handle-invoke [config-name operation] (fn [request] - (jmx/with-connection {:host @jmx-remote-host :port @jmx-remote-port} + (jmx/with-connection (get-connection-map config-name) (let [mbean (get-in request [:params :bean]) args (get-in request [:params :args]) types (get-in request [:params :types])] {:body (try-invoke mbean operation args types)})))) -(defn- handle-list-beans [] +(defn- handle-list-beans [config-name] (fn [request] - (jmx/with-connection {:host @jmx-remote-host :port @jmx-remote-port} - (doall (list-beans @object-pattern))))) + (jmx/with-connection (get-connection-map config-name) + (doall (list-beans (get-object-pattern config-name)))))) (defroutes app-routes - (GET "/list" [] (handle-list-beans)) - (GET "/describe/:operation" [operation] (handle-describe operation)) - (GET "/invoke/:operation" [operation] (handle-invoke operation)) + (GET "/:config/list" [config] (handle-list-beans (keyword config))) + (GET "/:config/describe/:operation" + [config operation] + (handle-describe (keyword config) operation)) + (GET "/:config/invoke/:operation" [config operation] (handle-invoke (keyword config) operation)) (route/not-found "Not Found")) +(defn wrap-exception-handling [next-handler] + (fn [request] + (try + (next-handler request) + (catch Exception exception {:body (get-error-response exception)})))) + (def app (-> app-routes (wrap-reload) + (wrap-exception-handling) (wrap-json-response) (wrap-defaults api-defaults))) -(defn- reset-if-set [config key atom] - (when-let [value (key config)] - (reset! atom value))) - (defn -main [& args] - (when-let [config-file-path (first args)] - (let [config (edn/read-string (slurp config-file-path))] - (reset-if-set config :object-pattern object-pattern) - (reset-if-set config :jmx-remote-host jmx-remote-host) - (reset-if-set config :jmx-remote-port jmx-remote-port))) + (when-let [config-path (first args)] + (reset! config (merge @config (edn/read-string (slurp config-path))))) (reset! server (run-server app {:port 0})) (with-open [my-writer (writer "/var/tmp/mbeanz.port")] (.write my-writer (str (:local-port (meta @server)))))) diff --git a/test.edn b/test.edn new file mode 100644 index 0000000..94217c3 --- /dev/null +++ b/test.edn @@ -0,0 +1,6 @@ +{:local {:object-pattern "Adscale:*" + :jmx-remote-host "localhost" + :jmx-remote-port 11080} + :kafka {:object-pattern "kafka:*" + :jmx-remote-host "localhost" + :jmx-remote-port 11080}} diff --git a/test/mbeanz/handler_test.clj b/test/mbeanz/handler_test.clj index 0214b40..ab72b73 100644 --- a/test/mbeanz/handler_test.clj +++ b/test/mbeanz/handler_test.clj @@ -4,16 +4,24 @@ [ring.mock.request :as mock] [clojure.data.json :as json])) -(use-fixtures :once (fn [do-tests] (reset! object-pattern "java.lang:*") (do-tests))) +(use-fixtures :each (fn [do-tests] + (reset! config {:default {:object-pattern "java.lang:*" + :jmx-remote-host "localhost" + :jmx-remote-port 11080}}) + (do-tests))) (defn- request [url params cb] (let [mock-request (mock/query-string (mock/request :get url) params) response (app mock-request)] (cb response))) +(deftest unit-tests + (testing "get-connection-map" + (is (= (get-connection-map :default) {:host "localhost" :port 11080})))) + (deftest list-route (testing "list route" - (request "/list" {} + (request "/default/list" {} #(is (= (json/read-str (:body %)) [{"bean" "java.lang:type=Memory", "operation" "gc"} {"bean" "java.lang:type=MemoryPool,name=Code Cache", "operation" "resetPeakUsage"} @@ -34,12 +42,12 @@ (deftest describe-route (testing "operation with single signature" - (request "/describe/gc" {"bean" "java.lang:type=Memory"} + (request "/default/describe/gc" {"bean" "java.lang:type=Memory"} #(is (= (json/read-str (:body %)) [{"name" "gc", "description" "gc", "signature" []}])))) (testing "operation with multiple signatures" - (request "/describe/getThreadCpuTime" {"bean" "java.lang:type=Threading"} + (request "/default/describe/getThreadCpuTime" {"bean" "java.lang:type=Threading"} #(is (= (json/read-str (:body %)) [{"name" "getThreadCpuTime" "description" "getThreadCpuTime" @@ -50,23 +58,61 @@ (testing "inexistent operation" ;TODO: Make api return error (404): - (request "/describe/inexistent" {"bean" "java.lang:type=Threading"} + (request "/default/describe/inexistent" {"bean" "java.lang:type=Threading"} #(is (= (json/read-str (:body %)) []))))) (deftest invoke-route (testing "operation without arguments" - (request "/invoke/gc" {"bean" "java.lang:type=Memory"} + (request "/default/invoke/gc" {"bean" "java.lang:type=Memory"} #(is (= (json/read-str (:body %)) {"result" nil})))) (testing "operation with arguments invoked with wrong signature" - (request "/invoke/getThreadInfo" {"bean" "java.lang:type=Threading"} + (request "/default/invoke/getThreadInfo" {"bean" "java.lang:type=Threading"} #(is (= (json/read-str (:body %)) {"error" {"class" "class javax.management.ReflectionException" "message" "Operation getThreadInfo exists but not with this signature: ()"}})))) (testing "operation with single argument invoked with correct signature" ;TODO: Setup mock mbeans - (request "/invoke/getThreadCpuTime" + (request "/default/invoke/getThreadCpuTime" ;Hoping this thread id doesn't exist {"bean" "java.lang:type=Threading", "args" "99999999999", "types" "long"} #(is (= (json/read-str (:body %)) {"result" -1}))))) + +(deftest multiple-configs + (testing "config for key not found" + (request "/noooonexistant/list" + {} + #(is (= (json/read-str (:body %)) + {"error" {"class" "class java.lang.RuntimeException" + "message" "no config entry for :noooonexistant"}})))) + (testing "different output for different configs" + (reset! config {:lang {:object-pattern "java.lang:*" + :jmx-remote-host "localhost" + :jmx-remote-port 11080} + :logging {:object-pattern "java.util.logging:*" + :jmx-remote-host "localhost" + :jmx-remote-port 11080}}) + (request "/lang/list" {} + #(is (= (json/read-str (:body %)) + [{"bean" "java.lang:type=Memory", "operation" "gc"} + {"bean" "java.lang:type=MemoryPool,name=Code Cache", "operation" "resetPeakUsage"} + {"bean" "java.lang:type=MemoryPool,name=Compressed Class Space" + "operation" "resetPeakUsage"} + {"bean" "java.lang:type=MemoryPool,name=Metaspace", "operation" "resetPeakUsage"} + {"bean" "java.lang:type=MemoryPool,name=PS Eden Space", "operation" "resetPeakUsage"} + {"bean" "java.lang:type=MemoryPool,name=PS Old Gen", "operation" "resetPeakUsage"} + {"bean" "java.lang:type=MemoryPool,name=PS Survivor Space", "operation" "resetPeakUsage"} + {"bean" "java.lang:type=Threading", "operation" "dumpAllThreads"} + {"bean" "java.lang:type=Threading", "operation" "findDeadlockedThreads"} + {"bean" "java.lang:type=Threading", "operation" "findMonitorDeadlockedThreads"} + {"bean" "java.lang:type=Threading", "operation" "getThreadAllocatedBytes"} + {"bean" "java.lang:type=Threading", "operation" "getThreadCpuTime"} + {"bean" "java.lang:type=Threading", "operation" "getThreadInfo"} + {"bean" "java.lang:type=Threading", "operation" "getThreadUserTime"} + {"bean" "java.lang:type=Threading", "operation" "resetPeakThreadCount"}]))) + (request "/logging/list" {} + #(is (= (json/read-str (:body %)) + [{"bean" "java.util.logging:type=Logging", "operation" "getLoggerLevel"} + {"bean" "java.util.logging:type=Logging", "operation" "getParentLoggerName"} + {"bean" "java.util.logging:type=Logging", "operation" "setLoggerLevel"}])))))