From 238a201055152aa920e35396358b0ea0a22a4e2b Mon Sep 17 00:00:00 2001 From: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:58:08 +0200 Subject: [PATCH] jmx state metrics (#12369) Co-authored-by: Jay DeLuca --- .../jmx-metrics/javaagent/README.md | 61 +++++ .../jmx/engine/BeanAttributeExtractor.java | 12 +- .../instrumentation/jmx/engine/BeanGroup.java | 22 +- .../jmx/engine/MetricAttribute.java | 4 + .../instrumentation/jmx/engine/MetricDef.java | 8 +- .../jmx/engine/MetricExtractor.java | 7 +- .../jmx/engine/MetricInfo.java | 12 +- .../jmx/engine/MetricRegistrar.java | 10 +- .../instrumentation/jmx/yaml/JmxRule.java | 127 ++++++--- .../jmx/yaml/MetricStructure.java | 124 +++++++-- .../instrumentation/jmx/yaml/RuleParser.java | 15 +- .../jmx/yaml/StateMapping.java | 150 ++++++++++ .../jmx/engine/RuleParserTest.java | 259 ++++++++++++++---- .../jmx/yaml/StateMappingTest.java | 76 +++++ 14 files changed, 746 insertions(+), 141 deletions(-) create mode 100644 instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/StateMapping.java create mode 100644 instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/yaml/StateMappingTest.java diff --git a/instrumentation/jmx-metrics/javaagent/README.md b/instrumentation/jmx-metrics/javaagent/README.md index 844137872f82..9481cc61a9d2 100644 --- a/instrumentation/jmx-metrics/javaagent/README.md +++ b/instrumentation/jmx-metrics/javaagent/README.md @@ -221,6 +221,67 @@ Thus, the above definitions will create several metrics, named `my.kafka.streams The metric descriptions will remain undefined, unless they are provided by the queried MBeans. +### State Metrics + +Some JMX attributes expose current state as a non-numeric MBean attribute, in order to capture those as metrics it is recommended to use the special `state` metric type. +For example, with Tomcat connector, the `Catalina:type=Connector,port=*` MBean has `stateName` (of type `String`), we can define the following rule: + +```yaml +--- +rules: + - bean: Catalina:type=Connector,port=* + mapping: + stateName: + type: state + metric: tomcat.connector + metricAttribute: + port: param(port) + connector_state: + ok: STARTED + failed: [STOPPED,FAILED] + degraded: '*' +``` + +For a given value of `port`, let's say `8080` This will capture the `tomcat.connector.state` metric of type `updowncounter` with value `0` or `1` and the `state` metric attribute will have a value in [`ok`,`failed`,`degraded`]. +For every sample, 3 metrics will be captured for each value of `state` depending on the value of `stateName`: + +When `stateName` = `STARTED`, we have: + +- `tomcat.connector` value = `1`, attributes `port` = `8080` and `connector_state` = `ok` +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `failed` +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `degraded` + +When `stateName` = `STOPPED` or `FAILED`, we have: + +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `ok` +- `tomcat.connector` value = `1`, attributes `port` = `8080` and `connector_state` = `failed` +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `degraded` + +For other values of `stateName`, we have: + +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `ok` +- `tomcat.connector` value = `0`, attributes `port` = `8080` and `connector_state` = `failed` +- `tomcat.connector` value = `1`, attributes `port` = `8080` and `connector_state` = `degraded` + +Each state key can be mapped to one or more values of the MBean attribute using: +- a string literal or a string array +- a `*` character to provide default option and avoid enumerating all values, this value must be quoted in YAML + +Exactly one `*` value must be present in the mapping to ensure all possible values of the MBean attribute can be mapped to a state key. + +The default value indicated by `*` does not require a dedicated state key. For example, if we want to have `connector_state` metric attribute with values `on` or `off`, we can use: +```yaml + connector_state: + on: STARTED + off: [STOPPED,FAILED,'*'] +``` +In the particular case where only two values are defined, we can simplify further by explicitly defining one state and rely on default for the other. +```yaml + connector_state: + on: STARTED + off: '*' +``` + ### General Syntax Here is the general description of the accepted configuration file syntax. The whole contents of the file is case-sensitive, with exception for `type` as described in the table below. diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java index 6d2c4ebc2274..95b750be615e 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java @@ -151,7 +151,7 @@ AttributeInfo getAttributeInfo(MBeanServerConnection connection, ObjectName obje // Verify correctness of configuration by attempting to extract the metric value. // The value will be discarded, but its type will be checked. - Object sampleValue = extractAttributeValue(connection, objectName, logger); + Object sampleValue = getSampleValue(connection, objectName); // Only numbers can be used to generate metric values if (sampleValue instanceof Number) { @@ -194,6 +194,11 @@ AttributeInfo getAttributeInfo(MBeanServerConnection connection, ObjectName obje return null; } + @Nullable + protected Object getSampleValue(MBeanServerConnection connection, ObjectName objectName) { + return extractAttributeValue(connection, objectName, logger); + } + /** * Extracts the specified attribute value. In case the value is a CompositeData, drills down into * it to find the correct singleton value (usually a Number or a String). @@ -203,7 +208,7 @@ AttributeInfo getAttributeInfo(MBeanServerConnection connection, ObjectName obje * pattern * @param logger the logger to use, may be null. Typically we want to log any issues with the * attributes during MBean discovery, but once the attribute is successfully detected and - * confirmed to be eligble for metric evaluation, any further attribute extraction + * confirmed to be eligible for metric evaluation, any further attribute extraction * malfunctions will be silent to avoid flooding the log. * @return the attribute value, if found, or {@literal null} if an error occurred */ @@ -253,7 +258,8 @@ private Object extractAttributeValue(MBeanServerConnection connection, ObjectNam } @Nullable - Number extractNumericalAttribute(MBeanServerConnection connection, ObjectName objectName) { + protected Number extractNumericalAttribute( + MBeanServerConnection connection, ObjectName objectName) { Object value = extractAttributeValue(connection, objectName); if (value instanceof Number) { return (Number) value; diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java index dc4fb3ef5fbb..629e151192c0 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java @@ -5,7 +5,11 @@ package io.opentelemetry.instrumentation.jmx.engine; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import javax.annotation.Nullable; +import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.QueryExp; @@ -16,7 +20,7 @@ public class BeanGroup { // How to specify the MBean(s) @Nullable private final QueryExp queryExp; - private final ObjectName[] namePatterns; + private final List namePatterns; /** * Constructor for BeanGroup. @@ -25,17 +29,29 @@ public class BeanGroup { * @param namePatterns an array of ObjectNames used to look for MBeans; usually they will be * patterns. If multiple patterns are provided, they work as logical OR. */ - public BeanGroup(@Nullable QueryExp queryExp, ObjectName... namePatterns) { + private BeanGroup(@Nullable QueryExp queryExp, List namePatterns) { this.queryExp = queryExp; this.namePatterns = namePatterns; } + public static BeanGroup forSingleBean(String bean) throws MalformedObjectNameException { + return new BeanGroup(null, Collections.singletonList(new ObjectName(bean))); + } + + public static BeanGroup forBeans(List beans) throws MalformedObjectNameException { + List list = new ArrayList<>(); + for (String name : beans) { + list.add(new ObjectName(name)); + } + return new BeanGroup(null, list); + } + @Nullable QueryExp getQueryExp() { return queryExp; } - ObjectName[] getNamePatterns() { + List getNamePatterns() { return namePatterns; } } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java index 882f1049c62d..22cc9ced3ba0 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java @@ -22,6 +22,10 @@ public MetricAttribute(String name, MetricAttributeExtractor extractor) { this.extractor = extractor; } + public boolean isStateAttribute() { + return extractor == null; + } + public String getAttributeName() { return name; } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java index 875e90b76cf1..3feb99e4741a 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.jmx.engine; +import java.util.List; + /** * A class providing a complete definition on how to create an Open Telemetry metric out of the JMX * system: how to extract values from MBeans and how to model, name and decorate them with @@ -74,7 +76,7 @@ public class MetricDef { private final BeanGroup beans; // Describes how to get the metric values and their attributes, and how to report them - private final MetricExtractor[] metricExtractors; + private final List metricExtractors; /** * Constructor for MetricDef. @@ -84,7 +86,7 @@ public class MetricDef { * MetricExtractor is provided, they should use unique metric names or unique metric * attributes */ - public MetricDef(BeanGroup beans, MetricExtractor... metricExtractors) { + public MetricDef(BeanGroup beans, List metricExtractors) { this.beans = beans; this.metricExtractors = metricExtractors; } @@ -93,7 +95,7 @@ BeanGroup getBeanGroup() { return beans; } - MetricExtractor[] getMetricExtractors() { + List getMetricExtractors() { return metricExtractors; } } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java index 2b9ce939168d..db76964f1320 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.jmx.engine; +import java.util.List; import javax.annotation.Nullable; /** @@ -22,14 +23,14 @@ public class MetricExtractor { private final BeanAttributeExtractor attributeExtractor; // Defines the Measurement attributes to be used when reporting the metric value. - private final MetricAttribute[] attributes; + private final List attributes; @Nullable private volatile DetectionStatus status; public MetricExtractor( BeanAttributeExtractor attributeExtractor, MetricInfo metricInfo, - MetricAttribute... attributes) { + List attributes) { this.attributeExtractor = attributeExtractor; this.metricInfo = metricInfo; this.attributes = attributes; @@ -43,7 +44,7 @@ BeanAttributeExtractor getMetricValueExtractor() { return attributeExtractor; } - MetricAttribute[] getAttributes() { + List getAttributes() { return attributes; } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java index fed1935ca764..9cb35ae53b15 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java @@ -19,7 +19,9 @@ public class MetricInfo { public enum Type { COUNTER, UPDOWNCOUNTER, - GAUGE + GAUGE, + /** state metric captured as updowncounter */ + STATE } // How to report the metric using OpenTelemetry API @@ -44,21 +46,21 @@ public MetricInfo( this.type = type == null ? Type.GAUGE : type; } - String getMetricName() { + public String getMetricName() { return metricName; } @Nullable - String getDescription() { + public String getDescription() { return description; } @Nullable - String getUnit() { + public String getUnit() { return unit; } - Type getType() { + public Type getType() { return type; } } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java index 44961345dda8..a984c7fd3d66 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java @@ -120,6 +120,13 @@ void enrollExtractor( } logger.log(INFO, "Created Gauge for {0}", metricName); } + break; + // CHECKSTYLE:OFF + case STATE: + { + // CHECKSTYLE:ON + throw new IllegalStateException("state metrics should not be registered"); + } } } @@ -173,9 +180,8 @@ static Consumer longTypeCallback(MetricExtractor extr */ static Attributes createMetricAttributes( MBeanServerConnection connection, ObjectName objectName, MetricExtractor extractor) { - MetricAttribute[] metricAttributes = extractor.getAttributes(); AttributesBuilder attrBuilder = Attributes.builder(); - for (MetricAttribute metricAttribute : metricAttributes) { + for (MetricAttribute metricAttribute : extractor.getAttributes()) { String attributeValue = metricAttribute.acquireAttributeValue(connection, objectName); if (attributeValue != null) { attrBuilder = attrBuilder.put(metricAttribute.getAttributeName(), attributeValue); diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java index 149b3bde338f..ab09fe3f18f9 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java @@ -9,6 +9,7 @@ import io.opentelemetry.instrumentation.jmx.engine.BeanAttributeExtractor; import io.opentelemetry.instrumentation.jmx.engine.BeanGroup; import io.opentelemetry.instrumentation.jmx.engine.MetricAttribute; +import io.opentelemetry.instrumentation.jmx.engine.MetricAttributeExtractor; import io.opentelemetry.instrumentation.jmx.engine.MetricDef; import io.opentelemetry.instrumentation.jmx.engine.MetricExtractor; import io.opentelemetry.instrumentation.jmx.engine.MetricInfo; @@ -18,6 +19,7 @@ import java.util.Map; import java.util.Set; import javax.annotation.Nullable; +import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; @@ -126,14 +128,9 @@ private static Map validateAttributeMapping(Map public MetricDef buildMetricDef() throws Exception { BeanGroup group; if (bean != null) { - group = new BeanGroup(null, new ObjectName(bean)); + group = BeanGroup.forSingleBean(bean); } else if (beans != null && !beans.isEmpty()) { - ObjectName[] objectNames = new ObjectName[beans.size()]; - int k = 0; - for (String oneBean : beans) { - objectNames[k++] = new ObjectName(oneBean); - } - group = new BeanGroup(null, objectNames); + group = BeanGroup.forBeans(beans); } else { throw new IllegalStateException("No ObjectName specified"); } @@ -143,8 +140,7 @@ public MetricDef buildMetricDef() throws Exception { } Set attrNames = mapping.keySet(); - MetricExtractor[] metricExtractors = new MetricExtractor[attrNames.size()]; - int n = 0; + List metricExtractors = new ArrayList<>(); for (String attributeName : attrNames) { BeanAttributeExtractor attrExtractor = BeanAttributeExtractor.fromName(attributeName); // This is essentially the same as 'attributeName' but with escape characters removed @@ -162,44 +158,107 @@ public MetricDef buildMetricDef() throws Exception { metricInfo = m.buildMetricInfo(prefix, niceAttributeName, getUnit(), getMetricType()); } - List attributeList; List ownAttributes = getAttributeList(); - if (ownAttributes != null && m != null && m.getAttributeList() != null) { - // MetricAttributes have been specified at two levels, need to combine them - attributeList = combineMetricAttributes(ownAttributes, m.getAttributeList()); - } else if (ownAttributes != null) { - attributeList = ownAttributes; - } else if (m != null && m.getAttributeList() != null) { - // Get the attributes from the metric - attributeList = m.getAttributeList(); + List metricAttributes = m != null ? m.getAttributeList() : null; + List attributeList = + combineMetricAttributes(ownAttributes, metricAttributes); + + // higher priority to metric level mapping, then jmx rule as fallback + StateMapping stateMapping = getEffectiveStateMapping(m, this); + + if (stateMapping.isEmpty()) { + metricExtractors.add(new MetricExtractor(attrExtractor, metricInfo, attributeList)); } else { - // There are no attributes at all - attributeList = new ArrayList(); - } - MetricExtractor metricExtractor = - new MetricExtractor( - attrExtractor, - metricInfo, - attributeList.toArray(new MetricAttribute[attributeList.size()])); - metricExtractors[n++] = metricExtractor; + // generate one metric extractor per state metric key + // each metric extractor will have the state attribute replaced with a constant + metricExtractors.addAll( + createStateMappingExtractors(stateMapping, attributeList, attrExtractor, metricInfo)); + } } return new MetricDef(group, metricExtractors); } + private static List createStateMappingExtractors( + StateMapping stateMapping, + List attributeList, + BeanAttributeExtractor attrExtractor, + MetricInfo metricInfo) { + + List extractors = new ArrayList<>(); + for (String key : stateMapping.getStateKeys()) { + List stateMetricAttributes = new ArrayList<>(); + + for (MetricAttribute ma : attributeList) { + // replace state metric attribute with constant key value + if (!ma.isStateAttribute()) { + stateMetricAttributes.add(ma); + } else { + stateMetricAttributes.add( + new MetricAttribute( + ma.getAttributeName(), MetricAttributeExtractor.fromConstant(key))); + } + } + + BeanAttributeExtractor stateMetricExtractor = + new BeanAttributeExtractor(attrExtractor.getAttributeName()) { + + @Override + protected Object getSampleValue( + MBeanServerConnection connection, ObjectName objectName) { + // metric actual type is sampled in the discovery process, so we have to + // make this extractor as extracting integers. + return 0; + } + + @Override + protected Number extractNumericalAttribute( + MBeanServerConnection connection, ObjectName objectName) { + String rawStateValue = attrExtractor.extractValue(connection, objectName); + String mappedStateValue = stateMapping.getStateValue(rawStateValue); + return key.equals(mappedStateValue) ? 1 : 0; + } + }; + + // state metric are always up/down counters + MetricInfo stateMetricInfo = + new MetricInfo( + metricInfo.getMetricName(), + metricInfo.getDescription(), + metricInfo.getUnit(), + MetricInfo.Type.UPDOWNCOUNTER); + + extractors.add( + new MetricExtractor(stateMetricExtractor, stateMetricInfo, stateMetricAttributes)); + } + return extractors; + } + private static List combineMetricAttributes( List ownAttributes, List metricAttributes) { + Map set = new HashMap<>(); - for (MetricAttribute ownAttribute : ownAttributes) { - set.put(ownAttribute.getAttributeName(), ownAttribute); + if (ownAttributes != null) { + for (MetricAttribute ownAttribute : ownAttributes) { + set.put(ownAttribute.getAttributeName(), ownAttribute); + } } - - // Let the metric level defined attributes override own attributes - for (MetricAttribute metricAttribute : metricAttributes) { - set.put(metricAttribute.getAttributeName(), metricAttribute); + if (metricAttributes != null) { + // Let the metric level defined attributes override own attributes + for (MetricAttribute metricAttribute : metricAttributes) { + set.put(metricAttribute.getAttributeName(), metricAttribute); + } } - return new ArrayList(set.values()); + return new ArrayList<>(set.values()); + } + + private static StateMapping getEffectiveStateMapping(Metric m, JmxRule rule) { + if (m == null || m.getStateMapping().isEmpty()) { + return rule.getStateMapping(); + } else { + return m.getStateMapping(); + } } } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java index 49e418816bdf..9967ac080c73 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java @@ -20,24 +20,39 @@ *
  • the metric attributes *
  • the unit * - *

    Known subclasses are JmxRule and Metric. + *

    Known subclasses are {@link JmxRule} and {@link Metric}. */ abstract class MetricStructure { // Used by the YAML parser + // // type: TYPE // metricAttribute: // KEY1: SPECIFICATION1 // KEY2: SPECIFICATION2 // unit: UNIT - - private Map metricAttribute; // unused, for YAML parser only + // + // For state metrics + // + // type: state + // metricAttribute: + // KEY1: SPECIFICATION1 + // state: + // state1: [a,b] + // state2: c + // state3: '*' + + private Map metricAttribute; + private StateMapping stateMapping = StateMapping.empty(); + private static final String STATE_MAPPING_DEFAULT = "*"; private String unit; private MetricInfo.Type metricType; private List metricAttributes; - public void setType(String t) { + MetricStructure() {} + + void setType(String t) { // Do not complain about case variations t = t.trim().toUpperCase(Locale.ROOT); this.metricType = MetricInfo.Type.valueOf(t); @@ -51,6 +66,15 @@ public void setUnit(String unit) { this.unit = validateUnit(unit.trim()); } + private static void addMappedValue( + StateMapping.Builder builder, String stateValue, String stateKey) { + if (stateValue.equals(STATE_MAPPING_DEFAULT)) { + builder.withDefaultState(stateKey); + } else { + builder.withMappedValue(stateValue, stateKey); + } + } + @CanIgnoreReturnValue private String validateUnit(String unit) { requireNonEmpty(unit, "Metric unit is empty"); @@ -58,22 +82,19 @@ private String validateUnit(String unit) { } /** - * When the YAML parser sets the metric attributes (as Strings), convert them immediately to - * MetricAttribute objects. Any errors during conversion will show in the context of the parsed - * YAML file. + * When the YAML parser sets the metric attributes, convert them immediately to MetricAttribute + * objects. Any errors during conversion will show in the context of the parsed YAML file. * * @param map the mapping of metric attribute keys to evaluating snippets */ - public void setMetricAttribute(Map map) { + public void setMetricAttribute(Map map) { this.metricAttribute = map; // pre-build the MetricAttributes - List attrList = new ArrayList<>(); - addMetricAttributes(attrList, map); - this.metricAttributes = attrList; + this.metricAttributes = addMetricAttributes(map); } // Used only for testing - public Map getMetricAttribute() { + public Map getMetricAttribute() { return metricAttribute; } @@ -91,18 +112,28 @@ protected void requireNonEmpty(String s, String msg) { } } - private static void addMetricAttributes( - List list, Map metricAttributeMap) { - if (metricAttributeMap != null) { - for (String key : metricAttributeMap.keySet()) { - String target = metricAttributeMap.get(key); - if (target == null) { - throw new IllegalStateException( - "nothing specified for metric attribute key '" + key + "'"); - } - list.add(buildMetricAttribute(key, target.trim())); + private List addMetricAttributes(Map metricAttributeMap) { + + List list = new ArrayList<>(); + for (String key : metricAttributeMap.keySet()) { + Object value = metricAttributeMap.get(key); + if (value == null) { + throw new IllegalStateException("nothing specified for metric attribute key '" + key + "'"); + } + + MetricAttribute attribute; + if (value instanceof String) { + attribute = buildMetricAttribute(key, ((String) value).trim()); + } else if (value instanceof Map) { + // here we use the structure to detect a state metric attribute and parse it. + attribute = buildStateMetricAttribute(key, (Map) value); + } else { + throw new IllegalArgumentException("unexpected metric attribute: " + value); } + + list.add(attribute); } + return list; } private static MetricAttribute buildMetricAttribute(String key, String target) { @@ -117,21 +148,58 @@ private static MetricAttribute buildMetricAttribute(String key, String target) { // Check for one of the cases as above if (target.startsWith("param(")) { if (k > 0) { + String jmxAttribute = target.substring(6, k).trim(); return new MetricAttribute( - key, MetricAttributeExtractor.fromObjectNameParameter(target.substring(6, k).trim())); + key, MetricAttributeExtractor.fromObjectNameParameter(jmxAttribute)); } } else if (target.startsWith("beanattr(")) { if (k > 0) { - return new MetricAttribute( - key, MetricAttributeExtractor.fromBeanAttribute(target.substring(9, k).trim())); + String jmxAttribute = target.substring(9, k).trim(); + return new MetricAttribute(key, MetricAttributeExtractor.fromBeanAttribute(jmxAttribute)); } } else if (target.startsWith("const(")) { if (k > 0) { - return new MetricAttribute( - key, MetricAttributeExtractor.fromConstant(target.substring(6, k).trim())); + String constantValue = target.substring(6, k).trim(); + return new MetricAttribute(key, MetricAttributeExtractor.fromConstant(constantValue)); } } - throw new IllegalArgumentException("Invalid metric attribute specification for '" + key + "'"); + String msg = "Invalid metric attribute specification for '" + key + "': " + target; + throw new IllegalArgumentException(msg); + } + + private MetricAttribute buildStateMetricAttribute(String key, Map stateMap) { + if (stateMap.isEmpty()) { + throw new IllegalArgumentException("state map is empty for key " + key); + } + if (!stateMapping.isEmpty()) { + throw new IllegalStateException("only a single state map is expected"); + } + + StateMapping.Builder builder = StateMapping.builder(); + + for (Map.Entry entry : stateMap.entrySet()) { + if (!(entry.getKey() instanceof String)) { + throw new IllegalArgumentException("unexpected state map key: " + entry.getKey()); + } + String stateKey = (String) entry.getKey(); + Object objValue = entry.getValue(); + if (objValue instanceof String) { + addMappedValue(builder, (String) objValue, stateKey); + } else if (objValue instanceof List) { + for (Object listEntry : (List) objValue) { + if (!(listEntry instanceof String)) { + throw new IllegalArgumentException("unexpected state list value: " + stateKey); + } + addMappedValue(builder, (String) listEntry, stateKey); + } + } + } + stateMapping = builder.build(); + return new MetricAttribute(key, null); + } + + public StateMapping getStateMapping() { + return stateMapping; } } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java index 73a81086b3ea..c4da81c7337d 100644 --- a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java @@ -97,9 +97,13 @@ private static Map parseMappings(@Nullable Map m Map mappings = new LinkedHashMap<>(); if (mappingYaml != null) { mappingYaml.forEach( - (name, metricYaml) -> - mappings.put( - name, metricYaml == null ? null : parseMetric((Map) metricYaml))); + (name, metricYaml) -> { + Metric m = null; + if (metricYaml != null) { + m = parseMetric((Map) metricYaml); + } + mappings.put(name, m); + }); } return mappings; } @@ -124,12 +128,13 @@ private static Metric parseMetric(Map metricYaml) { @SuppressWarnings("unchecked") private static void parseMetricStructure( Map metricStructureYaml, MetricStructure out) { + String type = (String) metricStructureYaml.remove("type"); if (type != null) { out.setType(type); } - Map metricAttribute = - (Map) metricStructureYaml.remove("metricAttribute"); + Map metricAttribute = + (Map) metricStructureYaml.remove("metricAttribute"); if (metricAttribute != null) { out.setMetricAttribute(metricAttribute); } diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/StateMapping.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/StateMapping.java new file mode 100644 index 000000000000..aa16c277a21f --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/StateMapping.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.yaml; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** State mapping for "state metrics", contains: */ +public class StateMapping { + + private static final StateMapping EMPTY = + new StateMapping(null, Collections.emptyMap(), Collections.emptySet()); + + /** default state to map entries that are not part of {@link #stateMapping} */ + @Nullable private final String defaultStateKey; + + /** maps values (keys) to their respective state (value) */ + private final Map stateMapping; + + /** set of all states, including {@link #defaultStateKey} */ + private final Set stateKeys; + + private StateMapping( + @Nullable String defaultState, Map stateMapping, Set stateKeys) { + this.defaultStateKey = defaultState; + this.stateMapping = stateMapping; + this.stateKeys = stateKeys; + } + + /** + * Returns {@literal} true when empty, {@literal false} otherwise + * + * @return {@literal true} when state mapping is empty, {@literal false} otherwise + */ + public boolean isEmpty() { + return stateKeys.isEmpty(); + } + + /** + * get state keys + * + * @return set of state keys, including the default one + */ + public Set getStateKeys() { + return stateKeys; + } + + /** + * get default state key + * + * @return default state key, {@literal null} when empty + */ + @Nullable + public String getDefaultStateKey() { + return defaultStateKey; + } + + /** + * Get mapped state value + * + * @param rawValue raw state value from JMX attribute + * @return mapped state value from raw value, default value or {@literal null} when empty + */ + @Nullable + public String getStateValue(String rawValue) { + String value = stateMapping.get(rawValue); + if (value == null) { + value = defaultStateKey; + } + return value; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Empty instance + * + * @return an empty {@link StateMapping} instance + */ + public static StateMapping empty() { + return EMPTY; + } + + public static class Builder { + + private String defaultState; + private final Map valueMapping; + private final Set stateKeys; + + private Builder() { + this.valueMapping = new HashMap<>(); + this.stateKeys = new HashSet<>(); + } + + /** + * Adds default state key + * + * @param state state key + * @return this + */ + @CanIgnoreReturnValue + public Builder withDefaultState(String state) { + if (defaultState != null) { + throw new IllegalStateException("default state already set"); + } + defaultState = state; + stateKeys.add(defaultState); + return this; + } + + /** + * Adds a mapped state value + * + * @param value raw value to be mapped + * @param state state value to map raw value to + * @return this + */ + @CanIgnoreReturnValue + public Builder withMappedValue(String value, String state) { + String currentMapping = valueMapping.putIfAbsent(value, state); + if (currentMapping != null) { + throw new IllegalStateException(value + " already mapped to " + currentMapping); + } + stateKeys.add(state); + return this; + } + + StateMapping build() { + if (stateKeys.isEmpty()) { + return EMPTY; + } + + if (defaultState == null) { + throw new IllegalStateException("missing default state"); + } + + return new StateMapping(defaultState, valueMapping, stateKeys); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java index 088199d21132..15b10df7cba9 100644 --- a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java @@ -10,6 +10,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import io.opentelemetry.instrumentation.jmx.yaml.JmxConfig; import io.opentelemetry.instrumentation.jmx.yaml.JmxRule; @@ -20,6 +22,10 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -30,7 +36,7 @@ class RuleParserTest { @BeforeAll static void setup() { parser = RuleParser.get(); - assertThat(parser == null).isFalse(); + assertThat(parser).isNotNull(); } /* @@ -68,19 +74,22 @@ static void setup() { @Test void testConf2() { - InputStream is = new ByteArrayInputStream(CONF2.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF2); List defs = config.getRules(); assertThat(defs).hasSize(2); JmxRule def1 = defs.get(0); - assertThat(def1.getBeans()).hasSize(2); - assertThat(def1.getMetricAttribute()).hasSize(2); + assertThat(def1.getBeans()).containsExactly("OBJECT:NAME1=*", "OBJECT:NAME2=*"); + assertThat(def1.getMetricAttribute()) + .hasSize(2) + .containsEntry("LABEL_KEY1", "param(PARAMETER)") + .containsEntry("LABEL_KEY2", "beanattr(ATTRIBUTE)"); Map attr = def1.getMapping(); - assertThat(attr).hasSize(4); + assertThat(attr) + .hasSize(4) + .containsKeys("ATTRIBUTE1", "ATTRIBUTE2", "ATTRIBUTE3", "ATTRIBUTE4"); Metric m1 = attr.get("ATTRIBUTE1"); assertThat(m1).isNotNull(); @@ -88,6 +97,21 @@ void testConf2() { assertThat(m1.getMetricType()).isEqualTo(MetricInfo.Type.GAUGE); assertThat(m1.getUnit()).isEqualTo("UNIT1"); assertThat(m1.getMetricAttribute()).containsExactly(entry("LABEL_KEY3", "const(CONSTANT)")); + + Metric m2 = attr.get("ATTRIBUTE2"); + assertThat(m2).isNotNull(); + assertThat(m2.getMetric()).isEqualTo("METRIC_NAME2"); + assertThat(m2.getDesc()).isEqualTo("DESCRIPTION2"); + assertThat(m2.getUnit()).isEqualTo("UNIT2"); + + JmxRule def2 = defs.get(1); + assertThat(def2.getBeans()).containsExactly("OBJECT:NAME3=*"); + assertThat(def2.getMetricAttribute()).isNull(); + + assertThat(def2.getMapping()).hasSize(1); + Metric m3 = def2.getMapping().get("ATTRIBUTE3"); + assertThat(m3.getMetric()).isEqualTo("METRIC_NAME3"); + assertThat(m3.getUnit()).isNull(); } private static final String CONF3 = @@ -103,9 +127,7 @@ void testConf2() { @Test void testConf3() { - InputStream is = new ByteArrayInputStream(CONF3.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF3); List defs = config.getRules(); assertThat(defs).hasSize(1); @@ -115,9 +137,15 @@ void testConf3() { assertThat(def1.getMetricAttribute()).isNull(); Map attr = def1.getMapping(); - assertThat(attr).hasSize(5).containsKey("ATTRIBUTE33"); + assertThat(attr) + .hasSize(5) + .containsKeys("ATTRIBUTE31", "ATTRIBUTE32", "ATTRIBUTE33", "ATTRIBUTE34", "ATTRIBUTE35"); + assertThat(attr.get("ATTRIBUTE32")).isNull(); assertThat(attr.get("ATTRIBUTE33")).isNull(); - assertThat(attr.get("ATTRIBUTE34")).isNotNull(); + Metric attribute34 = attr.get("ATTRIBUTE34"); + assertThat(attribute34).isNotNull(); + assertThat(attribute34.getMetric()).isEqualTo("METRIC_NAME34"); + assertThat(attr.get("ATTRIBUTE35")).isNull(); } /* @@ -149,38 +177,62 @@ void testConf3() { @Test void testConf4() throws Exception { - InputStream is = new ByteArrayInputStream(CONF4.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF4); List defs = config.getRules(); assertThat(defs).hasSize(1); - MetricDef metricDef = defs.get(0).buildMetricDef(); + JmxRule jmxDef = defs.get(0); + assertThat(jmxDef.getUnit()).isEqualTo("DEFAULT_UNIT"); + assertThat(jmxDef.getMetricType()).isEqualTo(MetricInfo.Type.UPDOWNCOUNTER); + + MetricDef metricDef = jmxDef.buildMetricDef(); assertThat(metricDef).isNotNull(); - assertThat(metricDef.getMetricExtractors()).hasSize(3); assertThat(metricDef.getMetricExtractors()) + .hasSize(3) .anySatisfy( m -> { assertThat(m.getMetricValueExtractor().getAttributeName()).isEqualTo("A.b"); - assertThat(m.getAttributes()).hasSize(3); - - MetricInfo mb1 = m.getInfo(); - assertThat(mb1.getMetricName()).isEqualTo("PREFIX.METRIC_NAME1"); - assertThat(mb1.getDescription()).isEqualTo("DESCRIPTION1"); - assertThat(mb1.getUnit()).isEqualTo("UNIT1"); - assertThat(mb1.getType()).isEqualTo(MetricInfo.Type.COUNTER); + assertThat(m.getAttributes()) + .hasSize(3) + .extracting("attributeName") + .contains("LABEL_KEY1", "LABEL_KEY2", "LABEL_KEY3"); + + MetricInfo metricInfo = m.getInfo(); + assertThat(metricInfo.getMetricName()).isEqualTo("PREFIX.METRIC_NAME1"); + assertThat(metricInfo.getDescription()).isEqualTo("DESCRIPTION1"); + assertThat(metricInfo.getUnit()).isEqualTo("UNIT1"); + assertThat(metricInfo.getType()).isEqualTo(MetricInfo.Type.COUNTER); + }) + .anySatisfy( + m -> { + assertThat(m.getMetricValueExtractor().getAttributeName()).isEqualTo("ATTRIBUTE2"); + assertThat(m.getAttributes()) + .hasSize(2) + .extracting("attributeName") + .contains("LABEL_KEY1", "LABEL_KEY2"); + + MetricInfo metricInfo = m.getInfo(); + assertThat(metricInfo.getMetricName()).isEqualTo("PREFIX.METRIC_NAME2"); + assertThat(metricInfo.getDescription()).isEqualTo("DESCRIPTION2"); + assertThat(metricInfo.getUnit()).isEqualTo("UNIT2"); }) .anySatisfy( m -> { assertThat(m.getMetricValueExtractor().getAttributeName()).isEqualTo("ATTRIBUTE3"); - MetricInfo mb3 = m.getInfo(); - assertThat(mb3.getMetricName()).isEqualTo("PREFIX.ATTRIBUTE3"); + MetricInfo metricInfo = m.getInfo(); + assertThat(metricInfo.getMetricName()).isEqualTo("PREFIX.ATTRIBUTE3"); + assertThat(metricInfo.getDescription()).isNull(); + // syntax extension - defining a default unit and type - assertThat(mb3.getType()).isEqualTo(MetricInfo.Type.UPDOWNCOUNTER); - assertThat(mb3.getUnit()).isEqualTo("DEFAULT_UNIT"); + assertThat(metricInfo.getType()) + .describedAs("default type should match jmx rule definition") + .isEqualTo(jmxDef.getMetricType()); + assertThat(metricInfo.getUnit()) + .describedAs("default unit should match jmx rule definition") + .isEqualTo(jmxDef.getUnit()); }); } @@ -193,9 +245,7 @@ void testConf4() throws Exception { @Test void testConf5() throws Exception { - InputStream is = new ByteArrayInputStream(CONF5.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF5); List defs = config.getRules(); assertThat(defs).hasSize(1); @@ -204,7 +254,7 @@ void testConf5() throws Exception { assertThat(metricDef).isNotNull(); assertThat(metricDef.getMetricExtractors()).hasSize(1); - MetricExtractor m1 = metricDef.getMetricExtractors()[0]; + MetricExtractor m1 = metricDef.getMetricExtractors().get(0); assertThat(m1.getMetricValueExtractor().getAttributeName()).isEqualTo("ATTRIBUTE"); assertThat(m1.getAttributes()).isEmpty(); @@ -227,9 +277,7 @@ void testConf5() throws Exception { @Test void testConf6() throws Exception { - InputStream is = new ByteArrayInputStream(CONF6.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF6); List defs = config.getRules(); assertThat(defs).hasSize(1); @@ -238,14 +286,16 @@ void testConf6() throws Exception { assertThat(metricDef).isNotNull(); assertThat(metricDef.getMetricExtractors()).hasSize(1); - MetricExtractor m1 = metricDef.getMetricExtractors()[0]; + MetricExtractor m1 = metricDef.getMetricExtractors().get(0); assertThat(m1.getMetricValueExtractor().getAttributeName()).isEqualTo("ATTRIBUTE"); // MetricAttribute set at the metric level should override the one set at the definition level - assertThat(m1.getAttributes()).hasSize(1); - assertThat(m1.getInfo().getMetricName()).isEqualTo("ATTRIBUTE"); + assertThat(m1.getAttributes()) + .hasSize(1) + .satisfiesExactly(a -> checkConstantMetricAttribute(a, "key1", "value2")); - MetricAttribute l1 = m1.getAttributes()[0]; - assertThat(l1.acquireAttributeValue(null, null)).isEqualTo("value2"); + assertThat(m1.getInfo().getMetricName()) + .describedAs("metric name should default to JMX attribute name") + .isEqualTo("ATTRIBUTE"); } private static final String CONF7 = @@ -261,9 +311,7 @@ void testConf6() throws Exception { @Test void testConf7() throws Exception { - InputStream is = new ByteArrayInputStream(CONF7.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF7); List defs = config.getRules(); assertThat(defs).hasSize(1); @@ -273,10 +321,13 @@ void testConf7() throws Exception { assertThat(metricDef.getMetricExtractors()).hasSize(1); // Test that the MBean attribute is correctly parsed - MetricExtractor m1 = metricDef.getMetricExtractors()[0]; + MetricExtractor m1 = metricDef.getMetricExtractors().get(0); assertThat(m1.getMetricValueExtractor().getAttributeName()).isEqualTo("ATTRIBUTE"); - assertThat(m1.getAttributes()).hasSize(2); assertThat(m1.getInfo().getMetricName()).isEqualTo("ATTRIBUTE"); + assertThat(m1.getAttributes()) + .hasSize(2) + .anySatisfy(a -> checkConstantMetricAttribute(a, "key1", "value1")) + .anySatisfy(a -> checkConstantMetricAttribute(a, "key2", "value2")); } private static final String EMPTY_CONF = "---\n"; @@ -290,9 +341,7 @@ void testConf7() throws Exception { @Test void testConf8() throws Exception { - InputStream is = new ByteArrayInputStream(CONF8.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(CONF8); List defs = config.getRules(); assertThat(defs).hasSize(1); @@ -301,7 +350,7 @@ void testConf8() throws Exception { assertThat(metricDef).isNotNull(); assertThat(metricDef.getMetricExtractors()).hasSize(1); - MetricExtractor m1 = metricDef.getMetricExtractors()[0]; + MetricExtractor m1 = metricDef.getMetricExtractors().get(0); assertThat(m1.getMetricValueExtractor().getAttributeName()).isEqualTo("Attr.with.dot"); assertThat(m1.getAttributes()).isEmpty(); @@ -312,25 +361,125 @@ void testConf8() throws Exception { assertThat(mb1.getUnit()).isNull(); } + private static final String CONF9 = + "--- # keep stupid spotlessJava at bay\n" + + "rules:\n" + + " - bean: my-test:type=9\n" + + " mapping:\n" + + " jmxStateAttribute:\n" + + " type: state\n" + + " metric: state_metric\n" + + " metricAttribute:\n" + + " state_attribute: \n" // --> only one state attribute allowed + + " ok: STARTED\n" // as simple string + + " failed: [STOPPED,FAILED]\n" // as array of strings + + " degraded: '*'\n" // degraded value for default + + ""; + + @Test + void testStateMetricConf() throws Exception { + JmxConfig config = parseConf(CONF9); + assertThat(config).isNotNull(); + + List rules = config.getRules(); + assertThat(rules).hasSize(1); + + JmxRule jmxRule = rules.get(0); + assertThat(jmxRule.getBean()).isEqualTo("my-test:type=9"); + Metric metric = jmxRule.getMapping().get("jmxStateAttribute"); + assertThat(metric.getMetricType()).isEqualTo(MetricInfo.Type.STATE); + + assertThat(metric.getStateMapping().isEmpty()).isFalse(); + assertThat(metric.getStateMapping().getStateKeys()).contains("ok", "failed", "degraded"); + assertThat(metric.getStateMapping().getDefaultStateKey()).isEqualTo("degraded"); + assertThat(metric.getStateMapping().getStateValue("STARTED")).isEqualTo("ok"); + assertThat(metric.getStateMapping().getStateValue("STOPPED")).isEqualTo("failed"); + assertThat(metric.getStateMapping().getStateValue("FAILED")).isEqualTo("failed"); + assertThat(metric.getStateMapping().getStateValue("OTHER")).isEqualTo("degraded"); + + Map metricAttributeMap = metric.getMetricAttribute(); + assertThat(metricAttributeMap).containsKey("state_attribute").hasSize(1); + assertThat(metricAttributeMap.get("state_attribute")).isInstanceOf(Map.class); + + ObjectName objectName = new ObjectName(jmxRule.getBean()); + MBeanServerConnection mockConnection = mock(MBeanServerConnection.class); + + // mock attribute value + when(mockConnection.getAttribute(objectName, "jmxStateAttribute")).thenReturn("STOPPED"); + + // mock attribute discovery + MBeanInfo mockBeanInfo = mock(MBeanInfo.class); + when(mockBeanInfo.getAttributes()) + .thenReturn( + new MBeanAttributeInfo[] { + new MBeanAttributeInfo( + "jmxStateAttribute", "java.lang.String", "", true, false, false) + }); + when(mockConnection.getMBeanInfo(objectName)).thenReturn(mockBeanInfo); + + MetricDef metricDef = jmxRule.buildMetricDef(); + assertThat(metricDef.getMetricExtractors()) + .hasSize(3) + .allSatisfy( + me -> { + assertThat(me.getInfo().getMetricName()).isEqualTo("state_metric"); + assertThat(me.getInfo().getType()).isEqualTo(MetricInfo.Type.UPDOWNCOUNTER); + + assertThat(me.getAttributes()).hasSize(1); + MetricAttribute stateAttribute = me.getAttributes().get(0); + assertThat(stateAttribute.getAttributeName()).isEqualTo("state_attribute"); + String stateAttributeValue = + stateAttribute.acquireAttributeValue(mockConnection, objectName); + + BeanAttributeExtractor attributeExtractor = me.getMetricValueExtractor(); + assertThat(attributeExtractor).isNotNull(); + assertThat(attributeExtractor.getSampleValue(null, null)) + .describedAs("sampled value must be an integer") + .isInstanceOf(Integer.class); + + assertThat(attributeExtractor.getAttributeInfo(mockConnection, objectName)) + .describedAs("attribute info must be provided as a regular int metric") + .isNotNull(); + + int expectedValue = stateAttributeValue.equals("failed") ? 1 : 0; + Number extractedValue = + attributeExtractor.extractNumericalAttribute(mockConnection, objectName); + assertThat(extractedValue) + .describedAs( + "metric value should be %d when '%s' attribute is '%s'", + expectedValue, stateAttribute.getAttributeName(), stateAttributeValue) + .isEqualTo(expectedValue); + }); + } + @Test void testEmptyConf() { - InputStream is = new ByteArrayInputStream(EMPTY_CONF.getBytes(StandardCharsets.UTF_8)); - JmxConfig config = parser.loadConfig(is); + JmxConfig config = parseConf(EMPTY_CONF); assertThat(config.getRules()).isEmpty(); } + private static void checkConstantMetricAttribute( + MetricAttribute attribute, String expectedName, String expectedValue) { + assertThat(attribute.getAttributeName()).isEqualTo(expectedName); + assertThat(attribute.acquireAttributeValue(null, null)).isEqualTo(expectedValue); + } + + private static JmxConfig parseConf(String s) { + InputStream is = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + JmxConfig jmxConfig = parser.loadConfig(is); + assertThat(jmxConfig).isNotNull(); + return jmxConfig; + } + /* * Negative tests */ private static void runNegativeTest(String yaml) { - InputStream is = new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)); - Assertions.assertThrows( Exception.class, () -> { - JmxConfig config = parser.loadConfig(is); - assertThat(config).isNotNull(); + JmxConfig config = parseConf(yaml); List defs = config.getRules(); assertThat(defs).hasSize(1); diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/yaml/StateMappingTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/yaml/StateMappingTest.java new file mode 100644 index 000000000000..f7a49a3c4add --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/yaml/StateMappingTest.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jmx.yaml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class StateMappingTest { + + @Test + void empty() { + StateMapping empty = StateMapping.builder().build(); + assertThat(empty).isSameAs(StateMapping.empty()); + + assertThat(empty.isEmpty()).isTrue(); + assertThat(empty.getDefaultStateKey()).isNull(); + assertThat(empty.getStateKeys()).isEmpty(); + assertThat(empty.getStateValue("any")).isNull(); + } + + @Test + void onlyDefault() { + StateMapping mapping = StateMapping.builder().withDefaultState("default").build(); + + assertThat(mapping.getDefaultStateKey()).isEqualTo("default"); + assertThat(mapping.getStateKeys()).containsExactly("default"); + assertThat(mapping.getStateValue("other")).isEqualTo("default"); + } + + @Test + void tryDuplicateDefault() { + assertThatThrownBy( + () -> + StateMapping.builder().withDefaultState("default").withDefaultState("default").build()); + } + + @Test + void tryMissingDefault() { + assertThatThrownBy(() -> StateMapping.builder().withMappedValue("value", "state").build()); + } + + @Test + void mapValues() { + StateMapping mapping = + StateMapping.builder() + .withDefaultState("unknown") + .withMappedValue("value1", "ok") + .withMappedValue("value1bis", "ok") + .withMappedValue("value2", "ko") + .build(); + + assertThat(mapping.getStateKeys()).hasSize(3).contains("ok", "ko", "unknown"); + assertThat(mapping.getDefaultStateKey()).isEqualTo("unknown"); + + assertThat(mapping.getStateValue("value1")).isEqualTo("ok"); + assertThat(mapping.getStateValue("value1bis")).isEqualTo("ok"); + assertThat(mapping.getStateValue("value2")).isEqualTo("ko"); + assertThat(mapping.getStateValue("other")).isEqualTo("unknown"); + } + + @Test + void tryDuplicateMappings() { + assertThatThrownBy( + () -> + StateMapping.builder() + .withDefaultState("default") + .withMappedValue("value", "state1") + .withMappedValue("value", "state2") + .build()); + } +}