Commit e50ad9de by Johannes Edmeier

Add context-path property for admin server

Cause it was often requested, now the context-path for the admin server is configurable. This allows to move the admin-ui and endpoints to other http- paths than "/". NOTE: I'd still advise not to add the admin to the application you want to monitor.
parent 0f759da9
......@@ -232,11 +232,16 @@ spring.boot.admin.password
|===
| Property name |Description |Default value
=======
| spring.boot.admin.context-path
| The context-path prefixes the path where the Admin Servers statics assets and api should be served. Relative to the Dispatcher-Servlet.
|
| spring.boot.admin.monitor.period
| Time interval in ms to update the status of applications with expired status-informations.
| 10.000
| spring.boot.admin.status-lifetime
| spring.boot.admin.monitor.status-lifetime
| Lifetime of iapplication statuses in ms. The applications /health-endpoint will not be queried until the lifetime has expired.
| 10.000
|===
......@@ -416,8 +421,8 @@ To enable pagerduty notifications you just have to add a generic service to your
== FAQs ==
[qanda]
Can I include spring-boot-admin into my business application?::
*tl;dr* you shouldn't. +
In general it is possible but be aware that the admin-ui is serverd on `/` so most likely you will get conflicts here. On the other hand in my opinion it makes no sense for an application to monitor itself. In case your application goes down your monitioring tool also does.
*tl;dr* You can, but you shouldn't. +
You can set `spring.boot.admin.context-path` to alter the path where the UI and REST-api is served, but depending on the complexity of your application you might get in trouble. On the other hand in my opinion it makes no sense for an application to monitor itself. In case your application goes down your monitioring tool also does.
How do I disable Spring Boot Admin Client for my unit tests?::
The AutoConfiguration is triggered by the presence of the `spring.boot.admin.url`. So if you don't set this property for your tests, the Spring Boot Admin Client is not active.
......
......@@ -49,7 +49,7 @@
<resources>
<resource>
<directory>target/dist</directory>
<targetPath>META-INF/resources</targetPath>
<targetPath>META-INF/spring-boot-admin-server-ui</targetPath>
</resource>
</resources>
</build>
......
......@@ -5,8 +5,26 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("spring.boot.admin")
public class AdminServerProperties {
/**
* The context-path prefixes the path where the Admin Servers statics assets and api should be
* served. Relative to the Dispatcher-Servlet.
*/
private String contextPath = "";
private MonitorProperties monitor = new MonitorProperties();
public void setContextPath(String pathPrefix) {
if (!pathPrefix.startsWith("/") || pathPrefix.endsWith("/")) {
throw new IllegalArgumentException(
"ContextPath must start with '/' and not end with '/'");
}
this.contextPath = pathPrefix;
}
public String getContextPath() {
return contextPath;
}
public MonitorProperties getMonitor() {
return monitor;
}
......
......@@ -19,6 +19,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
......@@ -31,8 +32,11 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.util.StringUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
......@@ -51,6 +55,7 @@ import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.StatusUpdater;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.web.PrefixHandlerMapping;
@Configuration
@EnableConfigurationProperties
......@@ -65,6 +70,9 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
@Autowired
private ApplicationStore applicationStore;
@Autowired
private ServerProperties server;
@Bean
@ConditionalOnMissingBean
public AdminServerProperties adminServerProperties() {
......@@ -95,6 +103,30 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
return false;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(adminServerProperties().getContextPath() + "/**")
.addResourceLocations("classpath:/META-INF/spring-boot-admin-server-ui/");
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
if (StringUtils.hasText(adminServerProperties().getContextPath())) {
registry.addRedirectViewController(adminServerProperties().getContextPath(),
server.getPath(adminServerProperties().getContextPath()) + "/");
}
registry.addViewController(adminServerProperties().getContextPath() + "/")
.setViewName("forward:index.html");
}
@Bean
public PrefixHandlerMapping prefixHandlerMapping() {
PrefixHandlerMapping prefixHandlerMapping = new PrefixHandlerMapping(registryController(),
journalController());
prefixHandlerMapping.setPrefix(adminServerProperties().getContextPath());
return prefixHandlerMapping;
}
/**
* @return Controller with REST-API for spring-boot applications to register itself.
*/
......
......@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.ZuulConfiguration;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
......@@ -28,13 +29,13 @@ import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.event.RoutesOutdatedEvent;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.PreDecorationFilter;
@Configuration
@AutoConfigureAfter({ AdminServerWebConfiguration.class })
public class RevereseZuulProxyConfiguration extends ZuulConfiguration {
@Autowired(required = false)
......@@ -46,11 +47,14 @@ public class RevereseZuulProxyConfiguration extends ZuulConfiguration {
@Autowired
private ApplicationRegistry registry;
@Autowired
private AdminServerProperties adminServer;
@Bean
@Override
public ApplicationRouteLocator routeLocator() {
return new ApplicationRouteLocator(this.server.getServletPrefix(), registry,
RegistryController.PATH);
adminServer.getContextPath() + "/api/applications");
}
@Bean
......
......@@ -18,7 +18,7 @@ package de.codecentric.boot.admin.controller;
import java.util.Collection;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.journal.ApplicationEventJournal;
......@@ -26,10 +26,9 @@ import de.codecentric.boot.admin.journal.ApplicationEventJournal;
/**
* REST-Controller for querying all client application events.
*
* @author Johannes Stelzer
* @author Johannes Edmeier
*/
@RestController
@RequestMapping("/api/journal")
@ResponseBody
public class JournalController {
private ApplicationEventJournal eventJournal;
......@@ -38,7 +37,7 @@ public class JournalController {
this.eventJournal = eventJournal;
}
@RequestMapping
@RequestMapping("/api/journal")
public Collection<ClientApplicationEvent> getJournal() {
return eventJournal.getEvents();
}
......
......@@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
......@@ -34,10 +34,8 @@ import de.codecentric.boot.admin.registry.ApplicationRegistry;
/**
* REST controller for controlling registration of managed applications.
*/
@RestController
@RequestMapping(value = RegistryController.PATH)
@ResponseBody
public class RegistryController {
public static final String PATH = "/api/applications";
private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class);
......@@ -53,7 +51,7 @@ public class RegistryController {
* @param app The application infos.
* @return The registered application.
*/
@RequestMapping(method = RequestMethod.POST)
@RequestMapping(value = "/api/applications", method = RequestMethod.POST)
public ResponseEntity<Application> register(@RequestBody Application app) {
LOGGER.debug("Register application {}", app.toString());
Application registeredApp = registry.register(app);
......@@ -66,7 +64,7 @@ public class RegistryController {
* @param name the name to search for
* @return List
*/
@RequestMapping(method = RequestMethod.GET)
@RequestMapping(value = "/api/applications", method = RequestMethod.GET)
public Collection<Application> applications(
@RequestParam(value = "name", required = false) String name) {
LOGGER.debug("Deliver registered applications with name={}", name);
......@@ -83,7 +81,7 @@ public class RegistryController {
* @param id The application identifier.
* @return The registered application.
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
@RequestMapping(value = "/api/applications/{id}", method = RequestMethod.GET)
public ResponseEntity<?> get(@PathVariable String id) {
LOGGER.debug("Deliver registered application with ID '{}'", id);
Application application = registry.getApplication(id);
......@@ -100,7 +98,7 @@ public class RegistryController {
* @param id The application id.
* @return the unregistered application.
*/
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@RequestMapping(value = "/api/applications/{id}", method = RequestMethod.DELETE)
public ResponseEntity<?> unregister(@PathVariable String id) {
LOGGER.debug("Unregister application with ID '{}'", id);
Application application = registry.deregister(id);
......
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.web;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* {@link HandlerMapping} to map {@code @RequestMapping} on objects and prefixes them. The semantics
* of {@code @RequestMapping} should be identical to a normal {@code @Controller}, but the Objects
* should not be annotated as {@code @Controller} (otherwise they will be mapped by the normal MVC
* mechanisms).
*
* @author Johannes Edmeier
*/
public class PrefixHandlerMapping extends RequestMappingHandlerMapping {
private String prefix = "";
private final Object handlers[];
public PrefixHandlerMapping(Object... handlers) {
this.handlers = handlers.clone();
setOrder(-50);
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
for (Object handler : handlers) {
detectHandlerMethods(handler);
}
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected void registerHandlerMethod(Object handler, Method method,
RequestMappingInfo mapping) {
if (mapping == null) {
return;
}
super.registerHandlerMethod(handler, method, withPrefix(mapping));
}
private RequestMappingInfo withPrefix(RequestMappingInfo mapping) {
List<String> newPatterns = getPatterns(mapping);
PatternsRequestCondition patterns = new PatternsRequestCondition(
newPatterns.toArray(new String[newPatterns.size()]));
return new RequestMappingInfo(patterns, mapping.getMethodsCondition(),
mapping.getParamsCondition(), mapping.getHeadersCondition(),
mapping.getConsumesCondition(), mapping.getProducesCondition(),
mapping.getCustomCondition());
}
private List<String> getPatterns(RequestMappingInfo mapping) {
List<String> newPatterns = new ArrayList<String>(
mapping.getPatternsCondition().getPatterns().size());
for (String pattern : mapping.getPatternsCondition().getPatterns()) {
newPatterns.add(prefix + pattern);
}
return newPatterns;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
}
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codecentric.boot.admin.web;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
public class PrefixHandlerMappingTest {
private final StaticApplicationContext context = new StaticApplicationContext();
private Method method;
@Before
public void init() throws Exception {
this.method = ReflectionUtils.findMethod(TestController.class, "invoke");
}
@Test
public void withoutPrefix() throws Exception {
TestController controller = new TestController();
PrefixHandlerMapping mapping = new PrefixHandlerMapping(controller);
mapping.setApplicationContext(this.context);
mapping.afterPropertiesSet();
assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/test")).getHandler(),
equalTo((Object) new HandlerMethod(controller, this.method)));
assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/noop")), nullValue());
}
@Test
public void withPrefix() throws Exception {
TestController controller = new TestController();
PrefixHandlerMapping mapping = new PrefixHandlerMapping(controller);
mapping.setApplicationContext(this.context);
mapping.setPrefix("/pre");
mapping.afterPropertiesSet();
assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/pre/test")).getHandler(),
equalTo((Object) new HandlerMethod(controller, this.method)));
assertThat(mapping.getHandler(new MockHttpServletRequest("GET", "/pre/noop")), nullValue());
}
private static class TestController {
@RequestMapping("/test")
public Object invoke() {
return null;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment