Skip to content

Commit

Permalink
Allow multiple profiles in config
Browse files Browse the repository at this point in the history
Par example:
```
{:local {:object-pattern "MyMbeanz:*"
         :jmx-remote-host "localhost"
         :jmx-remote-port 11080}
 :prod {:object-pattern "MyMbeanz:*"
        :jmx-remote-host "255.255.255.255"
        :jmx-remote-port 11080}}
```

The api can then be used like this:
```
$ cat /var/tmp/mbeanz.port
50123
$ curl localhost:50123/local/list
[{"bean": "SomeMbean", "operation": "someLocalOperation"}, ...]
$ curl localhost:50123/prod/list
[{"bean": "SomeMbean", "operation": "someLocalOperation"}, ...]
```
  • Loading branch information
Oskar Jung committed Oct 1, 2015
1 parent e508419 commit 1a66070
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 60 deletions.
26 changes: 8 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.conf
```

### Use the client
## Usage

Either put `mbeanz/mbeanz` in your path or use it from the project root.
`mbeanz <profile>` where profile is one of the profiles defined in `/usr/local/etc/mbeanz.conf`

## License

Expand Down
25 changes: 14 additions & 11 deletions mbeanz/mbeanz
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]
Expand All @@ -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()
Expand Down
58 changes: 35 additions & 23 deletions src/mbeanz/handler.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,69 @@

(defonce server (atom nil))

(defonce config (atom {:object-pattern "java.lang:*"
:jmx-remote-host "localhost"
:jmx-remote-port 11080}))
(defonce config (atom nil))

(defn- get-connection-map []
{:host (:jmx-remote-host @config) :port (:jmx-remote-port @config)})
(defn- get-error-response [exception]
(let [{:keys [message class]} (parse-exception exception)]
{:error {:class (str class) :message message}}))

(defn- handle-describe [operation]
(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 get-object-pattern [config-name]
(:object-pattern (config-name @config)))

(defn- handle-describe [config-name operation]
(fn [request]
(jmx/with-connection (get-connection-map)
(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 (get-connection-map)
(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 (get-connection-map)
(doall (list-beans (:object-pattern @config))))))
(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 -main [& args]
(when-let [config-path (first args)]
(reset! config (edn/read-string (slurp config-path))))
(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))))))
6 changes: 6 additions & 0 deletions test.edn
Original file line number Diff line number Diff line change
@@ -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}}
62 changes: 54 additions & 8 deletions test/mbeanz/handler_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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"
Expand All @@ -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"}])))))

0 comments on commit 1a66070

Please sign in to comment.