-
Notifications
You must be signed in to change notification settings - Fork 167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Adds a VaadinRequestInterceptor abstraction #17502
Conversation
6a7486c
to
a327e3b
Compare
@mstahv can you elaborate on that? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me in general.
Two concerns about naming of this new class and the fact that both servlet and service has these filters mutable.
If possible, I would preference to have immutable collection once it is initialised.
I will check how it can be done.
flow-server/src/main/java/com/vaadin/flow/server/VaadinFilter.java
Outdated
Show resolved
Hide resolved
flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
Outdated
Show resolved
Hide resolved
flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
Outdated
Show resolved
Hide resolved
flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
Outdated
Show resolved
Hide resolved
flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java
Outdated
Show resolved
Hide resolved
e922f3e
to
134785c
Compare
@mshabarov I've checked the Service init mechanism that you mentioned and decided to rewrite my proposal to use that mechanism. You can check the details here. I like it better because there is just 1 way of setting the interceptors common for spring and non-spring apps. PTAL |
Explaining my comment regarding the UI.access & requests:
I didn't look through the change proposal thoroughly, but by quickly skimming it through (mostly checkin names of new classes), this would help only with common XHR connections and user originated UI access 🤔 |
From my perspective we need to wrap all the entry points to the backend and everything going out of the backend. If we can control what goes into and out of the UI that would be perfect. Once we have such a list of entry and exit points I can instrument them (or at least try to :) ) |
Yeah, I'd like to claim that it is not very rare that interesting things (regarding observability/profiling) happens in these other kind of UI access calls. It for sure does not happen in every app, but for example in dashboard like apps, a lot. |
It makes sense to me what Matti said about having hooks for
For Vaadin Push connections (communications via WebSockets), the |
8340842
to
e5ad270
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My previous comments are fixed, awesome work! 👍
Three points I would like to fix and clarify before we could merge it:
- Wrapping
UI.access
calls with vaadin request interceptor to be able to add instrumentations for it (discussed it already with Matti T. in the comments above) - Cover
ErrorHandler
calls, so that the exceptions thrown in the UI update callbacks in the user's codes would be also wrapped and observable (see my comment below) - Maybe more global question: should we follow the same instrumentation list as we do for Vaadin Observability Kit, e.g. all Vaadin-provided request handlers, static file handler, data communicator and so on.
My vision is that we can do it iteratively, so we can apply this pull request, add instrumentation (another opened PR) for it and then extendVaadinRequestInterceptor
step-by-step with extra methods for concrete instrumentations and egress points.
What do you think @heruan and @Legioth ?
@@ -1544,6 +1582,18 @@ private void handleExceptionDuringRequest(VaadinRequest request, | |||
vaadinSession.lock(); | |||
} | |||
try { | |||
try { | |||
vaadinRequestInterceptors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleExceptionDuringRequest
doesn't actually handle all the errors during request processing.
It catches internal errors, but errors that might happen inside lambdas provided by developers (mainly UI updates, for instance, if you pass an UI update into beforeClientResponse
) are processed by ErrorHandler
, so that the exception is not thrown up to the invocation hierarchy, but handled by this error handler. See https://vaadin.com/docs/latest/advanced/custom-error-handler.
This might be a default error handler or a custom error handler, which, for example shows the notification with error message or changes component's styles.
Also, if you search in the project by string .getErrorHandler().error(
, you will find a few entries in the core code that catch exceptions and delegate them to the error handler.
Thus, to cover also the exceptions handled by ErrorHandler
objects, we may add a ErrorHandlerWrapper
that takes ErrorHandler
in VaadinSession::setErrorHandler
and decorates it with calls to request interceptors.
vaadin-spring/src/test/java/com/vaadin/flow/spring/service/SpringVaadinServletServiceTest.java
Outdated
Show resolved
Hide resolved
...server/src/main/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener
Outdated
Show resolved
Hide resolved
c2d8a0e
to
0d6c5ec
Compare
I was thinking that we could instrument
If we did this via the interceptor that I mentioned above we would also take care of the error handling?
I am all for baby steps ;) |
0d6c5ec
to
9052f7b
Compare
Yes, this makes sense to me to add a new aspect for pending access invocations. Let's do it in a separate pull request.
From the other hand, when the A simplest solution IMO is to make a decorator for error handler, whenever it is being added to the session here
VaadinRequestInterceptor::handleException . This would intercept also errors for async updates. Yes, this might be a bit confusing for API users, but I see no better and easy solution at the moment.
We discussed the design internally, and our decision now is to focus on the basic request/response instrumentation, not trying to repeat all the features implemented with OpenTelemetry standard in Vaadin Observability Kits, because it would need a really deep code changes in some cases. This definitely better to do in a separate pull requests later. For most of the users even having a request/response/error instrumentation is already a big feature. |
* @author Marcin Grzejszczak | ||
* @since 24.2 | ||
*/ | ||
public class VaadinRequestInterceptorServiceInitListener |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, why do we need this init listener?
Looks like doing this way, we make an extra loop forcing Flow to take a detour, whereas this can be "hardcoded" here
event.getAddedRequestHandlers().forEach(handlers::add); |
This might lead to not being able to autowire the interceptors as a Spring bean, but I believe this would be fine, you can still create a VaadinRequestInterceptorServiceInitListener
and say that it should be a AtComponent
in Spring based apps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean I think I was following what was being done now for RequestHandlers
. I've actually copy pasted the whole code that the request handlers are following and did the same for interceptors.
My analysis:
- A request interceptor
List<RequestHandler> handlers = createRequestHandlers();
is initialized. These handlers do not require any collaborators (they have default constructors only, which will not be the case when doing instrumentations - you need tracers, registries etc.)- I'm doing exactly the same for the interceptors
List<VaadinRequestInterceptor> requestInterceptors = createVaadinRequestInterceptors();
- I'm doing exactly the same for the interceptors
- when
VaadinService
is initialized it callsinstantiator.getServiceInitListeners().forEach(listener -> listener.serviceInit(event));
what will initialize the event with whatever the listeners want to add (e.g. custom interceptors etc.). The custom listeners would set interceptors such as the micrometer observation interceptor- I'm doing the same for interceptors - I read the entries from the event and add them to an unmodifiable list
- For the compatibility with Spring I've added a second constructor for constructor injection
I don't understand what I should actually do then if not what I've already done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me checkout your fork and try to refactor it without making VaadinRequestInterceptorServiceInitListener
. Will be back to you soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, thanks for being patient :)
I checked out your fork ("vaadinFilter" branch) and made the following changes on top of it:
- Removed changes in
Instantiator
,DefaultInstantiator
andSpringInstantiator
- Removed
VaadinRequestInterceptorServiceInitListener
- Removed
META-INF/services
file forVaadinRequestInterceptorServiceInitListener
- Removed
VaadinRequestInterceptorServiceInitListener
from SpringBootAutoConfiguration. - Added an implementation for request interceptor impl to
test-spring-boot
test module:
@Component
public class LoggerRequestInterceptor implements VaadinRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger("RequestInterceptor");
@Override
public void requestStart(VaadinRequest request, VaadinResponse response) {
logger.info("Request start, path into = {}" , request.getPathInfo());
}
@Override
public void handleException(VaadinRequest request, VaadinResponse response, VaadinSession vaadinSession, Exception t) {
}
@Override
public void requestEnd(VaadinRequest request, VaadinResponse response, VaadinSession session) {
logger.info("Request end, path into = {}" , request.getPathInfo());
}
}
- Added initialiser to
test-spring-boot
test module:
@Component
public class MyVaadinServiceInitListener implements VaadinServiceInitListener {
private final List<VaadinRequestInterceptor> interceptors;
public MyVaadinServiceInitListener(@Autowired List<VaadinRequestInterceptor> interceptors) {
this.interceptors = interceptors;
}
@Override
public void serviceInit(ServiceInitEvent event) {
interceptors.forEach(event::addVaadinRequestInterceptor);
}
}
- Changed some config in pom files to be able to quickly run the test app in
test-spring-boot
.
So now if you:
- Build the project with
mvn clean install -DskipTests -pl flow-server,vaadin-spring,flow-tests/vaadin-spring-tests -am
, - Navigate to test module
cd flow-tests/vaadin-spring-tests/test-spring-boot
- And finally run it with
mvn clean spring-boot:run -nsu
- And navigate to "localhost:8888",
You will see the interceptor in action, adding some log message to the server console.
Why I prefer to remove these codes:
- Main reason is that I want to have a Micrometer implementation in a separate module, e.g. "flow-micrometer". Thus, the
flow-server
itself shouldn't add any interceptors by default, butflow-micrometer
should - add the interceptors by adding the service init listener, like I showed above forMyVaadinServiceInitListener
, as a Spring Bean. - Service Init Listener API is for external usage (in the Vaadin projects, not inside Flow). Internally, we can and should hard-code the handlers/interceptors, added by default. In our case, the interceptors are empty by default. This might however change in the future, who knows what interceptors we might want to add by default.
If we do it with adding a listener and adding SPI into meta-inf resources, it would mean a redundant loop in the code. - Instantiator and Lookup additions doesn't seem clear - Flow can spot Service Init Listener without them. Service Init Listener can be found as a Spring Bean as a AtComponent, so no idea why we need an extra code for it.
- Not clear why SpringBootAutoConfiguration need a new bean (service init listener for interceptors). Again, you can create interceptors beans as I shown above in my example and autowire them to the listener class. All should work.
So, 2-4 seem to me an extra codes, interceptors API should work without it.
1 is proposed to be do in your second pull request.
Hope I haven't missed anything.
Here is IntelliJ IDEA patch that you can apply on your branch to see my codes (hope you are using IDEA :) )
remove_service_init_listener_and_add_a_usecase.patch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome! I mean we have the same outcome by having less code I'm all for it :D So how do we proceed now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot push into your branch, because I have no rights.
If you can apply my patch locally, could you commit it and push (only changes in flow-server
and vaadin-spring
, others are use case codes and local workarounds) it?
Also, tests I think need some tweaks, if you have a chance please do, otherwise I can help you with it.
@@ -119,6 +119,13 @@ public ServletRegistrationBean<SpringServlet> servletRegistrationBean( | |||
return registration; | |||
} | |||
|
|||
@Bean | |||
public VaadinRequestInterceptorServiceInitListener vaadinRequestInterceptorServiceInitListener( | |||
ObjectProvider<VaadinRequestInterceptor> interceptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a second look this seems to me as an overhead.
We don't have a similar API/ability to add request handler and whatever objects developers can add in ServiceInitListeners
.
Plus, projects which already have a service init listener can just add one line to their listener to add the interceptors.
See my another comment https://github.com/vaadin/flow/pull/17502/files#r1327153517.
So, if you have no big objections, I would propose to remove this from autoconfiguration for simplicity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have a similar API/ability to add request handler and whatever objects developers can add in ServiceInitListeners.
I mean you do, ServiceInitListener works via SPI and it can append things to the init event. I thought that it's done exactly for this purpose (https://vaadin.com/docs/latest/advanced/service-init-listener).
I can do whatever you want but I don't know how you would like this to work :) How will you want to add the request interceptors to the VaadinService?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is my vision of design so far:
flow-server
creates an empty list of interceptors by default and makes this protected, so developers can change it if they want. In the future we might want to add some interceptors by default (for whatever purpose).flow-server
collects interceptors fromServiceInitEvent
and makes an immutable collection that is used for further http request interception. (this is already done and look awesome).flow-micrometer
module in the Flow repo adds aVaadinRequestInterceptorServiceInitListener
as a Spring'sAtComponent
and injects there all needed Micrometer Interceptors. No SPI registration is needed. If this module is in the classpath of the project, this listener should be picked and executed by Flow.- Vaadin plain projects can register their own service init listeners with SPI and Vaadin Spring-based projects can register this listener as a component and autowire interceptors.
- No changes to
Instantiator
andSpringBootAutoConfiguration
is needed for the above points to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your solution so far seems to make it possible to only add the interceptors beans in the projects (without care about making own service init listener).
I would strongly recommend to not do it within this pull request.
It should be a separate feature and consistent with other kind of data in the service init event.
So far I would state that the request interceptors, as request handlers, should be always added through a service init listener defined in the project.
9052f7b
to
4c73948
Compare
While waiting for feedback I started playing around with this concept and I got sth like this out of it (check the What I'm currently thinking about is that we already have instrumentation coming form the Spring Web instrumentation (https://github.com/spring-projects/spring-boot/blob/v3.1.3/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java#L82-L96) of the incoming requests. So if we have a Vaadin application that is non-Spring definitely this new, server side instrumentation ( makes a lot of sense. In the Spring world we need to have IMO one or the other unless I'm missing sth. |
Great! Thanks for the enthusiasm 👍 🥇 |
@marcingrzejszczak So I propose to apply my last patch #17502 (comment), adapt the tests for it and then I propose to merge this PR, because it's going to be endless otherwise :) Then we can consider making a new module for instrumentation and then continue with command interceptor. |
4c73948
to
697bdb1
Compare
I've just applied the patch, let's see how the build goes... |
Please run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation looks good to me.
A few comments about leftover codes and imports, and one comment about test.
import java.util.ServiceLoader; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.stream.Stream; | ||
import java.util.stream.StreamSupport; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert these changes. This class doesn't have a meaningful additions now.
@@ -31,6 +27,10 @@ | |||
import com.vaadin.flow.server.communication.IndexHtmlRequestListener; | |||
import com.vaadin.flow.server.communication.UidlWriter; | |||
|
|||
import java.io.Serializable; | |||
import java.util.ServiceLoader; | |||
import java.util.stream.Stream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert these changes. This class doesn't have a meaningful additions now.
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.*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use the wildcard import, but specify the exact classes, like it was before.
</execution> | ||
</executions> | ||
</plugin> | ||
<!-- <plugin>--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert this. It is a leftover from a local run.
<groupId>com.vaadin</groupId> | ||
<artifactId>vaadin-dev-server</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. Please revert this. It is a left over from local test.
@@ -112,7 +118,7 @@ | |||
<execution> | |||
<goals> | |||
<goal>prepare-frontend</goal> | |||
<goal>build-frontend</goal> | |||
<!-- <goal>build-frontend</goal>--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. Please revert this. It is a left over from local test.
|
||
import jakarta.servlet.MultipartConfigElement; | ||
import java.util.HashMap; | ||
import java.util.Map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert imports change, no functionality is added into this class.
@@ -89,4 +95,21 @@ public void uiInitListenerAsSpringBean_listenerIsAutoregisteredAsUIInitiLietnerI | |||
Assert.assertSame(ui, listener.events.get(0).getUI()); | |||
} | |||
|
|||
@Test | |||
public void requestInterceptorsAreRegisteredOnTheService() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that I understand how this is supposed to work.
How MyRequestInterceptor
instances get injected into VaadinService
? I see no service init listener here.
Would it be more clear if you:
- Move
LoggerRequestInterceptor
andMyVaadinServiceInitListener
classes fromtest-spring-boot
module to here - Add
MyVaadinServiceInitListener
to the Spring context (maybe this isn't even needed if Spring test configuration can recognise the bean and add it automatically), so this listener would pass through theVaadinService
and it's interceptors are collected and added to the list by new logic in the flow-server. - Pass any
VaadinRequest
to theVaadinServlet
/VaadinService
and check that the interceptor does changes in the request/response (like you already does in flow-server test).
Making this kind of test, we would repeat the use case of how these interceptors are added to Flow in reality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this test fails with:
Error: Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.172 s <<< FAILURE! -- in com.vaadin.flow.spring.service.SpringVaadinServletServiceTest
Error: com.vaadin.flow.spring.service.SpringVaadinServletServiceTest.requestInterceptorsAreRegisteredOnTheService -- Time elapsed: 0.035 s <<< FAILURE!
org.opentest4j.AssertionFailedError: There should be 1 filter ==> expected: <1> but was: <0>
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:560)
at com.vaadin.flow.spring.service.SpringVaadinServletServiceTest.requestInterceptorsAreRegisteredOnTheService(SpringVaadinServletServiceTest.java:108)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, after the changes you've made I don't know how this is supposed to work but I'm not a Vaadin expert ;) I'm trying to make the tests work now. Will ping you once I can't figure things out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I've made the test pass, let me see if everything else is working fine and that formatting is not broken and I will push the changes! Your comments were very helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, now this test clearly shows that MyRequestInterceptor
is autowired to the listener component and gets used by VaadinService
.
LoggerRequestInterceptor
class is not needed I think. I suppose it is added to the list and invoked, but not used anyhow in tests.
So let's remove LoggerRequestInterceptor
and I'm ready to approve the PR.
Ok, hopefully everything is fine now. I do have problems with the wildcard imports and formatting because I can't seem to align with the intellij idea formatting style for this project. Anyways, hopefully that's resolved now |
@@ -89,4 +95,21 @@ public void uiInitListenerAsSpringBean_listenerIsAutoregisteredAsUIInitiLietnerI | |||
Assert.assertSame(ui, listener.events.get(0).getUI()); | |||
} | |||
|
|||
@Test | |||
public void requestInterceptorsAreRegisteredOnTheService() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, now this test clearly shows that MyRequestInterceptor
is autowired to the listener component and gets used by VaadinService
.
LoggerRequestInterceptor
class is not needed I think. I suppose it is added to the list and invoked, but not used anyhow in tests.
So let's remove LoggerRequestInterceptor
and I'm ready to approve the PR.
VaadinFilter simulates an around aspect around processing of a request related to vaadingh-17436
* renamed VaadinFilter to VaadinRequestInterceptor * Vaadin uses Lookup to init the VaadinService ** Added VaadinRequestInterceptors to ServiceInitEvent ** Added a VaadinRequestInterceptorServiceInitListener that will add interceptors to the init event ** The listener works with ServiceLocator based DI and Spring based DI (has both arg and non-arg constructor) ** That way we have 1 way of initializing the interceptors * Added the VaadinRequestInterceptorServiceInitListener to META-INF for automated locator discovery * Added the VaadinRequestInterceptorServiceInitListener as a bean for Spring Boot auto configuration * Extended the Instantiator mechanism to find all instances of a given class ** Implemented the method in Spring and non-Spring implementations
Co-authored-by: Mikhail Shabarov <[email protected]>
7c224e2
to
3a85d40
Compare
Before it is forgotten, can we get the 24.2 label? ;) |
I'm afraid no, because Flow 24.2 beta is out today and we lost the release train for it. I propose to target this and further instrumentation feature to Vaadin 24.3. |
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
This ticket/PR has been released with Vaadin 24.3.0.alpha1 and is also targeting the upcoming stable 24.3.0 version. |
Description
VaadinFilter simulates an around aspect around processing of a request. Prerequisite for #17494
Related to #17436
Type of change
Checklist
Additional for
Feature
type of change