Skip to content

Commit

Permalink
jmx state metrics (#12369)
Browse files Browse the repository at this point in the history
Co-authored-by: Jay DeLuca <[email protected]>
  • Loading branch information
SylvainJuge and jaydeluca authored Oct 17, 2024
1 parent 925c920 commit 238a201
Show file tree
Hide file tree
Showing 14 changed files with 746 additions and 141 deletions.
61 changes: 61 additions & 0 deletions instrumentation/jmx-metrics/javaagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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).
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<ObjectName> namePatterns;

/**
* Constructor for BeanGroup.
Expand All @@ -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<ObjectName> 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<String> beans) throws MalformedObjectNameException {
List<ObjectName> 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<ObjectName> getNamePatterns() {
return namePatterns;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public MetricAttribute(String name, MetricAttributeExtractor extractor) {
this.extractor = extractor;
}

public boolean isStateAttribute() {
return extractor == null;
}

public String getAttributeName() {
return name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<MetricExtractor> metricExtractors;

/**
* Constructor for MetricDef.
Expand All @@ -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<MetricExtractor> metricExtractors) {
this.beans = beans;
this.metricExtractors = metricExtractors;
}
Expand All @@ -93,7 +95,7 @@ BeanGroup getBeanGroup() {
return beans;
}

MetricExtractor[] getMetricExtractors() {
List<MetricExtractor> getMetricExtractors() {
return metricExtractors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.instrumentation.jmx.engine;

import java.util.List;
import javax.annotation.Nullable;

/**
Expand All @@ -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<MetricAttribute> attributes;

@Nullable private volatile DetectionStatus status;

public MetricExtractor(
BeanAttributeExtractor attributeExtractor,
MetricInfo metricInfo,
MetricAttribute... attributes) {
List<MetricAttribute> attributes) {
this.attributeExtractor = attributeExtractor;
this.metricInfo = metricInfo;
this.attributes = attributes;
Expand All @@ -43,7 +44,7 @@ BeanAttributeExtractor getMetricValueExtractor() {
return attributeExtractor;
}

MetricAttribute[] getAttributes() {
List<MetricAttribute> getAttributes() {
return attributes;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}

Expand Down Expand Up @@ -173,9 +180,8 @@ static Consumer<ObservableLongMeasurement> 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);
Expand Down
Loading

0 comments on commit 238a201

Please sign in to comment.