3

I created a docker container of my spring-boot application with actuator endpoint enabled. Assuming the container runs on port 8080, it is accessible at localhost:8080/actuator, which exposes the endpoints as follows:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        ...
}

Problem: I'm behind an apache2 proxy, which redirects as follows:

ProxyPass https://myserver.com/api http://localhost:8080
ProxyPassReverse https://myserver.com/api http://localhost:8080

Now if I go to https://myserver.com/api/actuator, I can see the endpoints, but the rewritten context path is missing here: http://myserver/actuator/health

Question: how can I force Spring to build the management endpoint paths with an additional context-path /api?

My desired actuator endpoint output would be:

http://myserver/api/actuator/health

{
    "_links": {
        "self": {
            "href": "http://myserver/api/actuator",
            "templated": false
        },
        "health": {
            "href": "http://myserver/api/actuator/health",
            "templated": false
        },
        ... should be applied to all endpoints
}

Is that possible?

membersound
  • 81,582
  • 193
  • 585
  • 1,120

3 Answers3

0

Actually this is nothing that can be solved in spring directly, but with X-Forwarded-Prefix in the proxy in front:

apache2 config:

<VirtualHost *:443>
    <Location "/api">
        ProxyPass http://localhost:8080
        ProxyPassReverse http://localhost:8080
        
        RequestHeader set X-Forwarded-Prefix "/api"
    </Location>
</VirtualHost>

application.properties: server.forward-headers-strategy=framework

membersound
  • 81,582
  • 193
  • 585
  • 1,120
0

You can change the path in application.properties:

management.endpoints.web.base-path=/v1
management.endpoints.web.path-mapping.health=test
Toni
  • 3,296
  • 2
  • 13
  • 34
-1

This code apply to WebFlux project. I think you can change it for yours project.("actuate.base.url" it is custom property in config)

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Configuration
public class ActuatorReplaceLinksConfiguration {

    @Bean
    @ConditionalOnProperty("actuate.base.url")
    public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(@Value("${actuate.base.url}") final String baseUrl,
                                                                           final WebEndpointsSupplier webEndpointsSupplier,
                                                                           final ControllerEndpointsSupplier controllerEndpointsSupplier,
                                                                           final EndpointMediaTypes endpointMediaTypes,
                                                                           final CorsEndpointProperties corsProperties,
                                                                           final WebEndpointProperties webEndpointProperties,
                                                                           final Environment environment) {
        final String basePath = webEndpointProperties.getBasePath();
        final EndpointMapping endpointMapping = new EndpointMapping(basePath);
        final Collection<ExposableWebEndpoint> endpoints = webEndpointsSupplier.getEndpoints();
        final List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
        allEndpoints.addAll(endpoints);
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        return new WebFluxEndpointHandlerMapping(endpointMapping, endpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolverOverride(allEndpoints, basePath, baseUrl), this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath));
    }

    private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment, String basePath) {
        return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment) == ManagementPortType.DIFFERENT);
    }

    private static class EndpointLinksResolverOverride extends EndpointLinksResolver {
        private final String baseUrl;
        public EndpointLinksResolverOverride(final List<ExposableEndpoint<?>> allEndpoints, final String basePath, final String baseUrl) {
            super(allEndpoints, basePath);
            this.baseUrl = baseUrl;
        }

        @Override
        public Map<String, Link> resolveLinks(final String requestUrl) {
            return Optional.ofNullable(baseUrl).filter(Predicate.not(String::isBlank)).map(s -> super.resolveLinks(requestUrl).entrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, entry -> new Link(entry.getValue().getHref().replace("http://localhost:8080", s)))))
                    .orElseGet(() -> super.resolveLinks(requestUrl));
        }
    }
}