diff options
-rw-r--r-- | build.xml | 5 | ||||
-rw-r--r-- | config/jmx.policy | 3 | ||||
-rw-r--r-- | src/clojure/contrib/jmx.clj | 124 | ||||
-rw-r--r-- | src/clojure/contrib/jmx/Bean.clj | 35 | ||||
-rw-r--r-- | src/clojure/contrib/jmx/client.clj | 95 | ||||
-rw-r--r-- | src/clojure/contrib/jmx/data.clj | 101 | ||||
-rw-r--r-- | src/clojure/contrib/jmx/server.clj | 18 | ||||
-rw-r--r-- | src/clojure/contrib/test_contrib.clj | 2 | ||||
-rw-r--r-- | src/clojure/contrib/test_contrib/test_jmx.clj | 165 |
9 files changed, 546 insertions, 2 deletions
@@ -34,7 +34,7 @@ <target name="test_contrib" description="Run contrib tests" if="hasclojure"> - <java classname="clojure.main"> + <java classname="clojure.main" fork="true"> <classpath> <path location="${build}"/> <path location="${src}"/> @@ -42,6 +42,7 @@ </classpath> <arg value="-e"/> <arg value="(require '(clojure.contrib [test-contrib :as main])) (main/run)"/> + <jvmarg value="-Djava.security.policy=config/jmx.policy"/> </java> </target> @@ -85,6 +86,7 @@ <arg value="clojure.contrib.pprint.PrettyWriter"/> <arg value="clojure.contrib.fnmap.PersistentFnMap"/> <arg value="clojure.contrib.condition.Condition"/> + <arg value="clojure.contrib.jmx.Bean"/> </java> </target> @@ -137,6 +139,7 @@ <arg value="clojure.contrib.java-utils"/> <arg value="clojure.contrib.javadoc.browse"/> <arg value="clojure.contrib.javadoc.browse-ui"/> + <arg value="clojure.contrib.jmx"/> <arg value="clojure.contrib.json.read"/> <arg value="clojure.contrib.json.write"/> <arg value="clojure.contrib.lazy-seqs"/> diff --git a/config/jmx.policy b/config/jmx.policy new file mode 100644 index 00000000..a8c7b106 --- /dev/null +++ b/config/jmx.policy @@ -0,0 +1,3 @@ +grant codebase "file:classes"{ + permission javax.management.MBeanTrustPermission "register"; +};
\ No newline at end of file diff --git a/src/clojure/contrib/jmx.clj b/src/clojure/contrib/jmx.clj new file mode 100644 index 00000000..44b89f99 --- /dev/null +++ b/src/clojure/contrib/jmx.clj @@ -0,0 +1,124 @@ +;; JMX support for Clojure + +;; by Stuart Halloway + +;; Copyright (c) Stuart Halloway, 2009. All rights reserved. The use +;; and distribution terms for this software are covered by the Eclipse +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this +;; distribution. By using this software in any fashion, you are +;; agreeing to be bound by the terms of this license. You must not +;; remove this notice, or any other, from this software. + +;; READ THESE CAVEATS: +;; Requires post-Clojure 1.0 git edge for clojure.test, clojure.backtrace. +;; This is prerelease. +;; This API will change. +;; A few features currently require Java 6 or later. +;; Send reports to stu@thinkrelevance.com. + +;; Usage +;; (require '[clojure.contrib.jmx :as jmx]) + +;; What beans do I have? +;; +;; (jmx/mbean-names "*:*") +;; -> #<HashSet [java.lang:type=MemoryPool,name=CMS Old Gen, +;; java.lang:type=Memory, ...] + +;; What attributes does a bean have? +;; +;; (jmx/attribute-names "java.lang:type=Memory") +;; -> (:Verbose :ObjectPendingFinalizationCount +;; :HeapMemoryUsage :NonHeapMemoryUsage) + +;; What is the value of an attribute? +;; +;; (jmx/read "java.lang:type=Memory" :ObjectPendingFinalizationCount) +;; -> 0 + +;; Can't I just have *all* the attributes in a Clojure map? +;; +;; (jmx/mbean "java.lang:type=Memory") +;; -> {:NonHeapMemoryUsage +;; {:used 16674024, :max 138412032, :init 24317952, :committed 24317952}, +;; :HeapMemoryUsage +;; {:used 18619064, :max 85393408, :init 0, :committed 83230720}, +;; :ObjectPendingFinalizationCount 0, +;; :Verbose false} + +;; Can I find and invoke an operation? +;; +;; (jmx/operation-names "java.lang:type=Memory") +;; -> (:gc) +;; (jmx/invoke "java.lang:type=Memory" :gc) +;; -> nil + +;; What about some other process? Just run *any* of the above code +;; inside a with-connection: +;; +;; (jmx/with-connection {:host "localhost", :port 3000} +;; (jmx/mbean "java.lang:type=Memory")) +;; -> {:ObjectPendingFinalizationCount 0, +;; :HeapMemoryUsage ... etc.} + +;; Can I serve my own beans? Sure, just drop a Clojure ref +;; into an instance of clojure.contrib.jmx.Bean, and the bean +;; will expose read-only attributes for every key/value pair +;; in the ref: +;; +;; (jmx/register-mbean +;; (Bean. +;; (ref {:string-attribute "a-string"})) +;; "my.namespace:name=Value") + +(ns clojure.contrib.jmx + (:refer-clojure :exclude [read]) + (:use clojure.contrib.def + [clojure.contrib.java-utils :only [as-str]] + [clojure.stacktrace :only (root-cause)] + [clojure.walk :only [postwalk]]) + (:import [clojure.lang Associative] + java.lang.management.ManagementFactory + [javax.management Attribute DynamicMBean MBeanInfo ObjectName RuntimeMBeanException MBeanAttributeInfo] + [javax.management.remote JMXConnectorFactory JMXServiceURL])) + +(defvar *connection* (ManagementFactory/getPlatformMBeanServer) + "The connection to be used for JMX ops. Defaults to the local process.") + +(load "jmx/data") +(load "jmx/client") +(load "jmx/server") + +(defn mbean-names + "Finds all MBeans matching a name on the current *connection*." + [n] + (.queryNames *connection* (as-object-name n) nil)) + +(defn attribute-names + "All attribute names available on an MBean." + [n] + (doall (map #(-> % .getName keyword) + (.getAttributes (mbean-info n))))) + +(defn operation-names + "All operation names available on an MBean." + [n] + (doall (map #(-> % .getName keyword) (operations n)))) + +(defn invoke [n op & args] + (if ( seq args) + (.invoke *connection* (as-object-name n) (as-str op) + (into-array args) + (into-array String (op-param-types n op))) + (.invoke *connection* (as-object-name n) (as-str op) + nil nil))) + +(defn mbean + "Like clojure.core/bean, but for JMX beans. Returns a read-only map of + a JMX bean's attributes. If an attribute it not supported, value is + set to the exception thrown." + [n] + (into {} (map (fn [attr-name] [(keyword attr-name) (read-supported n attr-name)]) + (attribute-names n)))) + diff --git a/src/clojure/contrib/jmx/Bean.clj b/src/clojure/contrib/jmx/Bean.clj new file mode 100644 index 00000000..1ee8e950 --- /dev/null +++ b/src/clojure/contrib/jmx/Bean.clj @@ -0,0 +1,35 @@ +(ns clojure.contrib.jmx.Bean + (:gen-class + :implements [javax.management.DynamicMBean] + :init init + :state state + :constructors {[Object] []}) + (:require [clojure.contrib.jmx :as jmx]) + (:import [javax.management DynamicMBean MBeanInfo AttributeList])) + +(defn -init [derefable] + [[] derefable]) + +; TODO: rest of the arguments, as needed +(defn generate-mbean-info [clj-bean] + (MBeanInfo. (.. clj-bean getClass getName) ; class name + "Clojure Dynamic MBean" ; description + (jmx/map->attribute-infos @(.state clj-bean)) ; attributes + nil ; constructors + nil ; operations + nil)) ; notifications + +(defn -getMBeanInfo + [this] + (generate-mbean-info this)) + +(defn -getAttribute + [this attr] + ((.state this) (keyword attr))) + +(defn -getAttributes + [this attrs] + (let [result (AttributeList.)] + (doseq [attr attrs] + (.add result (.getAttribute this attr))) + result))
\ No newline at end of file diff --git a/src/clojure/contrib/jmx/client.clj b/src/clojure/contrib/jmx/client.clj new file mode 100644 index 00000000..7af947d1 --- /dev/null +++ b/src/clojure/contrib/jmx/client.clj @@ -0,0 +1,95 @@ +;; JMX client APIs for Clojure +;; docs in clojure/contrib/jmx.clj!! + +;; by Stuart Halloway + +;; Copyright (c) Stuart Halloway, 2009. All rights reserved. The use +;; and distribution terms for this software are covered by the Eclipse +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this +;; distribution. By using this software in any fashion, you are +;; agreeing to be bound by the terms of this license. You must not +;; remove this notice, or any other, from this software. + + +(in-ns 'clojure.contrib.jmx) + +; TODO: needs an integration test +; TODO: why full package needed for JMXConnectorFactory? +(defmacro with-connection + "Execute body with JMX connection specified by opts (:port)." + [opts & body] + `(with-open [connector# (javax.management.remote.JMXConnectorFactory/connect + (JMXServiceURL. (jmx-url ~opts)) {})] + (binding [*connection* (.getMBeanServerConnection connector#)] + ~@body))) + +(defn mbean-info [n] + (.getMBeanInfo *connection* (as-object-name n))) + +(defn raw-read + "Read an mbean property. Returns low-level Java object model for composites, tabulars, etc. + Most callers should use read." + [n attr] + (.getAttribute *connection* (as-object-name n) (as-str attr))) + +(defvar read + (comp jmx->clj raw-read) + "Read an mbean property.") + +(defvar read-exceptions + [UnsupportedOperationException + InternalError + java.io.NotSerializableException + java.lang.ClassNotFoundException + javax.management.AttributeNotFoundException] + "Exceptions that might be thrown if you try to read an unsupported attribute. + by testing agains jconsole and Tomcat. This is dreadful and ad-hoc but I did not + want to swallow all exceptions.") + +(defn read-supported + "Calls read to read an mbean property, *returning* unsupported operation exceptions instead of throwing them. + Used to keep mbean from blowing up. Note that some terribly-behaved mbeans use java.lang.InternalError to + indicate an unsupported operation!" + [n attr] + (try + (read n attr) + (catch Throwable t + (let [cause (root-cause t)] + (if (some #(instance? % cause) read-exceptions) + cause + (throw t)))))) + +(defn write! [n attr value] + (.setAttribute + *connection* + (as-object-name n) + (Attribute. (as-str attr) value))) + +(defn attribute-info + "Get the MBeanAttributeInfo for an attribute" + [object-name attr-name] + (filter #(= (as-str attr-name) (.getName %)) + (.getAttributes (mbean-info object-name)))) + +(defn readable? + "Is attribute readable?" + [n attr] + (.isReadable () (mbean-info n))) + +(defn operations + "All oeprations available on an MBean." + [n] + (.getOperations (mbean-info n))) + +(defn operation + "The MBeanOperationInfo for operation op on mbean n. Used for invoke." + [n op] + (first (filter #(= (-> % .getName keyword) op) (operations n)))) + +(defn op-param-types + "The parameter types (as class name strings) for operation op on n. Used for invoke." + [n op] + (map #(-> % .getType) (.getSignature (operation n op)))) + + diff --git a/src/clojure/contrib/jmx/data.clj b/src/clojure/contrib/jmx/data.clj new file mode 100644 index 00000000..39f6588e --- /dev/null +++ b/src/clojure/contrib/jmx/data.clj @@ -0,0 +1,101 @@ +;; Conversions between JMX data structures and idiomatic Clojure +;; docs in clojure/contrib/jmx.clj!! + +;; by Stuart Halloway + +;; Copyright (c) Stuart Halloway, 2009. All rights reserved. The use +;; and distribution terms for this software are covered by the Eclipse +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this +;; distribution. By using this software in any fashion, you are +;; agreeing to be bound by the terms of this license. You must not +;; remove this notice, or any other, from this software. + + +(in-ns 'clojure.contrib.jmx) + +(declare jmx->clj) + +(defn jmx-url + "Build a JMX URL from options." + ([] (jmx-url {})) + ([overrides] + (let [opts (merge {:host "localhost", :port "3000"} overrides)] + (format "service:jmx:rmi:///jndi/rmi://%s:%s/jmxrmi" (opts :host) (opts :port))))) + +(defmulti as-object-name + "Interpret an object as a JMX ObjectName" + class) +(defmethod as-object-name String [n] (ObjectName. n)) +(defmethod as-object-name ObjectName [n] n) + +(defn composite-data->map [cd] + (into {} + (map (fn [attr] [(keyword attr) (jmx->clj (.get cd attr))]) + (.. cd getCompositeType keySet)))) + +(defn maybe-keywordize + "Convert a string key to a keyword, leaving other types alone. Used to + simplify keys in the tabular data API." + [s] + (if (string? s) (keyword s) s)) + +(defn maybe-atomize + "Convert a list of length 1 into its contents, leaving other things alone. + Used to simplify keys in the tabular data API." + [k] + (if (and (instance? java.util.List k) + (= 1 (count k))) + (first k) + k)) + +(defvar simplify-tabular-data-key + (comp maybe-keywordize maybe-atomize)) + +(defn tabular-data->map [td] + (into {} + ; the need for into-array here was a surprise, and may not + ; work for all examples. Are keys always arrays? + (map (fn [k] + [(simplify-tabular-data-key k) (jmx->clj (.get td (into-array k)))]) + (.keySet td)))) + +(defmulti jmx->clj + "Coerce JMX data structures into Clojure data" + (fn [x] + (cond + (instance? javax.management.openmbean.CompositeData x) :composite + (instance? javax.management.openmbean.TabularData x) :tabular + (instance? clojure.lang.Associative x) :map + :default :default))) +(defmethod jmx->clj :composite [c] (composite-data->map c)) +(defmethod jmx->clj :tabular [t] (tabular-data->map t)) +(defmethod jmx->clj :map [m] (into {} (zipmap (keys m) (map jmx->clj (vals m))))) +(defmethod jmx->clj :default [obj] obj) + +(def guess-attribute-map + {"java.lang.Integer" "int" + "java.lang.Boolean" "boolean" + "java.lang.Long" "long" + }) + +(defn guess-attribute-typename + "Guess the attribute typename for MBeanAttributeInfo based on the attribute value." + [value] + (let [classname (.getName (class value))] + (get guess-attribute-map classname classname))) + +(defn build-attribute-info + "Construct an MBeanAttributeInfo. Normally called with a key/value pair from a Clojure map." + ([attr-name attr-value] + (build-attribute-info + (as-str attr-name) + (guess-attribute-typename attr-value) + (as-str attr-name) true false false)) + ([name type desc readable? writable? is?] (MBeanAttributeInfo. name type desc readable? writable? is? ))) + +(defn map->attribute-infos + "Construct an MBeanAttributeInfo[] from a Clojure associative." + [attr-map] + (into-array (map (fn [[attr-name value]] (build-attribute-info attr-name value)) + attr-map))) diff --git a/src/clojure/contrib/jmx/server.clj b/src/clojure/contrib/jmx/server.clj new file mode 100644 index 00000000..c92fcf81 --- /dev/null +++ b/src/clojure/contrib/jmx/server.clj @@ -0,0 +1,18 @@ +;; JMX server APIs for Clojure +;; docs in clojure/contrib/jmx.clj!! + +;; by Stuart Halloway + +;; Copyright (c) Stuart Halloway, 2009. All rights reserved. The use +;; and distribution terms for this software are covered by the Eclipse +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this +;; distribution. By using this software in any fashion, you are +;; agreeing to be bound by the terms of this license. You must not +;; remove this notice, or any other, from this software. + +(in-ns 'clojure.contrib.jmx) + +(defn register-mbean [mbean mbean-name] + (.registerMBean *connection* mbean (as-object-name mbean-name))) + diff --git a/src/clojure/contrib/test_contrib.clj b/src/clojure/contrib/test_contrib.clj index 59859e1f..17f3e0e9 100644 --- a/src/clojure/contrib/test_contrib.clj +++ b/src/clojure/contrib/test_contrib.clj @@ -20,7 +20,7 @@ [:complex-numbers :fnmap :macro-utils :monads :pprint.pretty :pprint.cl-format :str-utils :shell-out :test-graph :test-dataflow :test-java-utils :test-lazy-seqs - :test-trace]) + :test-trace :test-jmx]) (def test-namespaces (map #(symbol (str "clojure.contrib.test-contrib." (name %))) diff --git a/src/clojure/contrib/test_contrib/test_jmx.clj b/src/clojure/contrib/test_contrib/test_jmx.clj new file mode 100644 index 00000000..a4013d22 --- /dev/null +++ b/src/clojure/contrib/test_contrib/test_jmx.clj @@ -0,0 +1,165 @@ +;; Tests for JMX support for Clojure (see also clojure/contrib/jmx.clj) + +;; by Stuart Halloway + +;; Copyright (c) Stuart Halloway, 2009. All rights reserved. The use +;; and distribution terms for this software are covered by the Eclipse +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this +;; distribution. By using this software in any fashion, you are +;; agreeing to be bound by the terms of this license. You must not +;; remove this notice, or any other, from this software. + +(ns clojure.contrib.test-contrib.test-jmx + (:import javax.management.openmbean.CompositeDataSupport + [javax.management MBeanAttributeInfo AttributeList] + [java.util.logging LogManager Logger] + clojure.contrib.jmx.Bean) + (:use clojure.test) + (:require [clojure.contrib [jmx :as jmx]])) + + +(defn =set [a b] + (= (set a) (set b))) + +(deftest finding-mbeans + (testing "as-object-name" + (are [cname object-name] + (= cname (.getCanonicalName object-name)) + "java.lang:type=Memory" (jmx/as-object-name "java.lang:type=Memory"))) + (testing "mbean-names" + (are [cnames object-name] + (= cnames (map #(.getCanonicalName %) object-name)) + ["java.lang:type=Memory"] (jmx/mbean-names "java.lang:type=Memory")))) + +; don't know which attributes are common on all JVM platforms. May +; need to change expectations. +(deftest reflecting-on-capabilities + (are [attr-list mbean-name] + (= (set attr-list) (set (jmx/attribute-names mbean-name))) + [:Verbose :ObjectPendingFinalizationCount :HeapMemoryUsage :NonHeapMemoryUsage] "java.lang:type=Memory") + (are [a b] + (= (set a) (set b)) + [:gc] (jmx/operation-names "java.lang:type=Memory"))) + +(deftest raw-reading-attributes + (let [mem "java.lang:type=Memory" + log "java.util.logging:type=Logging"] + (testing "simple scalar attributes" + (are [a b] (= a b) + false (jmx/raw-read mem :Verbose)) + (are [type attr] (instance? type attr) + Integer (jmx/raw-read mem :ObjectPendingFinalizationCount))))) + +(deftest reading-attributes + (testing "simple scalar attributes" + (are [type attr] (instance? type attr) + Integer (jmx/read "java.lang:type=Memory" :ObjectPendingFinalizationCount))) + (testing "composite attributes" + (are [ks attr] (=set ks (keys attr)) + [:used :max :init :committed] (jmx/read "java.lang:type=Memory" :HeapMemoryUsage))) + (testing "tabular attributes" + (is (map? (jmx/read "java.lang:type=Runtime" :SystemProperties))))) + +(deftest mbean-from-oname + (are [oname key-names] + (= (set key-names) (set (keys (jmx/mbean oname)))) + "java.lang:type=Memory" [:Verbose :ObjectPendingFinalizationCount :HeapMemoryUsage :NonHeapMemoryUsage])) + +(deftest writing-attributes + (let [mem "java.lang:type=Memory"] + (jmx/write! mem :Verbose true) + (is (true? (jmx/raw-read mem :Verbose))) + (jmx/write! mem :Verbose false))) + +(deftest test-invoke-operations + (testing "without arguments" + (jmx/invoke "java.lang:type=Memory" :gc)) + (testing "with arguments" + (.addLogger (LogManager/getLogManager) (Logger/getLogger "clojure.contrib.test_contrib.test_jmx")) + (jmx/invoke "java.util.logging:type=Logging" :setLoggerLevel "clojure.contrib.test_contrib.test_jmx" "WARNING"))) + +(deftest test-jmx->clj + (testing "it works recursively on maps" + (let [some-map {:foo (jmx/raw-read "java.lang:type=Memory" :HeapMemoryUsage)}] + (is (map? (:foo (jmx/jmx->clj some-map)))))) + (testing "it leaves everything else untouched" + (is (= "foo" (jmx/jmx->clj "foo"))))) + + +(deftest test-composite-data->map + (let [data (jmx/raw-read "java.lang:type=Memory" :HeapMemoryUsage) + prox (jmx/composite-data->map data)] + (testing "returns a map with keyword keys" + (is (= (set [:committed :init :max :used]) (set (keys prox))))))) + +(deftest test-tabular-data->map + (let [raw-props (jmx/raw-read "java.lang:type=Runtime" :SystemProperties) + props (jmx/tabular-data->map raw-props)] + (are [k] (contains? props k) + :java.class.path + :path.separator))) + +(deftest test-creating-attribute-infos + (let [infos (jmx/map->attribute-infos [[:a 1] [:b 2]]) + info (first infos)] + (testing "generates the right class" + (is (= (class (into-array MBeanAttributeInfo [])) (class infos)))) + (testing "generates the right instance data" + (are [result expr] (= result expr) + "a" (.getName info) + "a" (.getDescription info))))) + +(deftest various-beans-are-readable + (testing "that all java.lang beans can be read without error" + (doseq [mb (jmx/mbean-names "*:*")] + (jmx/mbean mb)))) + +(deftest test-jmx-url + (testing "creates default url" + (is (= "service:jmx:rmi:///jndi/rmi://localhost:3000/jmxrmi" (jmx/jmx-url)))) + (testing "creates custom url" + (is (= "service:jmx:rmi:///jndi/rmi://example.com:4000/jmxrmi" (jmx/jmx-url {:host "example.com" :port 4000}))))) + +;; ---------------------------------------------------------------------- +;; tests for clojure.contrib.jmx.Bean. + +(deftest dynamic-mbean-from-compiled-class + (let [mbean-name "clojure.contrib.test_contrib.test_jmx:name=Foo"] + (jmx/register-mbean + (Bean. + (ref {:string-attribute "a-string"})) + mbean-name) + (are [result expr] (= result expr) + "a-string" (jmx/read mbean-name :string-attribute) + {:string-attribute "a-string"} (jmx/mbean mbean-name) + ))) + +(deftest test-getAttribute + (let [state (ref {:a 1 :b 2}) + bean (Bean. state)] + (testing "accessing values" + (are [result expr] (= result expr) + 1 (.getAttribute bean "a"))))) + +(deftest test-bean-info + (let [state (ref {:a 1 :b 2}) + bean (Bean. state) + info (.getMBeanInfo bean)] + (testing "accessing info" + (are [result expr] (= result expr) + "clojure.contrib.jmx.Bean" (.getClassName info))))) + +(deftest test-getAttributes + (let [bean (Bean. (ref {:r 5 :d 4})) + atts (.getAttributes bean (into-array ["r" "d"]))] + (are [x y] (= x y) + AttributeList (class atts) + [5 4] (seq atts)))) + +(deftest test-guess-attribute-typename + (are [x y] (= x (jmx/guess-attribute-typename y)) + "int" 10 + "boolean" false + "java.lang.String" "foo" + "long" (long 10)))
\ No newline at end of file |