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 + + +