diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java new file mode 100644 index 00000000000..b5d3f31228a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.jmx; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.jmx.export.naming.KeyNamingStrategy; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; +import org.springframework.util.StringUtils; + +/** + * MBean naming strategy for metric keys. A metric name of + * counter.foo.bar.spam translates to an object name with + * type=counter, name=foo and value=bar.spam. This + * results in a more or less pleasing view with no tweaks in jconsole or jvisualvm. The + * domain is copied from the input key and the type in the input key is discarded. + * + * @author Dave Syer + */ +public class DefaultMetricNamingStrategy implements ObjectNamingStrategy { + + private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy(); + + @Override + public ObjectName getObjectName(Object managedBean, String beanKey) + throws MalformedObjectNameException { + ObjectName objectName = this.namingStrategy.getObjectName(managedBean, beanKey); + String domain = objectName.getDomain(); + Hashtable table = new Hashtable( + objectName.getKeyPropertyList()); + String name = objectName.getKeyProperty("name"); + if (name != null) { + table.remove("name"); + String[] parts = StringUtils.delimitedListToStringArray(name, "."); + table.put("type", parts[0]); + if (parts.length > 1) { + table.put(parts.length > 2 ? "name" : "value", parts[1]); + } + if (parts.length > 2) { + table.put("value", + name.substring(parts[0].length() + parts[1].length() + 2)); + } + } + return new ObjectName(domain, table); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java new file mode 100644 index 00000000000..db345b7833d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.jmx; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.management.MalformedObjectNameException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; + +/** + * A {@link MetricWriter} for MBeans. Each metric is registered as an individual MBean, so + * (for instance) it can be graphed and monitored. The object names are provided by an + * {@link ObjectNamingStrategy}, where the default is a + * {@link DefaultMetricNamingStrategy} which provides type, name + * and value keys by splitting up the metric name on periods. + * + * @author Dave Syer + */ +@ManagedResource(description = "MetricWriter for pushing metrics to JMX MBeans.") +public class JmxMetricWriter implements MetricWriter { + + private static Log logger = LogFactory.getLog(JmxMetricWriter.class); + + private final ConcurrentMap values = new ConcurrentHashMap(); + + private final MBeanExporter exporter; + + private ObjectNamingStrategy namingStrategy = new DefaultMetricNamingStrategy(); + + private String domain = "org.springframework.metrics"; + + public JmxMetricWriter(MBeanExporter exporter) { + this.exporter = exporter; + } + + public void setNamingStrategy(ObjectNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + @ManagedOperation + public void increment(String name, long value) { + increment(new Delta(name, value)); + } + + @Override + public void increment(Delta delta) { + MetricValue counter = getValue(delta.getName()); + counter.increment(delta.getValue().longValue()); + } + + @ManagedOperation + public void set(String name, double value) { + set(new Metric(name, value)); + } + + @Override + public void set(Metric value) { + MetricValue metric = getValue(value.getName()); + metric.setValue(value.getValue().doubleValue()); + } + + @Override + @ManagedOperation + public void reset(String name) { + MetricValue value = this.values.remove(name); + if (value != null) { + try { + // We can unregister the MBean, but if this writer is on the end of an + // Exporter the chances are it will be re-registered almost immediately. + this.exporter.unregisterManagedResource(this.namingStrategy + .getObjectName(value, getKey(name))); + } + catch (MalformedObjectNameException e) { + logger.warn("Could not unregister MBean for " + name); + } + } + } + + private MetricValue getValue(String name) { + if (!this.values.containsKey(name)) { + this.values.putIfAbsent(name, new MetricValue()); + MetricValue value = this.values.get(name); + try { + this.exporter.registerManagedResource(value, + this.namingStrategy.getObjectName(value, getKey(name))); + } + catch (Exception e) { + // Could not register mbean, maybe just a race condition + } + } + return this.values.get(name); + } + + private String getKey(String name) { + return String.format(this.domain + ":type=MetricValue,name=%s", name); + } + + @ManagedResource + public static class MetricValue { + + private double value; + + private long lastUpdated = 0; + + public void setValue(double value) { + if (this.value != value) { + this.lastUpdated = System.currentTimeMillis(); + } + this.value = value; + } + + public void increment(long value) { + this.lastUpdated = System.currentTimeMillis(); + this.value += value; + } + + @ManagedAttribute + public double getValue() { + return this.value; + } + + @ManagedAttribute + public Date getLastUpdated() { + return new Date(this.lastUpdated); + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java new file mode 100644 index 00000000000..9550f9a4c8d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Metrics integration with JMX. + */ +package org.springframework.boot.actuate.metrics.jmx; + diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java new file mode 100644 index 00000000000..3da8bcfebf1 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.jmx; + +import javax.management.ObjectName; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class DefaultMetricNamingStrategyTests { + + private DefaultMetricNamingStrategy strategy = new DefaultMetricNamingStrategy(); + + @Test + public void simpleName() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + } + + @Test + public void onePeriod() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("value")); + } + + @Test + public void twoPeriods() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar.spam"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name")); + assertEquals("Wrong name: " + name, "spam", name.getKeyProperty("value")); + } + + @Test + public void threePeriods() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar.spam.bucket"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name")); + assertEquals("Wrong name: " + name, "spam.bucket", name.getKeyProperty("value")); + } + +}