Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

clojure.contrib.jmx

  • Loading branch information...
commit f92114c9cce5e892765549cdb0df640dc3ab52a5 1 parent 2d0079c
@stuarthalloway stuarthalloway authored
View
5 build.xml
@@ -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"/>
View
3  config/jmx.policy
@@ -0,0 +1,3 @@
+grant codebase "file:classes"{
+ permission javax.management.MBeanTrustPermission "register";
+};
View
124 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))))
+
View
35 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))
View
95 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))))
+
+
View
101 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)))
View
18 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)))
+
View
2  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 %)))
View
165 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)))
Please sign in to comment.
Something went wrong with that request. Please try again.