diff --git a/flow-server/pom.xml b/flow-server/pom.xml index 5598dd73b79..d3cf19d24b0 100644 --- a/flow-server/pom.xml +++ b/flow-server/pom.xml @@ -135,6 +135,20 @@ commons-compress 1.23.0 + + + io.micrometer + micrometer-observation + ${micrometer.version} + true + + + io.micrometer + micrometer-jakarta + ${micrometer.version} + true + + com.vaadin diff --git a/flow-server/src/main/java/com/vaadin/flow/server/ObservedVaadinFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/ObservedVaadinFilter.java new file mode 100644 index 00000000000..1e031508930 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/ObservedVaadinFilter.java @@ -0,0 +1,65 @@ +package com.vaadin.flow.server; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.jakarta.instrument.binder.http.DefaultHttpJakartaServerServletRequestObservationConvention; +import io.micrometer.jakarta.instrument.binder.http.HttpJakartaServerServletRequestObservationContext; +import io.micrometer.jakarta.instrument.binder.http.HttpJakartaServerServletRequestObservationConvention; +import io.micrometer.jakarta.instrument.binder.http.JakartaHttpObservationDocumentation; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Micrometer Observation {@link VaadinFilter} that will start + * observations around processing of a request. + * + * @author Marcin Grzejszczak + * @since 24.2 + */ +public class ObservedVaadinFilter implements VaadinFilter { + + private static final String SCOPE_ATTRIBUTE = ObservedVaadinFilter.class.getName() + ".scope"; + + private final ObservationRegistry observationRegistry; + + private final HttpJakartaServerServletRequestObservationConvention convention; + + public ObservedVaadinFilter(ObservationRegistry observationRegistry, + @Nullable HttpJakartaServerServletRequestObservationConvention convention) { + this.observationRegistry = observationRegistry; + this.convention = convention; + } + + @Override + public void requestStart(VaadinRequest request, VaadinResponse response) { + if (request instanceof VaadinServletRequest servletRequest && response instanceof VaadinServletResponse servletResponse) { + HttpJakartaServerServletRequestObservationContext context = new HttpJakartaServerServletRequestObservationContext(servletRequest, servletResponse); + Observation observation = JakartaHttpObservationDocumentation.JAKARTA_SERVLET_SERVER_OBSERVATION.start(this.convention, DefaultHttpJakartaServerServletRequestObservationConvention.INSTANCE, () -> context, observationRegistry); + request.setAttribute(SCOPE_ATTRIBUTE, observation.openScope()); + } + } + + @Override + public void handleException(VaadinRequest request, VaadinResponse response, VaadinSession vaadinSession, Exception t) { + Observation.Scope scope = (Observation.Scope) request.getAttribute(SCOPE_ATTRIBUTE); + if (scope == null) { + return; + } + scope.getCurrentObservation().error(t); + } + + @Override + public void requestEnd(VaadinRequest request, VaadinResponse response, VaadinSession session) { + Observation.Scope scope = (Observation.Scope) request.getAttribute(SCOPE_ATTRIBUTE); + if (scope == null) { + return; + } + scope.close(); + Observation observation = scope.getCurrentObservation(); + if (!observation.isNoop() && response instanceof HttpServletResponse httpServletResponse) { + HttpJakartaServerServletRequestObservationContext ctx = (HttpJakartaServerServletRequestObservationContext) observation.getContext(); + ctx.setResponse(httpServletResponse); + } + observation.stop(); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinFilter.java new file mode 100644 index 00000000000..85abe419a7c --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinFilter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.flow.server; + +/** + * Used to provide an around-like aspect option around request processing. + * + * @author Marcin Grzejszczak + * @since 24.2 + */ +public interface VaadinFilter { + + /** + * Called when request is about to be processed. + * @param request request + * @param response response + */ + void requestStart(VaadinRequest request, VaadinResponse response); + + /** + * Called when an exception occurred + * @param request request + * @param response response + * @param vaadinSession session + * @param t exception + */ + void handleException(VaadinRequest request, + VaadinResponse response, VaadinSession vaadinSession, Exception t); + + /** + * Called in the finally block of processing a request. Will be called + * regardless of whether there was an exception or not. + * @param request request + * @param response response + * @param session session + */ + void requestEnd(VaadinRequest request, VaadinResponse response, + VaadinSession session); +} diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java index 98a9b134217..d2c60f93243 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java @@ -16,41 +16,6 @@ package com.vaadin.flow.server; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.Serializable; -import java.lang.reflect.Constructor; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.vaadin.flow.component.UI; import com.vaadin.flow.di.DefaultInstantiator; import com.vaadin.flow.di.Instantiator; @@ -64,26 +29,30 @@ import com.vaadin.flow.router.RouteData; import com.vaadin.flow.router.Router; import com.vaadin.flow.server.HandlerHelper.RequestType; -import com.vaadin.flow.server.communication.AtmospherePushConnection; -import com.vaadin.flow.server.communication.HeartbeatHandler; -import com.vaadin.flow.server.communication.IndexHtmlRequestListener; -import com.vaadin.flow.server.communication.IndexHtmlResponse; -import com.vaadin.flow.server.communication.JavaScriptBootstrapHandler; -import com.vaadin.flow.server.communication.PwaHandler; -import com.vaadin.flow.server.communication.SessionRequestHandler; -import com.vaadin.flow.server.communication.StreamRequestHandler; -import com.vaadin.flow.server.communication.UidlRequestHandler; -import com.vaadin.flow.server.communication.WebComponentBootstrapHandler; -import com.vaadin.flow.server.communication.WebComponentProvider; +import com.vaadin.flow.server.communication.*; import com.vaadin.flow.shared.ApplicationConstants; import com.vaadin.flow.shared.JsonConstants; import com.vaadin.flow.shared.Registration; import com.vaadin.flow.shared.communication.PushMode; - import elemental.json.Json; import elemental.json.JsonException; import elemental.json.JsonObject; import elemental.json.impl.JsonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.lang.reflect.Constructor; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static java.nio.charset.StandardCharsets.UTF_8; @@ -114,6 +83,8 @@ public abstract class VaadinService implements Serializable { + PushMode.class.getSimpleName() + "." + PushMode.DISABLED.name() + "." + SEPARATOR; + private List vaadinFilters = new ArrayList<>(); + /** * Attribute name for telling * {@link VaadinSession#valueUnbound(jakarta.servlet.http.HttpSessionBindingEvent)} @@ -1433,6 +1404,7 @@ public void requestStart(VaadinRequest request, VaadinResponse response) { } setCurrentInstances(request, response); request.setAttribute(REQUEST_START_TIME_ATTRIBUTE, System.nanoTime()); + vaadinFilters.forEach(vaadinFilter -> vaadinFilter.requestStart(request, response)); } /** @@ -1449,6 +1421,7 @@ public void requestStart(VaadinRequest request, VaadinResponse response) { */ public void requestEnd(VaadinRequest request, VaadinResponse response, VaadinSession session) { + vaadinFilters.forEach(vaadinFilter -> vaadinFilter.requestEnd(request, response, session)); if (session != null) { assert VaadinSession.getCurrent() == session; session.lock(); @@ -1544,6 +1517,7 @@ private void handleExceptionDuringRequest(VaadinRequest request, vaadinSession.lock(); } try { + vaadinFilters.forEach(vaadinFilter -> vaadinFilter.handleException(request, response, vaadinSession, t)); if (vaadinSession != null) { vaadinSession.getErrorHandler().error(new ErrorEvent(t)); } @@ -2374,6 +2348,14 @@ public static String getCsrfTokenAttributeName() { + ApplicationConstants.CSRF_TOKEN; } + public List getVaadinFilters() { + return vaadinFilters; + } + + public void setVaadinFilters(List vaadinFilters) { + this.vaadinFilters = vaadinFilters; + } + private void doSetClassLoader() { final String classLoaderName = getDeploymentConfiguration() == null ? null diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java index 28a4636baee..af64b7b570e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java @@ -15,19 +15,6 @@ */ package com.vaadin.flow.server; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.vaadin.flow.component.UI; import com.vaadin.flow.di.Lookup; import com.vaadin.flow.function.DeploymentConfiguration; @@ -37,7 +24,6 @@ import com.vaadin.flow.server.HandlerHelper.RequestType; import com.vaadin.flow.server.startup.ApplicationConfiguration; import com.vaadin.flow.shared.JsonConstants; - import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; @@ -45,6 +31,13 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; /** * The main servlet, which handles all incoming requests to the application. @@ -72,6 +65,8 @@ public class VaadinServlet extends HttpServlet { private static List whenFrontendMappingAvailable = new ArrayList<>(); + private List vaadinFilters = new ArrayList<>(); + /** * Called by the servlet container to indicate to a servlet that the servlet * is being placed into service. @@ -647,6 +642,14 @@ private VaadinServletContext initializeContext() { return vaadinServletContext; } + public List getVaadinFilters() { + return vaadinFilters; + } + + public void setVaadinFilters(List vaadinFilters) { + this.vaadinFilters = vaadinFilters; + } + /** * For internal use only. * diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServletService.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServletService.java index 05ea8926f3d..7aefcfb2bd8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServletService.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServletService.java @@ -16,9 +16,19 @@ package com.vaadin.flow.server; +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.internal.DevModeHandler; +import com.vaadin.flow.internal.DevModeHandlerManager; +import com.vaadin.flow.server.communication.FaviconHandler; +import com.vaadin.flow.server.communication.IndexHtmlRequestHandler; +import com.vaadin.flow.server.communication.PushRequestHandler; +import com.vaadin.flow.server.startup.ApplicationRouteRegistry; +import com.vaadin.flow.shared.ApplicationConstants; import jakarta.servlet.GenericServlet; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.InputStream; import java.net.MalformedURLException; @@ -27,18 +37,6 @@ import java.util.Objects; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.vaadin.flow.function.DeploymentConfiguration; -import com.vaadin.flow.internal.DevModeHandler; -import com.vaadin.flow.internal.DevModeHandlerManager; -import com.vaadin.flow.server.communication.FaviconHandler; -import com.vaadin.flow.server.communication.IndexHtmlRequestHandler; -import com.vaadin.flow.server.communication.PushRequestHandler; -import com.vaadin.flow.server.startup.ApplicationRouteRegistry; -import com.vaadin.flow.shared.ApplicationConstants; - /** * A service implementation connected to a {@link VaadinServlet}. * @@ -66,6 +64,7 @@ public VaadinServletService(VaadinServlet servlet, DeploymentConfiguration deploymentConfiguration) { super(deploymentConfiguration); this.servlet = servlet; + setVaadinFilters(servlet.getVaadinFilters()); } /** diff --git a/pom.xml b/pom.xml index 37b4c9de49d..ad8b17a5ee3 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,8 @@ 4.0.3 32.1.2-jre 2.1.1 + 1.12.0-SNAPSHOT + 1.2.0-SNAPSHOT 1.5 diff --git a/vaadin-spring/pom.xml b/vaadin-spring/pom.xml index 5e6ef829aac..2b8ef66d721 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -15,6 +15,10 @@ Provides Spring integration for Vaadin applications. jar + + 1.12.0-SNAPSHOT + + com.vaadin @@ -57,6 +61,11 @@ spring-boot-starter-web provided + + org.springframework.boot + spring-boot-starter-actuator + true + org.springframework @@ -103,6 +112,12 @@ ${project.version} true + + io.micrometer + micrometer-jakarta + ${micrometer.version} + true + org.springframework @@ -175,13 +190,13 @@ - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringBootAutoConfiguration.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringBootAutoConfiguration.java index 1c6322f2854..cdd8da25b6c 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringBootAutoConfiguration.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/SpringBootAutoConfiguration.java @@ -15,12 +15,19 @@ */ package com.vaadin.flow.spring; -import java.util.HashMap; -import java.util.Map; - +import com.vaadin.flow.server.Constants; +import com.vaadin.flow.server.ObservedVaadinFilter; +import com.vaadin.flow.server.VaadinFilter; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.spring.springnative.VaadinBeanFactoryInitializationAotProcessor; +import io.micrometer.jakarta.instrument.binder.http.HttpJakartaServerServletRequestObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.MultipartConfigElement; import org.atmosphere.cpr.ApplicationConfig; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -34,11 +41,9 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.socket.server.standard.ServerEndpointExporter; -import com.vaadin.flow.server.Constants; -import com.vaadin.flow.server.VaadinServlet; -import com.vaadin.flow.spring.springnative.VaadinBeanFactoryInitializationAotProcessor; - -import jakarta.servlet.MultipartConfigElement; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Spring boot auto-configuration class for Flow. @@ -85,6 +90,7 @@ public ServletContextInitializer contextInitializer() { @Bean public ServletRegistrationBean servletRegistrationBean( ObjectProvider multipartConfig, + ObjectProvider> vaadinFilters, VaadinConfigurationProperties configurationProperties) { String mapping = configurationProperties.getUrlMapping(); Map initParameters = new HashMap<>(); @@ -102,8 +108,10 @@ public ServletRegistrationBean servletRegistrationBean( initParameters.put(ApplicationConfig.JSR356_MAPPING_PATH, pushUrl); + SpringServlet springServlet = new SpringServlet(context, rootMapping); + vaadinFilters.ifAvailable(springServlet::setVaadinFilters); ServletRegistrationBean registration = new ServletRegistrationBean<>( - new SpringServlet(context, rootMapping), mapping); + springServlet, mapping); registration.setInitParameters(initParameters); registration .setAsyncSupported(configurationProperties.isAsyncSupported()); @@ -129,4 +137,14 @@ public ServerEndpointExporter websocketEndpointDeployer() { return new VaadinWebsocketEndpointExporter(); } + @Configuration(proxyBeanMethods = false) + @AutoConfigureAfter(ObservationAutoConfiguration.class) + @ConditionalOnClass(ObservationRegistry.class) + static class ObservabilityConfig { + + @Bean + ObservedVaadinFilter observedVaadinFilter(ObservationRegistry observationRegistry, ObjectProvider convention) { + return new ObservedVaadinFilter(observationRegistry, convention.getIfAvailable(() -> null)); + } + } }