diff --git a/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/HandlerRegistry.java b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/HandlerRegistry.java index d2e058e23..88ff00c69 100644 --- a/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/HandlerRegistry.java +++ b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/HandlerRegistry.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler; import io.opentelemetry.contrib.jfr.metrics.internal.ThreadGrouper; +import io.opentelemetry.contrib.jfr.metrics.internal.buffer.DirectBufferStatisticsHandler; import io.opentelemetry.contrib.jfr.metrics.internal.classes.ClassesLoadedHandler; import io.opentelemetry.contrib.jfr.metrics.internal.container.ContainerConfigurationHandler; import io.opentelemetry.contrib.jfr.metrics.internal.cpu.ContextSwitchRateHandler; @@ -72,7 +73,8 @@ static HandlerRegistry createDefault(MeterProvider meterProvider) { new ContainerConfigurationHandler(), new LongLockHandler(grouper), new ThreadCountHandler(), - new ClassesLoadedHandler()); + new ClassesLoadedHandler(), + new DirectBufferStatisticsHandler()); handlers.addAll(basicHandlers); var meter = diff --git a/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/Constants.java b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/Constants.java index 2c0ea6a2c..ccd5da25a 100644 --- a/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/Constants.java +++ b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/Constants.java @@ -26,11 +26,19 @@ private Constants() {} public static final String REGION_COUNT = "region.count"; public static final String COMMITTED = "committed"; public static final String RESERVED = "reserved"; + public static final String INITIAL_SIZE = "initialSize"; public static final String USED = "used"; public static final String COMMITTED_SIZE = "committedSize"; + public static final String RESERVED_SIZE = "reservedSize"; public static final String DAEMON = "daemon"; public static final String HEAP = "heap"; + public static final String NON_HEAP = "nonheap"; + public static final String NETWORK_MODE_READ = "read"; + public static final String NETWORK_MODE_WRITE = "write"; + public static final String DURATION = "duration"; + public static final String END_OF_MINOR_GC = "end of minor GC"; + public static final String END_OF_MAJOR_GC = "end of major GC"; public static final String METRIC_NAME_NETWORK_BYTES = "process.runtime.jvm.network.io"; public static final String METRIC_DESCRIPTION_NETWORK_BYTES = "Network read/write bytes"; @@ -38,25 +46,33 @@ private Constants() {} public static final String METRIC_DESCRIPTION_NETWORK_DURATION = "Network read/write duration"; public static final String METRIC_NAME_COMMITTED = "process.runtime.jvm.memory.committed"; public static final String METRIC_DESCRIPTION_COMMITTED = "Measure of memory committed"; - public static final String NETWORK_MODE_READ = "read"; - public static final String NETWORK_MODE_WRITE = "write"; public static final String METRIC_NAME_MEMORY = "process.runtime.jvm.memory.usage"; + public static final String METRIC_DESCRIPTION_MEMORY = "Measure of memory used"; public static final String METRIC_NAME_MEMORY_AFTER = "process.runtime.jvm.memory.usage_after_last_gc"; - public static final String METRIC_DESCRIPTION_MEMORY = "Measure of memory used"; public static final String METRIC_DESCRIPTION_MEMORY_AFTER = "Measure of memory used, as measured after the most recent garbage collection event on this pool"; public static final String METRIC_NAME_MEMORY_ALLOCATION = "process.runtime.jvm.memory.allocation"; public static final String METRIC_DESCRIPTION_MEMORY_ALLOCATION = "Allocation"; + public static final String METRIC_NAME_MEMORY_INIT = "process.runtime.jvm.memory.init"; + public static final String METRIC_DESCRIPTION_MEMORY_INIT = "Measure of initial memory requested"; + public static final String METRIC_NAME_MEMORY_LIMIT = "process.runtime.jvm.memory.limit"; + public static final String METRIC_DESCRIPTION_MEMORY_LIMIT = "Measure of max obtainable memory"; + public static final String METRIC_NAME_GC_DURATION = "process.runtime.jvm.gc.duration"; + public static final String METRIC_DESCRIPTION_GC_DURATION = + "Duration of JVM garbage collection actions"; public static final AttributeKey ATTR_THREAD_NAME = AttributeKey.stringKey("thread.name"); public static final AttributeKey ATTR_ARENA_NAME = AttributeKey.stringKey("arena"); public static final AttributeKey ATTR_NETWORK_MODE = AttributeKey.stringKey("mode"); public static final AttributeKey ATTR_TYPE = AttributeKey.stringKey("type"); public static final AttributeKey ATTR_POOL = AttributeKey.stringKey("pool"); + public static final AttributeKey ATTR_GC = AttributeKey.stringKey("pool"); + public static final AttributeKey ATTR_ACTION = AttributeKey.stringKey("action"); public static final AttributeKey ATTR_DAEMON = AttributeKey.booleanKey(DAEMON); public static final String UNIT_CLASSES = "{classes}"; public static final String UNIT_THREADS = "{threads}"; + public static final String UNIT_BUFFERS = "{buffers}"; public static final String UNIT_UTILIZATION = "1"; } diff --git a/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/buffer/DirectBufferStatisticsHandler.java b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/buffer/DirectBufferStatisticsHandler.java new file mode 100644 index 000000000..d0364c04d --- /dev/null +++ b/jfr-streaming/src/main/java/io/opentelemetry/contrib/jfr/metrics/internal/buffer/DirectBufferStatisticsHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jfr.metrics.internal.buffer; + +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_POOL; +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.BYTES; +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.UNIT_BUFFERS; +import static io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler.defaultMeter; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler; +import java.time.Duration; +import java.util.Optional; +import jdk.jfr.consumer.RecordedEvent; + +public final class DirectBufferStatisticsHandler implements RecordedEventHandler { + private static final String METRIC_NAME_USAGE = "process.runtime.jvm.buffer.usage"; + private static final String METRIC_NAME_LIMIT = "process.runtime.jvm.buffer.limit"; + private static final String METRIC_NAME_COUNT = "process.runtime.jvm.buffer.count"; + private static final String METRIC_DESCRIPTION_USAGE = "Measure of memory used by buffers"; + private static final String METRIC_DESCRIPTION_LIMIT = + "Measure of total memory capacity of buffers"; + private static final String METRIC_DESCRIPTION_COUNT = "Number of buffers in the pool"; + private static final String COUNT = "count"; + private static final String MAX_CAPACITY = "maxCapacity"; + private static final String MEMORY_USED = "memoryUsed"; + + private static final String EVENT_NAME = "jdk.DirectBufferStatistics"; + private static final Attributes ATTR = Attributes.of(ATTR_POOL, "direct"); + + private volatile long usage = 0; + private volatile long limit = 0; + private volatile long count = 0; + + public DirectBufferStatisticsHandler() { + initializeMeter(defaultMeter()); + } + + @Override + public void accept(RecordedEvent ev) { + if (ev.hasField(COUNT)) { + count = ev.getLong(COUNT); + } + if (ev.hasField(MAX_CAPACITY)) { + limit = ev.getLong(MAX_CAPACITY); + } + if (ev.hasField(MEMORY_USED)) { + usage = ev.getLong(MEMORY_USED); + } + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public void initializeMeter(Meter meter) { + meter + .upDownCounterBuilder(METRIC_NAME_USAGE) + .setDescription(METRIC_DESCRIPTION_USAGE) + .setUnit(BYTES) + .buildWithCallback( + measurement -> { + measurement.record(usage, ATTR); + }); + meter + .upDownCounterBuilder(METRIC_NAME_LIMIT) + .setDescription(METRIC_DESCRIPTION_LIMIT) + .setUnit(BYTES) + .buildWithCallback( + measurement -> { + measurement.record(limit, ATTR); + }); + meter + .upDownCounterBuilder(METRIC_NAME_COUNT) + .setDescription(METRIC_DESCRIPTION_COUNT) + .setUnit(UNIT_BUFFERS) + .buildWithCallback(measurement -> measurement.record(count, ATTR)); + } + + @Override + public Optional getPollingDuration() { + return Optional.of(Duration.ofSeconds(1)); + } +} diff --git a/jfr-streaming/src/test/java/io/opentelemetry/contrib/jfr/metrics/BufferMetricTest.java b/jfr-streaming/src/test/java/io/opentelemetry/contrib/jfr/metrics/BufferMetricTest.java new file mode 100644 index 000000000..ec66808b8 --- /dev/null +++ b/jfr-streaming/src/test/java/io/opentelemetry/contrib/jfr/metrics/BufferMetricTest.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jfr.metrics; + +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ATTR_POOL; +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.BYTES; +import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.UNIT_BUFFERS; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class BufferMetricTest extends AbstractMetricsTest { + + /** + * This is a basic test that allocates some buffers and tests to make sure the resulting JFR event + * was handled and turned into the expected metrics. + * + *

This test handles all 3 buffer related metrics defined in the OpenTelemetry Java runtime + * Semantic Conventions. + * + *

Currently JFR only has support for the "direct" buffer pool. The "mapped" and "mapped - + * 'non-volatile memory'" pools do not have corresponding JFR events. In the future, events should + * be added for those missing pools. + */ + @Test + void shouldHaveJfrLoadedClassesCountEvents() { + ByteBuffer buffer = ByteBuffer.allocateDirect(10000); + buffer.put("test".getBytes(StandardCharsets.UTF_8)); + + waitAndAssertMetrics( + metric -> + metric + .hasName("process.runtime.jvm.buffer.count") + .hasDescription("Number of buffers in the pool") + .hasUnit(UNIT_BUFFERS) + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point.satisfies( + pointData -> { + assertThat(pointData.getValue()).isGreaterThan(0); + assertThat(pointData.getAttributes()) + .isEqualTo(Attributes.of(ATTR_POOL, "direct")); + }))), + metric -> + metric + .hasName("process.runtime.jvm.buffer.limit") + .hasDescription("Measure of total memory capacity of buffers") + .hasUnit(BYTES) + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point.satisfies( + pointData -> { + assertThat(pointData.getValue()).isGreaterThan(0); + assertThat(pointData.getAttributes()) + .isEqualTo(Attributes.of(ATTR_POOL, "direct")); + }))), + metric -> + metric + .hasName("process.runtime.jvm.buffer.usage") + .hasDescription("Measure of memory used by buffers") + .hasUnit(BYTES) + .hasLongSumSatisfying( + sum -> + sum.hasPointsSatisfying( + point -> + point.satisfies( + pointData -> { + assertThat(pointData.getValue()).isGreaterThan(0); + assertThat(pointData.getAttributes()) + .isEqualTo(Attributes.of(ATTR_POOL, "direct")); + })))); + } +}