diff --git a/contrib/jmx-metrics/README.md b/contrib/jmx-metrics/README.md index c6e8bcab7..66eaaebbc 100644 --- a/contrib/jmx-metrics/README.md +++ b/contrib/jmx-metrics/README.md @@ -123,6 +123,7 @@ The currently available target systems are: | `otel.jmx.target.system` | | ------------------------ | +| [`jvm`](./docs/target-systems/jvm.md) | | [`cassandra`](./docs/target-systems/cassandra.md) | ### Configuration diff --git a/contrib/jmx-metrics/docs/target-systems/jvm.md b/contrib/jmx-metrics/docs/target-systems/jvm.md new file mode 100644 index 000000000..335dbc962 --- /dev/null +++ b/contrib/jmx-metrics/docs/target-systems/jvm.md @@ -0,0 +1,88 @@ +# JVM Metrics + +The JMX Metric Gatherer provides built in JVM metric gathering capabilities. +These metrics are sourced from Cassandra's exposed Dropwizard Metrics for each node: https://cassandra.apache.org/doc/latest/operating/metrics.html. + +## Metrics + +### Client Request Metrics + +* Name: `jvm.classes.loaded` +* Description: The number of loaded classes +* Unit: `1` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.gc.collections.count` +* Description: The total number of garbage collections that have occurred +* Unit: `1` +* Instrument Type: LongCounter + +* Name: `jvm.gc.collections.elapsed` +* Description: The approximate accumulated collection elapsed time +* Unit: `ms` +* Instrument Type: LongCounter + +* Name: `jvm.memory.heap.init` +* Description: The initial amount of memory that the JVM requests from the operating system for the heap +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.heap.max` +* Description: The maximum amount of memory can be used for the heap +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.heap.used` +* Description: The current heap memory usage +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.heap.committed` +* Description: The amount of memory that is guaranteed to be available for the heap +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.nonheap.init` +* Description: The initial amount of memory that the JVM requests from the operating system for non-heap purposes +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.nonheap.max` +* Description: The maximum amount of memory can be used for non-heap purposes +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.nonheap.used` +* Description: The current non-heap memory usage +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.nonheap.committed` +* Description: The amount of memory that is guaranteed to be available for non-heap purposes +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.pool.init` +* Description: The initial amount of memory that the JVM requests from the operating system for the memory pool +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.pool.max` +* Description: The maximum amount of memory can be used for the memory pool +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.pool.used` +* Description: The current memory pool memory usage +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.memory.pool.committed` +* Description: The amount of memory that is guaranteed to be available for the memory pool +* Unit: `by` +* Instrument Type: LongUpDownCounter + +* Name: `jvm.threads.count` +* Description: The current number of threads +* Unit: `1` +* Instrument Type: LongUpDownCounter diff --git a/contrib/jmx-metrics/src/main/resources/target-systems/jvm.groovy b/contrib/jmx-metrics/src/main/resources/target-systems/jvm.groovy index da1dcee0c..69d93d55a 100644 --- a/contrib/jmx-metrics/src/main/resources/target-systems/jvm.groovy +++ b/contrib/jmx-metrics/src/main/resources/target-systems/jvm.groovy @@ -14,10 +14,30 @@ * limitations under the License. */ -package io.opentelemetry.contrib.jmxmetrics +def classLoading = otel.mbean("java.lang:type=ClassLoading") +otel.instrument(classLoading, "jvm.classes.loaded", "number of loaded classes", + "1", "LoadedClassCount", otel.&longUpDownCounter) -// This is a placeholder for default metric functionality -// per https://github.com/open-telemetry/opentelemetry-java-contrib/issues/12 +def garbageCollector = otel.mbeans("java.lang:type=GarbageCollector,*") +otel.instrument(garbageCollector, "jvm.gc.collections.count", "total number of collections that have occurred", + "1", ["name" : { mbean -> mbean.name().getKeyProperty("name") }], + "CollectionCount", otel.&longCounter) +otel.instrument(garbageCollector, "jvm.gc.collections.elapsed", + "the approximate accumulated collection elapsed time in milliseconds", "ms", + ["name" : { mbean -> mbean.name().getKeyProperty("name") }], + "CollectionTime", otel.&longCounter) -def counter = otel.longCounter("placeholder.metric", "For testing purposes") -counter.add(1) +def memory = otel.mbean("java.lang:type=Memory") +otel.instrument(memory, "jvm.memory.heap", "current heap usage", + "by", "HeapMemoryUsage", otel.&longUpDownCounter) +otel.instrument(memory, "jvm.memory.nonheap", "current non-heap usage", + "by", "NonHeapMemoryUsage", otel.&longUpDownCounter) + +def memoryPool = otel.mbeans("java.lang:type=MemoryPool,*") +otel.instrument(memoryPool, "jvm.memory.pool", "current memory pool usage", + "by", ["name" : { mbean -> mbean.name().getKeyProperty("name") }], + "Usage", otel.&longUpDownCounter) + +def threading = otel.mbean("java.lang:type=Threading") +otel.instrument(threading, "jvm.threads.count", "number of threads", + "1", "ThreadCount", otel.&longUpDownCounter) diff --git a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.groovy b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.groovy index 4950e54ed..423aad5b3 100644 --- a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.groovy +++ b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/MBeanHelperTest.groovy @@ -77,7 +77,6 @@ class MBeanHelperTest extends Specification { then: "${quantity} returned" def returned = mbeanHelper.getAttribute("SomeAttribute") - println "MBeanHelperTest.represents #quantity MBean(s): ${returned}" returned == isSingle ? ["0"]: (0..100).collect {it as String}.sort() where: diff --git a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/CassandraIntegrationTests.groovy b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/CassandraIntegrationTests.groovy index 31d0f9de0..56012f61f 100644 --- a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/CassandraIntegrationTests.groovy +++ b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/CassandraIntegrationTests.groovy @@ -64,7 +64,6 @@ class CassandraIntegrationTests extends OtlpIntegrationTest { metrics.sort{ a, b -> a.name <=> b.name} then: 'they are of the expected size and content' metrics.size() == 23 - println "CassandraIntegrationTests.end to end: ${metrics}" def expectedMetrics = [ [ diff --git a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/JVMTargetSystemIntegrationTests.groovy b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/JVMTargetSystemIntegrationTests.groovy index d4a23d7cd..8e94c589f 100644 --- a/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/JVMTargetSystemIntegrationTests.groovy +++ b/contrib/jmx-metrics/src/test/groovy/io/opentelemetry/contrib/jmxmetrics/target-systems/JVMTargetSystemIntegrationTests.groovy @@ -16,7 +16,14 @@ package io.opentelemetry.contrib.jmxmetrics -import org.apache.hc.client5.http.fluent.Request +import io.opentelemetry.proto.metrics.v1.IntSum + +import io.opentelemetry.proto.common.v1.InstrumentationLibrary +import io.opentelemetry.proto.common.v1.StringKeyValue +import io.opentelemetry.proto.metrics.v1.InstrumentationLibraryMetrics +import io.opentelemetry.proto.metrics.v1.Metric +import io.opentelemetry.proto.metrics.v1.ResourceMetrics +import org.testcontainers.Testcontainers import spock.lang.Requires import spock.lang.Timeout @@ -24,40 +31,192 @@ import spock.lang.Timeout System.getProperty('ojc.integration.tests') == 'true' }) @Timeout(60) -class JVMTargetSystemIntegrationTests extends IntegrationTest { - - def receivedMetrics() { - def scraped = [] - for (int i = 0; i < 120; i++) { - def resp = Request.get("http://localhost:${jmxExposedPort}/metrics").execute() - def received = resp.returnContent().asString() - if (received != '') { - scraped = received.split('\n') - if (scraped.size() > 2) { - break - } - } - Thread.sleep(500) - } - return scraped - } +class JVMTargetSystemIntegrationTests extends OtlpIntegrationTest { def 'end to end'() { - setup: 'we configure JMX metrics gatherer and target server to use default JVM target system script' - configureContainers('jvm_config.properties', 0, 9123, false) + setup: 'we configure JMX metrics gatherer and target server to use JVM as target system' + Testcontainers.exposeHostPorts(otlpPort) + configureContainers('target-systems/jvm.properties', otlpPort, 0, false) expect: - when: 'we receive metrics from the prometheus endpoint' - def scraped = receivedMetrics() + when: 'we receive metrics from the JMX metrics gatherer' + List receivedMetrics = collector.receivedMetrics + then: 'they are of the expected size' + receivedMetrics.size() == 1 + + when: "we examine the received metric's instrumentation library metrics lists" + ResourceMetrics receivedMetric = receivedMetrics.get(0) + List ilMetrics = + receivedMetric.instrumentationLibraryMetricsList + then: 'they of the expected size' + ilMetrics.size() == 1 + + when: 'we examine the instrumentation library' + InstrumentationLibraryMetrics ilMetric = ilMetrics.get(0) + InstrumentationLibrary il = ilMetric.instrumentationLibrary + then: 'it is of the expected content' + il.name == 'io.opentelemetry.contrib.jmxmetrics' + il.version == '0.0.1' - then: 'they are of the expected format' - scraped.size() == 3 - scraped[0].contains( - '# HELP placeholder_metric For testing purposes') - scraped[1].contains( - '# TYPE placeholder_metric counter') - scraped[2].contains( - 'placeholder_metric 1.0') + when: 'we examine the instrumentation library metric metrics list' + ArrayList metrics = ilMetric.metricsList as ArrayList + metrics.sort{ a, b -> a.name <=> b.name} + then: 'they are of the expected size and content' + metrics.size() == 16 + + def expectedMetrics = [ + [ + 'jvm.classes.loaded', + 'number of loaded classes', + '1', + [] + ], + [ + 'jvm.gc.collections.count', + 'total number of collections that have occurred', + '1', + [ + "ConcurrentMarkSweep", + "ParNew"] + ], + [ + 'jvm.gc.collections.elapsed', + 'the approximate accumulated collection elapsed time in milliseconds', + 'ms', + [ + "ConcurrentMarkSweep", + "ParNew"] + ], + [ + 'jvm.memory.heap.committed', + 'current heap usage', + 'by', + [] + ], + [ + 'jvm.memory.heap.init', + 'current heap usage', + 'by', + [] + ], + [ + 'jvm.memory.heap.max', + 'current heap usage', + 'by', + [] + ], + [ + 'jvm.memory.heap.used', + 'current heap usage', + 'by', + [] + ], + [ + 'jvm.memory.nonheap.committed', + 'current non-heap usage', + 'by', + [] + ], + [ + 'jvm.memory.nonheap.init', + 'current non-heap usage', + 'by', + [] + ], + [ + 'jvm.memory.nonheap.max', + 'current non-heap usage', + 'by', + [] + ], + [ + 'jvm.memory.nonheap.used', + 'current non-heap usage', + 'by', + [] + ], + [ + 'jvm.memory.pool.committed', + 'current memory pool usage', + 'by', + [ + "Code Cache", + "Par Eden Space", + "CMS Old Gen", + "Compressed Class Space", + "Metaspace", + "Par Survivor Space"] + ], + [ + 'jvm.memory.pool.init', + 'current memory pool usage', + 'by', + [ + "Code Cache", + "Par Eden Space", + "CMS Old Gen", + "Compressed Class Space", + "Metaspace", + "Par Survivor Space"] + ], + [ + 'jvm.memory.pool.max', + 'current memory pool usage', + 'by', + [ + "Code Cache", + "Par Eden Space", + "CMS Old Gen", + "Compressed Class Space", + "Metaspace", + "Par Survivor Space"] + ], + [ + 'jvm.memory.pool.used', + 'current memory pool usage', + 'by', + [ + "Code Cache", + "Par Eden Space", + "CMS Old Gen", + "Compressed Class Space", + "Metaspace", + "Par Survivor Space"] + ], + [ + 'jvm.threads.count', + 'number of threads', + '1', + [] + ], + ].eachWithIndex{ item, index -> + Metric metric = metrics.get(index) + assert metric.name == item[0] + assert metric.description == item[1] + assert metric.unit == item[2] + assert metric.hasIntSum() + IntSum datapoints = metric.intSum + def expectedLabelCount = item[3].size() + def expectedLabels = item[3] as Set + + def expectedDatapointCount = expectedLabelCount == 0 ? 1 : expectedLabelCount + assert datapoints.dataPointsCount == expectedDatapointCount + + (0.. + def datapoint = datapoints.getDataPoints(i) + List labels = datapoint.labelsList + if (expectedLabelCount != 0) { + assert labels.size() == 1 + assert labels[0].key == 'name' + def value = labels[0].value + assert expectedLabels.remove(value) + } else { + assert labels.size() == 0 + } + } + + assert expectedLabels.size() == 0 + } cleanup: cassandraContainer.stop() diff --git a/contrib/jmx-metrics/src/test/resources/jvm_config.properties b/contrib/jmx-metrics/src/test/resources/target-systems/jvm.properties similarity index 74% rename from contrib/jmx-metrics/src/test/resources/jvm_config.properties rename to contrib/jmx-metrics/src/test/resources/target-systems/jvm.properties index 95a6e19c6..c74044f46 100644 --- a/contrib/jmx-metrics/src/test/resources/jvm_config.properties +++ b/contrib/jmx-metrics/src/test/resources/target-systems/jvm.properties @@ -1,10 +1,9 @@ otel.jmx.interval.milliseconds = 3000 -otel.exporter = prometheus -otel.prometheus.host = 0.0.0.0 -otel.prometheus.port = 9123 +otel.exporter = otlp otel.jmx.service.url = service:jmx:rmi:///jndi/rmi://cassandra:7199/jmxrmi otel.jmx.target.system = jvm # these will be overridden by cmd line otel.jmx.username = wrong_username otel.jmx.password = wrong_password +otel.otlp.endpoint = host.testcontainers.internal:80