diff --git a/instrumentation/jmx-metrics/javaagent/README.md b/instrumentation/jmx-metrics/javaagent/README.md new file mode 100644 index 000000000000..c2eef2a60c8b --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/README.md @@ -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: # can contain wildcards + metricAttribute: # optional metric attributes, they apply to all metrics below + : param() # is used as the key to extract value from actual ObjectName + : beanattr() # is used as the MBean attribute name to extract the value + prefix: # optional, useful for avoiding specifying metric names below + unit: # optional, redefines the default unit for the whole rule + type: # optional, redefines the default type for the whole rule + mapping: + : # an MBean attribute name defining the metric value + metric: # metric name will be + type: # optional, the default type is gauge + desc: # optional + unit: # optional + metricAttribute: # optional, will be used in addition to the shared metric attributes above + : const() # direct value for the metric attribute + : # use a.b to get access into CompositeData + metric: # optional, the default is the MBean attribute name + unit: # optional + : # metric name will be + : # metric name will be + - beans: # alternatively, if multiple object names are needed + - # at least one object name must be specified + - + mapping: + : # an MBean attribute name defining the metric value + metric: # metric name will be + type: updowncounter # optional + : # metric name will be +``` +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. diff --git a/instrumentation/jmx-metrics/javaagent/activemq.md b/instrumentation/jmx-metrics/javaagent/activemq.md new file mode 100644 index 000000000000..8cdc14dec307 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/activemq.md @@ -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 | diff --git a/instrumentation/jmx-metrics/javaagent/build.gradle.kts b/instrumentation/jmx-metrics/javaagent/build.gradle.kts new file mode 100644 index 000000000000..e9b22ef2bcc3 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +dependencies { + implementation(project(":instrumentation:jmx-metrics:library")) + + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +} diff --git a/instrumentation/jmx-metrics/javaagent/hadoop.md b/instrumentation/jmx-metrics/javaagent/hadoop.md new file mode 100644 index 000000000000..7e628fe0464a --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/hadoop.md @@ -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 | diff --git a/instrumentation/jmx-metrics/javaagent/jetty.md b/instrumentation/jmx-metrics/javaagent/jetty.md new file mode 100644 index 000000000000..e04622a17918 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/jetty.md @@ -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 | diff --git a/instrumentation/jmx-metrics/javaagent/kafka-broker.md b/instrumentation/jmx-metrics/javaagent/kafka-broker.md new file mode 100644 index 000000000000..2dddfbb19d82 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/kafka-broker.md @@ -0,0 +1,32 @@ +# Kafka Broker Metrics + +Here is the list of metrics based on MBeans exposed by Kafka broker.

