Compare commits
2 Commits
main
...
disc-lb-en
Author | SHA1 | Date |
---|---|---|
|
1de7b0cfdb | 5 years ago |
|
388ea7b8e9 | 5 years ago |
10 changed files with 475 additions and 9 deletions
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.client.discovery.endpoint; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; |
||||||
|
|
||||||
|
@Endpoint(id = "discovery") |
||||||
|
public class DiscoveryClientEndpoint { |
||||||
|
|
||||||
|
private final ReactiveDiscoveryClient discoveryClient; |
||||||
|
|
||||||
|
public DiscoveryClientEndpoint(ReactiveDiscoveryClient discoveryClient) { |
||||||
|
this.discoveryClient = discoveryClient; |
||||||
|
} |
||||||
|
|
||||||
|
@ReadOperation |
||||||
|
public Mono<List<String>> services() { |
||||||
|
return this.discoveryClient.getServices().collectList(); |
||||||
|
} |
||||||
|
|
||||||
|
@ReadOperation |
||||||
|
public Mono<List<ServiceInstance>> instances(@Selector String serviceId) { |
||||||
|
Flux<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId); |
||||||
|
return instances.collectList(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.client.discovery.endpoint; |
||||||
|
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; |
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter; |
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||||
|
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; |
||||||
|
import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false) |
||||||
|
@ConditionalOnClass(Endpoint.class) |
||||||
|
@AutoConfigureAfter(CompositeDiscoveryClientAutoConfiguration.class) |
||||||
|
public class DiscoveryClientEndpointAutoConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
@ConditionalOnAvailableEndpoint |
||||||
|
@ConditionalOnBean(ReactiveDiscoveryClient.class) |
||||||
|
public DiscoveryClientEndpoint discoveryClientEndpoint( |
||||||
|
ReactiveDiscoveryClient discoveryClient) { |
||||||
|
return new DiscoveryClientEndpoint(discoveryClient); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.client.loadbalancer.reactive.endpoint; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; |
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; |
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer.Factory; |
||||||
|
|
||||||
|
@Endpoint(id = "loadbalancer") |
||||||
|
public class LoadBalancerEndpoint { |
||||||
|
|
||||||
|
private final ObjectProvider<Factory<ServiceInstance>> clientFactory; |
||||||
|
|
||||||
|
public LoadBalancerEndpoint(ObjectProvider<Factory<ServiceInstance>> clientFactory) { |
||||||
|
this.clientFactory = clientFactory; |
||||||
|
} |
||||||
|
|
||||||
|
@ReadOperation |
||||||
|
public Mono<ServiceInstance> choose(@Selector String serviceId, |
||||||
|
@Selector String operation) { |
||||||
|
if (!"choose".equalsIgnoreCase(operation)) { |
||||||
|
return Mono.error( |
||||||
|
new IllegalArgumentException("Unknown operation: " + operation)); |
||||||
|
} |
||||||
|
Factory<ServiceInstance> factory = this.clientFactory.getIfAvailable(); |
||||||
|
if (factory == null) { |
||||||
|
return Mono.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
ReactiveLoadBalancer<ServiceInstance> loadBalancer = factory |
||||||
|
.getInstance(serviceId); |
||||||
|
return Mono.from(loadBalancer.choose()).flatMap(response -> { |
||||||
|
if (response.hasServer()) { |
||||||
|
return Mono.just(response.getServer()); |
||||||
|
} |
||||||
|
return Mono.empty(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.commons.util; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; |
||||||
|
import org.springframework.boot.actuate.health.Health; |
||||||
|
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; |
||||||
|
import org.springframework.web.reactive.function.client.WebClient; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link ReactiveHealthIndicator} that checks the configured {@link WebClient} for the |
||||||
|
* Spring Boot Health status format. |
||||||
|
* @author Spencer Gibb |
||||||
|
* @author Fabrizio Di Napoli |
||||||
|
*/ |
||||||
|
public class UrlHealthIndicator extends AbstractReactiveHealthIndicator { |
||||||
|
|
||||||
|
private final WebClient webClient; |
||||||
|
|
||||||
|
public UrlHealthIndicator(WebClient webClient) { |
||||||
|
this.webClient = webClient; |
||||||
|
} |
||||||
|
|
||||||
|
public UrlHealthIndicator(WebClient.Builder builder, String baseUrl) { |
||||||
|
this.webClient = builder.baseUrl(baseUrl).build(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Mono<Health> doHealthCheck(Health.Builder builder) { |
||||||
|
if (this.webClient == null) { |
||||||
|
return Mono.just(builder.up().build()); |
||||||
|
} |
||||||
|
|
||||||
|
return webClient.get().retrieve().bodyToMono(Map.class).map(map -> { |
||||||
|
Object status = map.get("status"); |
||||||
|
if (status instanceof String) { |
||||||
|
return builder.status(status.toString()).build(); |
||||||
|
} |
||||||
|
else { |
||||||
|
return builder.unknown() |
||||||
|
.withDetail("warning", "no status field in response").build(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.client.discovery.endpoint; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.SpringBootConfiguration; |
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||||
|
import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance; |
||||||
|
import org.springframework.test.context.ActiveProfiles; |
||||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||||
|
|
||||||
|
@RunWith(SpringRunner.class) |
||||||
|
@SpringBootTest(properties = { "management.endpoints.web.exposure.include=discovery" }, |
||||||
|
webEnvironment = RANDOM_PORT) |
||||||
|
@ActiveProfiles("lbendpoint") |
||||||
|
public class DiscoveryClientEndpointTests { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private WebTestClient client; |
||||||
|
|
||||||
|
@Test |
||||||
|
public void servicesWorks() { |
||||||
|
client.get().uri("/actuator/discovery").exchange().expectBody(List.class) |
||||||
|
.consumeWith(result -> { |
||||||
|
List<String> body = result.getResponseBody(); |
||||||
|
assertThat(body).contains("myservice", "anotherservice", |
||||||
|
"thirdservice"); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void instancesWorks() { |
||||||
|
client.get().uri("/actuator/discovery/myservice").exchange() |
||||||
|
.expectBodyList(SimpleServiceInstance.class).consumeWith(result -> { |
||||||
|
List body = result.getResponseBody(); |
||||||
|
assertThat(body).isNotEmpty(); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void noInstancesWorks() { |
||||||
|
client.get().uri("/actuator/discovery/unknownservice").exchange() |
||||||
|
.expectBodyList(SimpleServiceInstance.class).consumeWith(result -> { |
||||||
|
List body = result.getResponseBody(); |
||||||
|
assertThat(body).isEmpty(); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@SpringBootConfiguration |
||||||
|
@EnableAutoConfiguration |
||||||
|
protected static class TestConfiguration { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2013-2020 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 |
||||||
|
* |
||||||
|
* https://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 org.springframework.cloud.client.loadbalancer.reactive.endpoint; |
||||||
|
|
||||||
|
import java.util.Random; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.SpringBootConfiguration; |
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; |
||||||
|
import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryProperties.SimpleServiceInstance; |
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.CompletionContext; |
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; |
||||||
|
import org.springframework.cloud.client.loadbalancer.reactive.Response; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.test.context.ActiveProfiles; |
||||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||||
|
|
||||||
|
@RunWith(SpringRunner.class) |
||||||
|
@SpringBootTest(properties = { "management.endpoints.web.exposure.include=loadbalancer" }, |
||||||
|
webEnvironment = RANDOM_PORT) |
||||||
|
@ActiveProfiles("lbendpoint") |
||||||
|
public class LoadBalancerEndpointTests { |
||||||
|
|
||||||
|
private static final Random random = new Random(); |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private WebTestClient client; |
||||||
|
|
||||||
|
@Test |
||||||
|
public void chooseWorks() { |
||||||
|
client.get().uri("/actuator/loadbalancer/myservice/choose").exchange() |
||||||
|
.expectBody(SimpleServiceInstance.class).consumeWith(result -> { |
||||||
|
ServiceInstance body = result.getResponseBody(); |
||||||
|
assertThat(body).isNotNull(); |
||||||
|
assertThat(body.getHost()).isNotEmpty(); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void chooseWorksWithoutInstances() { |
||||||
|
client.get().uri("/actuator/loadbalancer/unknownservice/choose").exchange() |
||||||
|
.expectBody(SimpleServiceInstance.class).consumeWith(result -> { |
||||||
|
ServiceInstance body = result.getResponseBody(); |
||||||
|
assertThat(body).isNull(); |
||||||
|
// TODO: web response 404?
|
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static Mono<Response<ServiceInstance>> choose(ReactiveDiscoveryClient discoveryClient, |
||||||
|
String serviceId) { |
||||||
|
return discoveryClient.getInstances(serviceId).collectList().map(instances -> { |
||||||
|
if (instances.isEmpty()) { |
||||||
|
return new InstanceResponse(null); |
||||||
|
} |
||||||
|
int idx = random.nextInt(instances.size()); |
||||||
|
ServiceInstance instance = instances.get(idx); |
||||||
|
return new InstanceResponse(instance); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private static final class InstanceResponse implements Response<ServiceInstance> { |
||||||
|
|
||||||
|
private final ServiceInstance instance; |
||||||
|
|
||||||
|
private InstanceResponse(ServiceInstance instance) { |
||||||
|
this.instance = instance; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasServer() { |
||||||
|
return this.instance != null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ServiceInstance getServer() { |
||||||
|
return this.instance; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onComplete(CompletionContext completionContext) { |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@SpringBootConfiguration |
||||||
|
@EnableAutoConfiguration |
||||||
|
protected static class TestConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory( |
||||||
|
ReactiveDiscoveryClient discoveryClient) { |
||||||
|
return serviceId -> (ReactiveLoadBalancer<ServiceInstance>) request -> choose( |
||||||
|
discoveryClient, serviceId); |
||||||
|
}; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
spring: |
||||||
|
cloud: |
||||||
|
discovery: |
||||||
|
client: |
||||||
|
simple: |
||||||
|
instances: |
||||||
|
myservice: |
||||||
|
- service-id: myservice |
||||||
|
uri: http://ahost:8080 |
||||||
|
- service-id: myservice |
||||||
|
uri: http://chost:8081 |
||||||
|
- service-id: myservice |
||||||
|
uri: https://bhostsecure:8082 |
||||||
|
anotherservice: |
||||||
|
- service-id: myservice |
||||||
|
uri: http://dhost:8180 |
||||||
|
- service-id: myservice |
||||||
|
uri: https://fhost:8181 |
||||||
|
- service-id: myservice |
||||||
|
uri: http://ehost:8182 |
||||||
|
thirdservice: |
||||||
|
- service-id: myservice |
||||||
|
uri: http://ghost:8280 |
||||||
|
- service-id: myservice |
||||||
|
uri: http://hhost:8281 |
||||||
|
- service-id: myservice |
||||||
|
uri: http://ihost:8282 |
Loading…
Reference in new issue