diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/pom.xml b/observability-kit-tests/observability-kit-micrometer-spring-tests/pom.xml
new file mode 100644
index 0000000..3697d92
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/pom.xml
@@ -0,0 +1,168 @@
+
+
+ 4.0.0
+
+ com.vaadin
+ observability-kit-micrometer-test
+ 5.0-SNAPSHOT
+
+ observability-kit-spring-tests
+
+ war
+ Test Vaadin Observability Kit plain-Spring integration on a real Jetty deployment
+
+
+
+ com.vaadin
+ observability-kit-spring
+ ${project.version}
+
+
+ org.springframework
+ spring-webmvc
+
+
+ io.micrometer
+ micrometer-core
+
+
+ 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-spring
+ 5
+
+
+
+
+
+ start-jetty
+
+ start
+
+ pre-integration-test
+
+
+ stop-jetty
+
+ stop
+
+ post-integration-test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.6.3
+
+
+ enforce-banned-dependencies
+
+ enforce
+
+
+
+
+
+ org.springframework.boot
+
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/frontend/index.html b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/frontend/index.html
new file mode 100644
index 0000000..eb0c53b
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/frontend/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppConfiguration.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppConfiguration.java
new file mode 100644
index 0000000..10c6dbd
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * 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.spring.tests;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import com.vaadin.observability.spring.ObservabilityConfiguration;
+
+/**
+ * Plain Spring (non-Boot) configuration importing the observability-kit Spring
+ * wiring and providing a {@link MeterRegistry}. Component-scans the test
+ * package so views and the metrics servlet are picked up.
+ */
+@Configuration
+@ComponentScan
+@Import(ObservabilityConfiguration.class)
+public class AppConfiguration {
+
+ @Bean
+ public MeterRegistry meterRegistry() {
+ return new SimpleMeterRegistry();
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppWebAppInitializer.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppWebAppInitializer.java
new file mode 100644
index 0000000..a99ef7d
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/AppWebAppInitializer.java
@@ -0,0 +1,25 @@
+/**
+ * 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.spring.tests;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.flow.spring.VaadinMVCWebAppInitializer;
+
+/**
+ * Bootstraps the Spring MVC context with {@link AppConfiguration}.
+ */
+public class AppWebAppInitializer extends VaadinMVCWebAppInitializer {
+
+ @Override
+ protected Collection> getConfigurationClasses() {
+ return Collections.singletonList(AppConfiguration.class);
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/HelloView.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/HelloView.java
new file mode 100644
index 0000000..cd0bba8
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/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.spring.tests;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.Route;
+
+/**
+ * Landing view; navigating to it drives session/UI/navigation/request lifecycle
+ * through Flow so the micrometer binders fire.
+ */
+@Route("")
+public class HelloView extends Div {
+
+ public HelloView() {
+ Span greeting = new Span("Hello micrometer spring");
+ greeting.setId("greeting");
+ add(greeting);
+ }
+}
diff --git a/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/MetricsServlet.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/MetricsServlet.java
new file mode 100644
index 0000000..16d4125
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/main/java/com/vaadin/observability/spring/tests/MetricsServlet.java
@@ -0,0 +1,71 @@
+/**
+ * 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.spring.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 java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.Gauge;
+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 org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+/**
+ * Dumps the Spring-managed {@link MeterRegistry} as deterministic text the IT
+ * scrapes via HTTP.
+ */
+@WebServlet(urlPatterns = "/metrics", asyncSupported = false)
+public class MetricsServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException {
+ WebApplicationContext ctx = WebApplicationContextUtils
+ .getRequiredWebApplicationContext(getServletContext());
+ MeterRegistry registry = ctx.getBean(MeterRegistry.class);
+ resp.setContentType("text/plain; charset=utf-8");
+ try (PrintWriter writer = resp.getWriter()) {
+ registry.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(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-spring-tests/src/test/java/com/vaadin/observability/spring/tests/AbstractIT.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/test/java/com/vaadin/observability/spring/tests/AbstractIT.java
new file mode 100644
index 0000000..03fab4e
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/test/java/com/vaadin/observability/spring/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.spring.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 Spring 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-spring-tests/src/test/java/com/vaadin/observability/spring/tests/SpringMicrometerIT.java b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/test/java/com/vaadin/observability/spring/tests/SpringMicrometerIT.java
new file mode 100644
index 0000000..1b9f794
--- /dev/null
+++ b/observability-kit-tests/observability-kit-micrometer-spring-tests/src/test/java/com/vaadin/observability/spring/tests/SpringMicrometerIT.java
@@ -0,0 +1,90 @@
+/**
+ * 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.spring.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 plain-Spring Vaadin Flow page in Chrome and asserts the
+ * Spring-managed {@code MeterRegistry} (provided through
+ * {@link com.vaadin.observability.spring.ObservabilityConfiguration}) saw
+ * session/UI/request activity, scraped through a plain HTTP {@code GET
+ * /metrics}.
+ */
+public class SpringMicrometerIT extends AbstractIT {
+
+ @Override
+ protected String getTestPath() {
+ return "/";
+ }
+
+ @BrowserTest
+ public void viewLoadDrivesSessionAndUiMetrics() throws IOException {
+ SpanElement greeting = $(SpanElement.class).id("greeting");
+ Assertions.assertEquals("Hello micrometer spring", 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);
+ }
+
+ 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
index 62c31ab..f4b001b 100644
--- a/observability-kit-tests/pom.xml
+++ b/observability-kit-tests/pom.xml
@@ -13,6 +13,7 @@
observability-kit-micrometer-tests
+ observability-kit-micrometer-spring-tests