├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── lib └── collectd-api.jar ├── pom.xml └── src └── main └── java └── com └── e_gineering └── collectd ├── Attribute.java ├── AttributePermutation.java ├── Connection.java ├── FastJMX.java ├── ReadCycleResult.java ├── SelfTuningCollectionExecutor.java ├── SynchronousConnectorAdapter.java └── logging └── CollectdLogHandler.java /.gitignore: -------------------------------------------------------------------------------- 1 | #eclipse files 2 | .settings 3 | .classpath 4 | .project 5 | 6 | *.class 7 | 8 | # Mobile Tools for Java (J2ME) 9 | .mtj.tmp/ 10 | 11 | # Package Files # 12 | *.jar 13 | *.war 14 | *.ear 15 | 16 | # Tools, IDEs, desktops 17 | *.iml 18 | .DS_Store 19 | .idea 20 | target 21 | nb*.xml 22 | 23 | # Include the stuff in /lib 24 | !lib/** 25 | dependency-reduced-pom.xml 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | script: mvn verify 4 | 5 | jdk: 6 | - openjdk6 7 | - oraclejdk7 8 | - oraclejdk8 9 | 10 | cache: 11 | directories: 12 | - $HOME/.m2 13 | 14 | notifications: 15 | email: 16 | recipients: 17 | - eg.oss@e-gineering.com 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 E-Gineering, LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important Note : 1.0.0 has a new package... 2 | 3 | In the process of transferring this project to E-gineering, LLC (thanks, guys!) we've moved the maven coordinates a bit in order to start pushing 4 | artifacts to maven central. 5 | 6 | Note that all references to `org.collectd.FastJMX` have been changed to `com.e_gineering.collectd.FastJMX`, and the file name for jars has changed as 7 | well. 8 | 9 | On the bright side, you can now pull pre-built artifacts of FastJMX out of the interwebs, from the [Maven central repository](https://oss.sonatype.org/content/repositories/releases/com/e-gineering/collectd-fast-jmx/)! 10 | 11 | Or, for folks who want to use this some other way, here's the Maven coordinates: 12 | ``` 13 | 14 | com.e-gineering 15 | collectd-fast-jmx 16 | 1.0.0 17 | 18 | ``` 19 | 20 | ## FastJMX - Low-latency JMX collectd plugin [![Build Status](https://travis-ci.org/egineering-llc/collectd-fast-jmx.svg?branch=master)](https://travis-ci.org/egineering-llc/collectd-fast-jmx) 21 | 22 | 23 | The default GenericJMX plugin from collectd is great for basic collection of small numbers of metrics, but if you need to collect many metrics from one 24 | or more hosts, the latency to read the metrics can quickly exceed your interval time, and that's no fun. If you want to remotely collect metrics from 25 | multiple hosts you can forget about having short intervals, and some of the configuration settings aren't exactly obvious. 26 | Example: What do you mean I have to include the hostname? I gave you the serviceUrl! 27 | 28 | ### Introducing FastJMX! 29 | 30 | FastJMX does things differently than the GenericJMX plugin, but it does it in a manner that's configuration-compatible with the original plugin. 31 | (You read that right. There's just a few small tweaks to an existing configuration and FastJMX will take over) 32 | 33 | * FastJMX discovers all the matching beans when it first connects, then sets up listeners to the remote server so we get callbacks when any beans are added or removed from the server. This lets us identify all the permutations of the beans we need to read outside of the `read()` loop, which reduces `read()` latency, as well as internal GC stress and memory pressure. 34 | * Reconnections are attempted with increasing backoff sleep durations. Again, outside of the read loop, so that collecting metrics from connections which aren't failed continues to work. 35 | * Each attribute read from an mbean is it's own potential thread. The JDK 1.5 Concurrent packages are used to pool threads, inflict interval timeouts on the read cycle, and to make sure the queue is clear at the end of each `read()` invocation eliminating backlogged (lagged) metric reporting. If there isn't a metric polled in a timely manner, it's a dropped read. 36 | * Each `read()` cycle is timeslot protected (synchronized to the interval configured in collectd) so that old values and current values are *never* intermixed. 37 | * Each `` can define a custom `PluginName`, allowing segementation of reported metrics into different plugin buckets rather than everything being reported as "GenericJMX" or "FastJMX". 38 | * The port can be appended to the hostname using `IncludePortInHostname`. This is very helpful in separating data from multiple JVM instances on the same host without needing to specify an `InstancePrefix` on the ``. 39 | * Hostnames are automatically detected from the serviceURL. If the serviceURL is a complex type, like `service:jmx:rmi:///jndi/rmi://hostname:port/jmxrmi`, FastJMX will still properly parse the hostname and port. The `Hostname` property (part of the standard GenericJMX configuration) value is still respected if present. 40 | * FastJMX doesn't require connections be defined after the beans. `` (or ``, or just ``) and `` blocks can come in _any_ order. 41 | 42 | ### So how much faster is it? 43 | 44 | In real-world collection scenarios, large volume remote collections from multiple hosts over a VPN improved from ~2500ms to collect (with GenericJMX) to ~120ms. 45 | 46 | If you really want to know what FastJMX is doing, add `CollectInternal true` to the plugin configuration. This tells FastJMX to dispatch internal metrics (success, failure, error, latency, thread pool size) to collectd. 47 | 48 | ## Configuration 49 | ### Migrate from GenericJMX by... 50 | 51 | * Add the path to the fast-jmx jar in JVMARG 52 | * Include `LoadPlugin "com.e_gineering.collectd.FastJMX` in the `` block. 53 | 54 | ### Additional FastJMX Options: 55 | 56 | * Remove the `hostname` from the `` blocks. FastJMX will do it's best to detect it from the jmx URI if you don't include it. If parsing has an issue, you'll see a message in the log. 57 | * Asynch connection handling by default, but you can force synch by adding `Synchronous true` to a `` block. If the url contains `remoting-jmx` which is interpreted as [JBoss Remoting](https://github.com/jbossas/remoting-jmx) then the synchronous wrapper is auto-magic-ally enabled. 58 | * Single-attribute `` blocks can use the syntax ``. See the `` example below. 59 | * Include `PluginName` declarations in a `` block to change the plugin name it's reported as. Useful for grouping different MBeans as if they came from different applications, or subsystems. 60 | * Use `` or `` or ``. 61 | * `Composite` and `Table` can be used interchangeably within a `` block, and can be omitted (defaults to `false`). 62 | * `MaxThreads` can change the default maximum number of threads (512) to allow. 63 | * `CollectInternal` enables internal metrics FastJMX uses to be reported back to Collectd. 64 | * `TTL` can be used on a Connection to force a reconnect after `` many seconds have elapsed. This can be handy if your server isn't correctly maintining mbeans after redployments. Keep in mind this is seconds, so '43200' = 12 hours. 65 | * FastJMX can now traverse `TabularData` to pull out `CompositeData` values as tables, or track independent values. 66 | 67 | ``` 68 | LoadPlugin java 69 | 70 | JVMARG "-Djava.class.path=/path/to/collectd-api.jar:/path/to/collectd-fast-jmx.jar" 71 | 72 | LoadPlugin "com.e_gineering.collectd.FastJMX" 73 | 74 | 75 | 76 | MaxThreads 256 77 | CollectInternal true 78 | 79 | 80 | ObjectName "java.lang:type=ClassLoading" 81 | 82 | 83 | Type "gauge" 84 | InstancePrefix "loaded_classes" 85 | PluginName "JVM" 86 | 87 | 88 | 89 | # Time spent by the JVM compiling or optimizing. 90 | 91 | ObjectName "java.lang:type=Compilation" 92 | 93 | 94 | Type "total_time_in_ms" 95 | InstancePrefix "compilation_time" 96 | PluginName "JVM" 97 | 98 | 99 | 100 | # Garbage collector information 101 | 102 | ObjectName "java.lang:type=GarbageCollector,*" 103 | InstancePrefix "gc-" 104 | InstanceFrom "name" 105 | 106 | 107 | Type "total_time_in_ms" 108 | InstancePrefix "collection_time" 109 | PluginName "JVM" 110 | 111 | 112 | # Reads the Par Eden Space data as a composite table 113 | 114 | Type "java_memory" 115 | Composite true 116 | InstancePrefix "pool-eden-after" 117 | PluginName "JVM" 118 | 119 | 120 | # Reads only the "used" portion of the Par Eden Space 121 | 122 | type "java_memory" 123 | InstancePrefix "pool-eden-after-used" 124 | PluginName "JVM" 125 | 126 | 127 | 128 | # Memory usage by memory pool. 129 | 130 | ObjectName "java.lang:type=MemoryPool,*" 131 | InstancePrefix "memory_pool-" 132 | InstanceFrom "name" 133 | 134 | 135 | Type "java_memory" 136 | Composite true 137 | PluginName "JVM" 138 | 139 | 140 | 141 | 142 | 143 | ServiceURL "service:jmx:rmi:///jndi/rmi://host1:8098/jmxrmi" 144 | IncludePortInHostname true 145 | Collect "classes" 146 | Collect "compilation" 147 | Collect "garbage_collector" 148 | Collect "memory_pool" 149 | 150 | 151 | ServiceURL "service:jmx:rmi:///jndi/rmi://host1:8198/jmxrmi" 152 | IncludePortInHostname true 153 | Collect "classes" 154 | Collect "compilation" 155 | Collect "garbage_collector" 156 | Collect "memory_pool" 157 | 158 | 159 | ServiceURL "service:jmx:rmi:///jndi/rmi://host2:8398/jmxrmi" 160 | IncludePortInHostname true 161 | Collect "classes" 162 | Collect "compilation" 163 | Collect "garbage_collector" 164 | Collect "memory_pool" 165 | # Force the connection to reset every 4 hours. 166 | TTL 14400 167 | 168 | 169 | 170 | 171 | ``` 172 | 173 | ## Internal Metrics 174 | FastJMX collects some internal metrics that it uses to estimate an efficient pool size. 175 | If you enable internal metric collection (see above configuration options) and have the following types defined in types.db, the data will be submitted to collectd. 176 | 177 | ``` 178 | fastjmx_cycle value:GAUGE:0:U 179 | fastjmx_latency value:GAUGE:0:U 180 | ``` 181 | 182 | Once you've got collectd keeping your data, you may find these Collection3 graph configurations useful... 183 | ``` 184 | 185 | Module GenericStacked 186 | DataSources value 187 | RRDTitle "FastJMX Reads ({plugin_instance})" 188 | RRDFormat "%6.1lf" 189 | DSName "cancelled Incomplete " 190 | DSName " success Success " 191 | DSName " failed Failed " 192 | DSName " weight Weight " 193 | Order success cancelled failed weight 194 | Color failed ff0000 195 | Color cancelled ffb000 196 | Color success 00e000 197 | Color weight 0000ff 198 | Stacking on 199 | 200 | 201 | Module GenericStacked 202 | DataSources value 203 | RRDTitle "FastJMX Latency ({plugin_instance})" 204 | RRDFormat "%6.1lf" 205 | DSName "interval Interval" 206 | DSName "duration Latency " 207 | Order interval duration 208 | Color duration ffb000 209 | Color interval 00e000 210 | Stacking off 211 | 212 | ``` 213 | 214 | ## JBoss EAP 6.x, AS 7.x 215 | The JBoss remoting JMX provider has been tested with EAP 6.x, and should work properly with AS 7.x as well. 216 | As part of getting this to work, some 'workarounds' are included in the FastJMX code-base, which may also apply to other JMX protocol providers. In the case of the JBoss jmx remoting, appropriate bugs and feature requests have been filed. 217 | 218 | ### JBoss EAP 6 Classpath 219 | Here's an example JVMArg that works with jboss-eap-6.1 220 | ``` 221 | 222 | JVMArg "-Djava.class.path=/usr/share/collectd/java/collectd-api.jar:/usr/lib/jvm/java-7-oracle/lib/jconsole.jar:/usr/lib/jvm/java-7-oracle/lib/tools.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/remoting-jmx/main/remoting-jmx-1.1.0.Final-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/remoting3/main/jboss-remoting-3.2.16.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/logging/main/jboss-logging-3.1.2.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/xnio/main/xnio-api-3.0.7.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/xnio/nio/main/xnio-nio-3.0.7.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/sasl/main/jboss-sasl-1.0.3.Final-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/marshalling/main/jboss-marshalling-1.3.18.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/marshalling/river/main/jboss-marshalling-river-1.3.18.GA-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/as/cli/main/jboss-as-cli-7.2.1.Final-redhat-10.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/staxmapper/main/staxmapper-1.1.0.Final-redhat-2.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/as/protocol/main/jboss-as-protocol-7.2.1.Final-redhat-10.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/dmr/main/jboss-dmr-1.1.6.Final-redhat-1.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/as/controller-client/main/jboss-as-controller-client-7.2.1.Final-redhat-10.jar:/opt/appserver/jboss-eap-6.1/modules/system/layers/base/org/jboss/threads/main/jboss-threads-2.1.0.Final-redhat-1.jar:/usr/share/collectd/java/collectd-fast-jmx-1.0-SNAPSHOT.jar" 223 | LoadPlugin "com.e_gineering.collectd.FastJMX" 224 | 225 | ... 226 | 227 | 228 | ``` 229 | 230 | To connect as an administrator you shoudln't need to change anything in the jboss configuration. 231 | The following Connection block should work in this scenario. 232 | ``` 233 | 234 | ServiceURL "service:jmx:remoting-jmx://yourhostname:9999" 235 | Username "admin" 236 | Password "aR3allyStrongP@sswordThatOthersCanSee" 237 | ttl 300 238 | IncludePortInHostname false 239 | Collect "classes" 240 | ... 241 | 242 | 243 | ``` 244 | 245 | To connect as a normal application user, and expose JMX over the 'remoting' port in EAP 6.1, your domain (or standalone) configuration should include `use-management-endpoint="false"`, like so: 246 | ``` 247 | 248 | 249 | 250 | 251 | 252 | ``` 253 | 254 | This changes the port from 9999 (the default management port) to 4447 (the remoting port) and requires an *application* user rather than an *administration* user. 255 | 256 | You can add the application user using the 'add-user' script from the JBoss Bin dir: 257 | ``` 258 | $JBOSS_HOME/bin/add-user.sh --silent -a --user jmx --password 259 | ``` 260 | 261 | Then in your Connection block, you can use: 262 | ``` 263 | 264 | ServiceURL "service:jmx:remoting-jmx://yourhostname:4447" 265 | Username "jmx" 266 | Password "!amUnprivi1eged" 267 | ttl 300 268 | IncludePortInHostname false 269 | Collect "classes" 270 | ... 271 | 272 | ``` 273 | 274 | Which exposes a non-privileged username / password. 275 | 276 | *WARNING WARNING WARNING* This Unprivileged user will be able to invoke MBeans via JMX. *WARNING WARNING WARNING*\ 277 | 278 | ## Debugging & Troubleshooting 279 | 280 | There are a couple additional configuration options worth nothing, which are helpful if you're troubleshooting an issue. 281 | 282 | * `LogLevel` sets the Plugins internal Java log level. By default this is 'INFO'. Meaning any log message generated internall that's INFO or greater will be logged to Collectd at the approprate (corresponding) Collectd Log Level... 283 | * `ForceLoggingTo` Lets you override the normal behavior of mapping Java log levels to collectd log levels, and forces all java log output to be logged at this collectd level. 284 | 285 | So under normal operation, things logged in java as SEVERE are logged at ERROR in Collectd, etc. 286 | 287 | Setting `ForceLoggingTo "INFO"` will make all Java logging output log in Collectd at INFO. 288 | 289 | If your normal Collectd configuration sets the collectd log level to WARNING, but you want to get 'INFO' from the FastJMX plugin, you can do this: 290 | 291 | ``` 292 | 293 | LogLevel "INFO" 294 | ForceLoggingTo "WARNING" 295 | 296 | ... 297 | 298 | ``` 299 | 300 | If you'd like to see FINE logging from FastJMX use: 301 | 302 | ``` 303 | 304 | LogLevel "FINE" 305 | ForceLoggingTo "WARNING" 306 | 307 | ``` 308 | 309 | Basically, you're setting the java logger write any messages >= `FINE`, and to write those messages as Collectd `WARNING` messages. 310 | It gives a little more control over the verbosity of this single plugin. 311 | 312 | -------------------------------------------------------------------------------- /lib/collectd-api.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-gineering/collectd-fast-jmx/2e2ffbcee3ff88c0b57449d36c9e3c5e91cf12c2/lib/collectd-api.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 3.0.1 9 | 10 | 11 | com.e-gineering 12 | collectd-fast-jmx 13 | 1.0.0 14 | 15 | collectd FastJMX 16 | jar 17 | A collectd plugin for lower-latency JMX operations. 18 | http://github.com/egineering-llc/collectd-fast-jmx 19 | 20 | 21 | E-gineering, LLC. 22 | http://www.e-gineering.com 23 | 24 | 25 | 26 | 27 | Bryan Varner 28 | bryan.varner@e-gineering.com 29 | 30 | 31 | 32 | 33 | https://github.com/egineering-llc/collectd-fast-jmx/issues 34 | GitHub Issues 35 | 36 | 37 | 38 | 39 | Apache License, Version 2.0 40 | http://www.apache.org/licenses/LICENSE-2.0 41 | repo 42 | 43 | 44 | 45 | 46 | https://github.com/egineering-llc/collectd-fast-jmx 47 | scm:git:git://github.com/egineering-llc/collectd-fast-jmx.git 48 | scm:git:git@github.com:egineering-llc/collectd-fast-jmx.git 49 | 50 | 51 | 52 | 53 | 1.5 54 | 1.5 55 | UTF-8 56 | 57 | 58 | 59 | 60 | ossrh 61 | https://oss.sonatype.org/content/repositories/snapshots 62 | 63 | 64 | ossrh 65 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 66 | 67 | 68 | 69 | 70 | 71 | org.collectd 72 | collectd-api 73 | 1.0 74 | system 75 | ${basedir}/lib/collectd-api.jar 76 | 77 | 78 | org.apache.commons 79 | commons-math3 80 | 3.3 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-javadoc-plugin 89 | 2.10.3 90 | 91 | 92 | attach-javadocs 93 | 94 | jar 95 | 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-source-plugin 102 | 2.4 103 | 104 | 105 | attach-sources 106 | 107 | jar 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-shade-plugin 115 | 2.3 116 | 117 | 118 | 119 | *:* 120 | 121 | org/collectd/api/** 122 | 123 | 124 | 125 | 126 | 127 | 128 | package 129 | 130 | shade 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | eg.oss 141 | 142 | false 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-gpg-plugin 149 | 1.5 150 | 151 | 152 | sign-artifacts 153 | verify 154 | 155 | sign 156 | 157 | 158 | 159 | 160 | 161 | org.sonatype.plugins 162 | nexus-staging-maven-plugin 163 | 1.6.3 164 | true 165 | 166 | ossrh 167 | https://oss.sonatype.org/ 168 | false 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/Attribute.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import org.collectd.api.DataSet; 4 | 5 | import javax.management.ObjectName; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.LinkedHashMap; 10 | import java.util.List; 11 | 12 | /** 13 | * Defines the parameters needed to build a set of AttributePermutations combined with a ConnectionDefinition. 14 | */ 15 | public class Attribute { 16 | private String beanAlias; 17 | private String pluginName; 18 | 19 | private ObjectName findName; 20 | private String beanInstancePrefix; 21 | private List beanInstanceFrom = new ArrayList(); 22 | 23 | private LinkedHashMap> attributes = new LinkedHashMap>(); 24 | private DataSet dataset; 25 | private String valueInstancePrefix; 26 | private List valueInstanceFrom = new ArrayList(); 27 | private boolean composite; 28 | 29 | public Attribute(final List attributes, final String pluginName, final DataSet dataset, 30 | final String valueInstancePrefix, final List valueInstanceFrom, final boolean composite, 31 | final String beanAlias, final ObjectName findName, 32 | final String beanInstancePrefix, final List beanInstanceFrom) { 33 | this.beanAlias = beanAlias; 34 | this.pluginName = pluginName; 35 | this.findName = findName; 36 | this.beanInstancePrefix = beanInstancePrefix; 37 | this.beanInstanceFrom = beanInstanceFrom; 38 | for (String attribute : attributes) { 39 | this.attributes.put(attribute, Arrays.asList(attribute.split("\\."))); 40 | } 41 | this.dataset = dataset; 42 | this.valueInstancePrefix = valueInstancePrefix; 43 | this.valueInstanceFrom = valueInstanceFrom; 44 | this.composite = composite; 45 | } 46 | 47 | public String getBeanAlias() { 48 | return beanAlias; 49 | } 50 | 51 | public ObjectName getObjectName() { 52 | return findName; 53 | } 54 | 55 | public String getPluginName() { 56 | return pluginName; 57 | } 58 | 59 | public List getBeanInstanceFrom() { 60 | return beanInstanceFrom; 61 | } 62 | 63 | public DataSet getDataSet() { 64 | return dataset; 65 | } 66 | 67 | public String getBeanInstancePrefix() { 68 | return beanInstancePrefix; 69 | } 70 | 71 | public List getValueInstanceFrom() { 72 | return valueInstanceFrom; 73 | } 74 | 75 | public String getValueInstancePrefix() { 76 | return valueInstancePrefix; 77 | } 78 | 79 | public HashMap> getAttributes() { 80 | return attributes; 81 | } 82 | 83 | public boolean isComposite() { 84 | return composite; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/AttributePermutation.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import org.collectd.api.DataSource; 4 | import org.collectd.api.PluginData; 5 | import org.collectd.api.ValueList; 6 | 7 | import javax.management.AttributeNotFoundException; 8 | import javax.management.InstanceNotFoundException; 9 | import javax.management.MBeanServerConnection; 10 | import javax.management.ObjectName; 11 | import javax.management.openmbean.CompositeData; 12 | import javax.management.openmbean.OpenType; 13 | import javax.management.openmbean.TabularData; 14 | import java.io.IOException; 15 | import java.math.BigDecimal; 16 | import java.math.BigInteger; 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | import java.util.concurrent.Callable; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | 26 | /** 27 | * Defines an actual permutation of an Attribute to be read from a Connection. 28 | */ 29 | public class AttributePermutation implements Callable, Comparable { 30 | private static Logger logger = Logger.getLogger(AttributePermutation.class.getName()); 31 | 32 | private ObjectName objectName; 33 | private Connection connection; 34 | private Attribute attribute; 35 | private PluginData pluginData; 36 | private ValueList valueList; 37 | 38 | private long lastRunDuration = 0l; 39 | private boolean interruptedOrFailed = false; 40 | private int consecutiveNotFounds = 0; 41 | private List dispatch = new ArrayList(1); 42 | 43 | private AttributePermutation(final ObjectName objectName, final Connection connection, final Attribute attribute, final PluginData pd, final ValueList vl) { 44 | this.objectName = objectName; 45 | this.connection = connection; 46 | this.attribute = attribute; 47 | this.pluginData = pd; 48 | this.valueList = vl; 49 | } 50 | 51 | public static List create(final ObjectName[] objectNames, final Connection connection, final Attribute context) { 52 | // This method takes into account the beanInstanceFrom and valueInstanceFrom properties to create many AttributePermutations. 53 | if (objectNames.length == 0) { 54 | logger.warning("No MBeans matched " + context.getObjectName() + " @ " + connection.getRawUrl()); 55 | return new ArrayList(0); 56 | } 57 | 58 | List permutations = new ArrayList(); 59 | 60 | PluginData pd = new PluginData(); 61 | pd.setHost(connection.getHostname()); 62 | if (context.getPluginName() != null) { 63 | pd.setPlugin(context.getPluginName()); 64 | } else { 65 | pd.setPlugin("FastJMX"); 66 | } 67 | 68 | for (ObjectName objName : objectNames) { 69 | // Issue #16, possibly issue #6 - Attempt to get the MBeanInfo prior to adding the permutation to be collected. 70 | // Based on Pull Request #17, but relocated to the 'initialization' code for a permutation. 71 | // If we're unable to obtain the MBeanInfo, the permutation is not added. 72 | try { 73 | connection.getServerConnection().getMBeanInfo(objName); // If this doesn't work, we won't add permutations to collect the bean info. 74 | 75 | PluginData permutationPD = new PluginData(pd); 76 | List beanInstanceList = new ArrayList(); 77 | StringBuilder beanInstance = new StringBuilder(); 78 | 79 | for (String propertyName : context.getBeanInstanceFrom()) { 80 | String propertyValue = objName.getKeyProperty(propertyName); 81 | 82 | if (propertyValue == null) { 83 | logger.severe("No such property [" + propertyName + "] in ObjectName [" + objName + "] for bean instance creation."); 84 | } else { 85 | beanInstanceList.add(propertyValue); 86 | } 87 | } 88 | 89 | if (connection.getConnectionInstancePrefix() != null) { 90 | beanInstance.append(connection.getConnectionInstancePrefix()); 91 | } 92 | 93 | if (context.getBeanInstancePrefix() != null) { 94 | if (beanInstance.length() > 0) { 95 | beanInstance.append("-"); 96 | } 97 | beanInstance.append(context.getBeanInstancePrefix()); 98 | } 99 | 100 | for (int i = 0; i < beanInstanceList.size(); i++) { 101 | if (i > 0) { 102 | beanInstance.append("-"); 103 | } 104 | beanInstance.append(beanInstanceList.get(i)); 105 | } 106 | permutationPD.setPluginInstance(beanInstance.toString()); 107 | 108 | ValueList vl = new ValueList(permutationPD); 109 | vl.setType(context.getDataSet().getType()); 110 | 111 | List attributeInstanceList = new ArrayList(); 112 | for (String propertyName : context.getValueInstanceFrom()) { 113 | String propertyValue = objName.getKeyProperty(propertyName); 114 | if (propertyValue == null) { 115 | logger.severe("no such property [" + propertyName + "] in ObjectName [" + objName + "] for attribute instance creation."); 116 | } else { 117 | attributeInstanceList.add(propertyValue); 118 | } 119 | } 120 | 121 | StringBuilder attributeInstance = new StringBuilder(); 122 | if (context.getValueInstancePrefix() != null) { 123 | attributeInstance.append(context.getValueInstancePrefix()); 124 | } 125 | 126 | for (int i = 0; i < attributeInstanceList.size(); i++) { 127 | if (i > 0) { 128 | attributeInstance.append("-"); 129 | } 130 | attributeInstance.append(attributeInstanceList.get(i)); 131 | } 132 | vl.setTypeInstance(attributeInstance.toString()); 133 | 134 | permutations.add(new AttributePermutation(objName, connection, context, permutationPD, vl)); 135 | } catch (Exception ex) { 136 | logger.warning("Unable to obtain MBeanInfo for " + objName + " @ " + connection.getRawUrl() ); 137 | } 138 | } 139 | 140 | return permutations; 141 | } 142 | 143 | public Connection getConnection() { 144 | return connection; 145 | } 146 | 147 | public ObjectName getObjectName() { 148 | return objectName; 149 | } 150 | 151 | public List getValues() { 152 | return dispatch; 153 | } 154 | 155 | /** 156 | * Implements Comparable, allowing for a natural sort ordering of previous successful execution duration. 157 | *

