spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.81k stars 40.6k forks source link

Allow specifying a different management access log prefix #14948

Open frzme opened 5 years ago

frzme commented 5 years ago

Currently access logs created by the management server (when the management server is run on a different port) automatically get the prefix "management_" prepended (implemented in org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementChildContextConfiguration ). For configurations of access logs which write to stdout (see https://github.com/ImmobilienScout24/tomcat-stdout-accesslog/issues/2 for an example on how to configure that in tomcat) it would be useful to be able to drop this prefix. There might also be people interested in configuring this static prefix.

Please expose the prefix as a property and thus make it configurable by end users.

frzme commented 5 years ago

Note: as a workaround I tried deactivating the customizer by overriding the Bean like this:

@ManagementContextConfiguration(ManagementContextType.CHILD)
class ServletManagementChildContextConfiguration {
    @Bean
    @Primary
    @ConditionalOnClass(name = "org.apache.catalina.valves.AccessLogValve")
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatAccessLogCustomizer() {
        return server -> {

        };
    }
}

This however turned out to not work, any hints on a possible workaround? I then tried to run the same functionality as implemented in ServletManagementChildContextConfiguration but that ended up changing the valve in my main context. Any hints for me to understand this behaviour?

philwebb commented 5 years ago

For a workaround you could your write your own WebServerFactoryCustomizer ordered above ours that removes the default AccessLogValve from getEngineValves and replaces it with subclass that overrides setPrefix but doesn't call super.

skarzhevskyy commented 2 years ago

workaround code that works for us

/**
 * Add this class to /META-INF/spring.factories under the {@code org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration}
 */
@ManagementContextConfiguration(value = ManagementContextType.CHILD)
public class MyAppManagementConfig {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> managementServerustomizer() {
        return factory -> {
            AccessLogValve accessLogValve = findAccessLogValve(factory);
            if ((accessLogValve != null) && "management_stdout".equals(accessLogValve.getPrefix())) {
                accessLogValve.setPrefix("stdout");
            }
        };
    }

    private static AccessLogValve findAccessLogValve(TomcatServletWebServerFactory factory) {
        return (AccessLogValve) factory.getEngineValves().stream()
                .filter(AccessLogValve.class::isInstance)
                .findFirst().orElse(null);
    }
}
virgilsb commented 1 year ago

Hi! Was there any progress with this feature?

Note for the workaround: Since spring-boot 2.7, the ManagementConfig class needs to be specified in this file: /META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports, according to the documentation and release notes.

akundrock commented 1 year ago

Also confirming I have this issue while running in kubernetes to set up actuator health and liveness probes on a non-standard port. (Web servers can use 8080 by default and we dont want to expose metrics through the webserver)

AdiWehrli commented 1 year ago

I also did some research on this behalf, but nothing really worked. The solution https://github.com/spring-projects/spring-boot/issues/14948#issuecomment-1120117801 did only work for the normal web server but not for management server (debugged it).

So I came along to patch the class org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementChildContextConfiguration to statically set the access log prefix to stdout:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.actuate.autoconfigure.web.servlet;

import jakarta.servlet.Filter;
import org.apache.catalina.Valve;
import org.apache.catalina.valves.AccessLogValve;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.TomcatServletWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.UndertowServletWebServerFactoryCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.util.StringUtils;

import java.util.Iterator;

/**
 * Patched by Adi Wehrli, 03-MAY-2023: change access log prefix to "" because of errors during startup due to the fact that the
 * management server port is not the same as the web server port.
 */
@ManagementContextConfiguration(
        value = ManagementContextType.CHILD,
        proxyBeanMethods = false
)
@ConditionalOnWebApplication(
        type = Type.SERVLET
)
class ServletManagementChildContextConfiguration {
    ServletManagementChildContextConfiguration() {
    }

    @Bean
    ServletManagementWebServerFactoryCustomizer servletManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) {
        return new ServletManagementWebServerFactoryCustomizer(beanFactory);
    }

    @Bean
    @ConditionalOnClass(
            name = {"org.apache.catalina.valves.AccessLogValve"}
    )
    TomcatAccessLogCustomizer tomcatManagementAccessLogCustomizer() {
        return new TomcatAccessLogCustomizer();
    }

    static class ServletManagementWebServerFactoryCustomizer extends ManagementWebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
        ServletManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) {
            super(beanFactory, new Class[]{ServletWebServerFactoryCustomizer.class, TomcatServletWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, UndertowServletWebServerFactoryCustomizer.class, UndertowWebServerFactoryCustomizer.class});
        }

        protected void customize(ConfigurableServletWebServerFactory webServerFactory, ManagementServerProperties managementServerProperties, ServerProperties serverProperties) {
            super.customize(webServerFactory, managementServerProperties, serverProperties);
            webServerFactory.setContextPath(this.getContextPath(managementServerProperties));
        }

        private String getContextPath(ManagementServerProperties managementServerProperties) {
            String basePath = managementServerProperties.getBasePath();
            return StringUtils.hasText(basePath) ? basePath : "";
        }
    }

    static class TomcatAccessLogCustomizer extends AccessLogCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
        TomcatAccessLogCustomizer() {
        }

        public void customize(TomcatServletWebServerFactory factory) {
            AccessLogValve accessLogValve = this.findAccessLogValve(factory);
            if (accessLogValve != null) {
                accessLogValve.setPrefix(this.customizePrefix(accessLogValve.getPrefix()));
            }
        }

        private AccessLogValve findAccessLogValve(TomcatServletWebServerFactory factory) {
            Iterator engineValves = factory.getEngineValves().iterator();

            Valve engineValve;
            do {
                if (!engineValves.hasNext()) {
                    return null;
                }

                engineValve = (Valve)engineValves.next();
            } while(!(engineValve instanceof AccessLogValve));

            AccessLogValve accessLogValve = (AccessLogValve)engineValve;
            return accessLogValve;
        }
    }

    abstract static class AccessLogCustomizer implements Ordered {
        AccessLogCustomizer() {
        }

        protected String customizePrefix(String prefix) {
            return "stdout";
        }

        public int getOrder() {
            return 1;
        }
    }

    @Configuration(
            proxyBeanMethods = false
    )
    @ConditionalOnClass({EnableWebSecurity.class, Filter.class})
    @ConditionalOnBean(
            name = {"springSecurityFilterChain"},
            search = SearchStrategy.ANCESTORS
    )
    static class ServletManagementContextSecurityConfiguration {
        ServletManagementContextSecurityConfiguration() {
        }

        @Bean
        Filter springSecurityFilterChain(HierarchicalBeanFactory beanFactory) {
            BeanFactory parent = beanFactory.getParentBeanFactory();
            return (Filter)parent.getBean("springSecurityFilterChain", Filter.class);
        }

        @Bean
        @ConditionalOnBean(
                name = {"securityFilterChainRegistration"},
                search = SearchStrategy.ANCESTORS
        )
        DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(HierarchicalBeanFactory beanFactory) {
            return (DelegatingFilterProxyRegistrationBean)beanFactory.getParentBeanFactory().getBean("securityFilterChainRegistration", DelegatingFilterProxyRegistrationBean.class);
        }
    }
}

I also removed the Undertow and Jetty parts as I do not need them.

soann-dewasme1 commented 10 months ago

Hello, Has someone fix this ?

As https://github.com/spring-projects/spring-boot/issues/14948#issuecomment-1532826032 mention it, https://github.com/spring-projects/spring-boot/issues/14948#issuecomment-1120117801 do not work with management server on another port.

But I don't want to trick springboot engine...