spring-boot icon indicating copy to clipboard operation
spring-boot copied to clipboard

Servlet Filters not applied to management context

Open raman-babich opened this issue 3 years ago • 7 comments

Spring boot webmvc service and spring boot webflux service have inconsistent behavior for http server requests metrics(prometheus). Webflux service exposes http_server_requests_seconds that includes metrics for management endpoints and service endpoints but for webmvc there are only metrics for service endpoints. I am attaching the minimal example(for spring boot v2.6.9) to be more clear.

spring-http-server-metrics.zip

raman-babich avatar Jul 20 '22 15:07 raman-babich

Thanks for the samples.

You have configured the management server to run on a separate port. This results in a child application context being created for the management server and the servlet and reactive stacks treat context hierarchies differently. The key difference is that WebFlux does not consider beans in ancestor contexts. This means that MetricsWebFilter is not found and therefore isn't included in the management server's HttpHandler. As a result, no metrics are recorded for requests to any management endpoints.

This is very similar to https://github.com/spring-projects/spring-boot/issues/27504. Flagging for discussion at a team meeting so that we can consider our options.

wilkinsona avatar Jul 20 '22 17:07 wilkinsona

My description above is back to front. It's the servlet filter that's not picked up in the child context so metrics don't appear when using MVC but do appear with WebFlux

wilkinsona avatar Sep 07 '22 15:09 wilkinsona

See https://github.com/spring-projects/spring-boot/issues/27504#issuecomment-1240381510 for an update on this issue. The fact that WebFilter from the app context are applied to the management context makes me wonder if we shouldn't reconsider the parent/child relationship between those context.

bclozel avatar Sep 08 '22 08:09 bclozel

@bclozel is going to review the current status of things now that the ordering issue has been fixed in Framework.

wilkinsona avatar Nov 02 '22 16:11 wilkinsona

I've reviewed the behavior after recent changes and it's still the same.

In the case of WebFlux, the Framework web infrastructure itself looks for beans by type in the current application context. This lookup operation involves context parent lookup, which explains why WebFilter beans defined in the main application context are also registered in the management context.

For Spring MVC, the situation is different since Filter and FilterRegistrationBean are detected and applied through initiazers (see ServletContextInitializerBeans). In this case, the ListableBeanFactory lookup only considers the current bean factory and not the hierarchy. We could change this behavior by using methods in BeanFactoryUtils, but this is a rather important change and echoes my previous comment.

At this point, the inconsistency is accidental:

  • Spring WebFlux did not implement this behavior on purpose and considers that a parent/child context situation is unusual
  • The FilterRegistrationBean support in Spring Boot only considers the child context components on purpose so far

bclozel avatar Nov 07 '22 13:11 bclozel

Is there a workaround in Spring Boot 3? The previous hack that I used:

@Component
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
public class ManagementContextFactoryBeanPostProcessor
        implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof ManagementContextFactory managementContextFactory) {
            return (ManagementContextFactory) (parent, configurationClasses) -> {
                var context = managementContextFactory.createManagementContext(parent, configurationClasses);
                if (context instanceof GenericWebApplicationContext genericWebApplicationContext) {
                    genericWebApplicationContext.registerBean(ForwardedHeaderFilterRegistrationBean.class);
                }
                return context;
            };
        }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    public static class ForwardedHeaderFilterRegistrationBean
            extends FilterRegistrationBean<ForwardedHeaderFilter> {

        public ForwardedHeaderFilterRegistrationBean() {
            setFilter(new ForwardedHeaderFilter());
            setOrder(Ordered.HIGHEST_PRECEDENCE);
        }

    }

}

doesn't seem to work anymore since ManagementContextFactory is no longer an interface but a final class and the method signature for createManagementContext has also changed.

johanhaleby avatar Dec 20 '22 07:12 johanhaleby

A workaround that I used on Spring Boot 3 (tested on 3.1.0):

    @Bean
    @Primary
    public static ManagementContextFactory myServletWebChildContextFactory() {
        return new ManagementContextFactory(WebApplicationType.SERVLET, ServletWebServerFactory.class,
                ServletWebServerFactoryAutoConfiguration.class, MyForwardedHeaderFilterAutoConfiguration.class);
    }
    
    static class MyForwardedHeaderFilterAutoConfiguration {
        @Bean
        public FilterRegistrationBean<ForwardedHeaderFilter> myForwardedHeaderFilter() {
            ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
            FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
            registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
            registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
            return registration;
        }
    }

It should be enough to add the code above anywhere to the root context.

The idea is to have custom autoconfiguration class that defines the FilterRegistrationBean we need (the one that is actually autocreated in the parent context, but not registered in the management context). We can then create a ManagementContextFactory bean that we annotate with @Primary (to let spring use this one instead) and in the factory method we add our auto-config class ass the last parameter - this way our FilterRegistrationBean bean gets processed within the management context and filter should be registered successfuly.

martinnemec3 avatar Jul 20 '23 15:07 martinnemec3