diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml
index 9aa0082..4252777 100644
--- a/.github/workflows/validation.yml
+++ b/.github/workflows/validation.yml
@@ -16,6 +16,13 @@ jobs:
name: Build and test
runs-on: ubuntu-latest
steps:
+ - name: Check secrets
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ run: |
+ [ -z "${{secrets.VAADIN_PRO_KEY}}" ] \
+ && echo "🚫 **VAADIN_PRO_KEY** is not defined, check that **${{github.repository}}** repo has a valid secret" \
+ | tee -a $GITHUB_STEP_SUMMARY && exit 1 || exit 0
+
- name: Checkout
uses: actions/checkout@v4
@@ -26,6 +33,12 @@ jobs:
java-version: '21'
cache: maven
+ - name: Set TB License
+ run: |
+ TB_LICENSE=${{secrets.VAADIN_PRO_KEY}}
+ mkdir -p ~/.vaadin/
+ echo '{"username":"'$(echo $TB_LICENSE | cut -d / -f1)'","proKey":"'$(echo $TB_LICENSE | cut -d / -f2)'"}' > ~/.vaadin/proKey
+
- name: Check formatting (Spotless)
run: mvn -B -ntp spotless:check
diff --git a/.gitignore b/.gitignore
index 2690486..9db6b66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ Thumbs.db
# Superpowers specs & plans (local only, not committed)
docs/superpowers/
+*.bundle
+**/src/main/frontend/generated
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/pom.xml b/observability-kit-tests/observability-kit-micrometer-tests/pom.xml
new file mode 100644
index 0000000..90a7e56
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ observability-kit-micrometer-test
+ 5.0-SNAPSHOT
+
+ observability-kit-micrometer-tests
+
+ war
+ Test Vaadin Micrometer integration on a real Jetty deployment
+
+
+
+ com.vaadin
+ observability-kit-micrometer
+ ${project.version}
+
+
+ com.vaadin
+ vaadin-testbench-core-junit5
+ test
+
+
+ com.vaadin
+ flow-server
+ ${flow.version}
+
+
+ com.vaadin
+ flow-client
+ ${flow.version}
+
+
+ com.vaadin
+ flow-html-components
+ ${flow.version}
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${servlet.api.version}
+ provided
+
+
+ com.vaadin
+ flow-html-components-testbench
+ ${flow.version}
+ test
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+ jetty:run
+
+
+
+ com.vaadin
+ flow-maven-plugin
+ ${flow.version}
+
+
+
+ prepare-frontend
+ build-frontend
+
+ compile
+
+
+
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-maven-plugin
+ 12.1.8
+
+
+
+ 8089
+ observability-kit-micrometer
+ 5
+
+
+
+
+
+ start-jetty
+
+ start
+
+ pre-integration-test
+
+
+ stop-jetty
+
+ stop
+
+ post-integration-test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/frontend/index.html b/observability-kit-tests/observability-kit-micrometer-tests/src/main/frontend/index.html
new file mode 100644
index 0000000..eb0c53b
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/frontend/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/AppServlet.java b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/AppServlet.java
new file mode 100644
index 0000000..c24c770
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/AppServlet.java
@@ -0,0 +1,23 @@
+/**
+ * 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.tests;
+
+import jakarta.servlet.annotation.WebServlet;
+
+import com.vaadin.flow.server.VaadinServlet;
+
+/**
+ * Explicit {@link VaadinServlet} declaration. Required here because the WAR
+ * also declares {@link MetricsServlet}, which suppresses Vaadin's automatic
+ * servlet registration. The more-specific {@code /metrics} mapping still wins
+ * over this catch-all for that path.
+ */
+@WebServlet(asyncSupported = true, urlPatterns = { "/*" })
+public class AppServlet extends VaadinServlet {
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/HelloView.java b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/HelloView.java
new file mode 100644
index 0000000..4666140
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/HelloView.java
@@ -0,0 +1,27 @@
+/**
+ * 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.tests;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.Route;
+
+/**
+ * Simple landing view; opening it drives a real Vaadin session, UI and
+ * navigation through the framework so the micrometer binders fire.
+ */
+@Route("")
+public class HelloView extends Div {
+
+ public HelloView() {
+ Span greeting = new Span("Hello micrometer");
+ greeting.setId("greeting");
+ add(greeting);
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MetricsServlet.java b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MetricsServlet.java
new file mode 100644
index 0000000..6a0e832
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MetricsServlet.java
@@ -0,0 +1,64 @@
+/**
+ * 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.tests;
+
+import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Timer;
+
+/**
+ * Dumps the shared {@link MicrometerRegistry} as a deterministic, sorted
+ * plain-text format that the IT can scrape via HTTP.
+ */
+@WebServlet(urlPatterns = "/metrics", asyncSupported = false)
+public class MetricsServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ resp.setContentType("text/plain; charset=utf-8");
+ try (PrintWriter writer = resp.getWriter()) {
+ MicrometerRegistry.INSTANCE.getMeters().stream()
+ .sorted((a, b) -> a.getId().getName()
+ .compareTo(b.getId().getName()))
+ .forEach(meter -> writeMeter(writer, meter));
+ }
+ }
+
+ private void writeMeter(PrintWriter writer, Meter meter) {
+ String id = formatId(meter);
+ if (meter instanceof Counter c) {
+ writer.printf("%s count=%.0f%n", id, c.count());
+ } else if (meter instanceof Gauge g) {
+ writer.printf("%s value=%.0f%n", id, g.value());
+ } else if (meter instanceof Timer t) {
+ writer.printf("%s count=%d total_ms=%.3f%n", id, t.count(),
+ t.totalTime(java.util.concurrent.TimeUnit.MILLISECONDS));
+ }
+ }
+
+ private String formatId(Meter meter) {
+ StringBuilder sb = new StringBuilder(meter.getId().getName());
+ for (Tag tag : meter.getId().getTagsAsIterable()) {
+ sb.append(' ').append(tag.getKey()).append('=')
+ .append(tag.getValue());
+ }
+ return sb.toString();
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerRegistry.java b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerRegistry.java
new file mode 100644
index 0000000..e115078
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerRegistry.java
@@ -0,0 +1,24 @@
+/**
+ * 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.tests;
+
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+
+/**
+ * Static holder so the Vaadin micrometer binders and the {@link MetricsServlet}
+ * share the same {@link SimpleMeterRegistry} for the duration of the
+ * deployment.
+ */
+public final class MicrometerRegistry {
+
+ static final SimpleMeterRegistry INSTANCE = new SimpleMeterRegistry();
+
+ private MicrometerRegistry() {
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerSetup.java b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerSetup.java
new file mode 100644
index 0000000..adb77ec
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerSetup.java
@@ -0,0 +1,31 @@
+/**
+ * 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.tests;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+
+import com.vaadin.observability.micrometer.ObservabilityKit;
+import com.vaadin.observability.micrometer.ObservabilitySettings;
+
+/**
+ * Boots observability-kit-micrometer at servlet-context startup so the
+ * SPI-loaded {@code MetricsServiceInitListener} can pick up the registry when
+ * Vaadin's service initializes.
+ */
+@WebListener
+public class MicrometerSetup implements ServletContextListener {
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ ObservabilityKit.install(MicrometerRegistry.INSTANCE,
+ ObservabilitySettings.builder().build());
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/AbstractIT.java b/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/AbstractIT.java
new file mode 100644
index 0000000..ddaaa9f
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/AbstractIT.java
@@ -0,0 +1,133 @@
+/**
+ * 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.tests;
+
+import java.lang.management.ManagementFactory;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeDriverService;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.net.PortProber;
+import org.slf4j.LoggerFactory;
+
+import com.vaadin.testbench.BrowserTestBase;
+import com.vaadin.testbench.DriverSupplier;
+import com.vaadin.testbench.IPAddress;
+import com.vaadin.testbench.Parameters;
+import com.vaadin.testbench.TestBench;
+
+/**
+ * Base class for the Micrometer integration tests. Spins up a headless Chrome
+ * driver (or connects to a hub when configured) and navigates to the view under
+ * test before each test method.
+ */
+@Execution(ExecutionMode.SAME_THREAD)
+abstract class AbstractIT extends BrowserTestBase implements DriverSupplier {
+
+ static final int SERVER_PORT = Integer.getInteger("serverPort", 8080);
+
+ static String hostName;
+
+ static boolean isHub;
+
+ @BeforeAll
+ public static void setupClass() {
+ String hubHost = Parameters.getHubHostname();
+ isHub = hubHost != null && !hubHost.isEmpty();
+ hostName = isHub ? IPAddress.findSiteLocalAddress() : "localhost";
+ }
+
+ @BeforeEach
+ public void setup() {
+ getDriver().get(getRootURL() + getTestPath());
+ }
+
+ /**
+ * Gets the absolute path to the test, starting with a "/".
+ *
+ * @return the path to the test, appended to {@link #getRootURL()} for the
+ * full test URL.
+ */
+ protected abstract String getTestPath();
+
+ /**
+ * Returns the URL to the root of the server, e.g. "http://localhost:8080".
+ *
+ * @return the URL to the root
+ */
+ protected String getRootURL() {
+ return "http://" + getDeploymentHostname() + ":" + getDeploymentPort();
+ }
+
+ /**
+ * Used to determine what port the test is running on.
+ *
+ * @return the port the test is running on, by default 8080
+ */
+ protected int getDeploymentPort() {
+ return SERVER_PORT;
+ }
+
+ /**
+ * Used to determine what host the test is running on.
+ *
+ * @return the host name of the deployment
+ */
+ protected String getDeploymentHostname() {
+ return hostName;
+ }
+
+ @Override
+ public WebDriver createDriver() {
+ if (!isJavaInDebugMode() && !isHub) {
+ return createHeadlessChromeDriver();
+ }
+ // Let the super class create the driver (e.g. against a hub).
+ return null;
+ }
+
+ private WebDriver createHeadlessChromeDriver() {
+ for (int i = 0; i < 3; i++) {
+ try {
+ return tryCreateHeadlessChromeDriver();
+ } catch (Exception e) {
+ LoggerFactory.getLogger(getClass()).warn(
+ "Unable to create chromedriver on attempt " + i, e);
+ }
+ }
+ throw new RuntimeException(
+ "Gave up trying to create a chromedriver instance");
+ }
+
+ private static WebDriver tryCreateHeadlessChromeDriver() {
+ ChromeOptions headlessOptions = createHeadlessChromeOptions();
+
+ int port = PortProber.findFreePort();
+ ChromeDriverService service = new ChromeDriverService.Builder()
+ .usingPort(port).withSilent(true).build();
+ ChromeDriver chromeDriver = new ChromeDriver(service, headlessOptions);
+ return TestBench.createDriver(chromeDriver);
+ }
+
+ static ChromeOptions createHeadlessChromeOptions() {
+ final ChromeOptions options = new ChromeOptions();
+ options.addArguments("--headless", "--disable-gpu");
+ return options;
+ }
+
+ static boolean isJavaInDebugMode() {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments()
+ .toString().contains("jdwp");
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/StandaloneMetricsIT.java b/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/StandaloneMetricsIT.java
new file mode 100644
index 0000000..1410e1c
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/StandaloneMetricsIT.java
@@ -0,0 +1,129 @@
+/**
+ * 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.tests;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Assertions;
+
+import com.vaadin.flow.component.html.testbench.SpanElement;
+import com.vaadin.testbench.BrowserTest;
+
+/**
+ * Drives a real Vaadin Flow page in Chrome and asserts that the
+ * vaadin-micrometer binders moved the corresponding meters, scraped through a
+ * plain HTTP {@code GET /metrics}.
+ */
+public class StandaloneMetricsIT extends AbstractIT {
+
+ @Override
+ protected String getTestPath() {
+ return "/";
+ }
+
+ @BrowserTest
+ public void viewLoadDrivesSessionAndUiMetrics() throws IOException {
+ SpanElement greeting = $(SpanElement.class).id("greeting");
+ Assertions.assertEquals("Hello micrometer", greeting.getText());
+
+ String metrics = fetchMetrics();
+
+ Assertions.assertTrue(
+ meterValue(metrics, "vaadin.sessions.created", "count") >= 1.0,
+ "expected vaadin.sessions.created counter > 0, got:\n"
+ + metrics);
+ Assertions.assertTrue(
+ meterValue(metrics, "vaadin.ui.created", "count") >= 1.0,
+ "expected vaadin.ui.created counter > 0, got:\n" + metrics);
+ Assertions.assertTrue(
+ meterValue(metrics, "vaadin.sessions.active", "value") >= 1.0,
+ "expected vaadin.sessions.active gauge > 0, got:\n" + metrics);
+ Assertions.assertTrue(metrics.contains("vaadin.request.duration"),
+ "expected at least one vaadin.request.duration sample, got:\n"
+ + metrics);
+ }
+
+ @BrowserTest
+ public void clientMetricsArriveViaUidlAfterFlush() throws IOException {
+ // Wait until the view rendered, ensuring the collector is attached
+ // and the client script has installed itself.
+ $(SpanElement.class).id("greeting");
+
+ // Force an immediate flush of whatever the client buffered so the
+ // IT does not have to wait for the 5 s periodic timer. window.
+ // __vaadinMicrometer.flush is exposed by VaadinMetricsClient.js
+ // explicitly for tests.
+ executeScript("window.__vaadinMicrometer && window."
+ + "__vaadinMicrometer.flush();");
+
+ // Poll the /metrics endpoint until at least one vaadin.client.*
+ // sample appears. We give the server up to ~5 s to ingest, then
+ // fail if the collector never delivered anything.
+ String metrics = pollUntilClientMetrics();
+
+ Assertions.assertTrue(metrics.contains("vaadin.client."),
+ "expected at least one vaadin.client.* meter in registry, "
+ + "got:\n" + metrics);
+ }
+
+ private String pollUntilClientMetrics() throws IOException {
+ long deadline = System.currentTimeMillis() + 5_000L;
+ String last = "";
+ while (System.currentTimeMillis() < deadline) {
+ last = fetchMetrics();
+ if (last.contains("vaadin.client.")) {
+ return last;
+ }
+ try {
+ Thread.sleep(250L);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ return last;
+ }
+
+ private String fetchMetrics() throws IOException {
+ HttpURLConnection conn = (HttpURLConnection) URI
+ .create(getRootURL() + "/metrics").toURL().openConnection();
+ conn.setRequestMethod("GET");
+ Assertions.assertEquals(200, conn.getResponseCode());
+ StringBuilder out = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ conn.getInputStream(), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ out.append(line).append('\n');
+ }
+ }
+ return out.toString();
+ }
+
+ /**
+ * Finds the numeric value of {@code field} on the first line that starts
+ * with {@code meterName}. Returns {@code -1.0} if not present.
+ */
+ private static double meterValue(String metricsBody, String meterName,
+ String field) {
+ Pattern pattern = Pattern.compile(
+ "^" + Pattern.quote(meterName) + "(?:\\s.*)?\\s"
+ + Pattern.quote(field) + "=([0-9]+(?:\\.[0-9]+)?)",
+ Pattern.MULTILINE);
+ Matcher m = pattern.matcher(metricsBody);
+ return m.find() ? Double.parseDouble(m.group(1)) : -1.0;
+ }
+}
diff --git a/observability-kit-tests/pom.xml b/observability-kit-tests/pom.xml
new file mode 100644
index 0000000..62c31ab
--- /dev/null
+++ b/observability-kit-tests/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ observability-kit
+ 5.0-SNAPSHOT
+
+ observability-kit-micrometer-test
+
+ pom
+ Integration tests for Observability-kit Micrometer integration
+
+
+ observability-kit-micrometer-tests
+
+
+
+
+
+ com.vaadin
+ vaadin-testbench-bom
+ ${testbench.version}
+ import
+ pom
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 64bfcee..b3c95f9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
5.11.4
5.20.0
4.0.5
+ 10.2-SNAPSHOT
3.0.1
21
@@ -223,4 +224,17 @@
+
+
+ it-tests
+
+
+ !skipIt
+
+
+
+ observability-kit-tests
+
+
+