SLING-11456 export mbeans as metrics
diff --git a/pom.xml b/pom.xml
index 546bd18..600fce6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,12 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.12.0</version>
+ <scope>provided</scope>
+ </dependency>
<!-- Apache Felix -->
<dependency>
<groupId>org.apache.felix</groupId>
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java b/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java
new file mode 100644
index 0000000..96deabe
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.sling.commons.metrics.internal;
+
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.commons.metrics.MetricsService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This ServiceFactory allows to export JMX object names as metrics (gauge).
+ *
+ */
+
+@Component()
+@Designate(ocd=JmxExporterFactory.Config.class, factory=true)
+public class JmxExporterFactory {
+
+
+ @ObjectClassDefinition(name="JMX to Metrics Exporter")
+ public @interface Config {
+
+ @AttributeDefinition(name="objectnames", description="export all attribute of the MBeans matching these objectnames as Sling Metrics"
+ + "(see https://docs.oracle.com/en/java/javase/11/docs/api/java.management/javax/management/ObjectName.html")
+ String[] objectnames();
+
+ @AttributeDefinition
+ String webconsole_configurationFactory_nameHint() default "Pattern: {objectnames}";
+ }
+
+
+ private static final Logger LOG = LoggerFactory.getLogger(JmxExporterFactory.class);
+
+ @Reference
+ MetricsService metrics;
+
+ MBeanServer server;
+
+ @Activate
+ public void activate(Config config) {
+ server = ManagementFactory.getPlatformMBeanServer();
+ registerMetrics(config.objectnames());
+ }
+
+
+ /**
+ * Register all applicable metrics for an objectname pattern
+ * @param pattern describes a objectname pattern
+ */
+ private void registerMetrics(String[] patterns) {
+
+ for (String patternString : patterns) {
+ try {
+ ObjectName pattern = new ObjectName(patternString);
+ Set<ObjectName> allMBeans = server.queryNames(pattern, null);
+ if (allMBeans.isEmpty()) {
+ LOG.info("pattern {} does not match any MBean", patternString);
+ } else {
+ allMBeans.forEach(objectname -> {
+ LOG.debug("registering properties for {}", objectname.toString());
+ try {
+ registerMBeanProperties(objectname);
+ } catch (IntrospectionException | InstanceNotFoundException | ReflectionException e) {
+ LOG.error("Cannot register metrics for objectname = {}", objectname.toString(),e);
+ }
+ });
+ }
+ } catch (MalformedObjectNameException e) {
+ LOG.error("cannot create an objectname from pattern {}",patternString,e);
+ }
+ }
+
+ }
+
+
+ protected void registerMBeanProperties(ObjectName objectname) throws InstanceNotFoundException, ReflectionException, IntrospectionException {
+ MBeanInfo info = server.getMBeanInfo(objectname);
+ MBeanAttributeInfo[] attributes = info.getAttributes();
+ for (MBeanAttributeInfo attr : attributes) {
+ LOG.debug("Checking mbean = {}, name = {}, type={}",objectname, attr.getName(), attr.getType());
+
+ Supplier<?> supplier = null;
+ if ("int".equals(attr.getType())) {
+ supplier = getSupplier(objectname, attr.getName(),0);
+ } else if ("long".equals(attr.getType())) {
+ supplier = getSupplier(objectname, attr.getName(),0L);
+ } else if ("java.lang.String".equals(attr.getType())) {
+ supplier = getSupplier(objectname,attr.getName(),"");
+ } else if ("double".equals(attr.getType())) {
+ supplier = getSupplier(objectname,attr.getName(), Double.valueOf(0.0));
+ }
+
+ if (supplier != null) {
+ String metricName = toMetricName(objectname, attr.getName());
+ LOG.info("Registering metric {} from MBean (objectname=[{}], name={}, type={})",
+ metricName,objectname.toString(), attr.getName(), attr.getType());
+ metrics.gauge(metricName, supplier);
+ }
+ }
+ }
+
+
+ private <T> Supplier<T> getSupplier ( ObjectName name, String attributeName, T defaultValue ) {
+
+ Supplier<T> supplier = () -> {
+ try {
+ return (T) server.getAttribute(name, attributeName);
+ } catch (InstanceNotFoundException | AttributeNotFoundException | ReflectionException
+ | MBeanException e) {
+ LOG.warn("error when retrieving value for MBean (objectname=[{}], attribute={})",name, attributeName,e);
+ return defaultValue;
+ }
+
+ };
+ return supplier;
+ }
+
+
+ protected String toMetricName(ObjectName objectName, String attributeName) {
+ String name = "sling"; // default domain
+
+ if (!StringUtils.isBlank(objectName.getDomain())) {
+ name = objectName.getDomain();
+ }
+ Hashtable<String,String> allkeys = objectName.getKeyPropertyList();
+ List<String> keyValues = new ArrayList<>(allkeys.values());
+ Collections.sort(keyValues);
+
+ for (String s: keyValues) {
+ name += "." + s;
+ }
+ name += "." + attributeName;
+ return name;
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java b/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java
new file mode 100644
index 0000000..cfa62ba
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.sling.commons.metrics.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+
+import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import javax.management.InstanceAlreadyExistsException;
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanRegistrationException;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.NotCompliantMBeanException;
+import javax.management.ObjectName;
+
+import org.apache.sling.commons.metrics.MetricsService;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JmxExporterFactoryTest {
+
+ @Rule
+ public OsgiContext context = new OsgiContext();
+
+ @Captor
+ ArgumentCaptor<Supplier<Integer>> intSupplierCaptor;
+
+ @Captor
+ ArgumentCaptor<Supplier<Long>> longSupplierCaptor;
+
+ @Captor
+ ArgumentCaptor<Supplier<String>> stringSupplierCaptor;
+
+ @Captor
+ ArgumentCaptor<Supplier<Double>> doubleSupplierCaptor;
+
+ JmxExporterFactory exporter;
+
+ private static final String OBJECT_NAME_0 = "org.apache.sling.whiteboard.jmxexporter.impl0:type=sample1";
+ private static final String OBJECT_NAME_1 = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2:type=sample2";
+ private static final String OBJECT_NAME_2 = "org.apache.sling.whiteboard.jmxexporter.impl1:type=sample3";
+
+ // Query which will only match OBJECT_NAME_0 and OBJECT_NAME_1
+ private static final String OBJECT_NAME_QUERY = "org.apache.sling.whiteboard.jmxexporter.impl0*:type=*";
+
+ private static final String EXPECTED_0_INT_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Int";
+ private static final String EXPECTED_0_LONG_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Long";
+ private static final String EXPECTED_0_STRING_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.String";
+ private static final String EXPECTED_0_DOUBLE_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Double";
+
+ private static final String EXPECTED_1_INT_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2.sample2.Int";
+ private static final String EXPECTED_1_LONG_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2.sample2.Long";
+
+ private static final String EXPECTED_2_INT_NAME = "org.apache.sling.whiteboard.jmxexporter.impl1.sample3.Int";
+
+ private static final Double STATIC_DOUBLE = 1.0;
+
+ MetricsService metrics;
+
+ SimpleBean mbeans[] = { new SimpleBean(0,0L), new SimpleBean(1,1L), new SimpleBean(2,2L)};
+
+
+ @Before
+ public void setup() throws MalformedObjectNameException, InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+
+ server.registerMBean(mbeans[0],new ObjectName(OBJECT_NAME_0));
+ server.registerMBean(mbeans[1],new ObjectName(OBJECT_NAME_1));
+ server.registerMBean(mbeans[2],new ObjectName(OBJECT_NAME_2));
+
+ exporter = new JmxExporterFactory();
+ metrics = Mockito.mock(MetricsService.class);
+ context.registerService(MetricsService.class, metrics);
+ }
+
+ @After
+ public void shutdown() throws MBeanRegistrationException, InstanceNotFoundException, MalformedObjectNameException {
+ MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+ server.unregisterMBean(new ObjectName(OBJECT_NAME_0));
+ server.unregisterMBean(new ObjectName(OBJECT_NAME_1));
+ server.unregisterMBean(new ObjectName(OBJECT_NAME_2));
+ }
+
+ @Test
+ public void test() {
+ Map<String,Object> props = new HashMap<>();
+ props.put("objectnames", new String[]{OBJECT_NAME_QUERY});
+ context.registerInjectActivateService(exporter, props);
+
+ // Integer
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_INT_NAME), intSupplierCaptor.capture());
+ assertEquals(new Integer(0),intSupplierCaptor.getValue().get());
+ mbeans[0].setInt(10);
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_INT_NAME), intSupplierCaptor.capture());
+ assertEquals(new Integer(10),intSupplierCaptor.getValue().get());
+
+ // Long
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_LONG_NAME), longSupplierCaptor.capture());
+ assertEquals(new Long(0L),longSupplierCaptor.getValue().get());
+
+ // String
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_STRING_NAME), stringSupplierCaptor.capture());
+ assertEquals("sample",stringSupplierCaptor.getValue().get());
+
+ // Double
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_DOUBLE_NAME), doubleSupplierCaptor.capture());
+ assertEquals(STATIC_DOUBLE,doubleSupplierCaptor.getValue().get());
+
+ // MBean 1
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_1_INT_NAME), intSupplierCaptor.capture());
+ assertEquals(new Integer(1),intSupplierCaptor.getValue().get());
+
+ Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_1_LONG_NAME), longSupplierCaptor.capture());
+ assertEquals(new Long(1L),longSupplierCaptor.getValue().get());
+
+ // verify that no metrics for MBean2 have been registered
+ Mockito.verify(metrics, never()).gauge(Mockito.eq(EXPECTED_2_INT_NAME), intSupplierCaptor.capture());
+
+ }
+
+ static class SimpleBean implements SimpleBeanMBean {
+
+
+ int internalInt = 0;
+ long internalLong = 0L;
+
+ public SimpleBean(int i, long l) {
+ internalInt = i;
+ internalLong = l;
+ }
+
+ @Override
+ public int getInt() {
+ return internalInt;
+ }
+
+ @Override
+ public long getLong() {
+ return internalLong;
+ }
+
+ public void setInt(int value) {
+ internalInt = value;
+ }
+
+ public String getString() {
+ return "sample";
+ }
+
+ public double getDouble() {
+ return STATIC_DOUBLE;
+ }
+
+ }
+
+
+ static public interface SimpleBeanMBean {
+
+ public int getInt();
+ public long getLong();
+ public String getString();
+ public double getDouble();
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java b/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
index dd8f339..8282eb5 100644
--- a/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
+++ b/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
@@ -54,6 +54,7 @@
// Commons Metrics
testBundle("bundle.filename"),
mavenBundle().groupId("io.dropwizard.metrics").artifactId("metrics-core").versionAsInProject(),
+ mavenBundle().groupId("org.apache.commons").artifactId("commons-lang3").versionAsInProject(),
junitBundles()
);
}