diff --git a/.gitignore b/.gitignore index 9db6b66..a23e096 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Thumbs.db docs/superpowers/ *.bundle **/src/main/frontend/generated + +# Claude Code tooling (worktrees, local settings) +.claude/ diff --git a/observability-kit-spring/src/main/java/com/vaadin/observability/spring/ObservabilityConfiguration.java b/observability-kit-spring/src/main/java/com/vaadin/observability/spring/ObservabilityConfiguration.java index 36a04d6..ef8fb4f 100644 --- a/observability-kit-spring/src/main/java/com/vaadin/observability/spring/ObservabilityConfiguration.java +++ b/observability-kit-spring/src/main/java/com/vaadin/observability/spring/ObservabilityConfiguration.java @@ -15,7 +15,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.vaadin.flow.server.VaadinRequest; import com.vaadin.observability.micrometer.MetricsServiceInitListener; import com.vaadin.observability.micrometer.ObservabilitySettings; @@ -78,7 +77,7 @@ public class ObservabilityConfiguration { @Value("${vaadin.observability.requests:true}") boolean requests, @Value("${vaadin.observability.errors:true}") boolean errors, @Value("${vaadin.observability.traces:true}") boolean traces, - @Value("${vaadin.observability.traces.session-id:false}") boolean tracesSessionId, + @Value("${vaadin.observability.traces-session-id:false}") boolean tracesSessionId, @Value("${vaadin.observability.route-cardinality-limit:200}") int routeCardinalityLimit, @Value("${vaadin.observability.client:true}") boolean client, @Value("${vaadin.observability.client-rate-per-session:100}") int clientRatePerSession) { @@ -119,38 +118,4 @@ MetricsServiceInitListener metricsServiceInitListener( observationRegistry.getIfAvailable(), settings); } - /** - * Spring-aware subclass that skips the default Observation handler - * registration: in Spring/Boot setups the framework already registers a - * {@code DefaultMeterObservationHandler} on the shared - * {@link ObservationRegistry} (via Boot's - * {@code ObservationAutoConfiguration} or the user's own - * {@code @Configuration}), so re-registering here would double-emit Timers. - * It also delegates HTTP observation enrichment to - * {@link SpringHttpObservationEnricher}. - */ - static class SpringMetricsServiceInitListener - extends MetricsServiceInitListener { - - SpringMetricsServiceInitListener(MeterRegistry registry, - ObservationRegistry observationRegistry, - ObservabilitySettings settings) { - super(registry, observationRegistry, settings); - } - - @Override - protected void installDefaultObservationHandlers( - ObservationRegistry observationRegistry, - MeterRegistry registry) { - // No-op: Spring Boot Actuator's ObservationAutoConfiguration - // registers DefaultMeterObservationHandler; re-registering would - // double-emit Timers. - } - - @Override - protected void enrichHttpObservation(VaadinRequest request, - String requestType) { - SpringHttpObservationEnricher.enrich(request, requestType); - } - } } diff --git a/observability-kit-spring/src/main/java/com/vaadin/observability/spring/SpringMetricsServiceInitListener.java b/observability-kit-spring/src/main/java/com/vaadin/observability/spring/SpringMetricsServiceInitListener.java new file mode 100644 index 0000000..a20d0ea --- /dev/null +++ b/observability-kit-spring/src/main/java/com/vaadin/observability/spring/SpringMetricsServiceInitListener.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.observability.spring; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; + +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.observability.micrometer.MetricsServiceInitListener; +import com.vaadin.observability.micrometer.ObservabilitySettings; + +/** + * Spring/Boot-aware {@link MetricsServiceInitListener} that skips the default + * Observation handler registration and enriches the HTTP server observation. + *

