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