This is an automated email from the ASF dual-hosted git repository. sk0x50 pushed a commit to branch ignite-3.0.0-beta1 in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit a879e4e14df49e5017dcf11ebdaa506fd5be58ff Author: Kirill Gusakov <[email protected]> AuthorDate: Mon Oct 31 23:38:54 2022 +0300 IGNITE-17357 JMX metrics exporter (#1234) --- .../apache/ignite/internal/util/IgniteUtils.java | 19 ++ .../metrics/exporters/TestPullMetricExporter.java | 13 +- .../metrics/exporters/TestPushMetricExporter.java | 14 +- .../ignite/internal/metrics/MetricManager.java | 28 ++- .../metrics/exporters/BasicMetricExporter.java | 18 +- .../internal/metrics/exporters/MetricExporter.java | 35 +++- .../metrics/exporters/PushMetricExporter.java | 5 +- .../JmxExporterConfigurationSchema.java | 6 +- .../metrics/exporters/jmx/JmxExporter.java | 165 ++++++++++++++++ .../metrics/exporters/jmx/MetricSetMbean.java | 181 ++++++++++++++++++ .../ignite/internal/metrics/JmxExporterTest.java | 211 +++++++++++++++++++++ .../internal/metrics/exporters/TestExporter.java | 22 ++- 12 files changed, 682 insertions(+), 35 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java index 20f2f36a7a..93d032a976 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java @@ -52,6 +52,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.util.worker.IgniteWorker; import org.apache.ignite.lang.IgniteInternalException; @@ -103,6 +105,11 @@ public class IgniteUtils { /** Class cache. */ private static final ConcurrentMap<ClassLoader, ConcurrentMap<String, Class<?>>> classCache = new ConcurrentHashMap<>(); + /** + * Root package for JMX MBeans. + */ + private static final String JMX_MBEAN_PACKAGE = "org.apache"; + /** * Get JDK version. * @@ -862,4 +869,16 @@ public class IgniteUtils { return result; } + + /** + * Produce new MBean name according to received group and name. + * + * @param group pkg:group=value part of MBean name. + * @param name pkg:name=value part of MBean name. + * @return new ObjectName. + * @throws MalformedObjectNameException if MBean name can't be formed from the received arguments. + */ + public static ObjectName makeMbeanName(String group, String name) throws MalformedObjectNameException { + return new ObjectName(String.format("%s:group=%s,name=%s", JMX_MBEAN_PACKAGE, group, name)); + } } diff --git a/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPullMetricExporter.java b/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPullMetricExporter.java index 2d2da80e18..d0bb8c2f95 100644 --- a/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPullMetricExporter.java +++ b/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPullMetricExporter.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.ignite.internal.metrics.Metric; +import org.apache.ignite.internal.metrics.MetricProvider; import org.apache.ignite.internal.metrics.MetricSet; /** @@ -52,7 +53,9 @@ public class TestPullMetricExporter extends BasicMetricExporter<TestPullMetricsE } @Override - public void start() { + public void start(MetricProvider metricProvider, TestPullMetricsExporterView conf) { + super.start(metricProvider, conf); + executorService.execute(() -> { while (true) { waitForRequest(); @@ -93,6 +96,14 @@ public class TestPullMetricExporter extends BasicMetricExporter<TestPullMetricsE return EXPORTER_NAME; } + @Override + public void addMetricSet(MetricSet metricSet) { + } + + @Override + public void removeMetricSet(String metricSetName) { + } + private void waitForRequest() { synchronized (obj) { try { diff --git a/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPushMetricExporter.java b/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPushMetricExporter.java index 668ff987e5..2a990a206b 100644 --- a/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPushMetricExporter.java +++ b/modules/metrics/src/integrationTest/java/org/apache/ignite/internal/metrics/exporters/TestPushMetricExporter.java @@ -35,10 +35,10 @@ public class TestPushMetricExporter extends PushMetricExporter<TestPushMetricsEx private long period; @Override - public void init(MetricProvider metricsProvider, TestPushMetricsExporterView configuration) { - super.init(metricsProvider, configuration); - + public void start(MetricProvider metricsProvider, TestPushMetricsExporterView configuration) { period = configuration.period(); + + super.start(metricsProvider, configuration); } public static void setOutputStream(OutputStream outputStream) { @@ -84,4 +84,12 @@ public class TestPushMetricExporter extends PushMetricExporter<TestPushMetricsEx public String name() { return EXPORTER_NAME; } + + @Override + public void addMetricSet(MetricSet metricSet) { + } + + @Override + public void removeMetricSet(String metricSetName) { + } } diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java index 01b603b283..bb491208dd 100644 --- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/MetricManager.java @@ -107,6 +107,8 @@ public class MetricManager implements IgniteComponent { for (MetricExporter metricExporter : enabledMetricExporters.values()) { metricExporter.stop(); } + + enabledMetricExporters.clear(); } /** @@ -143,7 +145,13 @@ public class MetricManager implements IgniteComponent { * @return Metric set, or {@code null} if already enabled. */ public MetricSet enable(MetricSource src) { - return registry.enable(src); + MetricSet enabled = registry.enable(src); + + if (enabled != null) { + enabledMetricExporters.values().forEach(e -> e.addMetricSet(enabled)); + } + + return enabled; } /** @@ -153,7 +161,13 @@ public class MetricManager implements IgniteComponent { * @return Metric set, or {@code null} if already enabled. */ public MetricSet enable(final String srcName) { - return registry.enable(srcName); + MetricSet enabled = registry.enable(srcName); + + if (enabled != null) { + enabledMetricExporters.values().forEach(e -> e.addMetricSet(enabled)); + } + + return enabled; } /** @@ -178,15 +192,19 @@ public class MetricManager implements IgniteComponent { */ public void disable(MetricSource src) { registry.disable(src); + + enabledMetricExporters.values().forEach(e -> e.removeMetricSet(src.name())); } /** * Disable metric source by name. See {@link MetricRegistry#disable(String)}. * - * @param srcName Source name. + * @param srcName Metric source name. */ public void disable(final String srcName) { registry.disable(srcName); + + enabledMetricExporters.values().forEach(e -> e.removeMetricSet(srcName)); } /** @@ -215,9 +233,7 @@ public class MetricManager implements IgniteComponent { MetricExporter<T> exporter = availableExporters.get(exporterName); if (exporter != null) { - exporter.init(metricsProvider, exporterConfiguration); - - exporter.start(); + exporter.start(metricsProvider, exporterConfiguration); enabledMetricExporters.put(exporter.name(), exporter); } else { diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/BasicMetricExporter.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/BasicMetricExporter.java index 85ef05cd22..b61e89ef32 100644 --- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/BasicMetricExporter.java +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/BasicMetricExporter.java @@ -31,19 +31,29 @@ public abstract class BasicMetricExporter<CfgT extends ExporterView> implements /** Metrics provider. */ private MetricProvider metricsProvider; - protected CfgT configuration; + /** Exporter's configuration view. */ + private CfgT configuration; /** {@inheritDoc} */ @Override - public void init(MetricProvider metricsProvider, CfgT configuration) { + public void start(MetricProvider metricsProvider, CfgT configuration) { this.metricsProvider = metricsProvider; this.configuration = configuration; } /** {@inheritDoc} */ @Override - public void reconfigure(@Nullable CfgT newValue) { - configuration = newValue; + public synchronized void reconfigure(@Nullable CfgT newVal) { + configuration = newVal; + } + + /** + * Returns current exporter configuration. + * + * @return Current exporter configuration + */ + protected synchronized CfgT configuration() { + return configuration; } /** diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/MetricExporter.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/MetricExporter.java index 55759fd4d2..2bae2dfcbf 100644 --- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/MetricExporter.java +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/MetricExporter.java @@ -18,6 +18,7 @@ package org.apache.ignite.internal.metrics.exporters; import org.apache.ignite.internal.metrics.MetricProvider; +import org.apache.ignite.internal.metrics.MetricSet; import org.apache.ignite.internal.metrics.exporters.configuration.ExporterConfiguration; import org.apache.ignite.internal.metrics.exporters.configuration.ExporterView; @@ -32,18 +33,13 @@ import org.apache.ignite.internal.metrics.exporters.configuration.ExporterView; * Pull exporters should extend {@link BasicMetricExporter}. */ public interface MetricExporter<CfgT extends ExporterView> { - /** - * Initialize metric exporter with the provider of available metrics. - * - * @param metricProvider Metrics provider - * @param configuration CfgT - */ - void init(MetricProvider metricProvider, CfgT configuration); - /** * Start metrics exporter. Here all needed listeners, schedulers etc. should be started. + * + * @param metricProvider Provider of metric sources. + * @param configuration Exporter configuration view. */ - void start(); + void start(MetricProvider metricProvider, CfgT configuration); /** * Stop and cleanup work for current exporter must be implemented here. @@ -60,7 +56,26 @@ public interface MetricExporter<CfgT extends ExporterView> { /** * Invokes, when exporter's configuration was updated. * - * @param newValue new configuration. + * <p>Be careful: this method will be invoked from the separate configuration events' thread pool. + * Appropriate thread-safe logic falls on the shoulders of implementations. + * + * @param newValue New configuration view. */ void reconfigure(CfgT newValue); + + /** + * {@link org.apache.ignite.internal.metrics.MetricManager} invokes this method, + * when new metric source was enabled. + * + * @param metricSet Named metric set. + */ + void addMetricSet(MetricSet metricSet); + + /** + * {@link org.apache.ignite.internal.metrics.MetricManager} invokes this method, + * when the metric source was disabled. + * + * @param metricSetName Name of metric set to remove. + */ + void removeMetricSet(String metricSetName); } diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/PushMetricExporter.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/PushMetricExporter.java index 7287c893bd..71704e05be 100644 --- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/PushMetricExporter.java +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/PushMetricExporter.java @@ -23,6 +23,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.metrics.MetricProvider; import org.apache.ignite.internal.metrics.exporters.configuration.ExporterView; import org.apache.ignite.internal.thread.NamedThreadFactory; import org.apache.ignite.internal.util.IgniteUtils; @@ -44,7 +45,9 @@ public abstract class PushMetricExporter<CfgT extends ExporterView> extends Basi /** {@inheritDoc} */ @Override - public synchronized void start() { + public synchronized void start(MetricProvider metricProvider, CfgT conf) { + super.start(metricProvider, conf); + scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("metrics-exporter-" + name(), log)); diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/configuration/JmxExporterConfigurationSchema.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/configuration/JmxExporterConfigurationSchema.java index 68fe32e8c3..039aca7487 100644 --- a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/configuration/JmxExporterConfigurationSchema.java +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/configuration/JmxExporterConfigurationSchema.java @@ -17,13 +17,13 @@ package org.apache.ignite.internal.metrics.exporters.configuration; +import static org.apache.ignite.internal.metrics.exporters.jmx.JmxExporter.JMX_EXPORTER_NAME; + import org.apache.ignite.configuration.annotation.PolymorphicConfigInstance; /** * Jmx exporter configuration. */ -// TODO: Should be clarified under IGNITE-17357. -// TODO: Now it is needed to satisfy configuration framework, we can't have polymorphic config with 0 instances -@PolymorphicConfigInstance("jmx") +@PolymorphicConfigInstance(value = JMX_EXPORTER_NAME) public class JmxExporterConfigurationSchema extends ExporterConfigurationSchema { } diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/JmxExporter.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/JmxExporter.java new file mode 100644 index 0000000000..35dfdaadc3 --- /dev/null +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/JmxExporter.java @@ -0,0 +1,165 @@ +/* + * 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.ignite.internal.metrics.exporters.jmx; + +import static org.apache.ignite.internal.util.IgniteUtils.makeMbeanName; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; +import javax.management.JMException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.metrics.MetricProvider; +import org.apache.ignite.internal.metrics.MetricSet; +import org.apache.ignite.internal.metrics.exporters.BasicMetricExporter; +import org.apache.ignite.internal.metrics.exporters.configuration.JmxExporterView; + +/** + * Exporter for Ignite metrics to JMX API. + * For each enabled {@link org.apache.ignite.internal.metrics.MetricSource} exporter provides + * a separate MBean with corresponding attribute per source's metric. + */ +public class JmxExporter extends BasicMetricExporter<JmxExporterView> { + /** + * Exporter name. Must be the same for configuration and exporter itself. + */ + public static final String JMX_EXPORTER_NAME = "jmx"; + + /** + * Group attribute of {@link ObjectName} shared for all metric MBeans. + */ + private static final String JMX_METRIC_GROUP = "metrics"; + + /** + * Logger. + */ + private static IgniteLogger LOG = Loggers.forClass(JmxExporter.class); + + /** + * Current registered MBeans. + */ + private final List<ObjectName> mbeans = new ArrayList<>(); + + /** + * {@inheritDoc} + */ + @Override + public synchronized void start(MetricProvider metricsProvider, JmxExporterView configuration) { + super.start(metricsProvider, configuration); + + for (MetricSet metricSet : metricsProvider.metrics().get1().values()) { + register(metricSet); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void stop() { + mbeans.forEach(this::unregBean); + + mbeans.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public String name() { + return JMX_EXPORTER_NAME; + } + + /** + * {@inheritDoc} + * + * <p>Register new MBean for received metric set. + */ + @Override + public synchronized void addMetricSet(MetricSet metricSet) { + register(metricSet); + } + + /** + * {@inheritDoc} + * + * <p>Unregister MBean for removed metric set. + */ + @Override + public synchronized void removeMetricSet(String metricSet) { + unregister(metricSet); + } + + /** + * Register new MBean per metric set. + * + * @param metricSet Metric set. + */ + private void register(MetricSet metricSet) { + try { + MetricSetMbean metricSetMbean = new MetricSetMbean(metricSet); + + ObjectName mbean = ManagementFactory.getPlatformMBeanServer() + .registerMBean( + metricSetMbean, + makeMbeanName(JMX_METRIC_GROUP, metricSet.name())) + .getObjectName(); + + mbeans.add(mbean); + } catch (JMException e) { + LOG.error("MBean for metric set " + metricSet.name() + " can't be created.", e); + } + } + + /** + * Unregister MBean for specific metric set. + * + * @param metricSetName Metric set name. + */ + private void unregister(String metricSetName) { + try { + ObjectName mbeanName = makeMbeanName(JMX_METRIC_GROUP, metricSetName); + + boolean rmv = mbeans.remove(mbeanName); + + if (rmv) { + unregBean(mbeanName); + } else { + LOG.warn("Tried to unregister the MBean for non-registered metric set " + metricSetName); + } + } catch (MalformedObjectNameException e) { + LOG.error("MBean for metric set " + metricSetName + " can't be unregistered.", e); + } + } + + /** + * Unregister MBean by its name. + * + * @param bean MBean name to unregister. + */ + private void unregBean(ObjectName bean) { + try { + ManagementFactory.getPlatformMBeanServer().unregisterMBean(bean); + } catch (JMException e) { + LOG.error("Failed to unregister MBean: " + bean, e); + } + } +} diff --git a/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/MetricSetMbean.java b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/MetricSetMbean.java new file mode 100644 index 0000000000..86da7a92ac --- /dev/null +++ b/modules/metrics/src/main/java/org/apache/ignite/internal/metrics/exporters/jmx/MetricSetMbean.java @@ -0,0 +1,181 @@ +/* + * 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.ignite.internal.metrics.exporters.jmx; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.ReflectionException; +import org.apache.ignite.internal.metrics.DistributionMetric; +import org.apache.ignite.internal.metrics.DoubleMetric; +import org.apache.ignite.internal.metrics.IntMetric; +import org.apache.ignite.internal.metrics.LongMetric; +import org.apache.ignite.internal.metrics.Metric; +import org.apache.ignite.internal.metrics.MetricManager; +import org.apache.ignite.internal.metrics.MetricSet; + +/** + * MBean implementation, which produce JMX API representation for {@link MetricSet}. + * Every {@link Metric} of metric set will be represented by MBean's attribute with the same name. + */ +public class MetricSetMbean implements DynamicMBean { + /** + * Metric set. + */ + private MetricSet metricSet; + + /** + * Constructs new MBean. + * + * @param metricSet Metric set. + */ + public MetricSetMbean(MetricSet metricSet) { + this.metricSet = metricSet; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getAttribute(String attribute) throws AttributeNotFoundException { + if (attribute.equals("MBeanInfo")) { + return getMBeanInfo(); + } + + Metric metric = metricSet.get(attribute); + + if (metric instanceof DoubleMetric) { + return ((DoubleMetric) metric).value(); + } else if (metric instanceof IntMetric) { + return ((IntMetric) metric).value(); + } else if (metric instanceof LongMetric) { + return ((LongMetric) metric).value(); + } else if (metric instanceof DistributionMetric) { + return ((DistributionMetric) metric).value(); + } + + throw new AttributeNotFoundException("Unknown metric class " + metric.getClass()); + } + + /** + * {@inheritDoc} + */ + @Override + public AttributeList getAttributes(String[] attributes) { + AttributeList list = new AttributeList(); + + try { + for (String attribute : attributes) { + Object val = getAttribute(attribute); + + list.add(val); + } + } catch (AttributeNotFoundException e) { + throw new IllegalArgumentException(e); + } + + return list; + } + + /** + * {@inheritDoc} + */ + @Override + public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException { + if ("getAttribute".equals(actionName)) { + try { + return getAttribute((String) params[0]); + } catch (AttributeNotFoundException e) { + throw new MBeanException(e); + } + } + + throw new UnsupportedOperationException("invoke is not supported."); + } + + /** + * {@inheritDoc} + */ + @Override + public MBeanInfo getMBeanInfo() { + Iterator<Metric> iter = metricSet.iterator(); + + List<MBeanAttributeInfo> attrs = new ArrayList<>(); + + iter.forEachRemaining(metric -> { + attrs.add(new MBeanAttributeInfo( + metric.name(), + metricClass(metric), + metric.description() != null ? metric.description() : metric.name(), + true, + false, + false)); + }); + + return new MBeanInfo( + MetricManager.class.getName(), + metricSet.name(), + attrs.toArray(new MBeanAttributeInfo[attrs.size()]), + null, + null, + null); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAttribute(Attribute attribute) { + throw new UnsupportedOperationException("setAttribute is not supported."); + } + + /** + * {@inheritDoc} + */ + @Override + public AttributeList setAttributes(AttributeList attributes) { + throw new UnsupportedOperationException("setAttributes is not supported."); + } + + /** + * Extract class of metric value. + * + * @param metric Metric. + * @return Class of metric value. + */ + private String metricClass(Metric metric) { + if (metric instanceof DoubleMetric) { + return Double.class.getName(); + } else if (metric instanceof IntMetric) { + return Integer.class.getName(); + } else if (metric instanceof LongMetric) { + return Long.class.getName(); + } else if (metric instanceof DistributionMetric) { + return long[].class.getName(); + } + + throw new IllegalArgumentException("Unknown metric class " + metric.getClass()); + } +} diff --git a/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/JmxExporterTest.java b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/JmxExporterTest.java new file mode 100644 index 0000000000..fb5e5de1dc --- /dev/null +++ b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/JmxExporterTest.java @@ -0,0 +1,211 @@ +/* + * 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.ignite.internal.metrics; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MBeanServerInvocationHandler; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension; +import org.apache.ignite.internal.configuration.testframework.InjectConfiguration; +import org.apache.ignite.internal.metrics.configuration.MetricConfiguration; +import org.apache.ignite.internal.metrics.exporters.configuration.JmxExporterView; +import org.apache.ignite.internal.metrics.exporters.jmx.JmxExporter; +import org.apache.ignite.internal.util.IgniteUtils; +import org.apache.ignite.lang.IgniteBiTuple; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Tests for {@link JmxExporter}. + */ +@ExtendWith({ConfigurationExtension.class}) +public class JmxExporterTest { + @InjectConfiguration(value = "mock.exporters = {jmx = {exporterName = jmx}}") + private MetricConfiguration metricConfiguration; + + private JmxExporterView jmxExporterConf; + + private static final MBeanServer mbeanSrv = ManagementFactory.getPlatformMBeanServer(); + + private static final String SRC_NAME = "testSource"; + + private static final String MTRC_NAME = "testMetric"; + + /** + * Metric set with all available metric types. + */ + private static final MetricSet metricSet = + new MetricSet( + SRC_NAME, + Map.of( + "intGauge", new IntGauge("intGauge", "", () -> 1), + "longGauge", new LongGauge("longGauge", "", () -> 1L), + "doubleGauge", new DoubleGauge("doubleGauge", "", () -> 1d), + "atomicInt", new AtomicIntMetric("atomicInt", ""), + "atomicLong", new AtomicLongMetric("atomicLong", ""), + "atomicDouble", new AtomicDoubleMetric("atomicDouble", ""), + "longAdder", new LongAdderMetric("longAdder", ""), + "doubleAdder", new DoubleAdderMetric("doubleAdder", ""), + "distributionMetric", new DistributionMetric("distributionMetric", "", new long[] {0, 1}), + "hitRate", new HitRateMetric("hitRate", "", Long.MAX_VALUE) + ) + ); + + private ObjectName mbeanName; + + private MetricProvider metricsProvider; + + private JmxExporter jmxExporter; + + @BeforeEach + void setUp() throws MalformedObjectNameException { + jmxExporterConf = (JmxExporterView) metricConfiguration.exporters().get("jmx").value(); + + mbeanName = IgniteUtils.makeMbeanName("metrics", SRC_NAME); + + jmxExporter = new JmxExporter(); + + metricsProvider = mock(MetricProvider.class); + } + + @AfterEach + void tearDown() throws MBeanRegistrationException { + try { + mbeanSrv.unregisterMBean(mbeanName); + } catch (InstanceNotFoundException e) { + // No op + } + } + + @Test + public void testStart() + throws ReflectionException, AttributeNotFoundException, MBeanException { + Map<String, MetricSet> metrics = Map.of(metricSet.name(), metricSet); + + when(metricsProvider.metrics()).thenReturn(new IgniteBiTuple<>(metrics, 1L)); + + jmxExporter.start(metricsProvider, jmxExporterConf); + + assertThatMbeanAttributeAndMetricValuesAreTheSame(); + } + + @Test + public void testAddMetric() + throws ReflectionException, AttributeNotFoundException, MBeanException { + when(metricsProvider.metrics()).thenReturn(new IgniteBiTuple<>(new HashMap<>(), 1L)); + + jmxExporter.start(metricsProvider, jmxExporterConf); + + assertThrows( + InstanceNotFoundException.class, + this::getMbeanInfo, + "Expected that mbean won't find, but it was"); + + jmxExporter.addMetricSet(metricSet); + + assertThatMbeanAttributeAndMetricValuesAreTheSame(); + } + + @Test + public void testRemoveMetric() + throws ReflectionException, AttributeNotFoundException, MBeanException { + Map<String, MetricSet> metrics = Map.of(metricSet.name(), metricSet); + + when(metricsProvider.metrics()).thenReturn(new IgniteBiTuple<>(metrics, 1L)); + + jmxExporter.start(metricsProvider, jmxExporterConf); + + assertThatMbeanAttributeAndMetricValuesAreTheSame(); + + jmxExporter.removeMetricSet("testSource"); + + assertThrows(InstanceNotFoundException.class, this::getMbeanInfo, + "Expected that mbean won't find, but it was"); + } + + @Test + public void testMetricUpdate() throws ReflectionException, AttributeNotFoundException, MBeanException { + var intMetric = new AtomicIntMetric(MTRC_NAME, ""); + + MetricSet metricSet = new MetricSet(SRC_NAME, Map.of(MTRC_NAME, intMetric)); + + when(metricsProvider.metrics()).thenReturn(new IgniteBiTuple<>(Map.of(metricSet.name(), metricSet), 1L)); + + jmxExporter.start(metricsProvider, jmxExporterConf); + + assertEquals(0, mbean().getAttribute(MTRC_NAME)); + + intMetric.add(1); + + // test, that MBean has no stale metric value. + assertEquals(1, mbean().getAttribute(MTRC_NAME)); + } + + /** + * Check, that all MBean attributes has the same values as original metric values. + */ + private void assertThatMbeanAttributeAndMetricValuesAreTheSame() + throws ReflectionException, AttributeNotFoundException, MBeanException { + for (Iterator<Metric> it = metricSet.iterator(); it.hasNext(); ) { + Metric metric = it.next(); + + Object beanAttribute = mbean().getAttribute(metric.name()); + + String errorMsg = "Wrong MBean attribute value for the metric with name " + metric.name(); + + if (metric instanceof IntMetric) { + assertEquals(((IntMetric) metric).value(), beanAttribute, errorMsg); + } else if (metric instanceof LongMetric) { + assertEquals(((LongMetric) metric).value(), beanAttribute, errorMsg); + } else if (metric instanceof DoubleMetric) { + assertEquals(((DoubleMetric) metric).value(), beanAttribute, errorMsg); + } else if (metric instanceof DistributionMetric) { + assertArrayEquals(((DistributionMetric) metric).value(), (long[]) beanAttribute, errorMsg); + } + } + } + + private DynamicMBean mbean() { + return MBeanServerInvocationHandler.newProxyInstance(mbeanSrv, mbeanName, DynamicMBean.class, false); + } + + private MBeanInfo getMbeanInfo() throws ReflectionException, InstanceNotFoundException, IntrospectionException { + return ManagementFactory.getPlatformMBeanServer().getMBeanInfo(mbeanName); + } +} diff --git a/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/exporters/TestExporter.java b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/exporters/TestExporter.java index d40b100911..b95795cf22 100644 --- a/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/exporters/TestExporter.java +++ b/modules/metrics/src/test/java/org/apache/ignite/internal/metrics/exporters/TestExporter.java @@ -18,6 +18,7 @@ package org.apache.ignite.internal.metrics.exporters; import org.apache.ignite.internal.metrics.MetricProvider; +import org.apache.ignite.internal.metrics.MetricSet; import org.jetbrains.annotations.Nullable; /** @@ -30,14 +31,11 @@ public class TestExporter extends BasicMetricExporter<TestExporterView> { private volatile int port; @Override - public void init(MetricProvider metricsProvider, TestExporterView configuration) { - super.init(metricsProvider, configuration); + public void start(MetricProvider metricsProvider, TestExporterView configuration) { + super.start(metricsProvider, configuration); port = configuration.port(); - } - @Override - public void start() { started = true; } @@ -52,10 +50,20 @@ public class TestExporter extends BasicMetricExporter<TestExporterView> { } @Override - public void reconfigure(@Nullable TestExporterView newValue) { + public synchronized void reconfigure(@Nullable TestExporterView newValue) { super.reconfigure(newValue); - port = configuration.port(); + port = configuration().port(); + } + + @Override + public void addMetricSet(MetricSet metricSet) { + + } + + @Override + public void removeMetricSet(String metricSetName) { + } public boolean isStarted() {
