JMX Metric Insight (#6573)

Solving
https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/6131
(JMX Support).

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
Peter Findeisen 2022-11-15 19:52:12 -08:00 committed by GitHub
parent 761751b08c
commit 4db65b6d1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 3326 additions and 0 deletions

View File

@ -0,0 +1,272 @@
# JMX Metric Insight
This subsystem provides a framework for collecting and reporting metrics provided by
[JMX](https://www.oracle.com/technical-resources/articles/javase/jmx.html) through
local [MBeans](https://docs.oracle.com/javase/tutorial/jmx/mbeans/index.html)
available within the instrumented application. The required MBeans and corresponding metrics can be described using a YAML configuration file. The individual metric configurations allow precise metric selection and identification.
The selected JMX metrics are reported using the Java Agent internal SDK. This means that they share the configuration and metric exporter with other metrics collected by the agent and are controlled by the same properties, for example `otel.metric.export.interval` or `otel.metrics.exporter`.
The Open Telemetry resource description for the metrics reported by JMX Metric Insight will be the same as for other metrics exported by the SDK, while the instrumentation scope will be `io.opentelemetry.jmx`.
To control the time interval between MBean detection attempts, one can use the `otel.jmx.discovery.delay` property, which defines the number of milliseconds to elapse between the first and the next detection cycle. JMX Metric Insight may dynamically adjust the time interval between further attempts, but it guarantees that the MBean discovery will run perpetually.
## Predefined metrics
JMX Metric Insight comes with a number of predefined configurations containing curated sets of JMX metrics for popular application servers or frameworks. To enable collection for the predefined metrics, specify a list of targets as the value for the `otel.jmx.target.system` property. For example
```bash
$ java -javaagent:path/to/opentelemetry-javaagent.jar \
-Dotel.jmx.target.system=jetty,kafka-broker \
... \
-jar myapp.jar
```
No targets are enabled by default. The supported target environments are listed below.
- [activemq](activemq.md)
- [jetty](jetty.md)
- [kafka-broker](kafka-broker.md)
- [tomcat](tomcat.md)
- [wildfly](wildfly.md)
- [hadoop](hadoop.md)
## Configuration File
To provide your own metric definitions, create a YAML configuration file, and specify its location using the `otel.jmx.config` property. For example
```bash
$ java -javaagent:path/to/opentelemetry-javaagent.jar \
-Dotel.jmx.config=path/to/config_file.yaml \
... \
-jar myapp.jar
```
### Basic Syntax
The configuration file can contain multiple entries (which we call _rules_), defining a number of metrics. Each rule must identify a set of MBeans and the name of the MBean attribute to query, along with additional information on how to report the values. Let's look at a simple example.
```yaml
---
rules:
- bean: java.lang:type=Threading
mapping:
ThreadCount:
metric: my.own.jvm.thread.count
type: updowncounter
desc: The current number of threads
unit: 1
```
MBeans are identified by unique [ObjectNames](https://docs.oracle.com/javase/8/docs/api/javax/management/ObjectName.html). In the example above, the object name `java.lang:type=Threading` identifies one of the standard JVM MBeans, which can be used to access a number of internal JVM statistics related to threads. For that MBean, we specify its attribute `ThreadCount` which reflects the number of currently active (alive) threads. The values of this attribute will be reported by a metric named `my.own.jvm.thread.count`. The declared OpenTelemetry type of the metric is declared as `updowncounter` which indicates that the value is a sum which can go up or down over time. Metric description and/or unit can also be specified.
All metrics reported by the service are backed by
[asynchronous instruments](https://opentelemetry.io/docs/reference/specification/metrics/api/#synchronous-and-asynchronous-instruments) which can be a
[Counter](https://opentelemetry.io/docs/reference/specification/metrics/api/#asynchronous-counter),
[UpDownCounter](https://opentelemetry.io/docs/reference/specification/metrics/api/#asynchronous-updowncounter), or a
[Gauge](https://opentelemetry.io/docs/reference/specification/metrics/api/#asynchronous-gauge) (the default).
To figure out what MBeans (or ObjectNames) and their attributes are available for your system, check its documentation, or use a universal MBean browsing tool, such as `jconsole`, available for every JDK version.
### Composite Types
The next example shows how the current heap size can be reported.
```yaml
---
rules:
- bean: java.lang:type=Memory
mapping:
HeapMemoryUsage.used:
metric: my.own.jvm.heap.used
type: updowncounter
desc: The current heap size
unit: By
HeapMemoryUsage.max:
metric: my.own.jvm.heap.max
type: updowncounter
desc: The maximum allowed heap size
unit: By
```
The MBean responsible for memory statistics, identified by ObjectName `java.lang:type=Memory` has an attribute named `HeapMemoryUsage`, which is of a `CompositeType`. This type represents a collection of fields with values (very much like the traditional `struct` data type). To access individual fields of the structure we use a dot which separates the MBean attribute name from the field name. The values are reported in bytes, which here we indicate by `By`. In the above example, the current heap size and the maximum allowed heap size will be reported as two metrics, named `my.own.jvm.heap.used`, and `my.own.jvm.heap.max`.
### Measurement Attributes
A more advanced example shows how to report similar metrics related to individual memory pools. A JVM can use a number of memory pools, some of them are part of the heap, and some are for JVM internal use. The number and the names of the memory pools depend on the JVM vendor, the Java version, and may even depend on the java command line options. Since the memory pools, in general, are unknown, we will use wildcard character for specifying memory pool name (in other words, we will use what is known as an ObjectName pattern).
```yaml
---
rules:
- bean: java.lang:name=*,type=MemoryPool
metricAttribute:
pool: param(name)
type: beanattr(Type)
mapping:
Usage.used:
metric: my.own.jvm.memory.pool.used
type: updowncounter
desc: Pool memory currently used
unit: By
Usage.max:
metric: my.own.jvm.memory.pool.max
type: updowncounter
desc: Maximum obtainable memory pool size
unit: By
```
The ObjectName pattern will match a number of MBeans, each for a different memory pool. The number and names of available memory pools, however, will be known only at runtime. To report values for all actual memory pools using only two metrics, we use metric attributes (referenced by the configuration file as `metricAttribute` elements). The first metric attribute, named `pool` will have its value derived from the ObjectName parameter `name` - which corresponds to the memory pool name. The second metric attribute, named `type` will get its value from the corresponding MBean attribute named `Type`. The values of this attribute are strings `HEAP` or `NON_HEAP` classifying the corresponding memory pool. Here the definition of the metric attributes is shared by both metrics, but it is also possible to define them at the individual metric level.
Using the above rule, when running on HotSpot JVM for Java 11, the following combinations of metric attributes will be reported.
- {pool="Compressed Class Space", type="NON_HEAP"}
- {pool="CodeHeap 'non-profiled nmethods'", type="NON_HEAP"}
- {pool="G1 Eden Space", type="HEAP"}
- {pool="G1 Old Gen", type="HEAP"}
- {pool="CodeHeap 'profiled nmethods'", type="NON_HEAP"}
- {pool="Metaspace", type="NON_HEAP"}
- {pool="CodeHeap 'non-nmethods'", type="NON_HEAP"}
- {pool="G1 Survivor Space", type="HEAP"}
**Note**: Heap and memory pool metrics above are given just as examples. The Java Agent already reports such metrics, no additional configuration is needed from the users.
### Mapping multiple MBean attributes to the same metric
Sometimes it is desired to merge several MBean attributes into a single metric, as shown in the next example.
```yaml
---
rules:
- bean: Catalina:type=GlobalRequestProcessor,name=*
metricAttribute:
handler: param(name)
type: counter
mapping:
bytesReceived:
metric: catalina.traffic
metricAttribute:
direction: const(in)
desc: The number of transmitted bytes
unit: By
bytesSent:
metric: catalina.traffic
metricAttribute:
direction: const(out)
desc: The number of transmitted bytes
unit: By
```
The referenced MBean has two attributes of interest, `bytesReceived`, and `bytesSent`. We want them to be reported by just one metric, but keeping the values separate by using metric attribute `direction`. This is achieved by specifying the same metric name `catalina.traffic` when mapping the MBean attributes to metrics. There will be two metric attributes provided: `handler`, which has a shared definition, and `direction`, which has its value (`in` or `out`) declared directly as constants, depending on the MBean attribute providing the metric value.
Keep in mind that when defining a metric multiple times like this, its type, unit and description must be exactly the same. Otherwise there will be complaints about attempts to redefine a metric in a non-compatible way.
The example also demonstrates that when specifying a number of MBean attribute mappings within the same rule, the metric type can be declared only once (outside of the `mapping` section).
Even when not reusing the metric name, special care also has to be taken when using ObjectName patterns (or specifying multiple ObjectNames - see the General Syntax section at the bottom of the page). Different ObjectNames matching the pattern must result in using different metric attribute values. Otherwise the same metric will be reported multiple times (using different metric values), which will likely clobber the previous values.
### Making shortcuts
While it is possible to define MBeans based metrics with fine details, sometimes it is desirable to provide the rules in compact format, minimizing the editing effort, but maintaining their efficiency and accuracy. The accepted YAML syntax allows to define some metric properties once per rule, which may lead to reduction in the amount of typing. This is especially visible if many related MBean attributes need to be covered, and is illustrated by the following example.
```yaml
---
rules:
- bean: kafka.streams:type=stream-thread-metrics,thread-id=*
metricAttribute:
threadId: param(thread-id)
prefix: my.kafka.streams.
unit: ms
mapping:
commit-latency-avg:
commit-latency-max:
poll-latency-avg:
poll-latency-max:
process-latency-avg:
process-latency-max:
punctuate-latency-avg:
punctuate-latency-max:
poll-records-avg:
unit: 1
poll-records-max:
unit: 1
- bean: kafka.streams:type=stream-thread-metrics,thread-id=*
metricAttribute:
threadId: param(thread-id)
prefix: my.kafka.streams.
unit: /s
type: gauge
mapping:
commit-rate:
process-rate:
task-created-rate:
task-closed-rate:
skipped-records-rate:
- bean: kafka.streams:type=stream-thread-metrics,thread-id=*
metricAttribute:
threadId: param(thread-id)
prefix: my.kafka.streams.totals.
unit: 1
type: counter
mapping:
commit-total:
poll-total:
process-total:
task-created-total:
task-closed-total:
```
Because we declared metric prefix (here `my.kafka.streams.`) and did not specify actual metric names, the metric names will be generated automatically, by appending the corresponding MBean attribute name to the prefix.
Thus, the above definitions will create several metrics, named `my.kafka.streams.commit-latency-avg`, `my.kafka.streams.commit-latency-max`, and so on. For the first configuration rule, the default unit has been changed to `ms`, which remains in effect for all MBean attribute mappings listed within the rule, unless they define their own unit. Similarly, the second configuration rule defines the unit as `/s`, valid for all the rates reported.
The metric descriptions will remain undefined, unless they are provided by the queried MBeans.
### 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.
```yaml
---
rules: # start of list of configuration rules
- bean: <OBJECTNAME> # can contain wildcards
metricAttribute: # optional metric attributes, they apply to all metrics below
<ATTRIBUTE1>: param(<PARAM>) # <PARAM> is used as the key to extract value from actual ObjectName
<ATTRIBUTE2>: beanattr(<ATTR>) # <ATTR> is used as the MBean attribute name to extract the value
prefix: <METRIC_NAME_PREFIX> # optional, useful for avoiding specifying metric names below
unit: <UNIT> # optional, redefines the default unit for the whole rule
type: <TYPE> # optional, redefines the default type for the whole rule
mapping:
<BEANATTR1>: # an MBean attribute name defining the metric value
metric: <METRIC_NAME1> # metric name will be <METRIC_NAME_PREFIX><METRIC_NAME1>
type: <TYPE> # optional, the default type is gauge
desc: <DESCRIPTION1> # optional
unit: <UNIT1> # optional
metricAttribute: # optional, will be used in addition to the shared metric attributes above
<ATTRIBUTE3>: const(<STR>) # direct value for the metric attribute
<BEANATTR2>: # use a.b to get access into CompositeData
metric: <METRIC_NAME2> # optional, the default is the MBean attribute name
unit: <UNIT2> # optional
<BEANATTR3>: # metric name will be <METRIC_NAME_PREFIX><BEANATTR3>
<BEANATTR4>: # metric name will be <METRIC_NAME_PREFIX><BEANATTR4>
- beans: # alternatively, if multiple object names are needed
- <OBJECTNAME1> # at least one object name must be specified
- <OBJECTNAME2>
mapping:
<BEANATTR5>: # an MBean attribute name defining the metric value
metric: <METRIC_NAME5> # metric name will be <METRIC_NAME5>
type: updowncounter # optional
<BEANATTR6>: # metric name will be <BEANATTR6>
```
The following table explains the used terms with more details.
| Syntactic Element | Description |
| ---------------- | --------------- |
| OBJECTNAME | A syntactically valid string representing an ObjectName (see [ObjectName constructor](https://docs.oracle.com/javase/8/docs/api/javax/management/ObjectName.html#ObjectName-java.lang.String-)). |
| ATTRIBUTE | Any well-formed string that can be used as a metric [attribute](https://opentelemetry.io/docs/reference/specification/common/#attribute) key. |
| ATTR | A non-empty string used as a name of the MBean attribute. The MBean attribute value must be a String, otherwise the specified metric attribute will not be used. |
| PARAM | A non-empty string used as a property key in the ObjectName identifying the MBean which provides the metric value. If the ObjectName does not have a property with the given key, the specified metric attribute will not be used. |
| METRIC_NAME_PREFIX | Any non-empty string which will be prepended to the specified metric (instrument) names. |
| METRIC_NAME | Any non-empty string. The string, prefixed by the optional prefix (see above) must satisfy [instrument naming rule](https://opentelemetry.io/docs/reference/specification/metrics/api/#instrument-naming-rule). |
| TYPE | One of `counter`, `updowncounter`, or `gauge`. The default is `gauge`. This value is case insensitive. |
| DESCRIPTION | Any string to be used as human-readable [description](https://opentelemetry.io/docs/reference/specification/metrics/api/#instrument-description) of the metric. If the description is not provided by the rule, an attempt will be made to extract one automatically from the corresponding MBean. |
| UNIT | A string identifying the [unit](https://opentelemetry.io/docs/reference/specification/metrics/api/#instrument-unit) of measurements reported by the metric. Enclose the string in single or double quotes if using unit annotations. |
| STR | Any string to be used directly as the metric attribute value. |
| BEANATTR | A non-empty string representing the MBean attribute defining the metric value. The attribute value must be a number. Special dot-notation _attributeName.itemName_ can be used to access numerical items within attributes of [CompositeType](https://docs.oracle.com/javase/8/docs/api/javax/management/openmbean/CompositeType.html). |
## Assumptions and Limitations
This version of JMX Metric Insight has a number of limitations.
- MBean attributes with the same name but belonging to different MBeans described by a single metric rule must have the same type (long or double).
- All MBeans which are described by the specified ObjectNames in a single rule must be registered with the same MBeanServer instance.
- While MBeanServers and MBeans can be created dynamically by the application, it is assumed that they will live indefinitely. Their disappearance may not be recognized properly, and may lead to some memory leaks.

View File

@ -0,0 +1,17 @@
# ActiveMQ Metrics
Here is the list of metrics based on MBeans exposed by ActiveMQ.
| Metric Name | Type | Attributes | Description |
| ---------------- | --------------- | ---------------- | --------------- |
| activemq.ProducerCount | UpDownCounter | destination, broker | The number of producers attached to this destination |
| activemq.ConsumerCount | UpDownCounter | destination, broker | The number of consumers subscribed to this destination |
| activemq.memory.MemoryPercentUsage | Gauge | destination, broker | The percentage of configured memory used |
| activemq.message.QueueSize | UpDownCounter | destination, broker | The current number of messages waiting to be consumed |
| activemq.message.ExpiredCount | Counter | destination, broker | The number of messages not delivered because they expired |
| activemq.message.EnqueueCount | Counter | destination, broker | The number of messages sent to this destination |
| activemq.message.DequeueCount | Counter | destination, broker | The number of messages acknowledged and removed from this destination |
| activemq.message.AverageEnqueueTime | Gauge | destination, broker | The average time a message was held on this destination |
| activemq.connections.CurrentConnectionsCount | UpDownCounter | | The total number of current connections |
| activemq.disc.StorePercentUsage | Gauge | | The percentage of configured disk used for persistent messages |
| activemq.disc.TempPercentUsage | Gauge | | The percentage of configured disk used for non-persistent messages |

View File

@ -0,0 +1,9 @@
plugins {
id("otel.javaagent-instrumentation")
}
dependencies {
implementation(project(":instrumentation:jmx-metrics:library"))
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
}

View File

@ -0,0 +1,15 @@
# Hadoop Metrics
Here is the list of metrics based on MBeans exposed by Hadoop.
| Metric Name | Type | Attributes | Description |
|-----------------------------------|---------------|------------------|-------------------------------------------------------|
| hadoop.capacity.CapacityUsed | UpDownCounter | node_name | Current used capacity across all data nodes |
| hadoop.capacity.CapacityTotal | UpDownCounter | node_name | Current raw capacity of data nodes |
| hadoop.block.BlocksTotal | UpDownCounter | node_name | Current number of allocated blocks in the system |
| hadoop.block.MissingBlocks | UpDownCounter | node_name | Current number of missing blocks |
| hadoop.block.CorruptBlocks | UpDownCounter | node_name | Current number of blocks with corrupt replicas |
| hadoop.volume.VolumeFailuresTotal | UpDownCounter | node_name | Total number of volume failures across all data nodes |
| hadoop.file.FilesTotal | UpDownCounter | node_name | Current number of files and directories |
| hadoop.file.TotalLoad | UpDownCounter | node_name | Current number of connection |
| hadoop.datanode.Count | UpDownCounter | node_name, state | The Number of data nodes |

View File

@ -0,0 +1,16 @@
# Jetty Metrics
Here is the list of metrics based on MBeans exposed by Jetty.
| Metric Name | Type | Attributes | Description |
|--------------------------------|---------------|--------------|------------------------------------------------------|
| jetty.session.sessionsCreated | Counter | resource | The number of sessions established in total |
| jetty.session.sessionTimeTotal | Counter | resource | The total time sessions have been active |
| jetty.session.sessionTimeMax | Gauge | resource | The maximum amount of time a session has been active |
| jetty.session.sessionTimeMean | Gauge | resource | The mean time sessions remain active |
| jetty.threads.busyThreads | UpDownCounter | | The current number of busy threads |
| jetty.threads.idleThreads | UpDownCounter | | The current number of idle threads |
| jetty.threads.maxThreads | UpDownCounter | | The maximum number of threads in the pool |
| jetty.threads.queueSize | UpDownCounter | | The current number of threads in the queue |
| jetty.io.selectCount | Counter | resource, id | The number of select calls |
| jetty.logging.LoggerCount | UpDownCounter | | The number of registered loggers by name |

View File

@ -0,0 +1,32 @@
# Kafka Broker Metrics
Here is the list of metrics based on MBeans exposed by Kafka broker. <br /><br />
Broker metrics:
| Metric Name | Type | Attributes | Description |
|------------------------------------|---------------|------------|----------------------------------------------------------------------|
| kafka.message.count | Counter | | The number of messages received by the broker |
| kafka.request.count | Counter | type | The number of requests received by the broker |
| kafka.request.failed | Counter | type | The number of requests to the broker resulting in a failure |
| kafka.request.time.total | Counter | type | The total time the broker has taken to service requests |
| kafka.request.time.50p | Gauge | type | The 50th percentile time the broker has taken to service requests |
| kafka.request.time.99p | Gauge | type | The 99th percentile time the broker has taken to service requests |
| kafka.request.queue | UpDownCounter | | Size of the request queue |
| kafka.network.io | Counter | direction | The bytes received or sent by the broker |
| kafka.purgatory.size | UpDownCounter | type | The number of requests waiting in purgatory |
| kafka.partition.count | UpDownCounter | | The number of partitions on the broker |
| kafka.partition.offline | UpDownCounter | | The number of partitions offline |
| kafka.partition.underReplicated | UpDownCounter | | The number of under replicated partitions |
| kafka.isr.operation.count | UpDownCounter | operation | The number of in-sync replica shrink and expand operations |
| kafka.lag.max | Gauge | | The max lag in messages between follower and leader replicas |
| kafka.controller.active.count | UpDownCounter | | The number of controllers active on the broker |
| kafka.leaderElection.count | Counter | | The leader election count |
| kafka.leaderElection.unclean.count | Counter | | Unclean leader election count - increasing indicates broker failures |
<br />
Log metrics:
| Metric Name | Type | Attributes | Description |
|---------------------------|---------|------------|----------------------------------|
| kafka.logs.flush.count | Counter | | Log flush count |
| kafka.logs.flush.time.50p | Gauge | | Log flush time - 50th percentile |
| kafka.logs.flush.time.99p | Gauge | | Log flush time - 99th percentile |

View File

@ -0,0 +1,104 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.javaagent.jmx;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import com.google.auto.service.AutoService;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.jmx.engine.JmxMetricInsight;
import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration;
import io.opentelemetry.instrumentation.jmx.yaml.RuleParser;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
/** An {@link AgentListener} that enables JMX metrics during agent startup. */
@AutoService(AgentListener.class)
public class JmxMetricInsightInstaller implements AgentListener {
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
ConfigProperties config = autoConfiguredSdk.getConfig();
if (config.getBoolean("otel.jmx.enabled", true)) {
JmxMetricInsight service =
JmxMetricInsight.createService(GlobalOpenTelemetry.get(), beanDiscoveryDelay(config));
MetricConfiguration conf = buildMetricConfiguration(config);
service.start(conf);
}
}
private static long beanDiscoveryDelay(ConfigProperties configProperties) {
Long discoveryDelay = configProperties.getLong("otel.jmx.discovery.delay");
if (discoveryDelay != null) {
return discoveryDelay;
}
// If discovery delay has not been configured, have a peek at the metric export interval.
// It makes sense for both of these values to be similar.
long exportInterval = configProperties.getLong("otel.metric.export.interval", 60000);
return exportInterval;
}
private static String resourceFor(String platform) {
return "/jmx/rules/" + platform + ".yaml";
}
private static void addRulesForPlatform(String platform, MetricConfiguration conf) {
String yamlResource = resourceFor(platform);
try (InputStream inputStream =
JmxMetricInsightInstaller.class.getResourceAsStream(yamlResource)) {
if (inputStream != null) {
JmxMetricInsight.getLogger().log(FINE, "Opened input stream {0}", yamlResource);
RuleParser parserInstance = RuleParser.get();
parserInstance.addMetricDefsTo(conf, inputStream);
} else {
JmxMetricInsight.getLogger().log(INFO, "No support found for {0}", platform);
}
} catch (Exception e) {
JmxMetricInsight.getLogger().warning(e.getMessage());
}
}
private static void buildFromDefaultRules(
MetricConfiguration conf, ConfigProperties configProperties) {
String targetSystem = configProperties.getString("otel.jmx.target.system", "");
String[] platforms = targetSystem.isEmpty() ? new String[0] : targetSystem.split(",");
for (String platform : platforms) {
addRulesForPlatform(platform, conf);
}
}
private static void buildFromUserRules(
MetricConfiguration conf, ConfigProperties configProperties) {
String jmxDir = configProperties.getString("otel.jmx.config");
if (jmxDir != null) {
JmxMetricInsight.getLogger().log(FINE, "JMX config file name: {0}", jmxDir);
RuleParser parserInstance = RuleParser.get();
try (InputStream inputStream = Files.newInputStream(new File(jmxDir.trim()).toPath())) {
parserInstance.addMetricDefsTo(conf, inputStream);
} catch (Exception e) {
JmxMetricInsight.getLogger().warning(e.getMessage());
}
}
}
private static MetricConfiguration buildMetricConfiguration(ConfigProperties configProperties) {
MetricConfiguration metricConfiguration = new MetricConfiguration();
buildFromDefaultRules(metricConfiguration, configProperties);
buildFromUserRules(metricConfiguration, configProperties);
return metricConfiguration;
}
}

View File

@ -0,0 +1,68 @@
---
rules:
- beans:
- org.apache.activemq:type=Broker,brokerName=*,destinationType=Queue,destinationName=*
- org.apache.activemq:type=Broker,brokerName=*,destinationType=Topic,destinationName=*
metricAttribute:
destination: param(destinationName)
broker: param(brokerName)
prefix: activemq.
mapping:
ProducerCount:
unit: '{producers}'
type: updowncounter
desc: The number of producers attached to this destination
ConsumerCount:
unit: '{consumers}'
type: updowncounter
desc: The number of consumers subscribed to this destination
MemoryPercentUsage:
metric: memory.MemoryPercentUsage
unit: '%'
type: gauge
desc: The percentage of configured memory used
QueueSize:
metric: message.QueueSize
unit: '{messages}'
type: updowncounter
desc: The current number of messages waiting to be consumed
ExpiredCount:
metric: message.ExpiredCount
unit: '{messages}'
type: counter
desc: The number of messages not delivered because they expired
EnqueueCount:
metric: message.EnqueueCount
unit: '{messages}'
type: counter
desc: The number of messages sent to this destination
DequeueCount:
metric: message.DequeueCount
unit: '{messages}'
type: counter
desc: The number of messages acknowledged and removed from this destination
AverageEnqueueTime:
metric: message.AverageEnqueueTime
unit: ms
type: gauge
desc: The average time a message was held on this destination
- bean: org.apache.activemq:type=Broker,brokerName=*
metricAttribute:
broker: param(brokerName)
prefix: activemq.
unit: '%'
type: gauge
mapping:
CurrentConnectionsCount:
metric: connections.CurrentConnectionsCount
type: updowncounter
unit: '{connections}'
desc: The total number of current connections
StorePercentUsage:
metric: disc.StorePercentUsage
desc: The percentage of configured disk used for persistent messages
TempPercentUsage:
metric: disc.TempPercentUsage
desc: The percentage of configured disk used for non-persistent messages

View File

@ -0,0 +1,63 @@
---
rules:
- bean: Hadoop:service=NameNode,name=FSNamesystem
unit: 1
prefix: hadoop.
metricAttribute:
node_name: param(tag.Hostname)
mapping:
CapacityUsed:
metric: capacity.CapacityUsed
type: updowncounter
unit: By
desc: Current used capacity across all data nodes
CapacityTotal:
metric: capacity.CapacityTotal
type: updowncounter
unit: By
BlocksTotal:
metric: block.BlocksTotal
type: updowncounter
unit: '{blocks}'
desc: Current number of allocated blocks in the system
MissingBlocks:
metric: block.MissingBlocks
type: updowncounter
unit: '{blocks}'
desc: Current number of missing blocks
CorruptBlocks:
metric: block.CorruptBlocks
type: updowncounter
unit: '{blocks}'
desc: Current number of blocks with corrupt replicas
VolumeFailuresTotal:
metric: volume.VolumeFailuresTotal
type: updowncounter
unit: '{volumes}'
desc: Total number of volume failures across all data nodes
metricAttribute:
direction: const(sent)
FilesTotal:
metric: file.FilesTotal
type: updowncounter
unit: '{files}'
desc: Current number of files and directories
TotalLoad:
metric: file.TotalLoad
type: updowncounter
unit: '{operations}'
desc: Current number of connections
NumLiveDataNodes:
metric: datenode.Count
type: updowncounter
unit: '{nodes}'
desc: The Number of data nodes
metricAttribute:
state: const(live)
NumDeadDataNodes:
metric: datenode.Count
type: updowncounter
unit: '{nodes}'
desc: The Number of data nodes
metricAttribute:
state: const(dead)

View File

@ -0,0 +1,56 @@
---
rules:
- bean: org.eclipse.jetty.server.session:context=*,type=sessionhandler,id=*
unit: s
prefix: jetty.session.
type: updowncounter
metricAttribute:
resource: param(context)
mapping:
sessionsCreated:
unit: '{sessions}'
type: counter
desc: The number of sessions established in total
sessionTimeTotal:
type: counter
desc: The total time sessions have been active
sessionTimeMax:
type: gauge
desc: The maximum amount of time a session has been active
sessionTimeMean:
type: gauge
desc: The mean time sessions remain active
- bean: org.eclipse.jetty.util.thread:type=queuedthreadpool,id=*
prefix: jetty.threads.
unit: '{threads}'
type: updowncounter
mapping:
busyThreads:
desc: The current number of busy threads
idleThreads:
desc: The current number of idle threads
maxThreads:
desc: The maximum number of threads in the pool
queueSize:
desc: The current number of threads in the queue
- bean: org.eclipse.jetty.io:context=*,type=managedselector,id=*
prefix: jetty.io.
metricAttribute:
resource: param(context)
id: param(id)
mapping:
selectCount:
type: counter
unit: 1
desc: The number of select calls
- bean: org.eclipse.jetty.logging:type=jettyloggerfactory,id=*
prefix: jetty.logging.
mapping:
LoggerCount:
type: updowncounter
unit: 1
desc: The number of registered loggers by name

View File

@ -0,0 +1,204 @@
---
rules:
# Broker metrics
- bean: kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec
mapping:
Count:
metric: kafka.message.count
type: counter
desc: The number of messages received by the broker
unit: '{messages}'
- bean: kafka.server:type=BrokerTopicMetrics,name=TotalFetchRequestsPerSec
metricAttribute:
type: const(fetch)
mapping:
Count:
metric: kafka.request.count
type: counter
desc: The number of requests received by the broker
unit: '{requests}'
- bean: kafka.server:type=BrokerTopicMetrics,name=TotalProduceRequestsPerSec
metricAttribute:
type: const(produce)
mapping:
Count:
metric: kafka.request.count
type: counter
desc: The number of requests received by the broker
unit: '{requests}'
- bean: kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec
metricAttribute:
type: const(fetch)
mapping:
Count:
metric: kafka.request.failed
type: counter
desc: The number of requests to the broker resulting in a failure
unit: '{requests}'
- bean: kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec
metricAttribute:
type: const(produce)
mapping:
Count:
metric: kafka.request.failed
type: counter
desc: The number of requests to the broker resulting in a failure
unit: '{requests}'
- beans:
- kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce
- kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchConsumer
- kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchFollower
metricAttribute:
type: param(request)
unit: ms
mapping:
Count:
metric: kafka.request.time.total
type: counter
desc: The total time the broker has taken to service requests
50thPercentile:
metric: kafka.request.time.50p
type: gauge
desc: The 50th percentile time the broker has taken to service requests
99thPercentile:
metric: kafka.request.time.99p
type: gauge
desc: The 99th percentile time the broker has taken to service requests
- bean: kafka.network:type=RequestChannel,name=RequestQueueSize
mapping:
Value:
metric: kafka.request.queue
type: updowncounter
desc: Size of the request queue
unit: '{requests}'
- bean: kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec
metricAttribute:
direction: const(in)
mapping:
Count:
metric: kafka.network.io
type: counter
desc: The bytes received or sent by the broker
unit: By
- bean: kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec
metricAttribute:
direction: const(out)
mapping:
Count:
metric: kafka.network.io
type: counter
desc: The bytes received or sent by the broker
unit: By
- beans:
- kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Produce
- kafka.server:type=DelayedOperationPurgatory,name=PurgatorySize,delayedOperation=Fetch
metricAttribute:
type: param(delayedOperation)
mapping:
Value:
metric: kafka.purgatory.size
type: updowncounter
desc: The number of requests waiting in purgatory
unit: '{requests}'
- bean: kafka.server:type=ReplicaManager,name=PartitionCount
mapping:
Value:
metric: kafka.partition.count
type: updowncounter
desc: The number of partitions on the broker
unit: '{partitions}'
- bean: kafka.controller:type=KafkaController,name=OfflinePartitionsCount
mapping:
Value:
metric: kafka.partition.offline
type: updowncounter
desc: The number of partitions offline
unit: '{partitions}'
- bean: kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions
mapping:
Value:
metric: kafka.partition.underReplicated
type: updowncounter
desc: The number of under replicated partitions
unit: '{partitions}'
- bean: kafka.server:type=ReplicaManager,name=IsrShrinksPerSec
metricAttribute:
operation: const(shrink)
mapping:
Count:
metric: kafka.isr.operation.count
type: updowncounter
desc: The number of in-sync replica shrink and expand operations
unit: '{operations}'
- bean: kafka.server:type=ReplicaManager,name=IsrExpandsPerSec
metricAttribute:
operation: const(expand)
mapping:
Count:
metric: kafka.isr.operation.count
type: updowncounter
desc: The number of in-sync replica shrink and expand operations
unit: '{operations}'
- bean: kafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=Replica
mapping:
Value:
metric: kafka.lag.max
desc: The max lag in messages between follower and leader replicas
unit: '{messages}'
- bean: kafka.controller:type=KafkaController,name=ActiveControllerCount
mapping:
Value:
metric: kafka.controller.active.count
type: updowncounter
desc: The number of controllers active on the broker
unit: '{controllers}'
- bean: kafka.controller:type=ControllerStats,name=LeaderElectionRateAndTimeMs
mapping:
Count:
metric: kafka.leaderElection.count
type: counter
desc: The leader election count
unit: '{elections}'
- bean: kafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec
mapping:
Count:
metric: kafka.leaderElection.unclean.count
type: counter
desc: Unclean leader election count - increasing indicates broker failures
unit: '{elections}'
# Log metrics
- bean: kafka.log:type=LogFlushStats,name=LogFlushRateAndTimeMs
unit: ms
type: gauge
prefix: kafka.logs.flush.
mapping:
Count:
type: counter
desc: Log flush count
50thPercentile:
metric: time.50p
desc: Log flush time - 50th percentile
99thPercentile:
metric: time.99p
desc: Log flush time - 99th percentile

View File

@ -0,0 +1,67 @@
---
rules:
- bean: Catalina:type=GlobalRequestProcessor,name=*
unit: 1
prefix: http.server.tomcat.
metricAttribute:
name: param(name)
mapping:
errorCount:
metric: errorCount
type: gauge
desc: The number of errors per second on all request processors
requestCount:
metric: requestCount
type: gauge
desc: The number of requests per second across all request processors
maxTime:
metric: maxTime
type: gauge
unit: ms
desc: The longest request processing time
processingTime:
metric: processingTime
type: counter
unit: ms
desc: Total time for processing all requests
bytesReceived:
metric: traffic
type: counter
unit: By
desc: The number of bytes transmitted
metricAttribute:
direction: const(received)
bytesSent:
metric: traffic
type: counter
unit: By
desc: The number of bytes transmitted
metricAttribute:
direction: const(sent)
- bean: Catalina:type=Manager,host=localhost,context=*
unit: 1
prefix: http.server.tomcat.
type: updowncounter
metricAttribute:
context: param(context)
mapping:
activeSessions:
metric: sessions.activeSessions
desc: The number of active sessions
- bean: Catalina:type=ThreadPool,name=*
unit: '{threads}'
prefix: http.server.tomcat.
type: updowncounter
metricAttribute:
name: param(name)
mapping:
currentThreadCount:
metric: threads
desc: Thread Count of the Thread Pool
metricAttribute:
state: const(idle)
currentThreadsBusy:
metric: threads
desc: Thread Count of the Thread Pool
metricAttribute:
state: const(busy)

View File

@ -0,0 +1,83 @@
---
rules:
- bean: jboss.as:deployment=*,subsystem=undertow
metricAttribute:
deployment: param(deployment)
prefix: wildfly.session.
type: counter
unit: 1
mapping:
sessionsCreated:
activeSessions:
type: updowncounter
expiredSessions:
rejectedSessions:
- bean: jboss.as:subsystem=undertow,server=*,http-listener=*
metricAttribute:
server: param(server)
listener: param(http-listener)
prefix: wildfly.request.
type: counter
unit: 1
mapping:
requestCount:
processingTime:
unit: ns
errorCount:
- bean: jboss.as:subsystem=undertow,server=*,http-listener=*
metricAttribute:
server: param(server)
listener: param(http-listener)
type: counter
unit: By
mapping:
bytesSent:
metric: wildfly.network.io
desc: Total number of bytes transferred
metricAttribute:
direction: const(out)
bytesReceived:
metric: wildfly.network.io
desc: Total number of bytes transferred
metricAttribute:
direction: const(in)
- bean: jboss.as:subsystem=datasources,data-source=*,statistics=pool
unit: 1
metricAttribute:
data_source: param(data-source)
mapping:
ActiveCount:
metric: wildfly.db.client.connections.usage
metricAttribute:
state: const(used)
desc: The number of open jdbc connections
IdleCount:
metric: wildfly.db.client.connections.usage
metricAttribute:
state: const(idle)
desc: The number of open jdbc connections
WaitCount:
metric: wildfly.db.client.connections.WaitCount
type: counter
- bean: jboss.as:subsystem=transactions
type: counter
prefix: wildfly.db.client.
unit: "{transactions}"
mapping:
numberOfTransactions:
metric: transaction.NumberOfTransactions
numberOfApplicationRollbacks:
metric: rollback.count
metricAttribute:
cause: const(application)
desc: The total number of transactions rolled back
numberOfResourceRollbacks:
metric: rollback.count
metricAttribute:
cause: const(resource)
desc: The total number of transactions rolled back
numberOfSystemRollbacks:
metric: rollback.count
metricAttribute:
cause: const(system)
desc: The total number of transactions rolled back

View File

@ -0,0 +1,13 @@
# Tomcat Metrics
Here is the list of metrics based on MBeans exposed by Tomcat.
| Metric Name | Type | Attributes | Description |
|--------------------------------------------|---------------|-----------------|-----------------------------------------------------------------|
| http.server.tomcat.sessions.activeSessions | UpDownCounter | context | The number of active sessions |
| http.server.tomcat.errorCount | Gauge | name | The number of errors per second on all request processors |
| http.server.tomcat.requestCount | Gauge | name | The number of requests per second across all request processors |
| http.server.tomcat.maxTime | Gauge | name | The longest request processing time |
| http.server.tomcat.processingTime | Counter | name | Represents the total time for processing all requests |
| http.server.tomcat.traffic | Counter | name, direction | The number of bytes transmitted |
| http.server.tomcat.threads | UpDownCounter | name, state | Thread Count of the Thread Pool |

View File

@ -0,0 +1,18 @@
# Wildfly Metrics
Here is the list of metrics based on MBeans exposed by Wildfly.
| Metric Name | Type | Attributes | Description |
|----------------------------------------------------|---------------|--------------------|-------------------------------------------------------------------------|
| wildfly.network.io | Counter | direction, server | Total number of bytes transferred |
| wildfly.request.errorCount | Counter | server, listener | The number of 500 responses that have been sent by this listener |
| wildfly.request.requestCount | Counter | server, listener | The number of requests this listener has served |
| wildfly.request.processingTime | Counter | server, listener | The total processing time of all requests handed by this listener |
| wildfly.session.expiredSession | Counter | deployment | Number of sessions that have expired |
| wildfly.session.rejectedSessions | Counter | deployment | Number of rejected sessions |
| wildfly.session.sessionsCreated | Counter | deployment | Total sessions created |
| wildfly.session.activeSessions | UpDownCounter | deployment | Number of active sessions |
| wildfly.db.client.connections.usage | Gauge | data_source, state | The number of open jdbc connections |
| wildfly.db.client.connections.WaitCount | Counter | data_source | The number of requests that had to wait to obtain a physical connection |
| wildfly.db.client.rollback.count | Counter | cause | The total number of transactions rolled back |
| wildfly.db.client.transaction.NumberOfTransactions | Counter | | The total number of transactions (top-level and nested) created |

View File

@ -0,0 +1,9 @@
plugins {
id("otel.library-instrumentation")
}
dependencies {
implementation("org.yaml:snakeyaml")
testImplementation(project(":testing-common"))
}

View File

@ -0,0 +1,56 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.annotation.Nullable;
/**
* A class holding relevant information about an MBean attribute which will be used for collecting
* metric values. The info comes directly from the relevant MBeans.
*/
class AttributeInfo {
private boolean usesDoubles;
@Nullable private String description;
AttributeInfo(Number sampleValue, @Nullable String description) {
if (sampleValue instanceof Byte
|| sampleValue instanceof Short
|| sampleValue instanceof Integer
|| sampleValue instanceof Long) {
// will use Long values
usesDoubles = false;
} else {
usesDoubles = true;
}
this.description = description;
}
boolean usesDoubleValues() {
return usesDoubles;
}
@Nullable
String getDescription() {
return description;
}
/**
* It is unlikely, but possible, that among the MBeans matching some ObjectName pattern,
* attributes with the same name but different types exist. In such cases we have to use a metric
* type which will be able to handle all of these attributes.
*
* @param other another AttributeInfo apparently for the same MBean attribute, must not be null
*/
void updateFrom(AttributeInfo other) {
if (other.usesDoubleValues()) {
usesDoubles = true;
}
if (description == null) {
description = other.getDescription();
}
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
/**
* A class responsible for extracting attribute values from MBeans. Objects of this class are
* immutable.
*/
public class BeanAttributeExtractor implements MetricAttributeExtractor {
private static final Logger logger = Logger.getLogger(BeanAttributeExtractor.class.getName());
// The attribute name to be used during value extraction from MBean
private final String baseName;
// In case when the extracted attribute is a CompositeData value,
// how to proceed to arrive at a usable elementary value
private final String[] nameChain;
/**
* Verify the attribute name and create a corresponding extractor object.
*
* @param rawName the attribute name, can be a reference to composite values
* @return the corresponding BeanAttributeExtractor
* @throws IllegalArgumentException if the attribute name is malformed
*/
public static BeanAttributeExtractor fromName(String rawName) {
if (rawName.isEmpty()) {
throw new IllegalArgumentException("Empty attribute name");
}
// Check if a CompositeType value is expected
int k = rawName.indexOf('.');
if (k < 0) {
return new BeanAttributeExtractor(rawName);
}
// Set up extraction from CompositeType values
String baseName = rawName.substring(0, k).trim();
String[] components = rawName.substring(k + 1).split("\\.");
// sanity check
if (baseName.isEmpty()) {
throw new IllegalArgumentException("Invalid attribute name '" + rawName + "'");
}
for (int j = 0; j < components.length; ++j) {
components[j] = components[j].trim();
if (components[j].isEmpty()) {
throw new IllegalArgumentException("Invalid attribute name '" + rawName + "'");
}
}
return new BeanAttributeExtractor(baseName, components);
}
public BeanAttributeExtractor(String baseName, String... nameChain) {
if (baseName == null || nameChain == null) {
throw new IllegalArgumentException("null argument for BeanAttributeExtractor");
}
this.baseName = baseName;
this.nameChain = nameChain;
}
/** Get a human readable name of the attribute to extract. Useful for logging or debugging. */
String getAttributeName() {
if (nameChain.length > 0) {
StringBuilder builder = new StringBuilder(baseName);
for (String component : nameChain) {
builder.append(".").append(component);
}
return builder.toString();
} else {
return baseName;
}
}
/**
* Verify that the MBean identified by the given ObjectName recognizes the configured attribute,
* including the internals of CompositeData and TabularData, if applicable, and that the provided
* values will be numerical.
*
* @param server the MBeanServer that reported knowledge of the ObjectName
* @param objectName the ObjectName identifying the MBean
* @return AttributeInfo if the attribute is properly recognized, or null
*/
@Nullable
AttributeInfo getAttributeInfo(MBeanServer server, ObjectName objectName) {
if (logger.isLoggable(FINE)) {
logger.log(FINE, "Resolving {0} for {1}", new Object[] {getAttributeName(), objectName});
}
try {
MBeanInfo info = server.getMBeanInfo(objectName);
MBeanAttributeInfo[] allAttributes = info.getAttributes();
for (MBeanAttributeInfo attr : allAttributes) {
if (baseName.equals(attr.getName())) {
String description = attr.getDescription();
// 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(server, objectName, logger);
// Only numbers can be used to generate metric values
if (sampleValue instanceof Number) {
return new AttributeInfo((Number) sampleValue, description);
} else {
// It is fairly normal to get null values, especially during startup,
// but it is much more suspicious to get non-numbers
Level logLevel = sampleValue == null ? FINE : INFO;
if (logger.isLoggable(logLevel)) {
logger.log(
logLevel,
"Unusable value {0} for attribute {1} and ObjectName {2}",
new Object[] {
sampleValue == null ? "NULL" : sampleValue.getClass().getName(),
getAttributeName(),
objectName
});
}
return null;
}
}
}
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Cannot find attribute {0} for ObjectName {1}",
new Object[] {baseName, objectName});
}
} catch (InstanceNotFoundException e) {
// Should not happen. The ObjectName we use has been provided by the MBeanServer we use.
logger.log(INFO, "The MBeanServer does not find {0}", objectName);
} catch (Exception e) {
logger.log(
FINE,
"Exception {0} while inspecting attributes for ObjectName {1}",
new Object[] {e, objectName});
}
return null;
}
/**
* 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).
*
* @param server the MBeanServer to use
* @param objectName the ObjectName specifying the MBean to use, it should not be a 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
* malfunctions will be silent to avoid flooding the log.
* @return the attribute value, if found, or null if an error occurred
*/
@Nullable
private Object extractAttributeValue(MBeanServer server, ObjectName objectName, Logger logger) {
try {
Object value = server.getAttribute(objectName, baseName);
int k = 0;
while (k < nameChain.length) {
if (value instanceof CompositeData) {
value = ((CompositeData) value).get(nameChain[k]);
} else if (value instanceof TabularData) {
value = ((TabularData) value).get(new String[] {nameChain[k]});
} else {
if (logger != null) {
logger.log(
FINE,
"Encountered a value of {0} while extracting attribute {1} for ObjectName {2}; unable to extract metric value",
new Object[] {
(value == null ? "NULL" : value.getClass().getName()),
getAttributeName(),
objectName
});
}
break;
}
k++;
}
return value;
} catch (Exception e) {
// We do not really care about the actual reason for failure
if (logger != null) {
logger.log(
FINE,
"Encountered {0} while extracting attribute {1} for ObjectName {2}; unable to extract metric value",
new Object[] {e, getAttributeName(), objectName});
}
}
return null;
}
@Nullable
private Object extractAttributeValue(MBeanServer server, ObjectName objectName) {
return extractAttributeValue(server, objectName, null);
}
@Nullable
Number extractNumericalAttribute(MBeanServer server, ObjectName objectName) {
Object value = extractAttributeValue(server, objectName);
if (value instanceof Number) {
return (Number) value;
}
return null;
}
@Override
@Nullable
public String extractValue(MBeanServer server, ObjectName objectName) {
return extractStringAttribute(server, objectName);
}
@Nullable
private String extractStringAttribute(MBeanServer server, ObjectName objectName) {
Object value = extractAttributeValue(server, objectName);
if (value instanceof String) {
return (String) value;
}
return null;
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
/**
* A class responsible for finding MBeans that match metric definitions specified by a set of
* MetricDefs.
*/
class BeanFinder {
private final MetricRegistrar registrar;
private MetricConfiguration conf;
private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
private final long discoveryDelay;
private final long maxDelay;
private long delay = 1000; // number of milliseconds until first attempt to discover MBeans
BeanFinder(MetricRegistrar registrar, long discoveryDelay) {
this.registrar = registrar;
this.discoveryDelay = Math.max(1000, discoveryDelay); // Enforce sanity
this.maxDelay = Math.max(60000, discoveryDelay);
}
void discoverBeans(MetricConfiguration conf) {
this.conf = conf;
exec.schedule(
new Runnable() {
@Override
public void run() {
refreshState();
// Use discoveryDelay as the increment for the actual delay
delay = Math.min(delay + discoveryDelay, maxDelay);
exec.schedule(this, delay, TimeUnit.MILLISECONDS);
}
},
delay,
TimeUnit.MILLISECONDS);
}
/**
* Go over all configured metric definitions and try to find matching MBeans. Once a match is
* found for a given metric definition, submit the definition to MetricRegistrar for further
* handling. Successive invocations of this method may find matches that were previously
* unavailable, in such cases MetricRegistrar will extend the coverage for the new MBeans
*/
private void refreshState() {
List<MBeanServer> servers = MBeanServerFactory.findMBeanServer(null);
for (MetricDef metricDef : conf.getMetricDefs()) {
resolveBeans(metricDef, servers);
}
}
/**
* Go over the specified list of MBeanServers and try to find any MBeans matching the specified
* MetricDef. If found, verify that the MBeans support the specified attributes, and set up
* collection of corresponding metrics.
*
* @param metricDef the MetricDef used to find matching MBeans
* @param servers the list of MBeanServers to query
*/
private void resolveBeans(MetricDef metricDef, List<MBeanServer> servers) {
BeanGroup beans = metricDef.getBeanGroup();
for (MBeanServer server : servers) {
// The set of all matching ObjectNames recognized by the server
Set<ObjectName> allObjectNames = new HashSet<>();
for (ObjectName pattern : beans.getNamePatterns()) {
Set<ObjectName> objectNames = server.queryNames(pattern, beans.getQueryExp());
allObjectNames.addAll(objectNames);
}
if (!allObjectNames.isEmpty()) {
resolveAttributes(allObjectNames, server, metricDef);
// Assuming that only one MBeanServer has the required MBeans
break;
}
}
}
/**
* Go over the collection of matching MBeans and try to find all matching attributes. For every
* successful match, activate metric value collection.
*
* @param objectNames the collection of ObjectNames identifying the MBeans
* @param server the MBeanServer which recognized the collection of ObjectNames
* @param metricDef the MetricDef describing the attributes to look for
*/
private void resolveAttributes(
Set<ObjectName> objectNames, MBeanServer server, MetricDef metricDef) {
for (MetricExtractor extractor : metricDef.getMetricExtractors()) {
// For each MetricExtractor, find the subset of MBeans that have the required attribute
List<ObjectName> validObjectNames = new ArrayList<>();
AttributeInfo attributeInfo = null;
for (ObjectName objectName : objectNames) {
AttributeInfo attr =
extractor.getMetricValueExtractor().getAttributeInfo(server, objectName);
if (attr != null) {
if (attributeInfo == null) {
attributeInfo = attr;
} else {
attributeInfo.updateFrom(attr);
}
validObjectNames.add(objectName);
}
}
if (!validObjectNames.isEmpty()) {
// Ready to collect metric values
registrar.enrollExtractor(server, validObjectNames, extractor, attributeInfo);
}
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.annotation.Nullable;
import javax.management.ObjectName;
import javax.management.QueryExp;
/**
* A class describing a set of MBeans which can be used to collect values for a metric. Objects of
* this class are immutable.
*/
public class BeanGroup {
// How to specify the MBean(s)
@Nullable private final QueryExp queryExp;
private final ObjectName[] namePatterns;
/**
* Constructor for BeanGroup.
*
* @param queryExp the QueryExp to be used to filter results when looking for MBeans
* @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) {
this.queryExp = queryExp;
this.namePatterns = namePatterns;
}
@Nullable
QueryExp getQueryExp() {
return queryExp;
}
ObjectName[] getNamePatterns() {
return namePatterns;
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import java.util.Collection;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* A class encapsulating a set of ObjectNames and the MBeanServer that recognized them. Objects of
* this class are immutable.
*/
class DetectionStatus {
private final MBeanServer server;
private final Collection<ObjectName> objectNames;
DetectionStatus(MBeanServer server, Collection<ObjectName> objectNames) {
this.server = server;
this.objectNames = objectNames;
}
MBeanServer getServer() {
return server;
}
Collection<ObjectName> getObjectNames() {
return objectNames;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import static java.util.logging.Level.INFO;
import io.opentelemetry.api.OpenTelemetry;
import java.util.logging.Logger;
/** Collecting and exporting JMX metrics. */
public class JmxMetricInsight {
private static final Logger logger = Logger.getLogger(JmxMetricInsight.class.getName());
private static final String INSTRUMENTATION_SCOPE = "io.opentelemetry.jmx";
private final OpenTelemetry openTelemetry;
private final long discoveryDelay;
public static JmxMetricInsight createService(OpenTelemetry ot, long discoveryDelay) {
return new JmxMetricInsight(ot, discoveryDelay);
}
public static Logger getLogger() {
return logger;
}
private JmxMetricInsight(OpenTelemetry openTelemetry, long discoveryDelay) {
this.openTelemetry = openTelemetry;
this.discoveryDelay = discoveryDelay;
}
public void start(MetricConfiguration conf) {
if (conf.isEmpty()) {
logger.log(
INFO,
"Empty JMX configuration, no metrics will be collected for InstrumentationScope "
+ INSTRUMENTATION_SCOPE);
} else {
MetricRegistrar registrar = new MetricRegistrar(openTelemetry, INSTRUMENTATION_SCOPE);
BeanFinder finder = new BeanFinder(registrar, discoveryDelay);
finder.discoverBeans(conf);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* A class representing a metric attribute. It is responsible for extracting the attribute value (to
* be reported as a Measurement attribute), and for holding the corresponding attribute name to be
* used. Objects of this class are immutable.
*/
public class MetricAttribute {
private final String name;
private final MetricAttributeExtractor extractor;
public MetricAttribute(String name, MetricAttributeExtractor extractor) {
this.name = name;
this.extractor = extractor;
}
public String getAttributeName() {
return name;
}
String acquireAttributeValue(MBeanServer server, ObjectName objectName) {
return extractor.extractValue(server, objectName);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.annotation.Nullable;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* MetricAttributeExtractors are responsible for obtaining values for populating metric attributes,
* i.e. measurement attributes.
*/
public interface MetricAttributeExtractor {
/**
* Provide a String value to be used as the value of a metric attribute.
*
* @param server MBeanServer to query, must not be null if the extraction is from an MBean
* attribute
* @param objectName the identifier of the MBean to query, must not be null if the extraction is
* from an MBean attribute, or from the ObjectName parameter
* @return the value of the attribute, can be null if extraction failed
*/
@Nullable
String extractValue(@Nullable MBeanServer server, @Nullable ObjectName objectName);
static MetricAttributeExtractor fromConstant(String constantValue) {
return (a, b) -> {
return constantValue;
};
}
static MetricAttributeExtractor fromObjectNameParameter(String parameterKey) {
if (parameterKey.isEmpty()) {
throw new IllegalArgumentException("Empty parameter name");
}
return (dummy, objectName) -> {
return objectName.getKeyProperty(parameterKey);
};
}
static MetricAttributeExtractor fromBeanAttribute(String attributeName) {
return BeanAttributeExtractor.fromName(attributeName);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import java.util.ArrayList;
import java.util.Collection;
/**
* A class responsible for maintaining the current configuration for JMX metrics to be collected.
*/
public class MetricConfiguration {
private final Collection<MetricDef> currentSet = new ArrayList<>();
public MetricConfiguration() {}
public boolean isEmpty() {
return currentSet.isEmpty();
}
public void addMetricDef(MetricDef def) {
currentSet.add(def);
}
Collection<MetricDef> getMetricDefs() {
return currentSet;
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
/**
* 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
* attributes using OpenTelemetry Metric API. Objects of this class are immutable.
*/
// Example: The rule described by the following YAML definition
//
// - bean: java.lang:name=*,type=MemoryPool
// metricAttribute:
// pool: param(name)
// type: beanattr(Type)
// mapping:
// Usage.used:
// metric: my.own.jvm.memory.pool.used
// type: updowncounter
// desc: Pool memory currently used
// unit: By
// Usage.max:
// metric: my.own.jvm.memory.pool.max
// type: updowncounter
// desc: Maximum obtainable memory pool size
// unit: By
//
// can be created using the following snippet:
//
// MetricAttribute poolAttribute =
// new MetricAttribute("pool", MetricAttributeExtractor.fromObjectNameParameter("name"));
// MetricAttribute typeAttribute =
// new MetricAttribute("type", MetricAttributeExtractor.fromBeanAttribute("Type"));
//
// MetricInfo poolUsedInfo =
// new MetricInfo(
// "my.own.jvm.memory.pool.used",
// "Pool memory currently used",
// "By",
// MetricInfo.Type.UPDOWNCOUNTER);
// MetricInfo poolLimitInfo =
// new MetricInfo(
// "my.own.jvm.memory.pool.limit",
// "Maximum obtainable memory pool size",
// "By",
// MetricInfo.Type.UPDOWNCOUNTER);
//
// MetricExtractor usageUsedExtractor =
// new MetricExtractor(
// new BeanAttributeExtractor("Usage", "used"),
// poolUsedInfo,
// poolAttribute,
// typeAttribute);
// MetricExtractor usageMaxExtractor =
// new MetricExtractor(
// new BeanAttributeExtractor("Usage", "max"),
// poolLimitInfo,
// poolAttribute,
// typeAttribute);
//
// MetricDef def =
// new MetricDef(
// new BeanGroup(null, new ObjectName("java.lang:name=*,type=MemoryPool")),
// usageUsedExtractor,
// usageMaxExtractor);
public class MetricDef {
// Describes the MBeans to use
private final BeanGroup beans;
// Describes how to get the metric values and their attributes, and how to report them
private final MetricExtractor[] metricExtractors;
/**
* Constructor for MetricDef.
*
* @param beans description of MBeans required to obtain metric values
* @param metricExtractors description of how to extract metric values; if more than one
* MetricExtractor is provided, they should use unique metric names or unique metric
* attributes
*/
public MetricDef(BeanGroup beans, MetricExtractor... metricExtractors) {
this.beans = beans;
this.metricExtractors = metricExtractors;
}
BeanGroup getBeanGroup() {
return beans;
}
MetricExtractor[] getMetricExtractors() {
return metricExtractors;
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.annotation.Nullable;
/**
* A class holding the info needed to support a single metric: how to define it in OpenTelemetry and
* how to provide the metric values.
*
* <p>Objects of this class are stateful, the DetectionStatus may change over time to keep track of
* all ObjectNames that should be used to deliver the metric values.
*/
public class MetricExtractor {
private final MetricInfo metricInfo;
// Defines the way to access the metric value (a number)
private final BeanAttributeExtractor attributeExtractor;
// Defines the Measurement attributes to be used when reporting the metric value.
private final MetricAttribute[] attributes;
@Nullable private volatile DetectionStatus status;
public MetricExtractor(
BeanAttributeExtractor attributeExtractor,
MetricInfo metricInfo,
MetricAttribute... attributes) {
this.attributeExtractor = attributeExtractor;
this.metricInfo = metricInfo;
this.attributes = attributes;
}
MetricInfo getInfo() {
return metricInfo;
}
BeanAttributeExtractor getMetricValueExtractor() {
return attributeExtractor;
}
MetricAttribute[] getAttributes() {
return attributes;
}
void setStatus(DetectionStatus status) {
this.status = status;
}
@Nullable
DetectionStatus getStatus() {
return status;
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import javax.annotation.Nullable;
/**
* A class providing the user visible characteristics (name, type, description and units) of a
* metric to be reported with OpenTelemetry.
*
* <p>Objects of this class are immutable.
*/
public class MetricInfo {
// OpenTelemetry asynchronous instrument types that can be used
public enum Type {
COUNTER,
UPDOWNCOUNTER,
GAUGE
}
// How to report the metric using OpenTelemetry API
private final String metricName; // used as Instrument name
@Nullable private final String description;
@Nullable private final String unit;
private final Type type;
/**
* Constructor for MetricInfo.
*
* @param metricName a String that will be used as a metric name, it should be unique
* @param description a human readable description of the metric
* @param unit a human readable unit of measurement
* @param type the instrument typ to be used for the metric
*/
public MetricInfo(
String metricName, @Nullable String description, String unit, @Nullable Type type) {
this.metricName = metricName;
this.description = description;
this.unit = unit;
this.type = type == null ? Type.GAUGE : type;
}
String getMetricName() {
return metricName;
}
@Nullable
String getDescription() {
return description;
}
@Nullable
String getUnit() {
return unit;
}
Type getType() {
return type;
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import static java.util.logging.Level.INFO;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.DoubleGaugeBuilder;
import io.opentelemetry.api.metrics.LongCounterBuilder;
import io.opentelemetry.api.metrics.LongUpDownCounterBuilder;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.logging.Logger;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/** A class responsible for maintaining the set of metrics to collect and report. */
class MetricRegistrar {
private static final Logger logger = Logger.getLogger(MetricRegistrar.class.getName());
private final Meter meter;
MetricRegistrar(OpenTelemetry openTelemetry, String instrumentationScope) {
meter = openTelemetry.getMeter(instrumentationScope);
}
/**
* Accepts a MetricExtractor for registration and activation.
*
* @param server the MBeanServer to use to query for metric values
* @param objectNames the Objectnames that are known to the server and that know the attribute
* that is required to get the metric values
* @param extractor the MetricExtractor responsible for getting the metric values
*/
void enrollExtractor(
MBeanServer server,
Collection<ObjectName> objectNames,
MetricExtractor extractor,
AttributeInfo attributeInfo) {
// For the first enrollment of the extractor we have to build the corresponding Instrument
DetectionStatus status = new DetectionStatus(server, objectNames);
boolean firstEnrollment;
synchronized (extractor) {
firstEnrollment = extractor.getStatus() == null;
// For successive enrollments, it is sufficient to refresh the status
extractor.setStatus(status);
}
if (firstEnrollment) {
MetricInfo metricInfo = extractor.getInfo();
String metricName = metricInfo.getMetricName();
MetricInfo.Type instrumentType = metricInfo.getType();
String description =
metricInfo.getDescription() != null
? metricInfo.getDescription()
: attributeInfo.getDescription();
String unit = metricInfo.getUnit();
switch (instrumentType) {
// CHECKSTYLE:OFF
case COUNTER:
{
// CHECKSTYLE:ON
LongCounterBuilder builder = meter.counterBuilder(metricName);
if (description != null) {
builder = builder.setDescription(description);
}
if (unit != null) {
builder = builder.setUnit(unit);
}
if (attributeInfo.usesDoubleValues()) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor));
} else {
builder.buildWithCallback(longTypeCallback(extractor));
}
logger.log(INFO, "Created Counter for {0}", metricName);
}
break;
// CHECKSTYLE:OFF
case UPDOWNCOUNTER:
{
// CHECKSTYLE:ON
LongUpDownCounterBuilder builder = meter.upDownCounterBuilder(metricName);
if (description != null) {
builder = builder.setDescription(description);
}
if (unit != null) {
builder = builder.setUnit(unit);
}
if (attributeInfo.usesDoubleValues()) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor));
} else {
builder.buildWithCallback(longTypeCallback(extractor));
}
logger.log(INFO, "Created UpDownCounter for {0}", metricName);
}
break;
// CHECKSTYLE:OFF
case GAUGE:
{
// CHECKSTYLE:ON
DoubleGaugeBuilder builder = meter.gaugeBuilder(metricName);
if (description != null) {
builder = builder.setDescription(description);
}
if (unit != null) {
builder = builder.setUnit(unit);
}
if (attributeInfo.usesDoubleValues()) {
builder.buildWithCallback(doubleTypeCallback(extractor));
} else {
builder.ofLongs().buildWithCallback(longTypeCallback(extractor));
}
logger.log(INFO, "Created Gauge for {0}", metricName);
}
}
}
}
/*
* A method generating metric collection callback for asynchronous Measurement
* of Double type.
*/
static Consumer<ObservableDoubleMeasurement> doubleTypeCallback(MetricExtractor extractor) {
return measurement -> {
DetectionStatus status = extractor.getStatus();
if (status != null) {
MBeanServer server = status.getServer();
for (ObjectName objectName : status.getObjectNames()) {
Number metricValue =
extractor.getMetricValueExtractor().extractNumericalAttribute(server, objectName);
if (metricValue != null) {
// get the metric attributes
Attributes attr = createMetricAttributes(server, objectName, extractor);
measurement.record(metricValue.doubleValue(), attr);
}
}
}
};
}
/*
* A method generating metric collection callback for asynchronous Measurement
* of Long type.
*/
static Consumer<ObservableLongMeasurement> longTypeCallback(MetricExtractor extractor) {
return measurement -> {
DetectionStatus status = extractor.getStatus();
if (status != null) {
MBeanServer server = status.getServer();
for (ObjectName objectName : status.getObjectNames()) {
Number metricValue =
extractor.getMetricValueExtractor().extractNumericalAttribute(server, objectName);
if (metricValue != null) {
// get the metric attributes
Attributes attr = createMetricAttributes(server, objectName, extractor);
measurement.record(metricValue.longValue(), attr);
}
}
}
};
}
/*
* An auxiliary method for collecting measurement attributes to go along
* the metric values
*/
static Attributes createMetricAttributes(
MBeanServer server, ObjectName objectName, MetricExtractor extractor) {
MetricAttribute[] metricAttributes = extractor.getAttributes();
AttributesBuilder attrBuilder = Attributes.builder();
for (MetricAttribute metricAttribute : metricAttributes) {
String attributeValue = metricAttribute.acquireAttributeValue(server, objectName);
if (attributeValue != null) {
attrBuilder = attrBuilder.put(metricAttribute.getAttributeName(), attributeValue);
}
}
return attrBuilder.build();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.yaml;
import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration;
import io.opentelemetry.instrumentation.jmx.engine.MetricDef;
import java.util.List;
/**
* JMX configuration as a set of JMX rules. Objects of this class are created and populated by the
* YAML parser.
*/
public class JmxConfig {
// Used by the YAML parser
// rules:
// - JMX_DEFINITION1
// - JMX_DEFINITION2
// The parser is guaranteed to call setRules with a non-null argument, or throw an exception
private List<JmxRule> rules;
public List<JmxRule> getRules() {
return rules;
}
public void setRules(List<JmxRule> rules) {
this.rules = rules;
}
/**
* Converts the rules from this object into MetricDefs and adds them to the specified
* MetricConfiguration.
*
* @param configuration MetricConfiguration to add MetricDefs to
* @throws an exception if the rule conversion cannot be performed
*/
void addMetricDefsTo(MetricConfiguration configuration) throws Exception {
for (JmxRule rule : rules) {
MetricDef metricDef = rule.buildMetricDef();
configuration.addMetricDef(metricDef);
}
}
}

View File

@ -0,0 +1,195 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.yaml;
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.MetricDef;
import io.opentelemetry.instrumentation.jmx.engine.MetricExtractor;
import io.opentelemetry.instrumentation.jmx.engine.MetricInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
/**
* This class represents a complete JMX metrics rule as defined by a YAML file. Objects of this
* class are created and populated by the YAML parser.
*/
public class JmxRule extends MetricStructure {
// Used by the YAML parser
// bean: OBJECT_NAME
// beans:
// - OBJECTNAME1
// - OBJECTNAME2
// prefix: METRIC_NAME_PREFIX
// mapping:
// ATTRIBUTE1:
// METRIC_FIELDS1
// ATTRIBUTE2:
// ATTRIBUTE3:
// METRIC_FIELDS3
// The parser never calls setters for these fields with null arguments
private String bean;
private List<String> beans;
private String prefix;
private Map<String, Metric> mapping;
public String getBean() {
return bean;
}
public void setBean(String bean) throws Exception {
this.bean = validateBean(bean);
}
public List<String> getBeans() {
return beans;
}
private static String validateBean(String name) throws MalformedObjectNameException {
String trimmed = name.trim();
// Check the syntax of the provided name by attempting to create an ObjectName from it.
new ObjectName(trimmed);
return trimmed;
}
public void setBeans(List<String> beans) throws Exception {
List<String> list = new ArrayList<>();
for (String name : beans) {
list.add(validateBean(name));
}
this.beans = list;
}
public void setPrefix(String prefix) {
this.prefix = validatePrefix(prefix.trim());
}
private String validatePrefix(String prefix) {
// Do not accept empty string.
// While it is theoretically acceptable, it probably indicates a user error.
requireNonEmpty(prefix, "The metric name prefix is empty");
return prefix;
}
public String getPrefix() {
return prefix;
}
public Map<String, Metric> getMapping() {
return mapping;
}
public void setMapping(Map<String, Metric> mapping) {
this.mapping = validateAttributeMapping(mapping);
}
private static Map<String, Metric> validateAttributeMapping(Map<String, Metric> mapping) {
if (mapping.isEmpty()) {
throw new IllegalStateException("No MBean attributes specified");
}
// Make sure that all attribute names are well-formed by creating the corresponding
// BeanAttributeExtractors
Set<String> attrNames = mapping.keySet();
for (String attributeName : attrNames) {
// check if BeanAttributeExtractors can be built without exceptions
BeanAttributeExtractor.fromName(attributeName);
}
return mapping;
}
/**
* Convert this rule to a complete MetricDefinition object. If the rule is incomplete or has
* consistency or semantic issues, an exception will be thrown.
*
* @return a valid MetricDefinition object
* @throws an exception if any issues within the rule are detected
*/
public MetricDef buildMetricDef() throws Exception {
BeanGroup group;
if (bean != null) {
group = new BeanGroup(null, new ObjectName(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);
} else {
throw new IllegalStateException("No ObjectName specified");
}
if (mapping == null || mapping.isEmpty()) {
throw new IllegalStateException("No MBean attributes specified");
}
Set<String> attrNames = mapping.keySet();
MetricExtractor[] metricExtractors = new MetricExtractor[attrNames.size()];
int n = 0;
for (String attributeName : attrNames) {
MetricInfo metricInfo;
Metric m = mapping.get(attributeName);
if (m == null) {
metricInfo =
new MetricInfo(
prefix == null ? attributeName : (prefix + attributeName),
null,
getUnit(),
getMetricType());
} else {
metricInfo = m.buildMetricInfo(prefix, attributeName, getUnit(), getMetricType());
}
BeanAttributeExtractor attrExtractor = BeanAttributeExtractor.fromName(attributeName);
List<MetricAttribute> attributeList;
List<MetricAttribute> 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();
} else {
// There are no attributes at all
attributeList = new ArrayList<MetricAttribute>();
}
MetricExtractor metricExtractor =
new MetricExtractor(
attrExtractor,
metricInfo,
attributeList.toArray(new MetricAttribute[attributeList.size()]));
metricExtractors[n++] = metricExtractor;
}
return new MetricDef(group, metricExtractors);
}
private static List<MetricAttribute> combineMetricAttributes(
List<MetricAttribute> ownAttributes, List<MetricAttribute> metricAttributes) {
Map<String, MetricAttribute> set = new HashMap<>();
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);
}
return new ArrayList<MetricAttribute>(set.values());
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.yaml;
import io.opentelemetry.instrumentation.jmx.engine.MetricInfo;
/**
* A class representing metric definition as a part of YAML metric rule. Objects of this class are
* created and populated by the YAML parser.
*/
public class Metric extends MetricStructure {
// Used by the YAML parser
// metric: METRIC_NAME
// desc: DESCRIPTION
// The parser never calls setters for these fields with null arguments
private String metric;
private String desc;
public String getMetric() {
return metric;
}
public void setMetric(String metric) {
this.metric = validateMetricName(metric.trim());
}
private String validateMetricName(String name) {
requireNonEmpty(name, "The metric name is empty");
return name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
// No constraints on description
this.desc = desc.trim();
}
MetricInfo buildMetricInfo(
String prefix, String attributeName, String defaultUnit, MetricInfo.Type defaultType) {
String metricName;
if (metric == null) {
metricName = prefix == null ? attributeName : (prefix + attributeName);
} else {
metricName = prefix == null ? metric : (prefix + metric);
}
MetricInfo.Type metricType = getMetricType();
if (metricType == null) {
metricType = defaultType;
}
String unit = getUnit();
if (unit == null) {
unit = defaultUnit;
}
return new MetricInfo(metricName, desc, unit, metricType);
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.yaml;
import io.opentelemetry.instrumentation.jmx.engine.MetricAttribute;
import io.opentelemetry.instrumentation.jmx.engine.MetricAttributeExtractor;
import io.opentelemetry.instrumentation.jmx.engine.MetricInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* An abstract class containing skeletal info about Metrics:
* <li>the metric type
* <li>the metric attributes
* <li>the unit
*
* <p>Known subclasses are JmxRule and Metric.
*/
abstract class MetricStructure {
// Used by the YAML parser
// type: TYPE
// metricAttribute:
// KEY1: SPECIFICATION1
// KEY2: SPECIFICATION2
// unit: UNIT
private String type; // unused, for YAML parser only
private Map<String, String> metricAttribute; // unused, for YAML parser only
private String unit;
private MetricInfo.Type metricType;
private List<MetricAttribute> metricAttributes;
public String getType() {
return type;
}
public void setType(String t) {
// Do not complain about case variations
t = t.trim().toUpperCase();
this.metricType = MetricInfo.Type.valueOf(t);
this.type = t;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = validateUnit(unit.trim());
}
private String validateUnit(String unit) {
requireNonEmpty(unit, "Metric unit is empty");
return 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.
*
* @param map the mapping of metric attribute keys to evaluating snippets
*/
public void setMetricAttribute(Map<String, String> map) {
this.metricAttribute = map;
// pre-build the MetricAttributes
List<MetricAttribute> attrList = new ArrayList<>();
addMetricAttributes(attrList, map);
this.metricAttributes = attrList;
}
// Used only for testing
public Map<String, String> getMetricAttribute() {
return metricAttribute;
}
public MetricInfo.Type getMetricType() {
return metricType;
}
protected List<MetricAttribute> getAttributeList() {
return metricAttributes;
}
protected void requireNonEmpty(String s, String msg) {
if (s.isEmpty()) {
throw new IllegalArgumentException(msg);
}
}
private static void addMetricAttributes(
List<MetricAttribute> list, Map<String, String> 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 static MetricAttribute buildMetricAttribute(String key, String target) {
// The recognized forms of target are:
// - param(STRING)
// - beanattr(STRING)
// - const(STRING)
// where STRING is the name of the corresponding parameter key, attribute name,
// or the direct value to use
int k = target.indexOf(')');
// Check for one of the cases as above
if (target.startsWith("param(")) {
if (k > 0) {
return new MetricAttribute(
key, MetricAttributeExtractor.fromObjectNameParameter(target.substring(6, k).trim()));
}
} else if (target.startsWith("beanattr(")) {
if (k > 0) {
return new MetricAttribute(
key, MetricAttributeExtractor.fromBeanAttribute(target.substring(9, k).trim()));
}
} else if (target.startsWith("const(")) {
if (k > 0) {
return new MetricAttribute(
key, MetricAttributeExtractor.fromConstant(target.substring(6, k).trim()));
}
}
throw new IllegalArgumentException("Invalid metric attribute specification for '" + key + "'");
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.yaml;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration;
import java.io.InputStream;
import java.util.logging.Logger;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
/** Parse a YAML file containing a number of rules. */
public class RuleParser {
// The YAML parser will create and populate objects of the following classes from the
// io.opentelemetry.instrumentation.runtimemetrics.jmx.conf.data package:
// - JmxConfig
// - JmxRule (a subclass of MetricStructure)
// - Metric (a subclass of MetricStructure)
// To populate the objects, the parser will call setter methods for the object fields with
// whatever comes as the result of parsing the YAML file. This means that the arguments for
// the setter calls will be non-null, unless the user will explicitly specify the 'null' literal.
// However, there's hardly any difference in user visible error messages whether the setter
// throws an IllegalArgumentException, or NullPointerException. Therefore, in all above
// classes we skip explicit checks for nullnes in the field setters, and let the setters
// crash with NullPointerException instead.
private static final Logger logger = Logger.getLogger(RuleParser.class.getName());
private static final RuleParser theParser = new RuleParser();
public static RuleParser get() {
return theParser;
}
private RuleParser() {}
public JmxConfig loadConfig(InputStream is) throws Exception {
Yaml yaml = new Yaml(new Constructor(JmxConfig.class));
return yaml.load(is);
}
/**
* Parse the YAML rules from the specified input stream and add them, after converting to the
* internal representation, to the provided metric configuration.
*
* @param conf the metric configuration
* @param is the InputStream with the YAML rules
*/
public void addMetricDefsTo(MetricConfiguration conf, InputStream is) {
try {
JmxConfig config = loadConfig(is);
if (config != null) {
logger.log(INFO, "Found {0} metric rules", config.getRules().size());
config.addMetricDefsTo(conf);
}
} catch (Exception exception) {
logger.log(WARNING, "Failed to parse YAML rules: " + rootCause(exception));
// It is essential that the parser exception is made visible to the user.
// It contains contextual information about any syntax issues found by the parser.
logger.log(WARNING, exception.toString());
}
}
/**
* Given an exception thrown by the parser, try to find the original cause of the problem.
*
* @param exception the exception thrown by the parser
* @return a String describing the probable root cause
*/
private static String rootCause(Throwable exception) {
String rootClass = "";
String message = null;
// Go to the bottom of it
for (; exception != null; exception = exception.getCause()) {
rootClass = exception.getClass().getSimpleName();
message = exception.getMessage();
}
return message == null ? rootClass : message;
}
}

View File

@ -0,0 +1,205 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class AttributeExtractorTest {
// An MBean used for this test
@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
public interface Test1MBean {
byte getByteAttribute();
short getShortAttribute();
int getIntAttribute();
long getLongAttribute();
float getFloatAttribute();
double getDoubleAttribute();
String getStringAttribute();
}
private static class Test1 implements Test1MBean {
@Override
public byte getByteAttribute() {
return 10;
}
@Override
public short getShortAttribute() {
return 11;
}
@Override
public int getIntAttribute() {
return 12;
}
@Override
public long getLongAttribute() {
return 13;
}
@Override
public float getFloatAttribute() {
return 14.0f;
}
@Override
public double getDoubleAttribute() {
return 15.0;
}
@Override
public String getStringAttribute() {
return "";
}
}
private static final String DOMAIN = "otel.jmx.test";
private static final String OBJECT_NAME = "otel.jmx.test:type=Test1";
private static ObjectName objectName;
private static MBeanServer theServer;
@BeforeAll
static void setUp() throws Exception {
theServer = MBeanServerFactory.createMBeanServer(DOMAIN);
Test1 test1 = new Test1();
objectName = new ObjectName(OBJECT_NAME);
theServer.registerMBean(test1, objectName);
}
@AfterAll
static void tearDown() {
MBeanServerFactory.releaseMBeanServer(theServer);
theServer = null;
}
@Test
void testSetup() throws Exception {
Set<ObjectName> set = theServer.queryNames(objectName, null);
assertThat(set == null).isFalse();
assertThat(set.size() == 1).isTrue();
assertThat(set.contains(objectName)).isTrue();
}
@Test
void testByteAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("ByteAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isFalse();
}
@Test
void testByteAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("ByteAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.longValue() == 10).isTrue();
}
@Test
void testShortAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("ShortAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isFalse();
}
@Test
void testShortAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("ShortAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.longValue() == 11).isTrue();
}
@Test
void testIntAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("IntAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isFalse();
}
@Test
void testIntAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("IntAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.longValue() == 12).isTrue();
}
@Test
void testLongAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("LongAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isFalse();
}
@Test
void testLongAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("LongAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.longValue() == 13).isTrue();
}
@Test
void testFloatAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("FloatAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isTrue();
}
@Test
void testFloatAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("FloatAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.doubleValue() == 14.0).isTrue(); // accurate representation
}
@Test
void testDoubleAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("DoubleAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isFalse();
assertThat(info.usesDoubleValues()).isTrue();
}
@Test
void testDoubleAttributeValue() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("DoubleAttribute");
Number number = extractor.extractNumericalAttribute(theServer, objectName);
assertThat(number == null).isFalse();
assertThat(number.doubleValue() == 15.0).isTrue(); // accurate representation
}
@Test
void testStringAttribute() throws Exception {
BeanAttributeExtractor extractor = new BeanAttributeExtractor("StringAttribute");
AttributeInfo info = extractor.getAttributeInfo(theServer, objectName);
assertThat(info == null).isTrue();
}
}

View File

@ -0,0 +1,465 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jmx.engine;
// This test is put in the io.opentelemetry.instrumentation.jmx.engine package
// because it needs to access package-private methods from a number of classes.
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.instrumentation.jmx.yaml.JmxConfig;
import io.opentelemetry.instrumentation.jmx.yaml.JmxRule;
import io.opentelemetry.instrumentation.jmx.yaml.Metric;
import io.opentelemetry.instrumentation.jmx.yaml.RuleParser;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class RuleParserTest {
private static RuleParser parser;
@BeforeAll
static void setup() throws Exception {
parser = RuleParser.get();
assertThat(parser == null).isFalse();
}
/*
* General syntax
*/
private static final String CONF2 =
"---\n"
+ "rules:\n"
+ " - beans:\n"
+ " - OBJECT:NAME1=*\n"
+ " - OBJECT:NAME2=*\n"
+ " metricAttribute:\n"
+ " LABEL_KEY1: param(PARAMETER)\n"
+ " LABEL_KEY2: beanattr(ATTRIBUTE)\n"
+ " prefix: METRIC_NAME_PREFIX\n"
+ " mapping:\n"
+ " ATTRIBUTE1:\n"
+ " metric: METRIC_NAME1\n"
+ " type: Gauge\n"
+ " desc: DESCRIPTION1\n"
+ " unit: UNIT1\n"
+ " metricAttribute:\n"
+ " LABEL_KEY3: const(CONSTANT)\n"
+ " ATTRIBUTE2:\n"
+ " metric: METRIC_NAME2\n"
+ " desc: DESCRIPTION2\n"
+ " unit: UNIT2\n"
+ " ATTRIBUTE3:\n"
+ " ATTRIBUTE4:\n"
+ " - beans:\n"
+ " - OBJECT:NAME3=*\n"
+ " mapping:\n"
+ " ATTRIBUTE3:\n"
+ " metric: METRIC_NAME3\n";
@Test
void testConf2() throws Exception {
InputStream is = new ByteArrayInputStream(CONF2.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 2).isTrue();
JmxRule def1 = defs.get(0);
assertThat(def1.getBeans().size() == 2).isTrue();
assertThat(def1.getMetricAttribute().size() == 2).isTrue();
Map<String, Metric> attr = def1.getMapping();
assertThat(attr == null).isFalse();
assertThat(attr.size() == 4).isTrue();
Metric m1 = attr.get("ATTRIBUTE1");
assertThat(m1 == null).isFalse();
assertThat("METRIC_NAME1".equals(m1.getMetric())).isTrue();
assertThat(m1.getMetricType() == MetricInfo.Type.GAUGE).isTrue();
assertThat("UNIT1".equals(m1.getUnit())).isTrue();
assertThat(m1.getMetricAttribute() == null).isFalse();
assertThat(m1.getMetricAttribute().size() == 1).isTrue();
assertThat("const(CONSTANT)".equals(m1.getMetricAttribute().get("LABEL_KEY3"))).isTrue();
}
private static final String CONF3 =
"rules:\n"
+ " - bean: OBJECT:NAME3=*\n"
+ " mapping:\n"
+ " ATTRIBUTE31:\n"
+ " ATTRIBUTE32:\n"
+ " ATTRIBUTE33:\n"
+ " ATTRIBUTE34:\n"
+ " metric: METRIC_NAME34\n"
+ " ATTRIBUTE35:\n";
@Test
void testConf3() throws Exception {
InputStream is = new ByteArrayInputStream(CONF3.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
JmxRule def1 = defs.get(0);
assertThat(def1.getBean() == null).isFalse();
assertThat(def1.getMetricAttribute() == null).isTrue();
Map<String, Metric> attr = def1.getMapping();
assertThat(attr.size() == 5).isTrue();
Set<String> keys = attr.keySet();
assertThat(keys.contains("ATTRIBUTE33")).isTrue();
assertThat(attr.get("ATTRIBUTE33") == null).isTrue();
assertThat(attr.get("ATTRIBUTE34") == null).isFalse();
}
/*
* Semantics
*/
private static final String CONF4 =
"---\n"
+ "rules:\n"
+ " - bean: my-test:type=4\n"
+ " metricAttribute:\n"
+ " LABEL_KEY1: param(PARAMETER)\n"
+ " LABEL_KEY2: beanattr(ATTRIBUTE)\n"
+ " prefix: PREFIX.\n"
+ " type: upDownCounter\n"
+ " unit: DEFAULT_UNIT\n"
+ " mapping:\n"
+ " A.b:\n"
+ " metric: METRIC_NAME1\n"
+ " type: counter\n"
+ " desc: DESCRIPTION1\n"
+ " unit: UNIT1\n"
+ " metricAttribute:\n"
+ " LABEL_KEY3: const(CONSTANT)\n"
+ " ATTRIBUTE2:\n"
+ " metric: METRIC_NAME2\n"
+ " desc: DESCRIPTION2\n"
+ " unit: UNIT2\n"
+ " ATTRIBUTE3:\n";
@Test
void testConf4() throws Exception {
InputStream is = new ByteArrayInputStream(CONF4.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
MetricDef metricDef = defs.get(0).buildMetricDef();
assertThat(metricDef == null).isFalse();
assertThat(metricDef.getMetricExtractors().length == 3).isTrue();
MetricExtractor m1 = metricDef.getMetricExtractors()[0];
BeanAttributeExtractor a1 = m1.getMetricValueExtractor();
assertThat("A.b".equals(a1.getAttributeName())).isTrue();
assertThat(m1.getAttributes().length == 3).isTrue();
MetricInfo mb1 = m1.getInfo();
assertThat("PREFIX.METRIC_NAME1".equals(mb1.getMetricName())).isTrue();
assertThat("DESCRIPTION1".equals(mb1.getDescription())).isTrue();
assertThat("UNIT1".equals(mb1.getUnit())).isTrue();
assertThat(MetricInfo.Type.COUNTER == mb1.getType()).isTrue();
MetricExtractor m3 = metricDef.getMetricExtractors()[2];
BeanAttributeExtractor a3 = m3.getMetricValueExtractor();
assertThat("ATTRIBUTE3".equals(a3.getAttributeName())).isTrue();
MetricInfo mb3 = m3.getInfo();
assertThat("PREFIX.ATTRIBUTE3".equals(mb3.getMetricName())).isTrue();
// syntax extension - defining a default unit and type
assertThat(MetricInfo.Type.UPDOWNCOUNTER == mb3.getType()).isTrue();
assertThat("DEFAULT_UNIT".equals(mb3.getUnit())).isTrue();
}
private static final String CONF5 = // minimal valid definition
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: my-test:type=5\n"
+ " mapping:\n"
+ " ATTRIBUTE:\n";
@Test
void testConf5() throws Exception {
InputStream is = new ByteArrayInputStream(CONF5.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
MetricDef metricDef = defs.get(0).buildMetricDef();
assertThat(metricDef == null).isFalse();
assertThat(metricDef.getMetricExtractors().length == 1).isTrue();
MetricExtractor m1 = metricDef.getMetricExtractors()[0];
BeanAttributeExtractor a1 = m1.getMetricValueExtractor();
assertThat("ATTRIBUTE".equals(a1.getAttributeName())).isTrue();
assertThat(m1.getAttributes().length == 0).isTrue();
MetricInfo mb1 = m1.getInfo();
assertThat("ATTRIBUTE".equals(mb1.getMetricName())).isTrue();
assertThat(MetricInfo.Type.GAUGE == mb1.getType()).isTrue();
assertThat(null == mb1.getUnit()).isTrue();
}
private static final String CONF6 = // merging metric attribute sets with same keys
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: my-test:type=6\n"
+ " metricAttribute:\n"
+ " key1: const(value1)\n"
+ " mapping:\n"
+ " ATTRIBUTE:\n"
+ " metricAttribute:\n"
+ " key1: const(value2)\n";
@Test
void testConf6() throws Exception {
InputStream is = new ByteArrayInputStream(CONF6.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
MetricDef metricDef = defs.get(0).buildMetricDef();
assertThat(metricDef == null).isFalse();
assertThat(metricDef.getMetricExtractors().length == 1).isTrue();
MetricExtractor m1 = metricDef.getMetricExtractors()[0];
BeanAttributeExtractor a1 = m1.getMetricValueExtractor();
assertThat("ATTRIBUTE".equals(a1.getAttributeName())).isTrue();
// MetricAttribute set at the metric level should override the one set at the definition level
assertThat(m1.getAttributes().length == 1).isTrue();
MetricAttribute l1 = m1.getAttributes()[0];
assertThat("value2".equals(l1.acquireAttributeValue(null, null))).isTrue();
MetricInfo mb1 = m1.getInfo();
assertThat("ATTRIBUTE".equals(mb1.getMetricName())).isTrue();
}
private static final String CONF7 =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: my-test:type=7\n"
+ " metricAttribute:\n"
+ " key1: const(value1)\n"
+ " mapping:\n"
+ " ATTRIBUTE:\n"
+ " metricAttribute:\n"
+ " key2: const(value2)\n";
@Test
void testConf7() throws Exception {
InputStream is = new ByteArrayInputStream(CONF7.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
MetricDef metricDef = defs.get(0).buildMetricDef();
assertThat(metricDef == null).isFalse();
assertThat(metricDef.getMetricExtractors().length == 1).isTrue();
// Test that the MBean attribute is correctly parsed
MetricExtractor m1 = metricDef.getMetricExtractors()[0];
BeanAttributeExtractor a1 = m1.getMetricValueExtractor();
assertThat("ATTRIBUTE".equals(a1.getAttributeName())).isTrue();
assertThat(m1.getAttributes().length == 2).isTrue();
MetricInfo mb1 = m1.getInfo();
assertThat("ATTRIBUTE".equals(mb1.getMetricName())).isTrue();
}
private static final String EMPTY_CONF = "---\n";
@Test
void testEmptyConf() throws Exception {
InputStream is = new ByteArrayInputStream(EMPTY_CONF.getBytes(Charset.forName("UTF-8")));
JmxConfig config = parser.loadConfig(is);
assertThat(config == null).isTrue();
}
/*
* Negative tests
*/
private static void runNegativeTest(String yaml) throws Exception {
InputStream is = new ByteArrayInputStream(yaml.getBytes(Charset.forName("UTF-8")));
Assertions.assertThrows(
Exception.class,
() -> {
JmxConfig config = parser.loadConfig(is);
assertThat(config != null).isTrue();
List<JmxRule> defs = config.getRules();
assertThat(defs.size() == 1).isTrue();
defs.get(0).buildMetricDef();
});
}
@Test
void testNoBeans() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules: # no bean\n"
+ " - mapping: # still no beans\n"
+ " A:\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testInvalidObjectName() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: BAD_OBJECT_NAME\n"
+ " mapping:\n"
+ " A:\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testEmptyMapping() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n "
+ "rules:\n"
+ " - bean: domain:type=6\n"
+ " mapping:\n";
runNegativeTest(yaml);
}
@Test
void testInvalidAttributeName() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " .used:\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testInvalidTag() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " ATTRIB:\n"
+ " metricAttribute:\n"
+ " LABEL: something\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testInvalidType() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " ATTRIB:\n"
+ " type: gage\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testInvalidTagFromAttribute() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " ATTRIB:\n"
+ " metricAttribute:\n"
+ " LABEL: beanattr(.used)\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testEmptyTagFromAttribute() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " ATTRIB:\n"
+ " metricAttribute:\n"
+ " LABEL: beanattr( )\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testEmptyTagFromParameter() throws Exception {
String yaml =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " ATTRIB:\n"
+ " metricAttribute:\n"
+ " LABEL: param( )\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testEmptyPrefix() throws Exception {
String yaml =
"---\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " prefix:\n"
+ " mapping:\n"
+ " A:\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testTypoInMetric() throws Exception {
String yaml =
"---\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " A:\n"
+ " metrics: METRIC_NAME\n";
runNegativeTest(yaml);
}
@Test
void testMessedUpSyntax() throws Exception {
String yaml =
"---\n"
+ "rules:\n"
+ " - bean: domain:name=you\n"
+ " mapping:\n"
+ " metricAttribute: # not valid here\n"
+ " key: const(value)\n"
+ " A:\n"
+ " metric: METRIC_NAME\n";
runNegativeTest(yaml);
}
}

View File

@ -294,6 +294,8 @@ include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:library")
include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing") include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing")
include(":instrumentation:jms-1.1:javaagent") include(":instrumentation:jms-1.1:javaagent")
include(":instrumentation:jms-1.1:javaagent-unit-tests") include(":instrumentation:jms-1.1:javaagent-unit-tests")
include(":instrumentation:jmx-metrics:javaagent")
include(":instrumentation:jmx-metrics:library")
include(":instrumentation:jsf:jsf-common:javaagent") include(":instrumentation:jsf:jsf-common:javaagent")
include(":instrumentation:jsf:jsf-common:testing") include(":instrumentation:jsf:jsf-common:testing")
include(":instrumentation:jsf:jsf-mojarra-1.2:javaagent") include(":instrumentation:jsf:jsf-mojarra-1.2:javaagent")