Commit 931ab9f9 by Johannes Edmeier

Merge branch '1.3.x'

parents 1de7d9d3 6aac13f1
......@@ -95,8 +95,11 @@ spring.boot.admin.url=http://localhost:8080
[[discover-clients-via-spring-cloud-discovery]]
==== Discover clients via Spring Cloud Discovery ====
If you already using Spring Cloud Discovery for your applications you don't have to add the Spring Boot Admin Client to your applications. Just make the Spring Boot Admin Server a DiscoveryClient, the rest is done by our AutoConfiguration. The following steps are for using Eureka.
Also have a look at the http://projects.spring.io/spring-cloud/spring-cloud.html#_spring_cloud_netflix[Spring Cloud Netflix documentation].
If you already using Spring Cloud Discovery for your applications you don't have to add the Spring Boot Admin Client to your applications. Just make the Spring Boot Admin Server a DiscoveryClient, the rest is done by our AutoConfiguration.
The following steps are for using Eureka, but other Spring Cloud Discovery implementations are supported as well. There is an example using Consul (see https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-consul/[spring-boot-admin-sample-consul]).
Also have a look at the http://projects.spring.io/spring-cloud/spring-cloud.html[Spring Cloud documentation].
. Add spring-cloud-starter-eureka to you dependencies:
+
......@@ -106,7 +109,6 @@ Also have a look at the http://projects.spring.io/spring-cloud/spring-cloud.html
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.0.6.RELEASE</version>
</dependency>
----
......@@ -133,7 +135,7 @@ public class SpringBootAdminApplication {
eureka.instance.client.serviceUrl.defaultZone: http://localhost:8761/eureka/
----
See also https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-discovery/[spring-boot-admin-sample-discovery].
See also https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-eureka/[spring-boot-admin-sample-eureka].
NOTE: You can include the Spring Boot Admin to your Eureka server. Add the dependencies, add `@EnableAdminServer` to your configuration and set `spring.boot.admin.context-path` to something different than `"/"` so that the Spring Boot Admin Server UI won't clash with Eureka's one.
......@@ -270,9 +272,7 @@ The setup is explained <<discover-clients-via-spring-cloud-discovery,above>>.
The informations from the discovered services are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration. You can use your own conversion by implementing the interface and adding the bean to your application context.
TIP: When *Eureka* discovery is active, the `EurekaServiceInstanceConverter` will use the discovered instances' `homePageUrl` and `healthCheckUrl`. In case the instances' `managment.context-path` is different from the `homePageUrl` you should add an entry `management.context-path` to the instances' `metadata`-map with the corresponding value.
TIP: When the default conversion kicks in, you can use the `spring.boot.admin.discovery.converter.*` properties to control the conversion for all your instances.
TIP: If you want to customize the default conversion of services you can either add `health.path` and/or `mangament.context-path` entries to the services metadata. This allows you to set the health or management path per application. In case you want to configure this for all of your discovered services, you can use the `spring.boot.admin.discovery.converter.*`-properties for your Spring Boot Admin Server configuration. The services' metadata takes precedence over the server configuration.
.Discovery configuration options
|===
......@@ -282,10 +282,6 @@ TIP: When the default conversion kicks in, you can use the `spring.boot.admin.di
| Enables the DiscoveryClient-support for the admin server.
| `true`
| spring.boot.admin.discovery.management.context-path _(deprecated)_
| If set this will be appended to the service-url from the discovery information.
|
| spring.boot.admin.discovery.converter.management-context-path
| Will be appended to the service-url of the discovered service when the managment-url is converted by the `DefaultServiceInstanceConverter`.
|
......@@ -293,6 +289,10 @@ TIP: When the default conversion kicks in, you can use the `spring.boot.admin.di
| spring.boot.admin.discovery.converter.health-endpoint
| Will be appended to the management-url of the discovered service when the health-url is converted by the `DefaultServiceInstanceConverter`.
| `"health"`
| spring.boot.admin.discovery.ignored-services
| This services will be ignored when using discovery and not registered as application.
|
|===
[[hazelcast-support]]
......
......@@ -16,7 +16,8 @@
<modules>
<module>spring-boot-admin-sample</module>
<module>spring-boot-admin-sample-war</module>
<module>spring-boot-admin-sample-discovery</module>
<module>spring-boot-admin-sample-eureka</module>
<module>spring-boot-admin-sample-consul</module>
<module>spring-boot-admin-sample-hazelcast</module>
</modules>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-samples</artifactId>
<version>1.4.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-boot-admin-sample-consul</artifactId>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>de.codecentric.boot.admin.SpringBootAdminApplication</mainClass>
<addResources>false</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
info:
version: 1.0.0
spring:
application:
name: consul-example
cloud:
config:
enabled: false
consul:
host: localhost
port: 8500
discovery:
tags: management.context-path=/foo, health.path=/ping
management.context-path: /foo
endpoints.health.path: /ping
spring.boot.admin.discovery:
ignored-services: consul
......@@ -7,7 +7,7 @@
<version>1.4.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-boot-admin-sample-discovery</artifactId>
<artifactId>spring-boot-admin-sample-eureka</artifactId>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
......
/*
* 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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;
import de.codecentric.boot.admin.config.EnableAdminServer;
@Configuration
@EnableAutoConfiguration
@EnableDiscoveryClient
@EnableAdminServer
public class SpringBootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAdminApplication.class, args);
}
}
......@@ -3,7 +3,7 @@ info:
spring:
application:
name: discovery-example
name: eureka-example
cloud:
config:
enabled: false
......@@ -13,4 +13,4 @@ eureka:
leaseRenewalIntervalInSeconds: 5
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
\ No newline at end of file
defaultZone: http://localhost:8761/eureka/
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<jmxConfigurator/>
</configuration>
\ No newline at end of file
......@@ -49,6 +49,7 @@ public class DiscoveryClientConfiguration {
@Bean
@ConditionalOnMissingBean
@ConfigurationProperties(prefix = "spring.boot.admin.discovery")
public ApplicationDiscoveryListener applicationDiscoveryListener(
ServiceInstanceConverter serviceInstanceConverter) {
ApplicationDiscoveryListener listener = new ApplicationDiscoveryListener(discoveryClient,
......@@ -62,6 +63,7 @@ public class DiscoveryClientConfiguration {
public static class EurekaConverterConfiguration {
@Bean
@ConditionalOnMissingBean({ ServiceInstanceConverter.class })
@ConfigurationProperties(prefix = "spring.boot.admin.discovery.converter")
public EurekaServiceInstanceConverter serviceInstanceConverter() {
return new EurekaServiceInstanceConverter();
}
......
......@@ -15,6 +15,11 @@
*/
package de.codecentric.boot.admin.discovery;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
......@@ -32,12 +37,18 @@ import de.codecentric.boot.admin.registry.ApplicationRegistry;
* @author Johannes Edmeier
*/
public class ApplicationDiscoveryListener {
private static final Logger LOGGER = LoggerFactory
.getLogger(ApplicationDiscoveryListener.class);
private final DiscoveryClient discoveryClient;
private final ApplicationRegistry registry;
private final HeartbeatMonitor monitor = new HeartbeatMonitor();
private ServiceInstanceConverter converter = new DefaultServiceInstanceConverter();
/**
* Set of serviceIds to be ignored and not to be registered as application.
*/
private Set<String> ignoredServices = new HashSet<>();
public ApplicationDiscoveryListener(DiscoveryClient discoveryClient,
ApplicationRegistry registry) {
this.discoveryClient = discoveryClient;
......@@ -68,16 +79,31 @@ public class ApplicationDiscoveryListener {
protected void discover() {
for (String serviceId : discoveryClient.getServices()) {
for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
registry.register(convert(instance));
if (!ignoredServices.contains(serviceId)) {
register(instance);
}
}
}
}
protected Application convert(ServiceInstance instance) {
return converter.convert(instance);
protected void register(ServiceInstance instance) {
try {
Application application = converter.convert(instance);
if (application != null) {
registry.register(application);
} else {
LOGGER.warn("No application for service {} registered", instance);
}
} catch (Exception ex) {
LOGGER.error("Couldn't register application for service {}", instance, ex);
}
}
public void setConverter(ServiceInstanceConverter converter) {
this.converter = converter;
}
}
\ No newline at end of file
public void setIgnoredServices(Set<String> ignoredServices) {
this.ignoredServices = ignoredServices;
}
}
......@@ -15,43 +15,76 @@
*/
package de.codecentric.boot.admin.discovery;
import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang.StringUtils.stripStart;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import de.codecentric.boot.admin.model.Application;
/**
* Converts any {@link ServiceInstance}s to {@link Application}s.
* Converts any {@link ServiceInstance}s to {@link Application}s. To customize the health- or
* management-url for all applications you can set healthEndpointPath or managementContextPath
* respectively. If you want to influence the url per service you can add
* <code>management.context-path</code> or <code>health.path</code> to the instances metadata.
*
* @author Johannes Edmeier
*/
public class DefaultServiceInstanceConverter implements ServiceInstanceConverter {
private static final String KEY_MANAGEMENT_PATH = "management.context-path";
private static final String KEY_HEALTH_PATH = "health.path";
private String managementContextPath = "";
private String healthEndpointPath = "health";
@Override
public Application convert(ServiceInstance instance) {
String serviceUrl = instance.getUri().toString();
String managementUrl = append(serviceUrl, managementContextPath);
String healthUrl = append(managementUrl, healthEndpointPath);
Application.Builder builder = Application.create(instance.getServiceId());
URI healthUrl = getHealthUrl(instance);
if (healthUrl != null) {
builder.withHealthUrl(healthUrl.toString());
}
return Application.create(instance.getServiceId()).withHealthUrl(healthUrl)
.withManagementUrl(managementUrl).withServiceUrl(serviceUrl).build();
}
URI managementUrl = getManagementUrl(instance);
if (managementUrl != null) {
builder.withManagementUrl(managementUrl.toString());
}
protected final String append(String uri, String path) {
String baseUri = uri.replaceFirst("/+$", "");
if (StringUtils.isEmpty(path)) {
return baseUri;
URI serviceUrl = getServiceUrl(instance);
if (serviceUrl != null) {
builder.withServiceUrl(serviceUrl.toString());
}
String normPath = path.replaceFirst("^/+", "").replaceFirst("/+$", "");
return baseUri + "/" + normPath;
return builder.build();
}
protected URI getHealthUrl(ServiceInstance instance) {
String healthPath = defaultIfEmpty(
instance.getMetadata().get(KEY_HEALTH_PATH), healthEndpointPath);
healthPath = stripStart(healthPath, "/");
return UriComponentsBuilder.fromUri(getManagementUrl(instance)).pathSegment(healthPath)
.build().toUri();
}
protected URI getManagementUrl(ServiceInstance instance) {
String managamentPath = defaultIfEmpty(
instance.getMetadata().get(KEY_MANAGEMENT_PATH), managementContextPath);
managamentPath = stripStart(managamentPath, "/");
return UriComponentsBuilder.fromUri(getServiceUrl(instance)).pathSegment(managamentPath)
.build().toUri();
}
protected URI getServiceUrl(ServiceInstance instance) {
return instance.getUri();
}
/**
* <code>management.context-path</code> to be appended to the url of the discovered service for
* the managment-url.
* Default <code>management.context-path</code> to be appended to the url of the discovered
* service for the managment-url.
*
* @param managementContextPath the management context-path.
*/
......@@ -60,7 +93,7 @@ public class DefaultServiceInstanceConverter implements ServiceInstanceConverter
}
/**
* path of the health-endpoint to be used for the health-url of the discovered service.
* Default path of the health-endpoint to be used for the health-url of the discovered service.
*
* @param healthEndpointPath the path for the health-endpoint.
*/
......
......@@ -15,12 +15,11 @@
*/
package de.codecentric.boot.admin.discovery;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient.EurekaServiceInstance;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.netflix.appinfo.InstanceInfo;
import de.codecentric.boot.admin.model.Application;
......@@ -29,35 +28,13 @@ import de.codecentric.boot.admin.model.Application;
*
* @author Johannes Edmeier
*/
public class EurekaServiceInstanceConverter implements ServiceInstanceConverter {
public class EurekaServiceInstanceConverter extends DefaultServiceInstanceConverter {
@Override
public Application convert(ServiceInstance instance) {
protected URI getHealthUrl(ServiceInstance instance) {
Assert.isInstanceOf(EurekaServiceInstance.class, instance,
"serviceInstance must be of type EurekaServiceInstance");
return convert(((EurekaServiceInstance) instance).getInstanceInfo());
}
private Application convert(InstanceInfo instanceInfo) {
String mgmtUrl = instanceInfo.getHomePageUrl();
String mgmtPath = instanceInfo.getMetadata().get("management.context-path");
if (StringUtils.hasText(mgmtPath)) {
mgmtUrl = append(mgmtUrl, mgmtPath);
}
return Application.create(instanceInfo.getAppName())
.withHealthUrl(instanceInfo.getHealthCheckUrl()).withManagementUrl(mgmtUrl)
.withServiceUrl(instanceInfo.getHomePageUrl()).build();
}
private String append(String mgmtUrl, String mgmtPath) {
if (mgmtUrl.endsWith("/")) {
mgmtUrl = mgmtUrl.substring(0, mgmtUrl.length() - 1);
}
if (!mgmtPath.startsWith("/")) {
mgmtPath = "/" + mgmtPath;
}
return mgmtUrl + mgmtPath;
return URI.create(((EurekaServiceInstance) instance).getInstanceInfo().getHealthCheckUrl());
}
}
......@@ -24,14 +24,5 @@
"type": "java.lang.Boolean",
"description": "Enable Spring Cloud Discovery support.",
"defaultValue": "true"
},
{
"name": "spring.boot.admin.discovery.management.context-path",
"type": "java.lang.String",
"description": "management-path suffix for discovered applications",
"defaultValue": "",
"deprecation" : {
"replacement" : "spring.boot.admin.discovery.converter.management-context-path"
}
}
]}
......@@ -51,6 +51,18 @@ public class ApplicationDiscoveryListenerTest {
}
@Test
public void test_ignore() {
when(discovery.getServices()).thenReturn(Collections.singletonList("service"));
when(discovery.getInstances("service")).thenReturn(Collections.singletonList(
(ServiceInstance) new DefaultServiceInstance("service", "localhost", 80, false)));
listener.setIgnoredServices(Collections.singleton("service"));
listener.onInstanceRegistered(new InstanceRegisteredEvent<>(new Object(), null));
assertEquals(0, registry.getApplications().size());
}
@Test
public void test_register_and_convert() {
when(discovery.getServices()).thenReturn(Collections.singletonList("service"));
when(discovery.getInstances("service")).thenReturn(Collections.singletonList(
......
......@@ -13,7 +13,19 @@ import de.codecentric.boot.admin.model.Application;
public class DefaultServiceInstanceConverterTest {
@Test
public void convert() {
public void test_convert_with_defaults() {
ServiceInstance service = new DefaultServiceInstance("test", "localhost", 80, false);
Application application = new DefaultServiceInstanceConverter().convert(service);
assertThat(application.getId(), nullValue());
assertThat(application.getName(), is("test"));
assertThat(application.getServiceUrl(), is("http://localhost:80"));
assertThat(application.getManagementUrl(), is("http://localhost:80"));
assertThat(application.getHealthUrl(), is("http://localhost:80/health"));
}
@Test
public void test_convert_with_custom_defaults() {
DefaultServiceInstanceConverter converter = new DefaultServiceInstanceConverter();
converter.setHealthEndpointPath("ping");
converter.setManagementContextPath("mgmt");
......@@ -28,4 +40,19 @@ public class DefaultServiceInstanceConverterTest {
assertThat(application.getHealthUrl(), is("http://localhost:80/mgmt/ping"));
}
@Test
public void test_convert_with_metadata() {
ServiceInstance service = new DefaultServiceInstance("test", "localhost", 80, false);
service.getMetadata().put("health.path", "ping");
service.getMetadata().put("management.context-path", "mgmt");
Application application = new DefaultServiceInstanceConverter().convert(service);
assertThat(application.getId(), nullValue());
assertThat(application.getName(), is("test"));
assertThat(application.getServiceUrl(), is("http://localhost:80"));
assertThat(application.getManagementUrl(), is("http://localhost:80/mgmt"));
assertThat(application.getHealthUrl(), is("http://localhost:80/mgmt/ping"));
}
}
......@@ -7,6 +7,7 @@ import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.URI;
import java.util.Collections;
import org.junit.Test;
......@@ -20,17 +21,15 @@ public class EurekaServiceInstanceConverterTest {
@Test
public void convert() {
EurekaServiceInstanceConverter converter = new EurekaServiceInstanceConverter();
InstanceInfo instanceInfo = mock(InstanceInfo.class);
when(instanceInfo.getAppName()).thenReturn("test");
when(instanceInfo.getHomePageUrl()).thenReturn("http://localhost:80");
when(instanceInfo.getHealthCheckUrl()).thenReturn("http://localhost:80/mgmt/ping");
when(instanceInfo.getMetadata())
.thenReturn(singletonMap("management.context-path", "/mgmt"));
EurekaServiceInstance service = mock(EurekaServiceInstance.class);
when(service.getInstanceInfo()).thenReturn(instanceInfo);
when(service.getUri()).thenReturn(URI.create("http://localhost:80"));
when(service.getServiceId()).thenReturn("test");
when(service.getMetadata()).thenReturn(singletonMap("management.context-path", "/mgmt"));
Application application = converter.convert(service);
Application application = new EurekaServiceInstanceConverter().convert(service);
assertThat(application.getId(), nullValue());
assertThat(application.getName(), is("test"));
......@@ -39,8 +38,8 @@ public class EurekaServiceInstanceConverterTest {
assertThat(application.getHealthUrl(), is("http://localhost:80/mgmt/ping"));
// no management url in metadata
when(instanceInfo.getMetadata()).thenReturn(Collections.<String, String> emptyMap());
application = converter.convert(service);
when(service.getMetadata()).thenReturn(Collections.<String, String> emptyMap());
application = new EurekaServiceInstanceConverter().convert(service);
assertThat(application.getManagementUrl(), is("http://localhost:80"));
}
}
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