158 | * Executions previously cancelled or failed will be treated as 'not run', and have a duration of '0', making them 159 | * 'less than' by comparison. If both objects being compared have a run duration of 0, they are sorted according to 160 | * the computed hashCode() values. 161 | * 162 | * @param o The other AttributePermutation to compare. 163 | * @return Lexical comparison result. 164 | */ 165 | public int compareTo(final AttributePermutation o) { 166 | 167 | int i = -1 * Long.valueOf(getLastRunDuration()).compareTo(Long.valueOf(o.getLastRunDuration())); 168 | if (i != 0) { 169 | return i; 170 | } 171 | 172 | return Integer.valueOf(hashCode()).compareTo(Integer.valueOf(o.hashCode())); 173 | } 174 | 175 | @Override 176 | public boolean equals(Object obj) { 177 | if (this == obj) { 178 | return true; 179 | } else if (obj instanceof AttributePermutation) { 180 | return hashCode() == obj.hashCode(); 181 | } else { 182 | return false; 183 | } 184 | } 185 | 186 | @Override 187 | public int hashCode() { 188 | return (connection.getHostname() + connection.getRawUrl() + objectName.toString() + pluginData.getSource() + valueList.getType()).hashCode(); 189 | } 190 | 191 | /** 192 | * Reads the attribute from the JMX Connection and submits it back to Collectd. 193 | */ 194 | public AttributePermutation call() throws Exception { 195 | long start = System.nanoTime(); 196 | dispatch.clear(); 197 | // Snapshot the value list for this 'call', Value lists are built at the end of the call(), 198 | // and if another thread call()s while this one is still running, it could trounce the interval 199 | // and report back duplicates to collectd. 200 | ValueList callVal = new ValueList(this.valueList); 201 | interruptedOrFailed = true; 202 | try { 203 | MBeanServerConnection mbs = connection.getServerConnection(); 204 | 205 | List values = new ArrayList(8); 206 | for (Map.Entry> attributePath : attribute.getAttributes().entrySet()) { 207 | Object value = null; 208 | StringBuilder path = new StringBuilder(); 209 | 210 | for (int i = 0; i < attributePath.getValue().size(); i++) { 211 | String node = attributePath.getValue().get(i); 212 | // If this is our first loop over the path, just get the attribute into the value object. 213 | if (i == 0) { 214 | path.append(node); 215 | 216 | try { 217 | value = mbs.getAttribute(objectName, node); 218 | } catch (AttributeNotFoundException anfe) { 219 | value = mbs.invoke(objectName, node, null, null); 220 | } 221 | consecutiveNotFounds = 0; 222 | if (Thread.currentThread().isInterrupted()) { 223 | return this; 224 | } 225 | } else { 226 | path.append(".").append(node); 227 | 228 | // Subsequent path traversals mean we need to inspect the value object and take appropriate action. 229 | if (value instanceof CompositeData) { 230 | CompositeData compositeValue = (CompositeData) value; 231 | value = compositeValue.get(node); 232 | } else if (value instanceof OpenType) { 233 | throw new UnsupportedOperationException("Handling of OpenType " + ((OpenType) value).getTypeName() + " is not yet implemented."); 234 | } else if (value instanceof TabularData) { 235 | // A java.lang:type=GarbageCollector mbean 236 | // has an LastGcInfo attribute that is a 237 | // CompositeData value, which interesting 238 | // sub-value called memoryUsageAfterGc and 239 | // memoryUsageBeforeGc, which expose the 240 | // javax.management.openmbean.TabularData 241 | // interface. Each table exposes each heap 242 | // and nonheap memory pool using the name 243 | // of its java.lang:type=MemoryPool as the 244 | // TabularData key. The memory pool is a 245 | // CompositeData value that can be further 246 | // examined with Attribute dot notation. 247 | // #mbean = java.lang:type=GarbageCollector,name=ParNew 248 | // LastGcInfo = { 249 | // memoryUsageAfterGc = { 250 | // ( Par Survivor Space ) = { 251 | // key = Par Survivor Space; 252 | // value = { 253 | // committed = 8716288; 254 | // init = 8716288; 255 | // max = 8716288; 256 | // used = 79040; 257 | // }; 258 | // }; 259 | // ... 260 | // }; 261 | // }; 262 | // 263 | // This java.conf entry would capture the state 264 | // of the "Par Eden Space" memory pool after the 265 | // most recent garbage collection invation: 266 | // 267 | // ObjectName "java.lang:type=GarbageCollector,*" 268 | // 269 | // Attribute "LastGcInfo.memoryUsageAfterGc.Par Eden Space" 270 | // Table true 271 | // Type "memory" # value:GAUGE:0:281474976710656 272 | // InstancePrefix "pool-eden-after" 273 | // 274 | // 275 | TabularData tabularValue = (TabularData) value; 276 | // The java API confirms this is always a Collection, but that the interface 277 | // definition remains Collection for compatibility. :-( 278 | Collection tableData = (Collection)tabularValue.values(); 279 | 280 | // Look for the first CompositeData with the key = to the current node, then loop again. 281 | for (CompositeData compositeData : tableData) { 282 | if (compositeData.get("key").equals(node)) { 283 | value = compositeData.get("value"); 284 | break; 285 | } 286 | } 287 | } else if (value != null) { 288 | // Try to traverse via Reflection. 289 | value = value.getClass().getDeclaredField(node).get(value); 290 | } else if (i + 1 == attributePath.getValue().size()) { 291 | // TODO: Configure this so users can try to track down what isn't working. 292 | // It's really annoying though, for things like LastGcInfo.duration, which are transient for things like CMS collectors. 293 | if (logger.isLoggable(Level.FINE)) { 294 | logger.fine("NULL read from " + path + " in " + objectName + " @ " + connection.getRawUrl()); 295 | } 296 | } 297 | } 298 | } 299 | values.add(value); 300 | } 301 | 302 | if (Thread.currentThread().isInterrupted()) { 303 | return this; 304 | } 305 | 306 | // If we're expecting CompositeData objects to be brokenConnection up like a table, handle it. 307 | if (attribute.isComposite()) { 308 | List cdList = new ArrayList(); 309 | Set keys = null; 310 | 311 | for (Object obj : values) { 312 | if (obj instanceof CompositeData) { 313 | if (keys == null) { 314 | keys = ((CompositeData) obj).getCompositeType().keySet(); 315 | } 316 | cdList.add((CompositeData) obj); 317 | } else { 318 | throw new IllegalArgumentException("At least one of the attributes from " + objectName + " @ " + connection.getRawUrl() + " was not a 'CompositeData' as requried when table|composite = 'true'"); 319 | } 320 | } 321 | 322 | for (String key : keys) { 323 | ValueList vl = new ValueList(callVal); 324 | vl.setTypeInstance(vl.getTypeInstance() + key); 325 | vl.setValues(genericCompositeToNumber(cdList, key)); 326 | if (logger.isLoggable(Level.FINEST)) { 327 | logger.finest("dispatch " + vl); 328 | } 329 | dispatch.add(vl); 330 | } 331 | } else if (!values.contains(null)) { 332 | ValueList vl = new ValueList(callVal); 333 | vl.setValues(genericListToNumber(values)); 334 | if (logger.isLoggable(Level.FINEST)) { 335 | logger.finest("dispatch " + vl); 336 | } 337 | dispatch.add(vl); 338 | } 339 | interruptedOrFailed = false; 340 | } catch (IOException ioe) { 341 | // This normally comes about as an issue with the underlying connection. Specifically in debugging JBoss 342 | // remoting connections, we may end up getting subclasses of IOException when the remoting connection has 343 | // failed. I believe the best way to handle this is with the existing 'not found' reconnect behavior. 344 | consecutiveNotFounds++; 345 | } catch (InstanceNotFoundException infe) { 346 | // The FastJMX plugin wasn't notified of the mbean unregister prior to the collect cycle. 347 | // This can be a valid case, if the server unregistered the bean during a read cycle (when the collection 348 | // of AttributePermutations is locked) -- and should result in the AttributePermutation being removed before 349 | // the next collect cycle. (proper behavior). 350 | // 351 | // If for some reason this exception occurs in consecutive read cycles, consider it a suspect server 352 | // implementation and schedule a reconnect for the connection, so we re-discover the MBeans on the server. 353 | // Turns out that this is incredibly common with JBoss AS >= EAP 6.x (7.x community and WildFly) due to the 354 | // transition to their remoting-jmx provider, which does not support MBeanDelegate listeners (at this time) 355 | // 356 | consecutiveNotFounds++; 357 | } catch (Exception ex) { 358 | throw ex; 359 | } finally { 360 | lastRunDuration = System.nanoTime() - start; 361 | } 362 | 363 | return this; 364 | } 365 | 366 | int getConsecutiveNotFounds() { 367 | return consecutiveNotFounds; 368 | } 369 | 370 | public long getLastRunDuration() { 371 | if (!interruptedOrFailed) { 372 | return lastRunDuration; 373 | } 374 | return 0l; 375 | } 376 | 377 | private List genericCompositeToNumber(final List cdlist, final String key) { 378 | List objects = new ArrayList(); 379 | 380 | for (int i = 0; i < cdlist.size(); i++) { 381 | CompositeData cd; 382 | Object value; 383 | 384 | cd = cdlist.get(i); 385 | value = cd.get(key); 386 | objects.add(value); 387 | } 388 | 389 | return genericListToNumber(objects); 390 | } 391 | 392 | 393 | private List genericListToNumber(final List objects) throws IllegalArgumentException { 394 | List ret = new ArrayList(); 395 | List dsrc = this.attribute.getDataSet().getDataSources(); 396 | 397 | for (int i = 0; i < objects.size(); i++) { 398 | ret.add(genericObjectToNumber(objects.get(i), dsrc.get(i).getType())); 399 | } 400 | 401 | return ret; 402 | } 403 | 404 | /** 405 | * Converts a generic (OpenType) object to a number. 406 | *

407 | * Returns null if a conversion is not possible or not implemented. 408 | */ 409 | private Number genericObjectToNumber(final Object obj, final int ds_type) throws IllegalArgumentException { 410 | if (obj instanceof String) { 411 | String str = (String) obj; 412 | 413 | try { 414 | if (ds_type == DataSource.TYPE_GAUGE) { 415 | return (new Double(str)); 416 | } else { 417 | return (new Long(str)); 418 | } 419 | } catch (NumberFormatException e) { 420 | return (null); 421 | } 422 | } else if (obj instanceof Byte) { 423 | return (new Byte((Byte) obj)); 424 | } else if (obj instanceof Short) { 425 | return (new Short((Short) obj)); 426 | } else if (obj instanceof Integer) { 427 | return (new Integer((Integer) obj)); 428 | } else if (obj instanceof Long) { 429 | return (new Long((Long) obj)); 430 | } else if (obj instanceof Float) { 431 | return (new Float((Float) obj)); 432 | } else if (obj instanceof Double) { 433 | return (new Double((Double) obj)); 434 | } else if (obj instanceof BigDecimal) { 435 | return (BigDecimal.ZERO.add((BigDecimal) obj)); 436 | } else if (obj instanceof BigInteger) { 437 | return (BigInteger.ZERO.add((BigInteger) obj)); 438 | } 439 | 440 | throw new IllegalArgumentException("Cannot convert type: " + obj.getClass().getSimpleName() + " to Number."); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/Connection.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import javax.management.InstanceNotFoundException; 4 | import javax.management.ListenerNotFoundException; 5 | import javax.management.MBeanServerConnection; 6 | import javax.management.MBeanServerDelegate; 7 | import javax.management.Notification; 8 | import javax.management.NotificationListener; 9 | import javax.management.remote.JMXConnectionNotification; 10 | import javax.management.remote.JMXConnector; 11 | import javax.management.remote.JMXConnectorFactory; 12 | import javax.management.remote.JMXServiceURL; 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Timer; 18 | import java.util.TimerTask; 19 | import java.util.UUID; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | 24 | /** 25 | * Defines permutations for a host connection. 26 | */ 27 | public class Connection implements NotificationListener { 28 | 29 | private static Logger logger = Logger.getLogger(Connection.class.getName()); 30 | private UUID connectionUuid; 31 | 32 | private String hostname; 33 | private String rawUrl; 34 | private JMXServiceURL serviceURL; 35 | private String username; 36 | private String password; 37 | private String connectionInstancePrefix; 38 | private List beanAliases; 39 | private long ttl; 40 | private boolean forceSynchronous; 41 | 42 | private NotificationListener notificationListener; 43 | private JMXConnector serverConnector; 44 | private MBeanServerConnection serverConnection; 45 | 46 | private Timer connectTimer; 47 | 48 | public Connection(final NotificationListener notificationListener, final String rawUrl, final String hostname, final JMXServiceURL serviceURL, final String username, 49 | final String password, final String connectionInstancePrefix, final List beanAliases, final long ttl, final boolean forceSynchronous) { 50 | this.connectionUuid = UUID.randomUUID(); 51 | this.notificationListener = notificationListener; 52 | this.rawUrl = rawUrl; 53 | this.hostname = hostname; 54 | this.serviceURL = serviceURL; 55 | this.username = username; 56 | this.password = password; 57 | this.connectionInstancePrefix = connectionInstancePrefix; 58 | this.beanAliases = beanAliases; 59 | this.ttl = ttl; 60 | this.forceSynchronous = forceSynchronous; 61 | 62 | this.serverConnector = null; 63 | this.serverConnection = null; 64 | this.connectTimer = new Timer("Connect-" + rawUrl, true); 65 | } 66 | 67 | public UUID getUUID() { 68 | return connectionUuid; 69 | } 70 | 71 | public void connect() { 72 | if (logger.isLoggable(Level.FINE)) { 73 | logger.fine("connect() for " + rawUrl); 74 | } 75 | ConnectTask task = new ConnectTask(0); 76 | connectTimer.schedule(task, task.getDelay()); 77 | } 78 | 79 | /** 80 | * Removes all NofiticationListeners and closes the connections. 81 | */ 82 | public void close() { 83 | if (logger.isLoggable(Level.FINE)) { 84 | logger.fine("Closing: " + rawUrl); 85 | } 86 | if (serverConnector != null) { 87 | if (logger.isLoggable(Level.FINE)) { 88 | logger.fine("Removing connection listeners for " + rawUrl); 89 | } 90 | 91 | try { 92 | serverConnector.removeConnectionNotificationListener(notificationListener); 93 | serverConnector.removeConnectionNotificationListener(this); 94 | } catch (ListenerNotFoundException lnfe) { 95 | logger.severe("Failed to unregister connection listeners for " + rawUrl); 96 | } 97 | 98 | try { 99 | serverConnector.close(); 100 | } catch (IOException ioe) { 101 | logger.warning("Exception closing JMXConnection: " + ioe.getMessage()); 102 | } 103 | } 104 | 105 | serverConnection = null; 106 | serverConnector = null; 107 | } 108 | 109 | 110 | public MBeanServerConnection getServerConnection() throws IOException { 111 | if (serverConnector == null && serverConnection == null) { 112 | throw new IOException("Not Connected to: " + rawUrl); 113 | } else if (serverConnector != null && serverConnection == null) { 114 | logger.warning("Returning serverConnector.getMbeanServerConnection(). POSSIBLE RACE."); 115 | return serverConnector.getMBeanServerConnection(); 116 | } 117 | return serverConnection; 118 | } 119 | 120 | void setMBeanServerConnection(MBeanServerConnection connection) { 121 | this.serverConnection = connection; 122 | } 123 | 124 | /** 125 | * Cleans up the serverConnection if we're closed or fail. 126 | * 127 | * @param notification The notification to handle 128 | * @param handback The handback object registered with the notification listener. 129 | */ 130 | public void handleNotification(final Notification notification, final Object handback) { 131 | logger.fine("Connection received Notification: " + notification); 132 | if (notification instanceof JMXConnectionNotification) { 133 | if (notification.getType().equals(JMXConnectionNotification.CLOSED) || 134 | notification.getType().equals(JMXConnectionNotification.FAILED)) { 135 | close(); 136 | } else if (notification.getType().equals(JMXConnectionNotification.OPENED)) { 137 | try { 138 | serverConnection = serverConnector.getMBeanServerConnection(); 139 | logger.fine("Got MBeanServerConnection: " + serverConnection.getClass().getName()); 140 | serverConnection.addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, notificationListener, null, this.connectionUuid); 141 | logger.fine("Added NotificationListener."); 142 | if (ttl > 0) { 143 | connectTimer.schedule(new ReconnectTask(), TimeUnit.MILLISECONDS.convert(ttl, TimeUnit.SECONDS)); 144 | } 145 | } catch (IOException ioe) { 146 | logger.warning("Could not get mbeanServerConnection to: " + rawUrl + " exception message: " + ioe.getMessage()); 147 | close(); 148 | ConnectTask backoffConnect = new ConnectTask(0); 149 | connectTimer.schedule(backoffConnect, backoffConnect.getDelay()); 150 | } catch (InstanceNotFoundException infe) { 151 | logger.config("Could not register MBeanServerDelegate. FastJMX will be unable to detect newly deployed or undeployed beans at: " + rawUrl + ".\n" + 152 | "You can configure a 'ttl' for this connection to force reconnection and rediscovery of MBeans on a periodic basis."); 153 | } 154 | } 155 | } 156 | } 157 | 158 | public String getRawUrl() { 159 | return rawUrl; 160 | } 161 | 162 | public String getHostname() { 163 | return hostname; 164 | } 165 | 166 | public String getConnectionInstancePrefix() { 167 | return connectionInstancePrefix; 168 | } 169 | 170 | public List getBeanAliases() { 171 | return beanAliases; 172 | } 173 | 174 | @Override 175 | public int hashCode() { 176 | return connectionUuid.hashCode(); 177 | } 178 | 179 | @Override 180 | public boolean equals(final Object obj) { 181 | if (this == obj) { 182 | return true; 183 | } else if (obj instanceof Connection) { 184 | Connection that = (Connection) obj; 185 | 186 | return this.rawUrl.equals(that.rawUrl) && 187 | this.hostname.equals(that.hostname) && 188 | (this.username == null ? that.username == null : this.username.equals(that.username)) && 189 | (this.password == null ? that.password == null : this.password.equals(that.password)) && 190 | (this.connectionInstancePrefix == null ? that.connectionInstancePrefix == null : this.connectionInstancePrefix.equals(that.connectionInstancePrefix)) && 191 | this.ttl == that.ttl && 192 | this.forceSynchronous == that.forceSynchronous; 193 | } 194 | return false; 195 | } 196 | 197 | private class ReconnectTask extends TimerTask { 198 | public void run() { 199 | logger.info("Error or TTL Expiration for " + rawUrl + " forcing reconnect.."); 200 | try { 201 | serverConnector.close(); 202 | } catch (IOException ioe) { 203 | logger.severe("Failure to close for TTL reconnect to: " + rawUrl); 204 | } 205 | } 206 | } 207 | 208 | 209 | private class ConnectTask extends TimerTask { 210 | private int connectBackoff = 0; 211 | 212 | private ConnectTask(final int backoffSeconds) { 213 | this.connectBackoff = backoffSeconds; 214 | } 215 | 216 | public long getDelay() { 217 | return TimeUnit.MILLISECONDS.convert(connectBackoff, TimeUnit.SECONDS); 218 | } 219 | 220 | @Override 221 | public void run() { 222 | this.cancel(); 223 | logger.info("Connecting to: " + rawUrl); 224 | 225 | if (connectBackoff == 0) { 226 | connectBackoff = 5; 227 | } else { 228 | connectBackoff *= connectBackoff / (connectBackoff / 2); 229 | } 230 | // Clamp the backoff to 5 minutes. 231 | if (connectBackoff > 300) { 232 | connectBackoff = 300; 233 | } 234 | 235 | // If we don't have a serverConnector, try to set one up and subscribe a listener. 236 | if (serverConnector == null) { 237 | Map environment = new HashMap(); 238 | if (password != null && username != null) { 239 | environment.put(JMXConnector.CREDENTIALS, new String[]{username, password}); 240 | } 241 | environment.put(JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER, this.getClass().getClassLoader()); 242 | 243 | try { 244 | serverConnector = JMXConnectorFactory.newJMXConnector(serviceURL, environment); 245 | if (forceSynchronous) { 246 | serverConnector = new SynchronousConnectorAdapter(serverConnector, Connection.this); 247 | } 248 | serverConnector.addConnectionNotificationListener(Connection.this, null, connectionUuid); 249 | serverConnector.addConnectionNotificationListener(notificationListener, null, connectionUuid); 250 | if (logger.isLoggable(Level.FINE)) { 251 | logger.fine("Invoking " + serverConnector.getClass().getName() + ".connect() on " + Thread.currentThread().getName()); 252 | } 253 | serverConnector.connect(environment); 254 | } catch (IOException ioe) { 255 | logger.warning("Could not connect to : " + rawUrl + " exception message: " + ioe.getMessage()); 256 | close(); 257 | logger.info("Scheduling reconnect to: " + rawUrl + " in " + connectBackoff + " seconds."); 258 | ConnectTask backoffConnect = new ConnectTask(connectBackoff); 259 | connectTimer.schedule(backoffConnect, backoffConnect.getDelay()); 260 | } 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/FastJMX.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import com.e_gineering.collectd.logging.CollectdLogHandler; 4 | import org.collectd.api.Collectd; 5 | import org.collectd.api.CollectdConfigInterface; 6 | import org.collectd.api.CollectdInitInterface; 7 | import org.collectd.api.CollectdReadInterface; 8 | import org.collectd.api.CollectdShutdownInterface; 9 | import org.collectd.api.DataSet; 10 | import org.collectd.api.OConfigItem; 11 | import org.collectd.api.OConfigValue; 12 | 13 | import javax.management.MBeanServerNotification; 14 | import javax.management.MalformedObjectNameException; 15 | import javax.management.Notification; 16 | import javax.management.NotificationListener; 17 | import javax.management.ObjectName; 18 | import javax.management.remote.JMXConnectionNotification; 19 | import javax.management.remote.JMXServiceURL; 20 | import java.io.IOException; 21 | import java.net.MalformedURLException; 22 | import java.util.ArrayList; 23 | import java.util.Collections; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Set; 27 | import java.util.UUID; 28 | import java.util.concurrent.TimeUnit; 29 | import java.util.logging.Level; 30 | import java.util.logging.Logger; 31 | 32 | /** 33 | * FastJMX is a Collectd plugin that allows for lower latency read cycles on remote JMX hosts. 34 | * It does so by maintaining a threadpool which is used to read attribute values from remote hosts during a read 35 | * cycle. The thread pool uses a histogram of the recent collections to project the most efficient pool size to 36 | * minimize read latency. 37 | */ 38 | public class FastJMX implements CollectdConfigInterface, CollectdInitInterface, CollectdReadInterface, CollectdShutdownInterface, NotificationListener { 39 | 40 | private long reads; 41 | private SelfTuningCollectionExecutor executor; 42 | 43 | private static List attributes = new ArrayList(); 44 | private static HashMap connections = new HashMap(); 45 | 46 | private static List collectablePermutations = 47 | Collections.synchronizedList(new ArrayList(100)); 48 | 49 | private static Logger logger = Logger.getLogger(FastJMX.class.getPackage().getName()); 50 | private static CollectdLogHandler handler = new CollectdLogHandler(); 51 | 52 | static { 53 | System.getProperties().put("sun.rmi.transport.tcp.connectTimeout", TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS)); 54 | System.getProperties().put("sun.rmi.transport.tcp.handshakeTimeout", TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS)); 55 | System.getProperties().put("sun.rmi.transport.tcp.responseTimeout", TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS)); 56 | 57 | // Configure java.util.logging 58 | logger.addHandler(handler); 59 | logger.setLevel(Level.INFO); 60 | } 61 | 62 | public FastJMX() { 63 | Collectd.registerConfig("FastJMX", this); 64 | Collectd.registerInit("FastJMX", this); 65 | Collectd.registerRead("FastJMX", this); 66 | Collectd.registerShutdown("FastJMX", this); 67 | } 68 | 69 | /** 70 | * Parses a config structure like the one below into the parts necessary to collect the information. 71 | *

72 | * Note the following changes from the GenericJMX Configuration: 73 | *

    74 | *
  • MaxThreads: Changes the maximum number of threads to allow. Default is 512.
  • 75 | *
  • CollectInternal: Reports internal metrics from FastJMX back to Collectd.
  • 76 | *
  • "MBean", "MXBean", and "Bean" are now interchangeable. This plugin also works with MXBeans.
  • 77 | *
  • The Hostname is auto-detected from the ServiceURL unless specified as the "string" portion of the Connection 78 | * definition. The "host" property is still supported for backwards-compatibility.
  • 79 | *
  • The Value block's string value is added to the "Attribute" list. The "Attribute" property is still supported for 80 | * backards-compatibility.
  • 81 | *
  • "table" and "composite" are aliases for each other.
  • 82 | *
  • "user" and "username" are aliases for each other.
  • 83 | *
  • "ttl" may be added to the 'Connection' definition. After a connection is live for this amount of time, it will be closed and re-opened.
  • 84 | *
  • "forceLoggingTo" will force all logging output to this Collectd log level.
  • 85 | *
  • "logLevel" Changes the internal java logging level (default is INFO). Can be used in conjunction with 'forceLoggingTo' to have FastJMX log more verbosely without affecting other collectd plugins.
  • 86 | *
87 | *
 88 | 	 * <Plugin "FastJMX">
 89 | 	 *   MaxThreads 384
 90 | 	 *   CollectInternal true
 91 | 	 *   ForceLoggingTo INFO
 92 | 	 *   loglevel FINEST
 93 | 	 *
 94 | 	 *   <MBean/MXBean/Bean "alias">
 95 | 	 *     ObjectName "java.lang:type=MemoryPool,*"
 96 | 	 *     InstancePrefix "memory_pool-"
 97 | 	 *     InstanceFrom "name"
 98 | 	 *
 99 | 	 *     <Value "Usage">
100 | 	 *       Type "gauge"
101 | 	 *       #InstancePrefix "value-"
102 | 	 *       #InstanceFrom "name"
103 | 	 *       Table|Composite true
104 | 	 *       #Attribute "Usage"
105 | 	 *       #Attribute min
106 | 	 *       #Attribute max
107 | 	 *       #Attribute full
108 | 	 *     </Value>
109 | 	 *   </MBean/Bean>
110 | 	 *
111 | 	 *   <Connection "host">
112 | 	 *       ServiceURL "service:jmx:rmi:///jndi/rmi://localhost:8675/jmxrmi"
113 | 	 *       Collect "alias"
114 | 	 *       User|Username "admin"
115 | 	 *       Password "foobar"
116 | 	 *       InstancePrefix "foo-"
117 | 	 *       ttl 300
118 | 	 *   </Connection>
119 | 	 * </Plugin>
120 | 	 * 
121 | */ 122 | public int config(final OConfigItem ci) { 123 | int maxThreads = 512; 124 | boolean collectInternal = false; 125 | 126 | for (OConfigItem pluginChild : ci.getChildren()) { 127 | if ("maxthreads".equalsIgnoreCase(pluginChild.getKey())) { 128 | maxThreads = getConfigNumber(pluginChild, maxThreads).intValue(); 129 | } else if ("collectinternal".equalsIgnoreCase(pluginChild.getKey())) { 130 | collectInternal = getConfigBoolean(pluginChild); 131 | } else if ("forceLoggingTo".equalsIgnoreCase(pluginChild.getKey())) { 132 | int forceLoggingTo = -1; 133 | String level = getConfigString(pluginChild); 134 | if (level.equalsIgnoreCase("ERROR")) { 135 | forceLoggingTo = Collectd.LOG_ERR; 136 | } else if (level.equalsIgnoreCase("WARNING")) { 137 | forceLoggingTo = Collectd.LOG_WARNING; 138 | } else if (level.equalsIgnoreCase("NOTICE")) { 139 | forceLoggingTo = Collectd.LOG_NOTICE; 140 | } else if (level.equalsIgnoreCase("INFO")) { 141 | forceLoggingTo = Collectd.LOG_INFO; 142 | } else if (level.equalsIgnoreCase("DEBUG")) { 143 | forceLoggingTo = Collectd.LOG_DEBUG; 144 | } else { 145 | logger.warning("Unable to force collectd logging level: '" + level + "' is not ERROR, WARNING, NOTICE, INFO, or DEBUG."); 146 | } 147 | handler.forceAllLoggingTo(forceLoggingTo); 148 | } else if ("loglevel".equalsIgnoreCase(pluginChild.getKey())) { 149 | String javaLevel = getConfigString(pluginChild); 150 | 151 | try { 152 | logger.setLevel(Level.parse(javaLevel)); 153 | } catch (IllegalArgumentException iae) { 154 | logger.severe("Unable to parse java Logging level from: '" + javaLevel + "'"); 155 | } 156 | } else if ("mbean".equalsIgnoreCase(pluginChild.getKey()) || "mxbean".equalsIgnoreCase(pluginChild.getKey()) || "bean".equalsIgnoreCase(pluginChild.getKey())) { 157 | String beanAlias = getConfigString(pluginChild).toLowerCase(); 158 | ObjectName matchObjectName = null; 159 | String beanInstancePrefix = null; 160 | List beanInstanceFrom = new ArrayList(); 161 | 162 | for (OConfigItem beanChild : pluginChild.getChildren()) { 163 | if ("objectname".equalsIgnoreCase(beanChild.getKey())) { 164 | try { 165 | matchObjectName = new ObjectName(getConfigString(beanChild)); 166 | } catch (MalformedObjectNameException ne) { 167 | matchObjectName = null; 168 | } 169 | } else if ("instanceprefix".equalsIgnoreCase(beanChild.getKey())) { 170 | beanInstancePrefix = getConfigString(beanChild); 171 | } else if ("instancefrom".equalsIgnoreCase(beanChild.getKey())) { 172 | beanInstanceFrom.add(getConfigString(beanChild)); 173 | } else if ("value".equalsIgnoreCase(beanChild.getKey())) { 174 | List valueAttributes = new ArrayList(); 175 | if (getConfigString(beanChild) != null) { 176 | valueAttributes.add(getConfigString(beanChild)); 177 | } 178 | 179 | DataSet valueDs = null; 180 | String valueInstancePrefix = null; 181 | List valueInstanceFrom = new ArrayList(); 182 | boolean composite = false; 183 | String pluginName = null; 184 | List beanAttributes = new ArrayList(); 185 | 186 | for (OConfigItem valueChild : beanChild.getChildren()) { 187 | if ("attribute".equalsIgnoreCase(valueChild.getKey())) { 188 | valueAttributes.add(getConfigString(valueChild)); 189 | } else if ("type".equalsIgnoreCase(valueChild.getKey())) { 190 | valueDs = Collectd.getDS(getConfigString(valueChild)); 191 | } else if ("instanceprefix".equalsIgnoreCase(valueChild.getKey())) { 192 | valueInstancePrefix = getConfigString(valueChild); 193 | } else if ("instancefrom".equalsIgnoreCase(valueChild.getKey())) { 194 | valueInstanceFrom.add(getConfigString(valueChild)); 195 | } else if ("table".equalsIgnoreCase(valueChild.getKey()) || "composite".equalsIgnoreCase(valueChild.getKey())) { 196 | composite = getConfigBoolean(valueChild); 197 | } else if ("pluginname".equalsIgnoreCase(valueChild.getKey())) { 198 | pluginName = getConfigString(valueChild); 199 | } 200 | } 201 | if (logger.isLoggable(Level.FINE)) { 202 | logger.fine("Adding " + beanAlias + " for " + matchObjectName); 203 | } 204 | 205 | // Adds the attribute definition. 206 | beanAttributes.add(new Attribute(valueAttributes, pluginName, valueDs, valueInstancePrefix, valueInstanceFrom, 207 | composite, beanAlias, matchObjectName, 208 | beanInstancePrefix, beanInstanceFrom)); 209 | 210 | // Make sure the number of attributes matches the number of datasource values. 211 | if (valueDs.getDataSources().size() == valueAttributes.size()) { 212 | attributes.addAll(beanAttributes); 213 | } else { 214 | logger.severe("The data set for bean '" + beanAlias + "' of type '" 215 | + valueDs.getType() + "' has " + valueDs.getDataSources().size() 216 | + " data sources, but there were " + valueAttributes.size() 217 | + " attributes configured. This bean will not be collected!"); 218 | } 219 | } 220 | } 221 | } else if ("connection".equalsIgnoreCase(pluginChild.getKey())) { 222 | String hostName = getConfigString(pluginChild); 223 | boolean hostnamePort = false; 224 | String rawUrl = null; 225 | JMXServiceURL serviceURL = null; 226 | String username = null; 227 | String password = null; 228 | String connectionInstancePrefix = null; 229 | List beanAliases = new ArrayList(); 230 | long ttl = -1; 231 | boolean forceSynchronous = false; 232 | 233 | for (OConfigItem connectionChild : pluginChild.getChildren()) { 234 | if ("user".equalsIgnoreCase(connectionChild.getKey()) || "username".equalsIgnoreCase(connectionChild.getKey())) { 235 | username = getConfigString(connectionChild); 236 | } else if ("password".equalsIgnoreCase(connectionChild.getKey())) { 237 | password = getConfigString(connectionChild); 238 | } else if ("instanceprefix".equalsIgnoreCase(connectionChild.getKey())) { 239 | connectionInstancePrefix = getConfigString(connectionChild); 240 | } else if ("serviceurl".equalsIgnoreCase(connectionChild.getKey())) { 241 | rawUrl = getConfigString(connectionChild); 242 | try { 243 | serviceURL = new JMXServiceURL(rawUrl); 244 | } catch (MalformedURLException me) { 245 | logger.severe("ServiceURL definition [" + getConfigString(connectionChild) + "] is invalid: " + me.getMessage()); 246 | serviceURL = null; 247 | } 248 | } else if ("collect".equalsIgnoreCase(connectionChild.getKey())) { 249 | beanAliases.add(getConfigString(connectionChild).toLowerCase()); 250 | } else if ("includeportinhostname".equalsIgnoreCase(connectionChild.getKey())) { 251 | hostnamePort = getConfigBoolean(connectionChild); 252 | } else if ("ttl".equalsIgnoreCase("ttl")) { 253 | ttl = getConfigNumber(connectionChild, ttl).longValue(); 254 | } else if ("synchronous".equalsIgnoreCase("synchronous")) { 255 | forceSynchronous = getConfigBoolean(connectionChild).booleanValue(); 256 | } 257 | } 258 | 259 | if (serviceURL != null) { 260 | int port = serviceURL.getPort(); 261 | if (!beanAliases.isEmpty()) { 262 | // Try to parse the host name from the serviceURL, if none was defined. 263 | if (hostName == null) { 264 | hostName = serviceURL.getHost(); 265 | 266 | // If that didn't work, try shortening the URL to something more manageable. 267 | if ((hostName == null || "".equals(hostName)) && rawUrl.lastIndexOf("://") > rawUrl.lastIndexOf(":///")) { 268 | try { 269 | JMXServiceURL shortUrl = 270 | new JMXServiceURL(rawUrl.substring(0, rawUrl.indexOf(":///")) 271 | + rawUrl.substring(rawUrl.lastIndexOf("://"))); 272 | hostName = shortUrl.getHost(); 273 | port = shortUrl.getPort(); 274 | } catch (MalformedURLException me) { 275 | hostName = Collectd.getHostname(); 276 | logger.warning("Unable to parse hostname from JMX service URL: [" + rawUrl + "]. Falling back to hostname reported by this collectd instance: [" + hostName + "]"); 277 | } 278 | } 279 | } 280 | 281 | if (hostnamePort) { 282 | hostName = hostName + "@" + port; 283 | } 284 | 285 | // JBoss remoting workaround. 286 | if (rawUrl.contains("remoting-jmx://")) { 287 | forceSynchronous = true; 288 | } 289 | 290 | // Now create the Connection and put it into our hashmap. 291 | Connection c = new Connection(this, rawUrl, hostName, serviceURL, username, password, connectionInstancePrefix, beanAliases, ttl, forceSynchronous); 292 | connections.put(c.getUUID(), c); 293 | } else { 294 | logger.severe("Excluding Connection for : " + serviceURL.toString() + ". No beans to collect."); 295 | } 296 | } else { 297 | logger.warning("Excluding host definition no ServiceURL defined."); 298 | } 299 | } else { 300 | logger.severe("Unknown config option: " + pluginChild.getKey()); 301 | } 302 | } 303 | 304 | this.executor = new SelfTuningCollectionExecutor(maxThreads, collectInternal); 305 | 306 | return 0; 307 | } 308 | 309 | 310 | /** 311 | * Attempts to open connections to all configured Connections. 312 | */ 313 | public int init() { 314 | this.reads = 0; 315 | 316 | // Open connections. 317 | for (Connection connectionEntry : connections.values()) { 318 | connectionEntry.connect(); 319 | } 320 | 321 | return 0; 322 | } 323 | 324 | 325 | /** 326 | * Attempts to read all identified permutations of beans for each connection before the next (interval - 500ms). 327 | * Any attributes not read by that time will be cancelled and no metrics will be gathered for those points. 328 | * 329 | * @return 0. Always 0. 330 | */ 331 | public int read() { 332 | if (logger.isLoggable(Level.FINE)) { 333 | logger.fine("FastJMX plugin: read()..."); 334 | } 335 | try { 336 | // Rollover in case we're _really_ long running. 337 | if (reads++ == Long.MAX_VALUE) { 338 | reads = 1; 339 | } 340 | 341 | synchronized (collectablePermutations) { 342 | // Make sure the most latent attributes are the first ones to be collected. 343 | Collections.sort(collectablePermutations); 344 | try { 345 | executor.invokeAll(collectablePermutations); 346 | } catch (InterruptedException ie) { 347 | logger.warning("Interrupted during read() cycle."); 348 | } 349 | } 350 | } catch (Throwable t) { 351 | logger.severe("Unexpected Throwable: " + t); 352 | } 353 | return 0; 354 | } 355 | 356 | // Remove any notification listeners... clean up our stuffs then stop. 357 | public int shutdown() { 358 | executor.shutdown(); 359 | 360 | for (Connection connectionEntry : connections.values()) { 361 | logger.info("Closing connection to: " + connectionEntry.getRawUrl()); 362 | connectionEntry.close(); 363 | } 364 | 365 | return 0; 366 | } 367 | 368 | /** 369 | * Creates AttributePermutations for the given connection, querying the remote MBeanServer for ObjectNames matching 370 | * defined collect attributes for the connection. 371 | * 372 | * @param connection The Connection to create permutations and start collecting. 373 | */ 374 | private void createPermutations(final Connection connection) { 375 | if (logger.isLoggable(Level.FINE)) { 376 | logger.fine("Creating AttributePermutations for " + connection.getRawUrl()); 377 | } 378 | // Create the AttributePermutation objects appropriate for this Connection. 379 | for (Attribute attrib : attributes) { 380 | // If the host is supposed to collect this attribute, look for matching objectNames on the host. 381 | if (connection.getBeanAliases().contains(attrib.getBeanAlias())) { 382 | if (logger.isLoggable(Level.FINE)) { 383 | logger.fine("Looking for " + attrib.getObjectName() + " @ " + connection.getRawUrl()); 384 | } 385 | try { 386 | Set instances = 387 | connection.getServerConnection().queryNames(attrib.getObjectName(), null); 388 | if (logger.isLoggable(Level.FINE)) { 389 | logger.fine("Found " + instances.size() + " instances of " + attrib.getObjectName() + " @ " + connection.getRawUrl()); 390 | } 391 | 392 | // Do the slowish part outside of the synchronized block. 393 | List permutations = AttributePermutation.create(instances.toArray(new ObjectName[instances.size()]), connection, attrib); 394 | synchronized (collectablePermutations) { 395 | collectablePermutations.addAll(permutations); 396 | } 397 | } catch (IOException ioe) { 398 | logger.severe("Failed to find " + attrib.getObjectName() + " @ " + connection.getRawUrl() + " Exception message: " + ioe.getMessage()); 399 | } 400 | } 401 | } 402 | } 403 | 404 | /** 405 | * Removes all AttributePermutations for the given collection from our list of things to collect in a read cycle. 406 | * 407 | * @param connection The Connection to no longer collect. 408 | */ 409 | private void removePermutations(final Connection connection) { 410 | if (logger.isLoggable(Level.FINE)) { 411 | logger.fine("Removing AttributePermutations for " + connection.getRawUrl()); 412 | } 413 | // Remove the AttributePermutation objects appropriate for this Connection. 414 | ArrayList toRemove = new ArrayList(); 415 | synchronized (collectablePermutations) { 416 | for (AttributePermutation permutation : collectablePermutations) { 417 | if (permutation.getConnection().equals(connection)) { 418 | toRemove.add(permutation); 419 | } 420 | } 421 | collectablePermutations.removeAll(toRemove); 422 | } 423 | } 424 | 425 | /** 426 | * Creates AttributePermutations matching the connection and objectName. 427 | * 428 | * @param connection The Connection. 429 | * @param objectName The name of an MBean, which may or may not match the attributes we're supposed to collect for 430 | * the connection. 431 | */ 432 | private void createPermutations(final Connection connection, final ObjectName objectName) { 433 | for (Attribute attribute : attributes) { 434 | // If the host is supposed to collect this attribute, and the objectName matches the attribute, add the permutation. 435 | if (connection.getBeanAliases().contains(attribute.getBeanAlias()) && attribute.getObjectName().apply(objectName)) { 436 | List permutations = AttributePermutation.create(new ObjectName[]{objectName}, connection, attribute); 437 | synchronized (collectablePermutations) { 438 | collectablePermutations.addAll(permutations); 439 | } 440 | } 441 | } 442 | } 443 | 444 | /** 445 | * Removes AttributePermutations matching the connection and objectName. 446 | * 447 | * @param connection The connection. 448 | * @param objectName The name of an MBean which may or may not match AttributePermutations being collected. 449 | */ 450 | private void removePermutations(final Connection connection, final ObjectName objectName) { 451 | synchronized (collectablePermutations) { 452 | ArrayList toRemove = new ArrayList(); 453 | for (AttributePermutation permutation : collectablePermutations) { 454 | if (permutation.getConnection().equals(connection) && permutation.getObjectName().equals(objectName)) { 455 | toRemove.add(permutation); 456 | } 457 | } 458 | collectablePermutations.removeAll(toRemove); 459 | } 460 | } 461 | 462 | /** 463 | * Handles JMXConnectionNotifications from Connection objects. 464 | * 465 | * @param notification The notification event 466 | * @param handback The Connection 467 | */ 468 | public void handleNotification(final Notification notification, final Object handback) { 469 | if (notification instanceof JMXConnectionNotification) { 470 | final Connection connection = connections.get(handback); 471 | 472 | // If we get a connection opened, assume that the connection previously failed and we weren't notified. 473 | // This can happen if you're running collectd in a VM and you suspend the VM for a long period of time. 474 | if (notification.getType().equals(JMXConnectionNotification.OPENED)) { 475 | removePermutations(connection); 476 | createPermutations(connection); 477 | } else if (notification.getType().equals(JMXConnectionNotification.CLOSED) || 478 | notification.getType().equals(JMXConnectionNotification.FAILED)) { 479 | // Remove the permutations and reconnect. 480 | removePermutations(connection); 481 | connection.connect(); 482 | } 483 | } else if (notification instanceof MBeanServerNotification) { 484 | Connection connection = connections.get(handback); 485 | MBeanServerNotification serverNotification = (MBeanServerNotification) notification; 486 | 487 | // A bean was added by a remote connection. Check it against the ObjectNames we're instrumenting. 488 | if (notification.getType().equals(MBeanServerNotification.REGISTRATION_NOTIFICATION)) { 489 | createPermutations(connection, serverNotification.getMBeanName()); 490 | } else if (notification.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) { 491 | removePermutations(connection, serverNotification.getMBeanName()); 492 | } 493 | } 494 | } 495 | 496 | /** 497 | * Gets the first value (if it exists) from the OConfigItem as a String 498 | * 499 | * @param ci 500 | * @return The string, or null if no string is found. 501 | */ 502 | private static String getConfigString(final OConfigItem ci) { 503 | List values; 504 | OConfigValue v; 505 | 506 | values = ci.getValues(); 507 | if (values.size() != 1) { 508 | return null; 509 | } 510 | 511 | v = values.get(0); 512 | if (v.getType() != OConfigValue.OCONFIG_TYPE_STRING) { 513 | return null; 514 | } 515 | 516 | return (v.getString()); 517 | } 518 | 519 | /** 520 | * Gets the first value (if it exists) from the OConfigItem as an int. 521 | * 522 | * @param ci 523 | * @param def A default value to return if no Number is found. 524 | * @return The int, or the value of def if no int is found. 525 | */ 526 | private static Number getConfigNumber(final OConfigItem ci, final Number def) { 527 | List values; 528 | OConfigValue v; 529 | 530 | values = ci.getValues(); 531 | if (values.size() != 1) { 532 | return def; 533 | } 534 | 535 | v = values.get(0); 536 | if (v.getType() != OConfigValue.OCONFIG_TYPE_NUMBER) { 537 | return def; 538 | } 539 | return v.getNumber(); 540 | } 541 | 542 | /** 543 | * Gets the first value (if it exists) from the OConfigItem as a Boolean 544 | * 545 | * @param ci 546 | * @return The Boolean value, or null if no boolean is found. 547 | */ 548 | private static Boolean getConfigBoolean(final OConfigItem ci) { 549 | List values; 550 | OConfigValue v; 551 | 552 | values = ci.getValues(); 553 | if (values.size() != 1) { 554 | return false; 555 | } 556 | 557 | v = values.get(0); 558 | if (v.getType() != OConfigValue.OCONFIG_TYPE_BOOLEAN) { 559 | return false; 560 | } 561 | 562 | return (new Boolean(v.getBoolean())); 563 | } 564 | } -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/ReadCycleResult.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.logging.Level; 5 | import java.util.logging.Logger; 6 | 7 | /** 8 | * Results of a read() cycle. 9 | */ 10 | public class ReadCycleResult { 11 | 12 | private static Logger logger = Logger.getLogger(ReadCycleResult.class.getPackage().getName()); 13 | 14 | private long started = 0; 15 | private long ended = 0; 16 | private long duration = 0; 17 | private long interval = 0; 18 | private int poolSize = 0; 19 | private int failed = 0; 20 | private int cancelled = 0; 21 | private int success = 0; 22 | private int total = 0; 23 | 24 | public ReadCycleResult(final int failed, final int cancelled, final int success, final long started, final long ended, final int poolSize, final long interval) { 25 | this.failed = failed; 26 | this.cancelled = cancelled; 27 | this.success = success; 28 | this.started = started; 29 | this.ended = ended; 30 | this.total = failed + cancelled + success; 31 | this.duration = ended - started; 32 | this.poolSize = poolSize; 33 | this.interval = TimeUnit.NANOSECONDS.convert(interval, TimeUnit.MILLISECONDS); 34 | } 35 | 36 | public int getPoolSize() { 37 | return poolSize; 38 | } 39 | 40 | public int getTotal() { 41 | return total; 42 | } 43 | 44 | public int hashCode() { 45 | return new Double(((success + failed) / duration) * Math.pow(2, poolSize)).hashCode(); 46 | } 47 | 48 | public long getStarted() { 49 | return started; 50 | } 51 | 52 | /** 53 | * Comparing this cycle to the other one, should we recalculate for optimal pool size? 54 | * 55 | * @param previousCycle The previous cycle to baseline against. 56 | * @return true if a recalculation should occur. 57 | */ 58 | public boolean triggerRecalculate(ReadCycleResult previousCycle) { 59 | if (previousCycle != null) { 60 | if (previousCycle.poolSize < poolSize) { 61 | if (logger.isLoggable(Level.FINE)) { 62 | logger.fine("Triggering recalculation due to pool growth."); 63 | } 64 | return true; 65 | } else if (this.cancelled > 0 && previousCycle.cancelled > 0 && this.cancelled > previousCycle.cancelled) { 66 | if (logger.isLoggable(Level.FINE)) { 67 | logger.fine("Triggering recalculation due to consecutive increases in cancellations."); 68 | } 69 | return true; 70 | } 71 | } 72 | return false; 73 | } 74 | 75 | public int getCancelled() { 76 | return cancelled; 77 | } 78 | 79 | /** 80 | * Returns a Double between 0 and 2.0 to serve as the jacobian weight for this ReadCycleResult. 81 | * (total - cancellations / total) * ((interval - duration) / interval) 82 | * 83 | * @return a double value to serve as jacobian weights for this ReadCycleResult. 84 | */ 85 | public double getWeight() { 86 | return (((double) total - cancelled) / total) + (((double) interval - duration) / interval); 87 | } 88 | 89 | public long getDurationMs() { 90 | return TimeUnit.MILLISECONDS.convert(duration, TimeUnit.NANOSECONDS); 91 | } 92 | 93 | public String toString() { 94 | return "[failed: " + failed + ", canceled: " + cancelled + ", success: " + success + "] Took " + TimeUnit.MILLISECONDS.convert(duration, TimeUnit.NANOSECONDS) + "ms in a pool of " + poolSize + " threads."; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/SelfTuningCollectionExecutor.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import org.apache.commons.math3.analysis.MultivariateMatrixFunction; 4 | import org.apache.commons.math3.analysis.MultivariateVectorFunction; 5 | import org.apache.commons.math3.analysis.UnivariateFunction; 6 | import org.apache.commons.math3.fitting.leastsquares.LeastSquaresBuilder; 7 | import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer; 8 | import org.apache.commons.math3.fitting.leastsquares.LeastSquaresProblem; 9 | import org.apache.commons.math3.fitting.leastsquares.LevenbergMarquardtOptimizer; 10 | import org.apache.commons.math3.linear.DiagonalMatrix; 11 | import org.apache.commons.math3.linear.RealMatrix; 12 | import org.apache.commons.math3.linear.RealVector; 13 | import org.apache.commons.math3.optim.MaxEval; 14 | import org.apache.commons.math3.optim.MaxIter; 15 | import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; 16 | import org.apache.commons.math3.optim.univariate.BrentOptimizer; 17 | import org.apache.commons.math3.optim.univariate.SearchInterval; 18 | import org.apache.commons.math3.optim.univariate.UnivariateObjectiveFunction; 19 | import org.apache.commons.math3.optim.univariate.UnivariatePointValuePair; 20 | import org.collectd.api.Collectd; 21 | import org.collectd.api.PluginData; 22 | import org.collectd.api.ValueList; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.Collections; 27 | import java.util.Comparator; 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.concurrent.CancellationException; 31 | import java.util.concurrent.ExecutionException; 32 | import java.util.concurrent.Future; 33 | import java.util.concurrent.LinkedBlockingQueue; 34 | import java.util.concurrent.ThreadFactory; 35 | import java.util.concurrent.ThreadPoolExecutor; 36 | import java.util.concurrent.TimeUnit; 37 | import java.util.logging.Level; 38 | import java.util.logging.Logger; 39 | 40 | 41 | /** 42 | * A class that implements a ring buffer histogram of ReadCycleResults, encapsulates a ThreadPoolExecutor, and 43 | * uses least squares estimation to divine an optimum pool size. 44 | *

45 | * The basic premise is that given the command to invoke all AttributePermutations, the pool should be tuned so that the 46 | * next invocation of invokeAll() has a better chance of success prior to time-out than the current invocation had. 47 | *

48 | * The actual goal, of course, is to find the optimal number of threads to execute the given tasks with. 49 | */ 50 | public class SelfTuningCollectionExecutor { 51 | 52 | private static Logger logger = Logger.getLogger(SelfTuningCollectionExecutor.class.getName()); 53 | 54 | private static ThreadGroup fastJMXThreads = new ThreadGroup("FastJMX"); 55 | private static ThreadGroup mbeanReaders = new ThreadGroup(fastJMXThreads, "MbeanReaders"); 56 | private static long loaded = System.nanoTime(); 57 | 58 | private static Comparator numberComparator = new Comparator() { 59 | public int compare(Number o1, Number o2) { 60 | return Double.compare(o1.doubleValue(), o2.doubleValue()); 61 | } 62 | }; 63 | 64 | private ReadCycleResult[] ring; 65 | private int index; 66 | 67 | private ThreadPoolExecutor threadPool; 68 | private int maxThreads; 69 | 70 | private int minIndependent; 71 | private boolean recalculateOptimum; 72 | 73 | private long interval = 0l; 74 | 75 | private ArrayList dispatchable = new ArrayList(); 76 | 77 | private ValueList fastJMXCycle; 78 | private ValueList fastJMXLatency; 79 | private ValueList fastJMXGauge; 80 | 81 | 82 | // Seed for a fibonacci sequence, which is used to manipulate pool sizing in search of data points for analysis. 83 | int fiba = 1; 84 | int fibb = 0; 85 | 86 | public SelfTuningCollectionExecutor(final int maximumThreads, final boolean collectInternal) { 87 | ring = new ReadCycleResult[45]; 88 | minIndependent = 7; 89 | maxThreads = maximumThreads; 90 | threadPool = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, 91 | new LinkedBlockingQueue(), new FastJMXThreadFactory()); 92 | threadPool.allowCoreThreadTimeOut(true); 93 | threadPool.setMaximumPoolSize(maximumThreads); 94 | 95 | this.clear(); 96 | 97 | fastJMXCycle = null; 98 | fastJMXLatency = null; 99 | if (collectInternal) { 100 | PluginData fastJMXPd = new PluginData(); 101 | fastJMXPd.setHost("localhost"); 102 | fastJMXPd.setPlugin("FastJMX"); 103 | 104 | fastJMXGauge = new ValueList(fastJMXPd); 105 | fastJMXGauge.setType(Collectd.getDS("gauge").getType()); 106 | 107 | if (Collectd.getDS("fastjmx_cycle") == null) { 108 | logger.severe("Cannot collect internal metrics. Please ensure types.db contains: 'fastjmx_cycle value:GAUGE:0:U'."); 109 | return; 110 | } 111 | 112 | if (Collectd.getDS("fastjmx_latency") == null) { 113 | logger.severe("Cannot collect internal metrics. Please ensure types.db contains: 'fastjmx_latency value:GAUGE:0:U'."); 114 | return; 115 | } 116 | 117 | fastJMXCycle = new ValueList(fastJMXPd); 118 | fastJMXCycle.setType(Collectd.getDS("fastjmx_cycle").getType()); 119 | fastJMXLatency = new ValueList(fastJMXPd); 120 | fastJMXLatency.setType(Collectd.getDS("fastjmx_latency").getType()); 121 | } 122 | } 123 | 124 | /** 125 | * Resets the histogram and pool sizes to their initial states. 126 | */ 127 | private void clear() { 128 | recalculateOptimum = true; 129 | resetFibonacci(); 130 | 131 | for (int i = 0; i < ring.length; i++) { 132 | ring[i] = null; 133 | } 134 | index = 0; 135 | } 136 | 137 | /** 138 | * Adds a new ReadCycleResult for consideration in future executions. 139 | * 140 | * @param cycle 141 | */ 142 | private void push(ReadCycleResult cycle) { 143 | if (cycle == null) { 144 | throw new IllegalArgumentException("Histogram does not support pushing 'null' values."); 145 | } 146 | 147 | if (logger.isLoggable(Level.FINE)) { 148 | logger.fine(cycle.toString()); 149 | } 150 | 151 | if (cycle.getTotal() <= 0) { 152 | return; 153 | } else if (cycle.getCancelled() > 0) { 154 | logger.warning("Failed to collect " + cycle.getCancelled() + " of " + cycle.getTotal() + " samples within read interval with " + threadPool.getCorePoolSize() + " threads."); 155 | } 156 | 157 | if (cycle.triggerRecalculate(peek())) { 158 | recalculateOptimum = true; 159 | } 160 | 161 | // Modify the ring buffer. 162 | ring[index] = cycle; 163 | index = (index + 1) % ring.length; 164 | 165 | if (recalculateOptimum) { 166 | int threadCount = calculateOptimum(); 167 | 168 | if (threadCount != threadPool.getCorePoolSize()) { 169 | if (logger.isLoggable(Level.FINE)) { 170 | logger.fine("Setting thread pool size: " + threadCount); 171 | } 172 | threadPool.setCorePoolSize(threadCount); 173 | threadPool.setMaximumPoolSize(threadCount); 174 | } 175 | } 176 | } 177 | 178 | /** 179 | * Shuts down the thread pool 180 | */ 181 | public void shutdown() { 182 | threadPool.shutdown(); 183 | try { 184 | // Wait a while for existing tasks to terminate 185 | if (!threadPool.awaitTermination(interval, TimeUnit.MILLISECONDS)) { 186 | threadPool.shutdownNow(); // Cancel currently executing tasks 187 | // Wait a while for tasks to respond to being cancelled 188 | if (!threadPool.awaitTermination(interval, TimeUnit.MILLISECONDS)) { 189 | logger.warning("ThreadPool did not terminate cleanly."); 190 | } 191 | } 192 | } catch (InterruptedException ie) { 193 | // (Re-)Cancel if current thread also interrupted 194 | threadPool.shutdownNow(); 195 | // Preserve interrupt status 196 | Thread.currentThread().interrupt(); 197 | } 198 | } 199 | 200 | /** 201 | * Invokes the AttributePermutations with the thread pool executor and returns the results. 202 | * 203 | * @param tasks The AttributePermutations to collect. 204 | * @return A list of java Futures for each of the tasks. 205 | * @throws InterruptedException If the thread is interrupted while waiting for the tasks to execute. 206 | */ 207 | public List> invokeAll(List tasks) throws InterruptedException { 208 | long start = System.nanoTime(); 209 | List> results; 210 | try { 211 | ReadCycleResult previousCycle = peek(); 212 | interval = 213 | TimeUnit.MILLISECONDS.convert((start - (previousCycle != null ? previousCycle.getStarted() : loaded)), TimeUnit.NANOSECONDS); 214 | if (interval * 2 > 0) { 215 | threadPool.setKeepAliveTime(interval * 2, TimeUnit.MILLISECONDS); 216 | } 217 | results = threadPool.invokeAll(tasks, interval - 500, TimeUnit.MILLISECONDS); 218 | } finally { 219 | threadPool.purge(); 220 | } 221 | 222 | int failed = 0; 223 | int cancelled = 0; 224 | int success = 0; 225 | 226 | for (int i = 0; i < results.size(); i++) { 227 | Future result = results.get(i); 228 | try { 229 | AttributePermutation attribute = result.get(); 230 | if (attribute.getConsecutiveNotFounds() > 0) { 231 | failed++; 232 | logger.warning("Failed to collect: " + attribute.getObjectName() + "@" + attribute.getConnection().getRawUrl() + " InstanceNotFound consecutive count=" + attribute.getConsecutiveNotFounds()); 233 | } else { 234 | dispatchable.addAll(result.get().getValues()); 235 | success++; 236 | } 237 | } catch (ExecutionException ex) { 238 | failed++; 239 | logger.warning("Failed to collect: " + ex.getCause()); 240 | } catch (CancellationException ce) { 241 | cancelled++; 242 | } catch (InterruptedException ie) { 243 | logger.warning("Interrupted while doing post-read interrogation."); 244 | break; 245 | } 246 | } 247 | 248 | ReadCycleResult cycle = 249 | new ReadCycleResult(failed, cancelled, success, start, System.nanoTime(), threadPool.getCorePoolSize(), interval); 250 | internalDispatch(dispatchable, fastJMXCycle, "failed", failed); 251 | internalDispatch(dispatchable, fastJMXCycle, "success", success); 252 | internalDispatch(dispatchable, fastJMXCycle, "cancelled", cancelled); 253 | internalDispatch(dispatchable, fastJMXCycle, "weight", cycle.getWeight()); 254 | internalDispatch(dispatchable, fastJMXLatency, "interval", interval); 255 | internalDispatch(dispatchable, fastJMXLatency, "duration", cycle.getDurationMs()); 256 | internalDispatch(dispatchable, fastJMXGauge, "threads", threadPool.getCorePoolSize()); 257 | 258 | push(cycle); 259 | 260 | // In a single pass, remove and clear the element. 261 | for (int i = dispatchable.size() - 1; i >= 0; i--) { 262 | dispatch(dispatchable.remove(i)); 263 | } 264 | 265 | return results; 266 | } 267 | 268 | private void dispatch(final ValueList vl) { 269 | vl.setInterval(interval); 270 | Collectd.dispatchValues(vl); 271 | } 272 | 273 | private void internalDispatch(final List appendTo, final ValueList copy, final String typeInstance, final Number value) { 274 | if (copy != null) { 275 | ValueList vl = new ValueList(copy); 276 | vl.setTypeInstance(typeInstance); 277 | vl.setValues(Arrays.asList(value)); 278 | appendTo.add(vl); 279 | } 280 | } 281 | 282 | 283 | /** 284 | * Looks at the last ReadCycleResult push()ed into the ring buffer 285 | * 286 | * @return 287 | */ 288 | private ReadCycleResult peek() { 289 | int pos = index; 290 | if (pos == 0) { 291 | pos = ring.length; 292 | } 293 | return ring[pos - 1]; 294 | } 295 | 296 | private void resetFibonacci() { 297 | resetFibonacci(Runtime.getRuntime().availableProcessors()); 298 | } 299 | 300 | private void resetFibonacci(int lowerBound) { 301 | int max; 302 | while (fiba + fibb > lowerBound) { 303 | max = fiba; 304 | 305 | fiba = fibb; 306 | fibb = max - fiba; 307 | } 308 | if (logger.isLoggable(Level.FINE)) { 309 | logger.fine("Fibonacci reset to : " + fiba + ":" + fibb); 310 | } 311 | } 312 | 313 | /** 314 | * Gets the next value in a fibonacci sequence.... 315 | * 316 | * @return 317 | */ 318 | private int getNextFibonacci() { 319 | int current = fiba; 320 | int next = fiba + fibb; 321 | fibb = fiba; 322 | fiba = next; 323 | 324 | if (current > maxThreads) { 325 | resetFibonacci(); 326 | current = getNextFibonacci(); 327 | } 328 | 329 | if (logger.isLoggable(Level.FINE)) { 330 | logger.fine("fibonacci sequence generated: " + current); 331 | } 332 | return current; 333 | } 334 | 335 | /** 336 | * Easily the most complex part of this class -- 337 | *

338 | * Using the ReadCycleResult objects in the ring buffer, organize the data into a hash map where the key is the 339 | * pool size, and the value is the duration it took to complete. 340 | * 341 | * @return 342 | */ 343 | private int calculateOptimum() { 344 | int threadCount = threadPool.getCorePoolSize(); 345 | if (recalculateOptimum) { 346 | HashMap> valueMap = new HashMap>(); 347 | 348 | for (int i = 0; i < ring.length; i++) { 349 | if (ring[i] != null && ring[i].getPoolSize() > 0) { 350 | List depPoints = valueMap.get(ring[i].getPoolSize()); 351 | if (depPoints == null) { 352 | depPoints = new ArrayList(5); 353 | } 354 | depPoints.add(ring[i]); 355 | valueMap.put(ring[i].getPoolSize(), depPoints); 356 | } 357 | } 358 | 359 | List valueKeys = new ArrayList(valueMap.keySet()); 360 | Collections.sort(valueKeys, numberComparator); 361 | 362 | if (logger.isLoggable(Level.FINE)) { 363 | logger.fine("" + valueKeys.size() + " of " + minIndependent + " unique pool sizes for optimal projection"); 364 | } 365 | if (valueKeys.size() < minIndependent) { 366 | threadCount = getNextFibonacci(); 367 | } else { 368 | // Compute the averages of the dependent variable and keep track of independent values and weights. 369 | double[] independent = new double[valueKeys.size()]; 370 | double[] observation = new double[valueKeys.size()]; 371 | double[] weights = new double[valueKeys.size()]; 372 | for (int i = 0; i < valueKeys.size(); i++) { 373 | Number key = valueKeys.get(i); 374 | independent[i] = key.doubleValue(); 375 | observation[i] = averageDuration(valueMap.get(key)); 376 | weights[i] = weight(valueMap.get(key)); 377 | if (logger.isLoggable(Level.FINE)) { 378 | logger.fine("Point: " + independent[i] + "," + observation[i] + " weight: " + weights[i]); 379 | } 380 | } 381 | 382 | QuadraticProblem qp = new QuadraticProblem(independent, observation, weights); 383 | 384 | LeastSquaresProblem problem = 385 | new LeastSquaresBuilder().model(qp, qp.getMatrixFunc()) 386 | .target(qp.calculateTarget()) 387 | .start(new double[]{1, 1, 1}) 388 | .maxEvaluations(100) 389 | .maxIterations(100) 390 | .weight(qp.getWeight()) 391 | .build(); 392 | 393 | LevenbergMarquardtOptimizer optimizer = new LevenbergMarquardtOptimizer(); 394 | LeastSquaresOptimizer.Optimum optimum = optimizer.optimize(problem); 395 | QuadraticFunction qFunc = new QuadraticFunction(optimum.getPoint()); 396 | 397 | BrentOptimizer bo = new BrentOptimizer(1e-10, 1e-14); 398 | UnivariatePointValuePair optimalMin = bo.optimize(GoalType.MINIMIZE, 399 | new SearchInterval(0, 512, 1), 400 | new UnivariateObjectiveFunction(qFunc), 401 | MaxEval.unlimited(), MaxIter.unlimited()); 402 | 403 | if (logger.isLoggable(Level.FINE)) { 404 | logger.fine("Found minimum value: " + optimalMin.getValue() + " @ " + optimalMin.getPoint()); 405 | } 406 | threadCount = Math.max((int) Math.round(optimalMin.getPoint()), 1); 407 | 408 | // If the thread count is bigger than the current fibonacci value, clamp it to the next fibonacci sequence value. 409 | if (threadCount > (fiba + fibb)) { 410 | threadCount = Math.min(fiba + fibb, threadCount); 411 | getNextFibonacci(); 412 | if (logger.isLoggable(Level.FINE)) { 413 | logger.fine("After limiting to next fibonacci value: " + threadCount); 414 | } 415 | } 416 | 417 | // If we find a minimum below our current pool size, reset the sequence. 418 | if (threadCount < threadPool.getCorePoolSize()) { 419 | if (logger.isLoggable(Level.FINE)) { 420 | logger.fine("Optimal threadpool size: " + threadCount + " less than current pool size!"); 421 | } 422 | resetFibonacci(); 423 | } 424 | } 425 | 426 | // Clamp the new value to maxThreads.... 427 | threadCount = Math.min(threadCount, maxThreads); 428 | } 429 | 430 | recalculateOptimum = false; 431 | // If we ever end up at maxThreads, reset. 432 | if (threadCount == maxThreads) { 433 | clear(); 434 | } 435 | return threadCount; 436 | } 437 | 438 | private Double averageDuration(List values) { 439 | double d = 0.0; 440 | for (int i = 0; i < values.size(); i++) { 441 | d += values.get(i).getDurationMs(); 442 | } 443 | return d / values.size(); 444 | } 445 | 446 | /** 447 | * Calculate average jacobian weight for the list of read cycle results. 448 | * 449 | * @param values 450 | * @return 451 | */ 452 | private Double weight(List values) { 453 | double d = 0.0; 454 | for (int i = 0; i < values.size(); i++) { 455 | d += values.get(i).getWeight(); 456 | } 457 | return Math.max(d, 0) / values.size(); 458 | } 459 | 460 | 461 | /** 462 | * Implementation of a 2nd degree quadratic univariate function, ax^2 + bx + c 463 | *

464 | * Using the output of the QuadraticProblem (least squares solving) this can be used with a further 465 | * optimizer to find the min value of the function. This min dependent variable value should coincide with the 466 | * optimum dependent variable (# of threads in our case) to execute the workload in a timely manner. 467 | */ 468 | private class QuadraticFunction implements UnivariateFunction { 469 | double a; 470 | double b; 471 | double c; 472 | 473 | public QuadraticFunction(RealVector vector) { 474 | a = vector.getEntry(0); 475 | b = vector.getEntry(1); 476 | c = vector.getEntry(2); 477 | } 478 | 479 | public double value(double x) { 480 | return (a * Math.pow(x, 2)) + (b * x) + c; 481 | } 482 | } 483 | 484 | /** 485 | * Creates a commons-math MultivariateVectorFunction that can feed a LeastSquaresProblem in order to project 486 | * optimial thread pool size. 487 | */ 488 | private static class QuadraticProblem implements MultivariateVectorFunction { 489 | private double[] x; 490 | private double[] y; 491 | private double[] w; 492 | 493 | public QuadraticProblem(double[] independent, double[] observation, double[] weights) { 494 | if (independent.length != observation.length && weights.length != observation.length) { 495 | throw new IllegalArgumentException("Independent, observation, and weights must have the same number of elements."); 496 | } 497 | 498 | x = independent; 499 | y = observation; 500 | w = weights; 501 | } 502 | 503 | public double[] calculateTarget() { 504 | double[] target = new double[y.length]; 505 | for (int i = 0; i < y.length; i++) { 506 | target[i] = y[i]; 507 | } 508 | return target; 509 | } 510 | 511 | private double[][] jacobian(double[] variables) { 512 | double[][] jacobian = new double[x.length][3]; 513 | for (int i = 0; i < jacobian.length; ++i) { 514 | jacobian[i][0] = x[i] * x[i]; 515 | jacobian[i][1] = x[i]; 516 | jacobian[i][2] = 1.0; 517 | } 518 | return jacobian; 519 | } 520 | 521 | public double[] value(double[] variables) { 522 | double[] values = new double[x.length]; 523 | for (int i = 0; i < values.length; ++i) { 524 | values[i] = (variables[0] * x[i] + variables[1]) * x[i] + variables[2]; 525 | } 526 | return values; 527 | } 528 | 529 | public MultivariateMatrixFunction getMatrixFunc() { 530 | return new MultivariateMatrixFunction() { 531 | public double[][] value(double[] point) { 532 | return jacobian(point); 533 | } 534 | }; 535 | } 536 | 537 | public RealMatrix getWeight() { 538 | return new DiagonalMatrix(w); 539 | } 540 | } 541 | 542 | private class FastJMXThreadFactory implements ThreadFactory { 543 | private int threadCount = 0; 544 | 545 | public Thread newThread(Runnable r) { 546 | Thread t = new Thread(mbeanReaders, r, "mbean-reader-" + threadCount++); 547 | 548 | t.setDaemon(mbeanReaders.isDaemon()); 549 | t.setPriority(Thread.MAX_PRIORITY - 2); 550 | return t; 551 | } 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/SynchronousConnectorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd; 2 | 3 | import javax.management.ListenerNotFoundException; 4 | import javax.management.MBeanServerConnection; 5 | import javax.management.Notification; 6 | import javax.management.NotificationFilter; 7 | import javax.management.NotificationListener; 8 | import javax.management.remote.JMXConnectionNotification; 9 | import javax.management.remote.JMXConnector; 10 | import javax.security.auth.Subject; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Vector; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | 18 | /** 19 | * Created by bvarner on 11/13/14. 20 | */ 21 | public class SynchronousConnectorAdapter implements JMXConnector { 22 | 23 | Vector listeners = new Vector(); 24 | AtomicLong notificatioCounter = new AtomicLong(); 25 | 26 | private JMXConnector delegate; 27 | private Connection manager; 28 | 29 | public SynchronousConnectorAdapter(final JMXConnector connector, Connection manager) { 30 | this.delegate = connector; 31 | this.manager = manager; 32 | } 33 | 34 | private void fireNotification(Notification n) { 35 | synchronized (listeners) { 36 | NotificationDef nd = null; 37 | for (int i = listeners.size() - 1; i >= 0; --i) { 38 | nd = listeners.get(i); 39 | if (nd.filter == null || nd.filter.isNotificationEnabled(n)) { 40 | nd.listener.handleNotification(n, nd.handback); 41 | } 42 | } 43 | } 44 | } 45 | 46 | public void connect() throws IOException { 47 | this.connect(null); 48 | } 49 | 50 | public void connect(Map env) throws IOException { 51 | try { 52 | delegate.connect(env); 53 | // Force setting the MBeanServerConnection prior to invoking the callbacks. This avoids a potential race. 54 | manager.setMBeanServerConnection(delegate.getMBeanServerConnection()); 55 | fireNotification(new JMXConnectionNotification(JMXConnectionNotification.OPENED, this, getConnectionId(), notificatioCounter.getAndIncrement(), "FastJMX SynchronousConnectorAdapter OPENED.", null)); 56 | } catch (IOException ioe) { 57 | throw ioe; 58 | } catch (Exception e) { // Hack hack hackity hack hack hack 59 | // This is a workaround for: https://issues.jboss.org/browse/REMJMX-90 60 | throw new IOException(e); 61 | } 62 | } 63 | 64 | public MBeanServerConnection getMBeanServerConnection() throws IOException { 65 | return delegate.getMBeanServerConnection(); 66 | } 67 | 68 | public MBeanServerConnection getMBeanServerConnection(Subject delegationSubject) throws IOException { 69 | return delegate.getMBeanServerConnection(delegationSubject); 70 | } 71 | 72 | public void close() throws IOException { 73 | fireNotification(new JMXConnectionNotification(JMXConnectionNotification.CLOSED, this, getConnectionId(), notificatioCounter.getAndIncrement(), "FastJMX SynchronousConnectorAdapter CLOSED.", null)); 74 | delegate.close(); 75 | } 76 | 77 | public void addConnectionNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) { 78 | NotificationDef nd = new NotificationDef(listener, filter, handback); 79 | synchronized(listeners) { 80 | if (!listeners.contains(nd)) { 81 | listeners.add(nd); 82 | } 83 | } 84 | } 85 | 86 | public void removeConnectionNotificationListener(NotificationListener listener) throws ListenerNotFoundException { 87 | List toRemove = new ArrayList(); 88 | synchronized (listeners) { 89 | for (NotificationDef def : listeners) { 90 | if (def.listener == listener) { 91 | toRemove.add(def); 92 | } 93 | } 94 | listeners.removeAll(toRemove); 95 | } 96 | } 97 | 98 | public void removeConnectionNotificationListener(NotificationListener l, NotificationFilter f, Object handback) throws ListenerNotFoundException { 99 | listeners.remove(new NotificationDef(l, f, handback)); 100 | } 101 | 102 | public String getConnectionId() throws IOException { 103 | return delegate.getConnectionId(); 104 | } 105 | 106 | 107 | private class NotificationDef { 108 | NotificationListener listener; 109 | NotificationFilter filter; 110 | Object handback; 111 | 112 | NotificationDef(NotificationListener listener, NotificationFilter filter, Object handback) { 113 | this.listener = listener; 114 | this.filter = filter; 115 | this.handback = handback; 116 | } 117 | 118 | public boolean equals(Object obj) { 119 | if (obj instanceof NotificationDef) { 120 | NotificationDef other = (NotificationDef)obj; 121 | return other.listener == this.listener && other.filter == this.filter && other.handback == this.handback; 122 | } 123 | return false; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/e_gineering/collectd/logging/CollectdLogHandler.java: -------------------------------------------------------------------------------- 1 | package com.e_gineering.collectd.logging; 2 | 3 | import org.collectd.api.Collectd; 4 | 5 | import java.util.logging.Formatter; 6 | import java.util.logging.Handler; 7 | import java.util.logging.Level; 8 | import java.util.logging.LogRecord; 9 | 10 | /** 11 | * Delegate global java.util.logging to the Collectd.log method. 12 | */ 13 | public class CollectdLogHandler extends Handler { 14 | int collectdLogLevel = -1; 15 | 16 | @Override 17 | public void publish(LogRecord record) { 18 | Formatter fmt = getFormatter(); 19 | StringBuilder message = new StringBuilder("FastJMX Plugin: "); 20 | if (fmt != null) { 21 | message.append(fmt.formatMessage(record)); 22 | } else { 23 | message.append(record.getMessage()); 24 | } 25 | 26 | // SEVERE = ERROR 27 | // WARNING = WARNING 28 | // INFO = INFO 29 | // CONFIG = NOTICE 30 | // FINE || FINER || FINEST = DEBUG 31 | 32 | if (collectdLogLevel > 0 && collectdLogLevel == Collectd.LOG_ERR) { 33 | Collectd.logError(message.toString()); 34 | } else if (collectdLogLevel >= 0 && collectdLogLevel == Collectd.LOG_WARNING) { 35 | Collectd.logWarning(message.toString()); 36 | } else if (collectdLogLevel >= 0 && collectdLogLevel == Collectd.LOG_INFO) { 37 | Collectd.logInfo(message.toString()); 38 | } else if (collectdLogLevel >= 0 && collectdLogLevel == Collectd.LOG_NOTICE) { 39 | Collectd.logNotice(message.toString()); 40 | } else if (collectdLogLevel >= 0 && collectdLogLevel == Collectd.LOG_DEBUG) { 41 | Collectd.logDebug(message.toString()); 42 | } else if (record.getLevel() == Level.SEVERE) { 43 | Collectd.logWarning(message.toString()); 44 | } else if (record.getLevel() == Level.WARNING) { 45 | Collectd.logWarning(message.toString()); 46 | } else if (record.getLevel() == Level.INFO) { 47 | Collectd.logInfo(message.toString()); 48 | } else if (record.getLevel() == Level.CONFIG) { 49 | Collectd.logNotice(message.toString()); 50 | } else { 51 | Collectd.logDebug(message.toString()); 52 | } 53 | } 54 | 55 | public void forceAllLoggingTo(int collectdLogLevel) { 56 | this.collectdLogLevel = collectdLogLevel; 57 | } 58 | 59 | @Override 60 | public void flush() { 61 | } 62 | 63 | @Override 64 | public void close() throws SecurityException { 65 | } 66 | } 67 | --------------------------------------------------------------------------------