diff --git a/observability-kit-micrometer/pom.xml b/observability-kit-micrometer/pom.xml
index 68e09f6..45af2a5 100644
--- a/observability-kit-micrometer/pom.xml
+++ b/observability-kit-micrometer/pom.xml
@@ -21,6 +21,16 @@
com.vaadinflow-server
+
+
+ com.vaadin
+ vaadin-dev-server
+ true
+ io.micrometermicrometer-core
diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java
index 0a3696a..e6bfb7f 100644
--- a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java
+++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/MetricsServiceInitListener.java
@@ -14,6 +14,7 @@
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.ObservationRegistry;
+import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
@@ -155,7 +156,20 @@ public void serviceInit(ServiceInitEvent event) {
ObservationRegistry or = observationRegistry != null
? observationRegistry
: ObservabilityKit.getObservationRegistry();
+ // Record the bound registry so the dev-mode Copilot metrics panel can
+ // read the live meters regardless of deployment type.
+ ObservabilityKit.setActiveMeterRegistry(r);
bind(event, r, or, s);
+ DeploymentConfiguration configuration = event.getSource()
+ .getDeploymentConfiguration();
+ boolean productionMode = configuration != null
+ ? configuration.isProductionMode()
+ : true;
+ if (!productionMode) {
+ event.getSource()
+ .addUIInitListener(uiEvent -> ObservabilityDevToolsClient
+ .inject(uiEvent.getUI()));
+ }
}
void bind(ServiceInitEvent event, MeterRegistry registry,
diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityDevToolsClient.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityDevToolsClient.java
new file mode 100644
index 0000000..e624293
--- /dev/null
+++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityDevToolsClient.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2000-2026 Vaadin Ltd
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ * See for the full
+ * license.
+ */
+package com.vaadin.observability.micrometer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.slf4j.LoggerFactory;
+
+import com.vaadin.flow.component.ComponentUtil;
+import com.vaadin.flow.component.UI;
+import com.vaadin.flow.internal.StringUtil;
+
+/**
+ * Loads the in-browser Vaadin Copilot metrics panel. The panel registers itself
+ * with Copilot's plugin API and pulls metric snapshots from the server over the
+ * dev-tools websocket (see {@code ObservabilityDevToolsHandler}).
+ *
+ * Injected once per UI and only in development mode; in production Copilot and
+ * the dev-tools connection do not exist, so this is never called.
+ */
+final class ObservabilityDevToolsClient {
+
+ private static final String INIT_KEY = "vaadinObservabilityDevToolsInitialized";
+ private static final String CLIENT_RESOURCE = "META-INF/frontend/VaadinObservabilityDevTools.js";
+
+ private ObservabilityDevToolsClient() {
+ }
+
+ static void inject(UI ui) {
+ if (ui == null || ComponentUtil.getData(ui, INIT_KEY) != null) {
+ return;
+ }
+ ComponentUtil.setData(ui, INIT_KEY, Boolean.TRUE);
+ ClassLoader loader = ObservabilityDevToolsClient.class.getClassLoader();
+ try (InputStream in = loader.getResourceAsStream(CLIENT_RESOURCE)) {
+ if (in == null) {
+ LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
+ "observability-kit dev-tools resource not found: {}",
+ CLIENT_RESOURCE);
+ return;
+ }
+ String js = StringUtil.removeComments(
+ new String(in.readAllBytes(), StandardCharsets.UTF_8),
+ true);
+ ui.getPage().executeJs(js);
+ } catch (IOException e) {
+ LoggerFactory.getLogger(ObservabilityDevToolsClient.class).warn(
+ "Could not load observability-kit dev-tools panel code", e);
+ }
+ }
+}
diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityKit.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityKit.java
index 2e8c634..068b0d9 100644
--- a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityKit.java
+++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/ObservabilityKit.java
@@ -27,6 +27,15 @@ public final class ObservabilityKit {
private static final AtomicReference OBSERVATION_REGISTRY = new AtomicReference<>();
private static final AtomicReference SETTINGS = new AtomicReference<>();
+ /**
+ * The registry instrumentation was actually bound to, recorded at
+ * {@code serviceInit} time. Unlike {@link #METER_REGISTRY} (only populated
+ * by {@link #install} in standalone deployments) this is set for every
+ * deployment type, including Spring where the registry arrives via DI. Used
+ * by the dev-mode Copilot metrics panel to read the live meters.
+ */
+ private static final AtomicReference ACTIVE_METER_REGISTRY = new AtomicReference<>();
+
private ObservabilityKit() {
}
@@ -55,6 +64,25 @@ static MeterRegistry getMeterRegistry() {
return METER_REGISTRY.get();
}
+ /**
+ * Records the registry instrumentation was bound to. Called from
+ * {@code MetricsServiceInitListener} for all deployment types.
+ */
+ static void setActiveMeterRegistry(MeterRegistry registry) {
+ ACTIVE_METER_REGISTRY.set(registry);
+ }
+
+ /**
+ * The registry instrumentation is currently publishing to, or {@code null}
+ * if instrumentation has not been bound. Read by the dev-mode Copilot
+ * metrics panel.
+ *
+ * @return the active meter registry, or {@code null}
+ */
+ public static MeterRegistry getActiveMeterRegistry() {
+ return ACTIVE_METER_REGISTRY.get();
+ }
+
static ObservationRegistry getObservationRegistry() {
return OBSERVATION_REGISTRY.get();
}
@@ -68,5 +96,6 @@ static void reset() {
METER_REGISTRY.set(null);
OBSERVATION_REGISTRY.set(null);
SETTINGS.set(null);
+ ACTIVE_METER_REGISTRY.set(null);
}
}
diff --git a/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/devtools/ObservabilityDevToolsHandler.java b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/devtools/ObservabilityDevToolsHandler.java
new file mode 100644
index 0000000..30951a0
--- /dev/null
+++ b/observability-kit-micrometer/src/main/java/com/vaadin/observability/micrometer/devtools/ObservabilityDevToolsHandler.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright (C) 2000-2026 Vaadin Ltd
+ *
+ * This program is available under Vaadin Commercial License and Service Terms.
+ *
+ * See for the full
+ * license.
+ */
+package com.vaadin.observability.micrometer.devtools;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.DistributionSummary;
+import io.micrometer.core.instrument.FunctionCounter;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.Measurement;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Timer;
+import tools.jackson.databind.JsonNode;
+
+import com.vaadin.base.devserver.DevToolsInterface;
+import com.vaadin.base.devserver.DevToolsMessageHandler;
+import com.vaadin.observability.micrometer.ObservabilityKit;
+
+/**
+ * Dev-mode bridge between the live Micrometer {@link MeterRegistry} and the
+ * Vaadin Copilot metrics panel.
+ *
+ * Discovered via the Java {@link java.util.ServiceLoader} by Flow's dev-tools
+ * server (see {@code META-INF/services}). On request from the panel it
+ * snapshots every {@code vaadin.*} meter and sends it to the browser over the
+ * shared dev-tools websocket. This is a developer-only convenience view; it has
+ * no effect in production where the dev-tools connection does not exist.
+ */
+public class ObservabilityDevToolsHandler implements DevToolsMessageHandler {
+
+ static final String COMMAND_REFRESH = "observability-kit-refresh";
+ static final String COMMAND_METRICS = "observability-kit-metrics";
+
+ /** Only meters under this prefix are exposed to the panel. */
+ private static final String METER_PREFIX = "vaadin.";
+
+ @Override
+ public void handleConnect(DevToolsInterface devToolsInterface) {
+ // Push an initial snapshot; the panel also pulls on demand.
+ sendSnapshot(devToolsInterface);
+ }
+
+ @Override
+ public boolean handleMessage(String command, JsonNode data,
+ DevToolsInterface devToolsInterface) {
+ if (COMMAND_REFRESH.equals(command)) {
+ sendSnapshot(devToolsInterface);
+ return true;
+ }
+ return false;
+ }
+
+ private void sendSnapshot(DevToolsInterface devToolsInterface) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("timestamp", System.currentTimeMillis());
+ payload.put("meters", snapshot());
+ devToolsInterface.send(COMMAND_METRICS, payload);
+ }
+
+ private List