Skip to content

Commit

Permalink
Enable Virtual Thread Binder if micrometer-java21 is on the Classpath
Browse files Browse the repository at this point in the history
This commit introduces automatic registration of the virtual thread meter binder when the `io.micrometer:micrometer-java21` dependency is present. The binder collects metrics related to virtual threads pinning and misbehavior (unable to unpark or start)

The binder is activated under the following conditions:
- The `micrometer-java21` dependency is available on the classpath.
- The application is running on Java 21 or higher.
- The `quarkus.micrometer.binder.virtual-threads.enabled` property is set to true (default).
  • Loading branch information
cescoffier committed Nov 25, 2024
1 parent cbd735f commit f35f7cf
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class JavaVersionUtil {
private static boolean IS_JAVA_16_OR_OLDER;
private static boolean IS_JAVA_17_OR_NEWER;
private static boolean IS_JAVA_19_OR_NEWER;
private static boolean IS_JAVA_21_OR_NEWER;

static {
performChecks();
Expand All @@ -28,12 +29,14 @@ static void performChecks() {
IS_JAVA_16_OR_OLDER = (first <= 16);
IS_JAVA_17_OR_NEWER = (first >= 17);
IS_JAVA_19_OR_NEWER = (first >= 19);
IS_JAVA_21_OR_NEWER = (first >= 21);
} else {
IS_JAVA_11_OR_NEWER = false;
IS_JAVA_13_OR_NEWER = false;
IS_JAVA_16_OR_OLDER = false;
IS_JAVA_17_OR_NEWER = false;
IS_JAVA_19_OR_NEWER = false;
IS_JAVA_21_OR_NEWER = false;
}

String vmVendor = System.getProperty("java.vm.vendor");
Expand All @@ -60,6 +63,10 @@ public static boolean isJava19OrHigher() {
return IS_JAVA_19_OR_NEWER;
}

public static boolean isJava21OrHigher() {
return IS_JAVA_21_OR_NEWER;
}

public static boolean isGraalvmJdk() {
return IS_GRAALVM_JDK;
}
Expand Down
32 changes: 32 additions & 0 deletions docs/src/main/asciidoc/virtual-threads.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,38 @@ public class LoomUnitExampleTest {
}
----

== Virtual thread metrics

You can enable the Micrometer Virtual Thread _binder_ by adding the following artifact to your application:

[source,xml]
----
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-java21</artifactId>
</dependency>
----

This binder keeps track of the number of pinning events and the number of virtual threads failed to be started or un-parked.
See the https://docs.micrometer.io/micrometer/reference/reference/jvm.html#_java_21_metrics[MicroMeter documentation] for more information.

You can explicitly disable the binder by setting the following property in your `application.properties`:

[source,properties]
----
# The binder is automatically enabled if the micrometer-java21 dependency is present
quarkus.micrometer.binder.virtual-threads.enabled=false
----

In addition, if the application is running on a JVM that does not support virtual threads (prior to Java 21), the binder is automatically disabled.

You can associate tags to the collected metrics by setting the following properties in your `application.properties`:

[source,properties]
----
quarkus.micrometer.binder.virtual-threads.tags=tag_1=value_1, tag_2=value_2
----

== Additional references

- https://dl.acm.org/doi/10.1145/3583678.3596895[Considerations for integrating virtual threads in a Java framework: a Quarkus example in a resource-constrained environment]
14 changes: 14 additions & 0 deletions extensions/micrometer/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,19 @@
</plugins>
</build>
</profile>

<profile>
<id>Java 21+</id>
<activation>
<jdk>[21,)</jdk>
</activation>
<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-java21</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.micrometer.deployment.binder;

import java.util.function.BooleanSupplier;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.micrometer.runtime.MicrometerRecorder;
import io.quarkus.micrometer.runtime.config.MicrometerConfig;

/**
* Add support for virtual thread metric collections.
*/
public class VirtualThreadBinderProcessor {
static final String VIRTUAL_THREAD_COLLECTOR_CLASS_NAME = "io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector";

static final String VIRTUAL_THREAD_BINDER_CLASS_NAME = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
static final Class<?> VIRTUAL_THREAD_BINDER_CLASS = MicrometerRecorder.getClassForName(VIRTUAL_THREAD_BINDER_CLASS_NAME);

static class VirtualThreadSupportEnabled implements BooleanSupplier {
MicrometerConfig mConfig;

public boolean getAsBoolean() {
return VIRTUAL_THREAD_BINDER_CLASS != null // The binder is in another Micrometer artifact
&& mConfig.checkBinderEnabledWithDefault(mConfig.binder.virtualThreads);
}
}

@BuildStep(onlyIf = VirtualThreadSupportEnabled.class)
AdditionalBeanBuildItem createCDIEventConsumer() {
return AdditionalBeanBuildItem.builder()
.addBeanClass(VIRTUAL_THREAD_COLLECTOR_CLASS_NAME)
.setUnremovable().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.micrometer.deployment.binder;

import static org.junit.jupiter.api.Assertions.assertTrue;

import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
import io.quarkus.test.QuarkusUnitTest;

public class VirtualThreadMetricsDisabledTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("test-logging.properties")
.overrideConfigKey("quarkus.micrometer.binder.virtual-threads.enabled", "true")

.overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false")
.overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false")
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
.withEmptyApplication();

@Inject
BeanManager beans;

@Test
void testNoInstancePresentIfDisabled() {
assertTrue(
beans.createInstance().select()
.stream().filter(this::isVirtualThreadCollector).findAny().isEmpty(),
"No VirtualThreadCollector expected");
}

private boolean isVirtualThreadCollector(Object bean) {
return bean.getClass().toString().equals(VirtualThreadCollector.class.toString());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.micrometer.deployment.binder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
import io.quarkus.test.QuarkusUnitTest;

@EnabledForJreRange(min = JRE.JAVA_21)
public class VirtualThreadMetricsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("test-logging.properties")
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
.withEmptyApplication();

@Inject
Instance<VirtualThreadCollector> collector;

@Test
void testInstancePresent() {
assertTrue(collector.isResolvable(), "VirtualThreadCollector expected");
}

@Test
void testBinderCreated() {
assertThat(collector.get().getBinder()).isNotNull();
}

@Test
void testTags() {
assertThat(collector.get().getTags()).isEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.micrometer.deployment.binder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
import io.quarkus.test.QuarkusUnitTest;

@EnabledForJreRange(min = JRE.JAVA_21)
public class VirtualThreadMetricsWithTagsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("test-logging.properties")
.overrideConfigKey("quarkus.micrometer.binder.virtual-threads.tags", "k1=v1, k2=v2")
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
.withEmptyApplication();

@Inject
Instance<VirtualThreadCollector> collector;

@Test
void testInstancePresent() {
assertTrue(collector.isResolvable(), "VirtualThreadCollector expected");
}

@Test
void testBinderCreated() {
assertThat(collector.get().getBinder()).isNotNull();
}

@Test
void testTags() {
assertThat(collector.get().getTags()).hasSize(2)
.anySatisfy(t -> {
assertThat(t.getKey()).isEqualTo("k1");
assertThat(t.getValue()).isEqualTo("v1");
})
.anySatisfy(t -> {
assertThat(t.getKey()).isEqualTo("k2");
assertThat(t.getValue()).isEqualTo("v2");
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package io.quarkus.micrometer.runtime.binder.virtualthreads;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.jboss.logging.Logger;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.quarkus.micrometer.runtime.config.MicrometerConfig;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.runtime.util.JavaVersionUtil;

/**
* A component collecting metrics about virtual threads.
* It will be only available when the virtual threads are enabled (Java 21+).
* <p>
* Note that metrics are collected using JFR events.
*/
@ApplicationScoped
public class VirtualThreadCollector {

private static final String VIRTUAL_THREAD_BINDER_CLASSNAME = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
private static final Logger LOGGER = Logger.getLogger(VirtualThreadCollector.class);

final MeterRegistry registry = Metrics.globalRegistry;

private final boolean enabled;
private final MeterBinder binder;
private final List<Tag> tags;

@Inject
public VirtualThreadCollector(MicrometerConfig mc) {
var config = mc.binder.virtualThreads;
this.enabled = JavaVersionUtil.isJava21OrHigher() && config.enabled.orElse(true);
MeterBinder instantiated = null;
if (enabled) {
if (config.tags.isPresent()) {
List<String> list = config.tags.get();
this.tags = list.stream().map(this::createTagFromEntry).collect(Collectors.toList());
} else {
this.tags = List.of();
}
try {
instantiated = instantiate(tags);
} catch (Exception e) {
LOGGER.warnf(e, "Failed to instantiate " + VIRTUAL_THREAD_BINDER_CLASSNAME);
}
} else {
this.tags = List.of();
}
this.binder = instantiated;
}

/**
* Use reflection to avoid calling a class touching Java 21+ APIs.
*
* @param tags the tags.
* @return the binder, {@code null} if the instantiation failed.
*/
public MeterBinder instantiate(List<Tag> tags) {
try {
Class<?> clazz = Class.forName(VIRTUAL_THREAD_BINDER_CLASSNAME);
return (MeterBinder) clazz.getDeclaredConstructor(Iterable.class).newInstance(tags);
} catch (Exception e) {
throw new IllegalStateException("Failed to instantiate " + VIRTUAL_THREAD_BINDER_CLASSNAME, e);
}
}

private Tag createTagFromEntry(String entry) {
String[] parts = entry.trim().split("=");
if (parts.length == 2) {
return Tag.of(parts[0], parts[1]);
} else {
throw new IllegalStateException("Invalid tag: " + entry + " (expected key=value)");
}
}

public MeterBinder getBinder() {
return binder;
}

public List<Tag> getTags() {
return tags;
}

public void init(@Observes StartupEvent event) {
if (enabled && binder != null) {
binder.bindTo(registry);
}
}

public void close(@Observes ShutdownEvent event) {
if (binder instanceof Closeable) {
try {
((Closeable) binder).close();
} catch (IOException e) {
LOGGER.warnf(e, "Failed to close " + VIRTUAL_THREAD_BINDER_CLASSNAME);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public static class BinderConfig {

public MPMetricsConfigGroup mpMetrics;

public VirtualThreadsConfigGroup virtualThreads;

/**
* Micrometer System metrics support.
* <p>
Expand Down
Loading

0 comments on commit f35f7cf

Please sign in to comment.