Skip to content

Commit cb54f92

Browse files
committed
Enable Virtual Thread Binder if micrometer-java21 is on the Classpath
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).
1 parent cbd735f commit cb54f92

File tree

16 files changed

+590
-0
lines changed

16 files changed

+590
-0
lines changed

core/runtime/src/main/java/io/quarkus/runtime/util/JavaVersionUtil.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class JavaVersionUtil {
1313
private static boolean IS_JAVA_16_OR_OLDER;
1414
private static boolean IS_JAVA_17_OR_NEWER;
1515
private static boolean IS_JAVA_19_OR_NEWER;
16+
private static boolean IS_JAVA_21_OR_NEWER;
1617

1718
static {
1819
performChecks();
@@ -28,12 +29,14 @@ static void performChecks() {
2829
IS_JAVA_16_OR_OLDER = (first <= 16);
2930
IS_JAVA_17_OR_NEWER = (first >= 17);
3031
IS_JAVA_19_OR_NEWER = (first >= 19);
32+
IS_JAVA_21_OR_NEWER = (first >= 21);
3133
} else {
3234
IS_JAVA_11_OR_NEWER = false;
3335
IS_JAVA_13_OR_NEWER = false;
3436
IS_JAVA_16_OR_OLDER = false;
3537
IS_JAVA_17_OR_NEWER = false;
3638
IS_JAVA_19_OR_NEWER = false;
39+
IS_JAVA_21_OR_NEWER = false;
3740
}
3841

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

66+
public static boolean isJava21OrHigher() {
67+
return IS_JAVA_21_OR_NEWER;
68+
}
69+
6370
public static boolean isGraalvmJdk() {
6471
return IS_GRAALVM_JDK;
6572
}

docs/src/main/asciidoc/virtual-threads.adoc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,45 @@ public class LoomUnitExampleTest {
550550
}
551551
----
552552

553+
== Virtual thread metrics
554+
555+
You can enable the Micrometer Virtual Thread _binder_ by adding the following artifact to your application:
556+
557+
[source,xml]
558+
----
559+
<dependency>
560+
<groupId>io.micrometer</groupId>
561+
<artifactId>micrometer-java21</artifactId>
562+
</dependency>
563+
----
564+
565+
This binder keeps track of the number of pinning events and the number of virtual threads failed to be started or un-parked.
566+
See the https://docs.micrometer.io/micrometer/reference/reference/jvm.html#_java_21_metrics[MicroMeter documentation] for more information.
567+
568+
You can explicitly disable the binder by setting the following property in your `application.properties`:
569+
570+
[source,properties]
571+
----
572+
# The binder is automatically enabled if the micrometer-java21 dependency is present
573+
quarkus.micrometer.binder.virtual-threads.enabled=false
574+
----
575+
576+
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.
577+
578+
You can associate tags to the collected metrics by setting the following properties in your `application.properties`:
579+
580+
[source,properties]
581+
----
582+
quarkus.micrometer.binder.virtual-threads.tags=tag_1=value_1, tag_2=value_2
583+
----
584+
585+
To compile the application to native and include the Micrometer Virtual Thread binder, you need to add the following property in your `application.properties`:
586+
587+
[source,properties]
588+
----
589+
quarkus.native.monitoring=jfr
590+
----
591+
553592
== Additional references
554593

555594
- 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]

extensions/micrometer/deployment/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,5 +202,19 @@
202202
</plugins>
203203
</build>
204204
</profile>
205+
206+
<profile>
207+
<id>Java 21+</id>
208+
<activation>
209+
<jdk>[21,)</jdk>
210+
</activation>
211+
<dependencies>
212+
<dependency>
213+
<groupId>io.micrometer</groupId>
214+
<artifactId>micrometer-java21</artifactId>
215+
<scope>test</scope>
216+
</dependency>
217+
</dependencies>
218+
</profile>
205219
</profiles>
206220
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.quarkus.micrometer.deployment.binder;
2+
3+
import java.util.function.BooleanSupplier;
4+
5+
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
6+
import io.quarkus.deployment.annotations.BuildStep;
7+
import io.quarkus.micrometer.runtime.MicrometerRecorder;
8+
import io.quarkus.micrometer.runtime.config.MicrometerConfig;
9+
10+
/**
11+
* Add support for virtual thread metric collections.
12+
*/
13+
public class VirtualThreadBinderProcessor {
14+
static final String VIRTUAL_THREAD_COLLECTOR_CLASS_NAME = "io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector";
15+
16+
static final String VIRTUAL_THREAD_BINDER_CLASS_NAME = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
17+
static final Class<?> VIRTUAL_THREAD_BINDER_CLASS = MicrometerRecorder.getClassForName(VIRTUAL_THREAD_BINDER_CLASS_NAME);
18+
19+
static class VirtualThreadSupportEnabled implements BooleanSupplier {
20+
MicrometerConfig mConfig;
21+
22+
public boolean getAsBoolean() {
23+
return VIRTUAL_THREAD_BINDER_CLASS != null // The binder is in another Micrometer artifact
24+
&& mConfig.checkBinderEnabledWithDefault(mConfig.binder.virtualThreads);
25+
}
26+
}
27+
28+
@BuildStep(onlyIf = VirtualThreadSupportEnabled.class)
29+
AdditionalBeanBuildItem createCDIEventConsumer() {
30+
return AdditionalBeanBuildItem.builder()
31+
.addBeanClass(VIRTUAL_THREAD_COLLECTOR_CLASS_NAME)
32+
.setUnremovable().build();
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.quarkus.micrometer.deployment.binder;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import jakarta.enterprise.inject.spi.BeanManager;
6+
import jakarta.inject.Inject;
7+
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
14+
public class VirtualThreadMetricsDisabledTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withConfigurationResource("test-logging.properties")
19+
.overrideConfigKey("quarkus.micrometer.binder.virtual-threads.enabled", "true")
20+
21+
.overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false")
22+
.overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false")
23+
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
24+
.withEmptyApplication();
25+
26+
@Inject
27+
BeanManager beans;
28+
29+
@Test
30+
void testNoInstancePresentIfDisabled() {
31+
assertTrue(
32+
beans.createInstance().select()
33+
.stream().filter(this::isVirtualThreadCollector).findAny().isEmpty(),
34+
"No VirtualThreadCollector expected");
35+
}
36+
37+
private boolean isVirtualThreadCollector(Object bean) {
38+
return bean.getClass().toString().equals(VirtualThreadCollector.class.toString());
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.quarkus.micrometer.deployment.binder;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import jakarta.enterprise.inject.Instance;
7+
import jakarta.inject.Inject;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.condition.EnabledForJreRange;
11+
import org.junit.jupiter.api.condition.JRE;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
14+
import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
15+
import io.quarkus.test.QuarkusUnitTest;
16+
17+
@EnabledForJreRange(min = JRE.JAVA_21)
18+
public class VirtualThreadMetricsTest {
19+
20+
@RegisterExtension
21+
static final QuarkusUnitTest config = new QuarkusUnitTest()
22+
.withConfigurationResource("test-logging.properties")
23+
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
24+
.withEmptyApplication();
25+
26+
@Inject
27+
Instance<VirtualThreadCollector> collector;
28+
29+
@Test
30+
void testInstancePresent() {
31+
assertTrue(collector.isResolvable(), "VirtualThreadCollector expected");
32+
}
33+
34+
@Test
35+
void testBinderCreated() {
36+
assertThat(collector.get().getBinder()).isNotNull();
37+
}
38+
39+
@Test
40+
void testTags() {
41+
assertThat(collector.get().getTags()).isEmpty();
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.quarkus.micrometer.deployment.binder;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import jakarta.enterprise.inject.Instance;
7+
import jakarta.inject.Inject;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.condition.EnabledForJreRange;
11+
import org.junit.jupiter.api.condition.JRE;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
14+
import io.quarkus.micrometer.runtime.binder.virtualthreads.VirtualThreadCollector;
15+
import io.quarkus.test.QuarkusUnitTest;
16+
17+
@EnabledForJreRange(min = JRE.JAVA_21)
18+
public class VirtualThreadMetricsWithTagsTest {
19+
20+
@RegisterExtension
21+
static final QuarkusUnitTest config = new QuarkusUnitTest()
22+
.withConfigurationResource("test-logging.properties")
23+
.overrideConfigKey("quarkus.micrometer.binder.virtual-threads.tags", "k1=v1, k2=v2")
24+
.overrideConfigKey("quarkus.redis.devservices.enabled", "false")
25+
.withEmptyApplication();
26+
27+
@Inject
28+
Instance<VirtualThreadCollector> collector;
29+
30+
@Test
31+
void testInstancePresent() {
32+
assertTrue(collector.isResolvable(), "VirtualThreadCollector expected");
33+
}
34+
35+
@Test
36+
void testBinderCreated() {
37+
assertThat(collector.get().getBinder()).isNotNull();
38+
}
39+
40+
@Test
41+
void testTags() {
42+
assertThat(collector.get().getTags()).hasSize(2)
43+
.anySatisfy(t -> {
44+
assertThat(t.getKey()).isEqualTo("k1");
45+
assertThat(t.getValue()).isEqualTo("v1");
46+
})
47+
.anySatisfy(t -> {
48+
assertThat(t.getKey()).isEqualTo("k2");
49+
assertThat(t.getValue()).isEqualTo("v2");
50+
});
51+
}
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.quarkus.micrometer.runtime.binder.virtualthreads;
2+
3+
import java.io.Closeable;
4+
import java.io.IOException;
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
8+
import jakarta.enterprise.context.ApplicationScoped;
9+
import jakarta.enterprise.event.Observes;
10+
import jakarta.inject.Inject;
11+
12+
import org.jboss.logging.Logger;
13+
14+
import io.micrometer.core.instrument.MeterRegistry;
15+
import io.micrometer.core.instrument.Metrics;
16+
import io.micrometer.core.instrument.Tag;
17+
import io.micrometer.core.instrument.binder.MeterBinder;
18+
import io.quarkus.micrometer.runtime.config.MicrometerConfig;
19+
import io.quarkus.runtime.ShutdownEvent;
20+
import io.quarkus.runtime.StartupEvent;
21+
import io.quarkus.runtime.util.JavaVersionUtil;
22+
23+
/**
24+
* A component collecting metrics about virtual threads.
25+
* It will be only available when the virtual threads are enabled (Java 21+).
26+
* <p>
27+
* Note that metrics are collected using JFR events.
28+
*/
29+
@ApplicationScoped
30+
public class VirtualThreadCollector {
31+
32+
private static final String VIRTUAL_THREAD_BINDER_CLASSNAME = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
33+
private static final Logger LOGGER = Logger.getLogger(VirtualThreadCollector.class);
34+
35+
final MeterRegistry registry = Metrics.globalRegistry;
36+
37+
private final boolean enabled;
38+
private final MeterBinder binder;
39+
private final List<Tag> tags;
40+
41+
@Inject
42+
public VirtualThreadCollector(MicrometerConfig mc) {
43+
var config = mc.binder.virtualThreads;
44+
this.enabled = JavaVersionUtil.isJava21OrHigher() && config.enabled.orElse(true);
45+
MeterBinder instantiated = null;
46+
if (enabled) {
47+
if (config.tags.isPresent()) {
48+
List<String> list = config.tags.get();
49+
this.tags = list.stream().map(this::createTagFromEntry).collect(Collectors.toList());
50+
} else {
51+
this.tags = List.of();
52+
}
53+
try {
54+
instantiated = instantiate(tags);
55+
} catch (Exception e) {
56+
LOGGER.warnf(e, "Failed to instantiate " + VIRTUAL_THREAD_BINDER_CLASSNAME);
57+
}
58+
} else {
59+
this.tags = List.of();
60+
}
61+
this.binder = instantiated;
62+
}
63+
64+
/**
65+
* Use reflection to avoid calling a class touching Java 21+ APIs.
66+
*
67+
* @param tags the tags.
68+
* @return the binder, {@code null} if the instantiation failed.
69+
*/
70+
public MeterBinder instantiate(List<Tag> tags) {
71+
try {
72+
Class<?> clazz = Class.forName(VIRTUAL_THREAD_BINDER_CLASSNAME);
73+
return (MeterBinder) clazz.getDeclaredConstructor(Iterable.class).newInstance(tags);
74+
} catch (Exception e) {
75+
throw new IllegalStateException("Failed to instantiate " + VIRTUAL_THREAD_BINDER_CLASSNAME, e);
76+
}
77+
}
78+
79+
private Tag createTagFromEntry(String entry) {
80+
String[] parts = entry.trim().split("=");
81+
if (parts.length == 2) {
82+
return Tag.of(parts[0], parts[1]);
83+
} else {
84+
throw new IllegalStateException("Invalid tag: " + entry + " (expected key=value)");
85+
}
86+
}
87+
88+
public MeterBinder getBinder() {
89+
return binder;
90+
}
91+
92+
public List<Tag> getTags() {
93+
return tags;
94+
}
95+
96+
public void init(@Observes StartupEvent event) {
97+
if (enabled && binder != null) {
98+
binder.bindTo(registry);
99+
}
100+
}
101+
102+
public void close(@Observes ShutdownEvent event) {
103+
if (binder instanceof Closeable) {
104+
try {
105+
((Closeable) binder).close();
106+
} catch (IOException e) {
107+
LOGGER.warnf(e, "Failed to close " + VIRTUAL_THREAD_BINDER_CLASSNAME);
108+
}
109+
}
110+
}
111+
112+
}

extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public static class BinderConfig {
109109

110110
public MPMetricsConfigGroup mpMetrics;
111111

112+
public VirtualThreadsConfigGroup virtualThreads;
113+
112114
/**
113115
* Micrometer System metrics support.
114116
* <p>

0 commit comments

Comments
 (0)