From aa0ca10242c9e5fe7e3af9510f8a08b5d748d605 Mon Sep 17 00:00:00 2001 From: Alex Kats <56042997+akats7@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:02:36 -0400 Subject: [PATCH] Added transformation closure to MBeanHelper (#960) --- jmx-metrics/README.md | 20 ++++ .../contrib/jmxmetrics/MBeanHelper.groovy | 30 +++++- .../contrib/jmxmetrics/OtelHelper.groovy | 11 +++ .../contrib/jmxmetrics/MBeanHelperTest.java | 97 ++++++++++++++++++- 4 files changed, 150 insertions(+), 8 deletions(-) diff --git a/jmx-metrics/README.md b/jmx-metrics/README.md index 29e2db023..407c20845 100644 --- a/jmx-metrics/README.md +++ b/jmx-metrics/README.md @@ -160,6 +160,26 @@ In cases where you'd like to share instrument names while creating datapoints fo - `otel.instrument(MBeanHelper mBeanHelper, String name, String description, Map> attributeLabelFuncs, Closure instrument)` - `unit` is "1" and `labelFuncs` are empty map. - `otel.instrument(MBeanHelper mBeanHelper, String name, Map> attributeLabelFuncs, Closure instrument)` - `description` is empty string, `unit` is "1" and `labelFuncs` are empty map +### MBeans with non-numeric attributes + +In cases where you'd like to create metrics based on non-numeric MBean attributes, the mbean helper methods provide the ability to pass a map of closures, to transform the original extracted attribute into one that can be consumed by the instrument callbacks. + +- `otel.mbean(String objectNameStr, Map> attributeTransformation)` + +- `otel.mbeans(String objectNameStr, Map> attributeTransformation)` + +- `otel.mbeans(List objectNameStrs, Map> attributeTransformation)` + +These methods provide the ability to easily convert the attributes you will be extracting from the mbeans, at the time of creation for the MBeanHelper. + + ```groovy + // In this example a String based health attribute is converted to a numeric binary value + def someBean = otel.mbean( + "SomeMBean", ["CustomAttrFromString": { mbean -> mbean.getProperty("Attribute") == "running" ? 1 : 0 }] + ) + otel.instrument(someBean, "my-metric", "CustomAttrFromString", otel.&longUpDownCounterCallback) + ``` + ## OpenTelemetry Synchronous Instrument Helpers - `otel.doubleCounter(String name, String description, String unit)` diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelper.groovy b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelper.groovy index 1ba98467e..9cabdecf2 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelper.groovy +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelper.groovy @@ -35,6 +35,7 @@ class MBeanHelper { private final JmxClient jmxClient private final boolean isSingle private final List objectNames + private final Map attributeTransformation private List mbeans @@ -42,15 +43,31 @@ class MBeanHelper { this.jmxClient = jmxClient this.objectNames = Collections.unmodifiableList([objectName]) this.isSingle = isSingle + this.attributeTransformation = [:] as Map> } MBeanHelper(JmxClient jmxClient, List objectNames) { this.jmxClient = jmxClient this.objectNames = Collections.unmodifiableList(objectNames) this.isSingle = false + this.attributeTransformation = [:] as Map> } - @PackageScope static List queryJmx(JmxClient jmxClient, String objNameStr) { + MBeanHelper(JmxClient jmxClient, String objectName, boolean isSingle, Map> attributeTransformation ) { + this.jmxClient = jmxClient + this.objectNames = Collections.unmodifiableList([objectName]) + this.isSingle = isSingle + this.attributeTransformation = attributeTransformation + } + + MBeanHelper(JmxClient jmxClient, List objectNames, Map> attributeTransformation) { + this.jmxClient = jmxClient + this.objectNames = Collections.unmodifiableList(objectNames) + this.isSingle = false + this.attributeTransformation = attributeTransformation + } + + @PackageScope static List queryJmx(JmxClient jmxClient, String objNameStr) { return queryJmx(jmxClient, new ObjectName(objNameStr)) } @@ -88,7 +105,7 @@ class MBeanHelper { def ofInterest = isSingle ? [mbeans[0]]: mbeans return ofInterest.collect { - getBeanAttribute(it, attribute) + getBeanAttributeWithTransform(it, attribute) } } @@ -100,13 +117,18 @@ class MBeanHelper { def ofInterest = isSingle ? [mbeans[0]]: mbeans return [ofInterest, attributes].combinations().collect { pair -> def (bean, attribute) = pair - new Tuple3(bean, attribute, getBeanAttribute(bean, attribute)) + new Tuple3(bean, attribute, getBeanAttributeWithTransform(bean, attribute)) } } + Object getBeanAttributeWithTransform(GroovyMBean bean, String attribute){ + def transformationClosure = attributeTransformation.get(attribute); + return transformationClosure != null ? transformationClosure(bean) : getBeanAttribute(bean, attribute) + } + static Object getBeanAttribute(GroovyMBean bean, String attribute) { try { - bean.getProperty(attribute) + bean.getProperty(attribute) } catch (AttributeNotFoundException e) { logger.warning("Expected attribute ${attribute} not found in mbean ${bean.name()}") null diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy index e3ab6f3a5..49f071d6a 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/OtelHelper.groovy @@ -83,6 +83,17 @@ class OtelHelper { return mbeanHelper } + MBeanHelper mbean(String objNameStr, Map> attributeTransformation) { + def mbeanHelper = new MBeanHelper(jmxClient, objNameStr, true, attributeTransformation) + mbeanHelper.fetch() + return mbeanHelper + } + + MBeanHelper mbeans(List objNameStrs, Map> attributeTransformation) { + def mbeanHelper = new MBeanHelper(jmxClient, objNameStrs, attributeTransformation) + mbeanHelper.fetch() + return mbeanHelper + } /** * Returns an updated @{link InstrumentHelper} associated with the provided {@link MBeanHelper} and its specified * attribute value(s). The parameters map to the InstrumentHelper constructor. diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.java index 6dbecd06f..8f696ee5b 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.java @@ -9,13 +9,17 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import groovy.lang.Closure; +import groovy.util.Eval; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import javax.management.MBeanServer; import javax.management.ObjectInstance; import javax.management.ObjectName; @@ -81,7 +85,6 @@ void multiObj() throws Exception { MBeanHelper mBeanHelper = new MBeanHelper(jmxClient, Arrays.asList(thingName + ",thing=0", thingName + ",thing=1")); mBeanHelper.fetch(); - assertThat(mBeanHelper.getAttribute("SomeAttribute")) .hasSameElementsAs( IntStream.range(0, 2).mapToObj(Integer::toString).collect(Collectors.toList())); @@ -116,6 +119,77 @@ void multiple() throws Exception { IntStream.range(0, 100).mapToObj(unused -> null).collect(Collectors.toList())); } + @Test + void transform() throws Exception { + String thingName = "io.opentelemetry.contrib.jmxmetrics:type=transform"; + Thing thing = new Thing("someValue"); + mbeanServer.registerMBean(thing, new ObjectName(thingName)); + Map> map = + Stream.of( + new Object[][] { + { + "SomeAttribute", + Eval.me( + "{mbean -> mbean.getProperty(\"SomeAttribute\") == 'someValue' ? 'otherValue' : 'someValue'}") + }, + }) + .collect(Collectors.toMap(data -> (String) data[0], data -> (Closure) data[1])); + MBeanHelper mBeanHelper = new MBeanHelper(jmxClient, thingName + ",*", true, map); + mBeanHelper.fetch(); + + assertThat(mBeanHelper.getAttribute("SomeAttribute")) + .hasSameElementsAs(Stream.of(new String[] {"otherValue"}).collect(Collectors.toList())); + } + + @Test + void transformMultipleAttributes() throws Exception { + String thingName = "io.opentelemetry.contrib.jmxmetrics:type=transformMultiple"; + Thing thing1 = new Thing("someValue", "anotherValue"); + ObjectName mbeanName = new ObjectName(thingName); + mbeanServer.registerMBean(thing1, mbeanName); + Map> map = + Stream.of( + new Object[][] { + { + "SomeAttribute", + Eval.me( + "{mbean -> mbean.getProperty(\"SomeAttribute\") == 'someValue' ? 'newValue' : 'someValue'}") + }, + { + "AnotherAttribute", + Eval.me( + "{mbean -> mbean.getProperty(\"AnotherAttribute\") == 'anotherValue' ? 'anotherNewValue' : 'anotherValue'}") + }, + }) + .collect(Collectors.toMap(data -> (String) data[0], data -> (Closure) data[1])); + MBeanHelper mBeanHelper = new MBeanHelper(jmxClient, thingName + ",*", true, map); + mBeanHelper.fetch(); + + assertThat(mBeanHelper.getAttribute("SomeAttribute")) + .hasSameElementsAs(Stream.of(new String[] {"newValue"}).collect(Collectors.toList())); + assertThat(mBeanHelper.getAttribute("AnotherAttribute")) + .hasSameElementsAs( + Stream.of(new String[] {"anotherNewValue"}).collect(Collectors.toList())); + } + + @Test + void customAttribute() throws Exception { + String thingName = "io.opentelemetry.contrib.jmxmetrics:type=custom"; + Thing thing = new Thing(""); + mbeanServer.registerMBean(thing, new ObjectName(thingName)); + Map> map = + Stream.of( + new Object[][] { + {"CustomAttribute", Eval.me("{mbean -> 'customValue'}")}, + }) + .collect(Collectors.toMap(data -> (String) data[0], data -> (Closure) data[1])); + MBeanHelper mBeanHelper = new MBeanHelper(jmxClient, thingName, true, map); + mBeanHelper.fetch(); + + assertThat(mBeanHelper.getAttribute("CustomAttribute")) + .hasSameElementsAs(Stream.of(new String[] {"customValue"}).collect(Collectors.toList())); + } + private static void registerThings(String thingName) throws Exception { for (int i = 0; i < 100; i++) { Thing thing = new Thing(Integer.toString(i)); @@ -127,19 +201,34 @@ private static void registerThings(String thingName) throws Exception { public interface ThingMBean { String getSomeAttribute(); + + String getAnotherAttribute(); } static class Thing implements ThingMBean { - private final String attrValue; + private final String attrValue1; + + private final String attrValue2; Thing(String attrValue) { - this.attrValue = attrValue; + this.attrValue1 = attrValue; + this.attrValue2 = ""; + } + + Thing(String attrValue1, String attrValue2) { + this.attrValue1 = attrValue1; + this.attrValue2 = attrValue2; } @Override public String getSomeAttribute() { - return attrValue; + return attrValue1; + } + + @Override + public String getAnotherAttribute() { + return attrValue2; } } }