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");
+ });
+ }
+
+}
diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/virtualthreads/VirtualThreadCollector.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/virtualthreads/VirtualThreadCollector.java
new file mode 100644
index 0000000000000..8503e03669842
--- /dev/null
+++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/virtualthreads/VirtualThreadCollector.java
@@ -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+).
+ *
+ * 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 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 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 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 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);
+ }
+ }
+ }
+
+}
diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java
index cef3d1f52a5e1..b5c51a3f002fb 100644
--- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java
+++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/MicrometerConfig.java
@@ -109,6 +109,8 @@ public static class BinderConfig {
public MPMetricsConfigGroup mpMetrics;
+ public VirtualThreadsConfigGroup virtualThreads;
+
/**
* Micrometer System metrics support.
*
diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/VirtualThreadsConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/VirtualThreadsConfigGroup.java
new file mode 100644
index 0000000000000..e739b78471163
--- /dev/null
+++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/VirtualThreadsConfigGroup.java
@@ -0,0 +1,35 @@
+package io.quarkus.micrometer.runtime.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+/**
+ * Build / static runtime config for the virtual thread metric collection.
+ */
+@ConfigGroup
+public class VirtualThreadsConfigGroup implements MicrometerConfig.CapabilityEnabled {
+ /**
+ * Virtual Threads metrics support.
+ *
+ * Support for virtual threads metrics will be enabled if Micrometer support is enabled,
+ * this value is set to {@code true} (default), the JVM supports virtual threads (Java 21+) and the
+ * {@code quarkus.micrometer.binder-enabled-default} property is true.
+ */
+ @ConfigItem
+ public Optional enabled;
+ /**
+ * The tags to be added to the metrics.
+ * Empty by default.
+ * When set, tags are passed as: {@code key1=value1,key2=value2}.
+ */
+ @ConfigItem
+ public Optional> tags;
+
+ @Override
+ public Optional getEnabled() {
+ return enabled;
+ }
+}