+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 | +
+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 | diff --git a/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java b/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java new file mode 100644 index 000000000000..89cde5ae5db9 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/java/io/opentelemetry/instrumentation/javaagent/jmx/JmxMetricInsightInstaller.java @@ -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; + } +} diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml new file mode 100644 index 000000000000..3b4e03334fa5 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/activemq.yaml @@ -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 diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml new file mode 100644 index 000000000000..f89de461230c --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/hadoop.yaml @@ -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) diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml new file mode 100644 index 000000000000..021e615cc069 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/jetty.yaml @@ -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 diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml new file mode 100644 index 000000000000..251c83091b01 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/kafka-broker.yaml @@ -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 diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml new file mode 100644 index 000000000000..e7041bcc34ac --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/tomcat.yaml @@ -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) diff --git a/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml new file mode 100644 index 000000000000..82aff5ee9a7f --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/src/main/resources/jmx/rules/wildfly.yaml @@ -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 diff --git a/instrumentation/jmx-metrics/javaagent/tomcat.md b/instrumentation/jmx-metrics/javaagent/tomcat.md new file mode 100644 index 000000000000..a2ea859d8077 --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/tomcat.md @@ -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 | diff --git a/instrumentation/jmx-metrics/javaagent/wildfly.md b/instrumentation/jmx-metrics/javaagent/wildfly.md new file mode 100644 index 000000000000..c88eee63be0f --- /dev/null +++ b/instrumentation/jmx-metrics/javaagent/wildfly.md @@ -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 | diff --git a/instrumentation/jmx-metrics/library/build.gradle.kts b/instrumentation/jmx-metrics/library/build.gradle.kts new file mode 100644 index 000000000000..722db6cf773e --- /dev/null +++ b/instrumentation/jmx-metrics/library/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + implementation("org.yaml:snakeyaml") + + testImplementation(project(":testing-common")) +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/AttributeInfo.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/AttributeInfo.java new file mode 100644 index 000000000000..ac1f8b6d0c4c --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/AttributeInfo.java @@ -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(); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java new file mode 100644 index 000000000000..d68f730de28e --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanAttributeExtractor.java @@ -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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java new file mode 100644 index 000000000000..fa44f293cc4e --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanFinder.java @@ -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 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 servers) { + BeanGroup beans = metricDef.getBeanGroup(); + + for (MBeanServer server : servers) { + // The set of all matching ObjectNames recognized by the server + Set allObjectNames = new HashSet<>(); + + for (ObjectName pattern : beans.getNamePatterns()) { + Set 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 objectNames, MBeanServer server, MetricDef metricDef) { + for (MetricExtractor extractor : metricDef.getMetricExtractors()) { + // For each MetricExtractor, find the subset of MBeans that have the required attribute + List 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); + } + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java new file mode 100644 index 000000000000..dc4fb3ef5fbb --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/BeanGroup.java @@ -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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/DetectionStatus.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/DetectionStatus.java new file mode 100644 index 000000000000..e012295fda19 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/DetectionStatus.java @@ -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 objectNames; + + DetectionStatus(MBeanServer server, Collection objectNames) { + this.server = server; + this.objectNames = objectNames; + } + + MBeanServer getServer() { + return server; + } + + Collection getObjectNames() { + return objectNames; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/JmxMetricInsight.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/JmxMetricInsight.java new file mode 100644 index 000000000000..44fe4e7269e0 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/JmxMetricInsight.java @@ -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); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java new file mode 100644 index 000000000000..2dfff5509752 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java @@ -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); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttributeExtractor.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttributeExtractor.java new file mode 100644 index 000000000000..61c9bc03c283 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttributeExtractor.java @@ -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); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricConfiguration.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricConfiguration.java new file mode 100644 index 000000000000..dee55e6b2ba0 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricConfiguration.java @@ -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 currentSet = new ArrayList<>(); + + public MetricConfiguration() {} + + public boolean isEmpty() { + return currentSet.isEmpty(); + } + + public void addMetricDef(MetricDef def) { + currentSet.add(def); + } + + Collection getMetricDefs() { + return currentSet; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java new file mode 100644 index 000000000000..875e90b76cf1 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricDef.java @@ -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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java new file mode 100644 index 000000000000..2b9ce939168d --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricExtractor.java @@ -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. + * + *

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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java new file mode 100644 index 000000000000..fed1935ca764 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricInfo.java @@ -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. + * + *

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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java new file mode 100644 index 000000000000..58bec6fb3887 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricRegistrar.java @@ -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 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 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 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(); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxConfig.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxConfig.java new file mode 100644 index 000000000000..d979dfb75752 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxConfig.java @@ -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 rules; + + public List getRules() { + return rules; + } + + public void setRules(List 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); + } + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java new file mode 100644 index 000000000000..0bb394cc05e3 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/JmxRule.java @@ -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 beans; + private String prefix; + private Map mapping; + + public String getBean() { + return bean; + } + + public void setBean(String bean) throws Exception { + this.bean = validateBean(bean); + } + + public List 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 beans) throws Exception { + List 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 getMapping() { + return mapping; + } + + public void setMapping(Map mapping) { + this.mapping = validateAttributeMapping(mapping); + } + + private static Map validateAttributeMapping(Map 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 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 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 attributeList; + List ownAttributes = getAttributeList(); + if (ownAttributes != null && m != null && m.getAttributeList() != null) { + // MetricAttributes have been specified at two levels, need to combine them + attributeList = combineMetricAttributes(ownAttributes, m.getAttributeList()); + } else if (ownAttributes != null) { + attributeList = ownAttributes; + } else if (m != null && m.getAttributeList() != null) { + // Get the attributes from the metric + attributeList = m.getAttributeList(); + } else { + // There are no attributes at all + attributeList = new ArrayList(); + } + + MetricExtractor metricExtractor = + new MetricExtractor( + attrExtractor, + metricInfo, + attributeList.toArray(new MetricAttribute[attributeList.size()])); + metricExtractors[n++] = metricExtractor; + } + + return new MetricDef(group, metricExtractors); + } + + private static List combineMetricAttributes( + List ownAttributes, List metricAttributes) { + Map 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(set.values()); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/Metric.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/Metric.java new file mode 100644 index 000000000000..96b5abd38751 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/Metric.java @@ -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); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java new file mode 100644 index 000000000000..17fcc382b16c --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java @@ -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: + *

  • the metric type + *
  • the metric attributes + *
  • the unit + * + *

    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 metricAttribute; // unused, for YAML parser only + private String unit; + + private MetricInfo.Type metricType; + private List 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 map) { + this.metricAttribute = map; + // pre-build the MetricAttributes + List attrList = new ArrayList<>(); + addMetricAttributes(attrList, map); + this.metricAttributes = attrList; + } + + // Used only for testing + public Map getMetricAttribute() { + return metricAttribute; + } + + public MetricInfo.Type getMetricType() { + return metricType; + } + + protected List getAttributeList() { + return metricAttributes; + } + + protected void requireNonEmpty(String s, String msg) { + if (s.isEmpty()) { + throw new IllegalArgumentException(msg); + } + } + + private static void addMetricAttributes( + List list, Map metricAttributeMap) { + if (metricAttributeMap != null) { + for (String key : metricAttributeMap.keySet()) { + String target = metricAttributeMap.get(key); + if (target == null) { + throw new IllegalStateException( + "nothing specified for metric attribute key '" + key + "'"); + } + list.add(buildMetricAttribute(key, target.trim())); + } + } + } + + private 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 + "'"); + } +} diff --git a/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java new file mode 100644 index 000000000000..c5080db7b500 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/RuleParser.java @@ -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; + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/AttributeExtractorTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/AttributeExtractorTest.java new file mode 100644 index 000000000000..852f7c063b43 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/AttributeExtractorTest.java @@ -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 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(); + } +} diff --git a/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java new file mode 100644 index 000000000000..79794f208f13 --- /dev/null +++ b/instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java @@ -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 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 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 defs = config.getRules(); + assertThat(defs.size() == 1).isTrue(); + + JmxRule def1 = defs.get(0); + assertThat(def1.getBean() == null).isFalse(); + assertThat(def1.getMetricAttribute() == null).isTrue(); + Map attr = def1.getMapping(); + assertThat(attr.size() == 5).isTrue(); + + Set 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 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 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 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 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 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); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b8b901cc8257..e4b6f19aec81 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -292,6 +292,8 @@ include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:library") include(":instrumentation:jetty-httpclient:jetty-httpclient-9.2:testing") include(":instrumentation:jms-1.1:javaagent") 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:testing") include(":instrumentation:jsf:jsf-mojarra-1.2:javaagent")