+ * In Spring/Boot setups the framework already registers a + * {@code DefaultMeterObservationHandler} on the shared + * {@link ObservationRegistry} (via Boot's {@code ObservationAutoConfiguration} + * or the user's own {@code @Configuration}), so re-registering here would + * double-emit Timers. HTTP observation enrichment is delegated to + * {@link SpringHttpObservationEnricher}, making the parent HTTP span render as + * e.g. {@code http post /vaadin/uidl} instead of the generic + * {@code http post /**}. + *

+ * This class is declared {@code public} so it can be reused by both + * {@link ObservabilityConfiguration} (plain-Spring import) and the Boot + * auto-configuration starter. + */ +public final class SpringMetricsServiceInitListener + extends MetricsServiceInitListener { + + /** + * Creates a new listener. + * + * @param registry + * the Micrometer meter registry, must not be {@code null} + * @param observationRegistry + * the Micrometer observation registry; may be {@code null} when + * no {@link ObservationRegistry} bean is present — traces will + * be skipped in that case + * @param settings + * the observability settings, must not be {@code null} + */ + public SpringMetricsServiceInitListener(MeterRegistry registry, + ObservationRegistry observationRegistry, + ObservabilitySettings settings) { + super(registry, observationRegistry, settings); + } + + @Override + protected void installDefaultObservationHandlers( + ObservationRegistry observationRegistry, MeterRegistry registry) { + // No-op: Spring Boot Actuator's ObservationAutoConfiguration + // registers DefaultMeterObservationHandler; re-registering would + // double-emit Timers. + } + + @Override + protected void enrichHttpObservation(VaadinRequest request, + String requestType) { + SpringHttpObservationEnricher.enrich(request, requestType); + } +} diff --git a/observability-kit-starter/pom.xml b/observability-kit-starter/pom.xml new file mode 100644 index 0000000..cac9253 --- /dev/null +++ b/observability-kit-starter/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + + com.vaadin + observability-kit + 5.0-SNAPSHOT + + + observability-kit-starter + jar + Observability Kit :: Spring Boot Starter + Spring Boot auto-configuration for Observability Kit. + + + + com.vaadin + observability-kit-micrometer + ${project.version} + + + com.vaadin + observability-kit-spring + ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-micrometer-metrics + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfiguration.java b/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfiguration.java new file mode 100644 index 0000000..fb450ef --- /dev/null +++ b/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfiguration.java @@ -0,0 +1,69 @@ +/** + * 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.boot; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.context.annotation.Bean; + +import com.vaadin.flow.server.VaadinService; +import com.vaadin.observability.micrometer.MetricsServiceInitListener; +import com.vaadin.observability.micrometer.ObservabilitySettings; +import com.vaadin.observability.spring.SpringMetricsServiceInitListener; + +/** + * Auto-configures the Observability Kit {@link MetricsServiceInitListener} when + * a {@link MeterRegistry} is present in the Spring context. + *

+ * The starter pulls in Boot's Micrometer metrics auto-configuration, so a + * {@link MeterRegistry} is wired out of the box and this listener activates + * without further setup; the application only chooses the registry backend + * (Prometheus, OTLP, ...) and whether to expose Actuator endpoints. Ordered + * after {@link MetricsAutoConfiguration} and + * {@link CompositeMeterRegistryAutoConfiguration} so that registry is visible + * when this runs. + *

+ * Activation is gated by the {@code vaadin.observability.enabled} property + * (default {@code true}); the listener is also skipped when the user supplies + * their own {@link MetricsServiceInitListener} bean. + */ +@AutoConfiguration(after = { MetricsAutoConfiguration.class, + CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass({ MeterRegistry.class, VaadinService.class }) +@ConditionalOnProperty(prefix = "vaadin.observability", name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(ObservabilityProperties.class) +public class ObservabilityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + ObservabilitySettings observabilitySettings( + ObservabilityProperties properties) { + return properties.toSettings(); + } + + @Bean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnMissingBean(MetricsServiceInitListener.class) + MetricsServiceInitListener metricsServiceInitListener( + MeterRegistry registry, + ObjectProvider observationRegistry, + ObservabilitySettings settings) { + return new SpringMetricsServiceInitListener(registry, + observationRegistry.getIfAvailable(), settings); + } +} diff --git a/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityProperties.java b/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityProperties.java new file mode 100644 index 0000000..7b556d2 --- /dev/null +++ b/observability-kit-starter/src/main/java/com/vaadin/observability/spring/boot/ObservabilityProperties.java @@ -0,0 +1,138 @@ +/** + * 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.boot; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.vaadin.observability.micrometer.ObservabilitySettings; + +/** + * Boot-bound configuration properties under the {@code vaadin.observability} + * prefix. Converted to a plain {@link ObservabilitySettings} via + * {@link #toSettings()}. + */ +@ConfigurationProperties(prefix = "vaadin.observability") +public class ObservabilityProperties { + + private boolean enabled = true; + private boolean sessions = true; + private boolean uis = true; + private boolean navigation = true; + private boolean requests = true; + private boolean errors = true; + private boolean client = true; + private boolean traces = true; + private boolean tracesSessionId = false; + private int routeCardinalityLimit = 200; + private int clientRatePerSession = 100; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isSessions() { + return sessions; + } + + public void setSessions(boolean sessions) { + this.sessions = sessions; + } + + public boolean isUis() { + return uis; + } + + public void setUis(boolean uis) { + this.uis = uis; + } + + public boolean isNavigation() { + return navigation; + } + + public void setNavigation(boolean navigation) { + this.navigation = navigation; + } + + public boolean isRequests() { + return requests; + } + + public void setRequests(boolean requests) { + this.requests = requests; + } + + public boolean isErrors() { + return errors; + } + + public void setErrors(boolean errors) { + this.errors = errors; + } + + public boolean isClient() { + return client; + } + + public void setClient(boolean client) { + this.client = client; + } + + public boolean isTraces() { + return traces; + } + + public void setTraces(boolean traces) { + this.traces = traces; + } + + public boolean isTracesSessionId() { + return tracesSessionId; + } + + public void setTracesSessionId(boolean tracesSessionId) { + this.tracesSessionId = tracesSessionId; + } + + public int getRouteCardinalityLimit() { + return routeCardinalityLimit; + } + + public void setRouteCardinalityLimit(int routeCardinalityLimit) { + this.routeCardinalityLimit = routeCardinalityLimit; + } + + public int getClientRatePerSession() { + return clientRatePerSession; + } + + public void setClientRatePerSession(int clientRatePerSession) { + this.clientRatePerSession = clientRatePerSession; + } + + /** + * Converts these properties to an {@link ObservabilitySettings} instance. + * The {@code enabled} flag is not included in settings; it only gates + * activation of the auto-configuration. + * + * @return a new {@link ObservabilitySettings} built from the current + * property values + */ + public ObservabilitySettings toSettings() { + return ObservabilitySettings.builder().sessions(sessions).uis(uis) + .navigation(navigation).requests(requests).errors(errors) + .client(client).traces(traces).tracesSessionId(tracesSessionId) + .routeCardinalityLimit(routeCardinalityLimit) + .clientRatePerSession(clientRatePerSession).build(); + } +} diff --git a/observability-kit-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/observability-kit-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4087369 --- /dev/null +++ b/observability-kit-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.vaadin.observability.spring.boot.ObservabilityAutoConfiguration diff --git a/observability-kit-starter/src/test/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfigurationTest.java b/observability-kit-starter/src/test/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfigurationTest.java new file mode 100644 index 0000000..53a6792 --- /dev/null +++ b/observability-kit-starter/src/test/java/com/vaadin/observability/spring/boot/ObservabilityAutoConfigurationTest.java @@ -0,0 +1,163 @@ +/** + * 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.boot; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import com.vaadin.observability.micrometer.MetricsServiceInitListener; +import com.vaadin.observability.micrometer.ObservabilitySettings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ObservabilityAutoConfiguration} using + * {@link ApplicationContextRunner}. + */ +class ObservabilityAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(ObservabilityAutoConfiguration.class)); + + /** + * Default context with a MeterRegistry bean: both ObservabilitySettings and + * MetricsServiceInitListener should be present. + */ + @Test + void defaultConfiguration_withMeterRegistry_registersBeansExpected() { + contextRunner + .withBean(SimpleMeterRegistry.class, SimpleMeterRegistry::new) + .run(context -> { + assertThat(context) + .hasSingleBean(ObservabilitySettings.class); + assertThat(context) + .hasSingleBean(MetricsServiceInitListener.class); + }); + } + + /** + * Real-starter behavior: with Boot's Micrometer metrics auto-configuration + * on the classpath (as the starter pulls it in) and no manually supplied + * registry, a {@link MeterRegistry} is wired out of the box and our + * listener activates on top of it. + */ + @Test + void metricsAutoConfigurationPresent_wiresRegistryAndListener() { + contextRunner + .withConfiguration( + AutoConfigurations.of(MetricsAutoConfiguration.class, + CompositeMeterRegistryAutoConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(MeterRegistry.class); + assertThat(context) + .hasSingleBean(MetricsServiceInitListener.class); + }); + } + + /** + * When vaadin.observability.enabled=false, the auto-configuration should + * not activate and no beans should be registered. + */ + @Test + void disabledProperty_doesNotRegisterBeans() { + contextRunner.withPropertyValues("vaadin.observability.enabled=false") + .run(context -> { + assertThat(context) + .doesNotHaveBean(MetricsServiceInitListener.class); + assertThat(context) + .doesNotHaveBean(ObservabilitySettings.class); + }); + } + + /** + * Property binding: sessions=false and route-cardinality-limit=42 should be + * reflected in the ObservabilitySettings bean. + */ + @Test + void propertyBinding_reflectedInSettings() { + contextRunner + .withBean(SimpleMeterRegistry.class, SimpleMeterRegistry::new) + .withPropertyValues("vaadin.observability.sessions=false", + "vaadin.observability.route-cardinality-limit=42") + .run(context -> { + assertThat(context) + .hasSingleBean(ObservabilitySettings.class); + ObservabilitySettings settings = context + .getBean(ObservabilitySettings.class); + assertThat(settings.isSessions()).isFalse(); + assertThat(settings.getRouteCardinalityLimit()) + .isEqualTo(42); + }); + } + + /** + * Without a MeterRegistry bean, the MetricsServiceInitListener should not + * be registered (gated by @ConditionalOnBean(MeterRegistry.class)). The + * ObservabilitySettings bean is NOT gated on a MeterRegistry, so it must + * still be present. + */ + @Test + void noMeterRegistry_doesNotRegisterListener() { + contextRunner.run(context -> { + assertThat(context) + .doesNotHaveBean(MetricsServiceInitListener.class); + assertThat(context).hasSingleBean(ObservabilitySettings.class); + }); + } + + /** + * User-supplied ObservabilitySettings bean: our auto-configured settings + * bean should back off (@ConditionalOnMissingBean), and the custom bean + * should be used. + */ + @Test + void userSuppliedSettings_autoConfigBacksOff() { + contextRunner + .withBean(SimpleMeterRegistry.class, + SimpleMeterRegistry::new) + .withBean(ObservabilitySettings.class, + () -> ObservabilitySettings.builder().sessions(false) + .build()) + .run(context -> { + assertThat(context) + .hasSingleBean(ObservabilitySettings.class); + assertThat(context.getBean(ObservabilitySettings.class) + .isSessions()).isFalse(); + }); + } + + /** + * User-supplied MetricsServiceInitListener bean: our auto-configured + * listener should back off (@ConditionalOnMissingBean), and the custom bean + * should be used. + */ + @Test + void userSuppliedListener_autoConfigBacksOff() { + MetricsServiceInitListener customListener = new MetricsServiceInitListener( + new SimpleMeterRegistry(), + ObservabilitySettings.builder().build()); + contextRunner + .withBean("custom", MetricsServiceInitListener.class, + () -> customListener) + .withBean(SimpleMeterRegistry.class, SimpleMeterRegistry::new) + .run(context -> { + assertThat(context) + .hasSingleBean(MetricsServiceInitListener.class); + assertThat( + context.getBean(MetricsServiceInitListener.class)) + .isSameAs(customListener); + }); + } +} diff --git a/observability-kit-tests/observability-kit-tests-common/pom.xml b/observability-kit-tests/observability-kit-tests-common/pom.xml new file mode 100644 index 0000000..f52dc27 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-common/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + com.vaadin + observability-kit-tests + 5.0-SNAPSHOT + + observability-kit-tests-common + + jar + Shared test base for Observability-kit integration tests + + + + com.vaadin + vaadin-testbench-core-junit5 + + + org.seleniumhq.selenium + selenium-java + + + org.junit.jupiter + junit-jupiter + + + + 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-tests-common/src/main/java/com/vaadin/observability/tests/common/AbstractIT.java similarity index 92% rename from observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/AbstractIT.java rename to observability-kit-tests/observability-kit-tests-common/src/main/java/com/vaadin/observability/tests/common/AbstractIT.java index ddaaa9f..6fb97c0 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/AbstractIT.java +++ b/observability-kit-tests/observability-kit-tests-common/src/main/java/com/vaadin/observability/tests/common/AbstractIT.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.common; import java.lang.management.ManagementFactory; @@ -28,12 +28,13 @@ 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. + * Base class for Observability Kit 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 { +public abstract class AbstractIT extends BrowserTestBase + implements DriverSupplier { static final int SERVER_PORT = Integer.getInteger("serverPort", 8080); diff --git a/observability-kit-tests/observability-kit-micrometer-tests/pom.xml b/observability-kit-tests/observability-kit-tests-micrometer/pom.xml similarity index 93% rename from observability-kit-tests/observability-kit-micrometer-tests/pom.xml rename to observability-kit-tests/observability-kit-tests-micrometer/pom.xml index 90a7e56..816302e 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/pom.xml +++ b/observability-kit-tests/observability-kit-tests-micrometer/pom.xml @@ -3,10 +3,10 @@ 4.0.0 com.vaadin - observability-kit-micrometer-test + observability-kit-tests 5.0-SNAPSHOT - observability-kit-micrometer-tests + observability-kit-tests-micrometer war Test Vaadin Micrometer integration on a real Jetty deployment @@ -17,6 +17,12 @@ observability-kit-micrometer ${project.version} + + com.vaadin + observability-kit-tests-common + ${project.version} + test + com.vaadin vaadin-testbench-core-junit5 diff --git a/observability-kit-tests/observability-kit-micrometer-tests/src/main/frontend/index.html b/observability-kit-tests/observability-kit-tests-micrometer/src/main/frontend/index.html similarity index 100% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/frontend/index.html rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/frontend/index.html 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-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/AppServlet.java similarity index 93% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/AppServlet.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/AppServlet.java index c24c770..e945af0 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/AppServlet.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/AppServlet.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import jakarta.servlet.annotation.WebServlet; 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-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/HelloView.java similarity index 93% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/HelloView.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/HelloView.java index 4666140..065cd89 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/HelloView.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/HelloView.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; 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-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MetricsServlet.java similarity index 97% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MetricsServlet.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MetricsServlet.java index 6a0e832..4893b28 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MetricsServlet.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MetricsServlet.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; 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-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerRegistry.java similarity index 92% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerRegistry.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerRegistry.java index e115078..2730dec 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerRegistry.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerRegistry.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 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-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerSetup.java similarity index 94% rename from observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerSetup.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerSetup.java index adb77ec..b9bda05 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/main/java/com/vaadin/observability/micrometer/tests/MicrometerSetup.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/main/java/com/vaadin/observability/tests/micrometer/MicrometerSetup.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; 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-tests-micrometer/src/test/java/com/vaadin/observability/tests/micrometer/StandaloneMetricsIT.java similarity index 97% rename from observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/StandaloneMetricsIT.java rename to observability-kit-tests/observability-kit-tests-micrometer/src/test/java/com/vaadin/observability/tests/micrometer/StandaloneMetricsIT.java index 1410e1c..2761704 100644 --- a/observability-kit-tests/observability-kit-micrometer-tests/src/test/java/com/vaadin/observability/micrometer/tests/StandaloneMetricsIT.java +++ b/observability-kit-tests/observability-kit-tests-micrometer/src/test/java/com/vaadin/observability/tests/micrometer/StandaloneMetricsIT.java @@ -6,7 +6,7 @@ * See for the full * license. */ -package com.vaadin.observability.micrometer.tests; +package com.vaadin.observability.tests.micrometer; import java.io.BufferedReader; import java.io.IOException; @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Assertions; import com.vaadin.flow.component.html.testbench.SpanElement; +import com.vaadin.observability.tests.common.AbstractIT; import com.vaadin.testbench.BrowserTest; /** diff --git a/observability-kit-tests/observability-kit-tests-spring/pom.xml b/observability-kit-tests/observability-kit-tests-spring/pom.xml new file mode 100644 index 0000000..6d5cf62 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + com.vaadin + observability-kit-tests + 5.0-SNAPSHOT + + observability-kit-tests-spring + + war + Test Vaadin Observability Kit on a plain-Spring (non-Boot) Jetty deployment + + Integration test that wires observability-kit-spring into a plain Spring MVC WAR + (no Spring Boot) and verifies session / UI / request metrics are recorded. + + + + + + com.vaadin + observability-kit-spring + ${project.version} + + + + org.springframework + spring-webmvc + + + + io.micrometer + micrometer-core + + + + 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 + observability-kit-tests-common + ${project.version} + test + + + com.vaadin + vaadin-testbench-core-junit5 + test + + + com.vaadin + flow-html-components-testbench + ${flow.version} + test + + + org.seleniumhq.selenium + selenium-java + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + 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 + + + 8090 + 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-no-spring-boot + + enforce + + validate + + + + + + org.springframework.boot + + + + true + + + + + + + + diff --git a/observability-kit-tests/observability-kit-tests-spring/src/main/frontend/index.html b/observability-kit-tests/observability-kit-tests-spring/src/main/frontend/index.html new file mode 100644 index 0000000..eb0c53b --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +

+ + diff --git a/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/AppConfiguration.java b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/AppConfiguration.java new file mode 100644 index 0000000..119fe1b --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/AppConfiguration.java @@ -0,0 +1,35 @@ +/** + * 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.tests.spring; + +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 that imports the Observability Kit + * Spring wiring and provides a {@link MeterRegistry}. Component-scans the test + * package for Spring-managed beans; the {@code @WebServlet} metrics servlet is + * registered by the servlet container, not by Spring. + */ +@Configuration +@ComponentScan +@Import(ObservabilityConfiguration.class) +public class AppConfiguration { + + @Bean + public MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } +} diff --git a/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/AppWebAppInitializer.java b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/AppWebAppInitializer.java new file mode 100644 index 0000000..e5e06de --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/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.tests.spring; + +import java.util.Collection; +import java.util.List; + +import com.vaadin.flow.spring.VaadinMVCWebAppInitializer; + +/** + * Bootstraps the plain-Spring MVC context with {@link AppConfiguration}. + */ +public class AppWebAppInitializer extends VaadinMVCWebAppInitializer { + + @Override + protected Collection> getConfigurationClasses() { + return List.of(AppConfiguration.class); + } +} diff --git a/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/HelloView.java b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/HelloView.java new file mode 100644 index 0000000..8ac4e85 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/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.tests.spring; + +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 spring"); + greeting.setId("greeting"); + add(greeting); + } +} diff --git a/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/MetricsServlet.java b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/MetricsServlet.java new file mode 100644 index 0000000..6c5ae82 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/main/java/com/vaadin/observability/tests/spring/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.tests.spring; + +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 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 { + 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-tests-spring/src/test/java/com/vaadin/observability/tests/spring/SpringMetricsIT.java b/observability-kit-tests/observability-kit-tests-spring/src/test/java/com/vaadin/observability/tests/spring/SpringMetricsIT.java new file mode 100644 index 0000000..eed3a83 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-spring/src/test/java/com/vaadin/observability/tests/spring/SpringMetricsIT.java @@ -0,0 +1,86 @@ +/** + * 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.tests.spring; + +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 com.vaadin.flow.component.html.testbench.SpanElement; +import com.vaadin.observability.tests.common.AbstractIT; +import com.vaadin.testbench.BrowserTest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Drives a real plain-Spring Vaadin Flow page in Chrome and asserts that the + * Spring-managed {@code MeterRegistry} (provided through + * {@code ObservabilityConfiguration}) recorded session / UI / request activity. + */ +public class SpringMetricsIT extends AbstractIT { + + @Override + protected String getTestPath() { + return "/"; + } + + @BrowserTest + public void viewLoadDrivesSessionAndUiMetrics() throws IOException { + SpanElement greeting = $(SpanElement.class).id("greeting"); + assertThat(greeting.getText()).isEqualTo("Hello micrometer spring"); + + String metrics = fetchMetrics(); + + assertThat(meterValue(metrics, "vaadin.sessions.created", "count")) + .as("vaadin.sessions.created count") + .isGreaterThanOrEqualTo(1.0); + assertThat(meterValue(metrics, "vaadin.ui.created", "count")) + .as("vaadin.ui.created count").isGreaterThanOrEqualTo(1.0); + assertThat(meterValue(metrics, "vaadin.sessions.active", "value")) + .as("vaadin.sessions.active value").isGreaterThanOrEqualTo(1.0); + assertThat(metrics).withFailMessage( + "expected a vaadin.request.duration sample in the /metrics output") + .contains("vaadin.request.duration"); + } + + private String fetchMetrics() throws IOException { + HttpURLConnection conn = (HttpURLConnection) URI + .create(getRootURL() + "/metrics").toURL().openConnection(); + conn.setRequestMethod("GET"); + assertThat(conn.getResponseCode()).isEqualTo(200); + 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/observability-kit-tests-starter/pom.xml b/observability-kit-tests/observability-kit-tests-starter/pom.xml new file mode 100644 index 0000000..3d00266 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/pom.xml @@ -0,0 +1,163 @@ + + + 4.0.0 + + com.vaadin + observability-kit-tests + 5.0-SNAPSHOT + + observability-kit-tests-starter + + jar + Test Vaadin Observability Kit Starter on Spring Boot + Prometheus + + Integration test that boots a Spring Boot application using observability-kit-starter + auto-configuration and verifies session / UI / request metrics are exported via the + Actuator Prometheus endpoint. + + + + + + com.vaadin + observability-kit-starter + ${project.version} + + + com.vaadin + vaadin-spring + + + com.vaadin + flow-server + ${flow.version} + + + com.vaadin + flow-html-components + ${flow.version} + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + + com.vaadin + observability-kit-tests-common + ${project.version} + test + + + com.vaadin + vaadin-testbench-core-junit5 + test + + + com.vaadin + flow-html-components-testbench + ${flow.version} + test + + + org.seleniumhq.selenium + selenium-java + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + + + + + com.vaadin + flow-maven-plugin + ${flow.version} + + + + prepare-frontend + build-frontend + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + + diff --git a/observability-kit-tests/observability-kit-tests-starter/src/main/frontend/index.html b/observability-kit-tests/observability-kit-tests-starter/src/main/frontend/index.html new file mode 100644 index 0000000..7e3cdd4 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/src/main/frontend/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + +
+ + diff --git a/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/Application.java b/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/Application.java new file mode 100644 index 0000000..1b213e1 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/Application.java @@ -0,0 +1,20 @@ +/** + * 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.tests.starter; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/HelloView.java b/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/HelloView.java new file mode 100644 index 0000000..9942811 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/src/main/java/com/vaadin/observability/tests/starter/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.tests.starter; + +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 boot"); + greeting.setId("greeting"); + add(greeting); + } +} diff --git a/observability-kit-tests/observability-kit-tests-starter/src/main/resources/application.properties b/observability-kit-tests/observability-kit-tests-starter/src/main/resources/application.properties new file mode 100644 index 0000000..3d946f4 --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 +management.endpoints.web.exposure.include=prometheus,metrics +management.endpoint.prometheus.access=unrestricted diff --git a/observability-kit-tests/observability-kit-tests-starter/src/test/java/com/vaadin/observability/tests/starter/BootMetricsIT.java b/observability-kit-tests/observability-kit-tests-starter/src/test/java/com/vaadin/observability/tests/starter/BootMetricsIT.java new file mode 100644 index 0000000..3155ace --- /dev/null +++ b/observability-kit-tests/observability-kit-tests-starter/src/test/java/com/vaadin/observability/tests/starter/BootMetricsIT.java @@ -0,0 +1,92 @@ +/** + * 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.tests.starter; + +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 com.vaadin.flow.component.html.testbench.SpanElement; +import com.vaadin.observability.tests.common.AbstractIT; +import com.vaadin.testbench.BrowserTest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Drives a Spring Boot + {@code observability-kit-starter} app in Chrome and + * asserts the auto-configured binders move Vaadin meters into the Prometheus + * registry, scraping results via the Actuator Prometheus endpoint. + * + *

+ * Prometheus / OpenMetrics treats the {@code _created} suffix as a special + * timestamp marker and drops it from counter names, so the Micrometer counter + * {@code vaadin.sessions.created} is emitted as {@code vaadin_sessions_total} + * (and {@code vaadin.ui.created} as {@code vaadin_ui_total}). + */ +public class BootMetricsIT extends AbstractIT { + + @Override + protected String getTestPath() { + return "/"; + } + + @BrowserTest + public void viewLoadDrivesMetricsExposedViaActuator() throws IOException { + SpanElement greeting = $(SpanElement.class).id("greeting"); + assertThat(greeting.getText()).isEqualTo("Hello micrometer boot"); + + String body = fetchPrometheus(); + + assertThat(meterValue(body, "vaadin_sessions_total")) + .as("vaadin_sessions_total").isGreaterThanOrEqualTo(1.0); + assertThat(meterValue(body, "vaadin_ui_total")).as("vaadin_ui_total") + .isGreaterThanOrEqualTo(1.0); + assertThat(meterValue(body, "vaadin_sessions_active")) + .as("vaadin_sessions_active").isGreaterThanOrEqualTo(1.0); + assertThat(body).withFailMessage( + "expected a vaadin_request_duration_seconds sample in the Prometheus scrape") + .contains("vaadin_request_duration_seconds"); + } + + private String fetchPrometheus() throws IOException { + HttpURLConnection conn = (HttpURLConnection) URI + .create(getRootURL() + "/actuator/prometheus").toURL() + .openConnection(); + conn.setRequestMethod("GET"); + assertThat(conn.getResponseCode()).isEqualTo(200); + 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(); + } + + /** + * Returns the numeric value of the first Prometheus sample line whose + * metric name matches {@code meterName}. Lines with or without labels are + * accepted. Returns {@code -1.0} if no match is found. + */ + private static double meterValue(String prometheusBody, String meterName) { + Pattern pattern = Pattern.compile( + "^" + Pattern.quote(meterName) + "(?:\\{[^}]*\\})?\\s+" + + "([0-9]+(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?)", + Pattern.MULTILINE); + Matcher m = pattern.matcher(prometheusBody); + 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..0a925ab 100644 --- a/observability-kit-tests/pom.xml +++ b/observability-kit-tests/pom.xml @@ -6,13 +6,16 @@ observability-kit 5.0-SNAPSHOT - observability-kit-micrometer-test + observability-kit-tests pom - Integration tests for Observability-kit Micrometer integration + Integration tests for Observability-kit - observability-kit-micrometer-tests + observability-kit-tests-common + observability-kit-tests-micrometer + observability-kit-tests-spring + observability-kit-tests-starter diff --git a/pom.xml b/pom.xml index b3c95f9..4b53e3b 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ observability-kit-micrometer observability-kit-spring + observability-kit-starter