spring-projects / spring-boot

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.
https://spring.io/projects/spring-boot
Apache License 2.0
75.25k stars 40.7k forks source link

Servlet Filters not applied to management context #31811

Open raman-babich opened 2 years ago

raman-babich commented 2 years ago

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

wilkinsona commented 2 years ago

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 commented 2 years ago

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

bclozel commented 2 years ago

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.

wilkinsona commented 2 years ago

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

bclozel commented 2 years ago

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:

johanhaleby commented 1 year ago

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.

martinnemec3 commented 1 year ago

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.