diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 243db5b0cb..e2cc0b181b 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -399,6 +399,73 @@ it can use the domain name from the server hostname as a proxy for the zone. If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). We take `eureka.client.availabilityZones`, which is a map from region name to a list of zones, and pull out the first zone for the instance's own region (that is, the `eureka.client.region`, which defaults to "us-east-1", for compatibility with native Netflix). +The following sections provide additional details and practical guidance on using Spring Cloud LoadBalancer with Eureka. + +==== How Load Balancing Works + +Spring Cloud LoadBalancer works together with Eureka to provide client-side load balancing. + +When a client makes a request using a service ID (for example, `http://STORES/api`), the following steps occur: + +---- +Client → DiscoveryClient → LoadBalancer → ServiceInstance → Request Execution +---- + +1. The client uses a logical service name instead of a fixed URL. +2. The `DiscoveryClient` retrieves all available instances from Eureka. +3. Spring Cloud LoadBalancer selects one instance. +4. The request is sent to the selected instance. + +==== Example Usage + +[source,java] +---- +@Autowired +private RestTemplate restTemplate; + +public String callService() { + return restTemplate.getForObject("http://STORES/api", String.class); +} +---- + +In this example: +- `STORES` is resolved using Eureka. +- A service instance is selected using Spring Cloud LoadBalancer. + +==== Zone Preference + +In distributed systems, services are often deployed across multiple zones. + +Setting zone metadata helps LoadBalancer prefer instances in the same zone: + +---- +eureka.instance.metadataMap.zone=zone1 +---- + +This reduces latency and improves fault tolerance. + +==== Troubleshooting + +Common issues: + +*No instances found* +- Ensure the service is registered with Eureka. +- Verify `spring.application.name`. + +*Load balancing not working* +- Ensure Spring Cloud LoadBalancer dependency is present. + +*Incorrect zone selection* +- Check `metadataMap.zone` configuration. +- Ensure consistent zone naming. + +==== Best Practices + +- Always define `spring.application.name`. +- Use zone metadata in multi-zone deployments. +- Avoid hardcoding service URLs. +- Monitor logs for debugging load balancing behavior. + === AOT and Native Image Support Spring Cloud Netflix Eureka Client integration supports Spring AOT transformations and native images, however, only with refresh mode disabled. @@ -419,7 +486,6 @@ To include Eureka Server in your project, use the starter with a group ID of `or See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train. NOTE: If your project already uses Thymeleaf as its template engine, the Freemarker templates of the Eureka server may not be loaded correctly. In this case it is necessary to configure the template loader manually: - .application.yml ---- spring: diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientStatusMappingProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientStatusMappingProperties.java new file mode 100644 index 0000000000..d7606d48cd --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientStatusMappingProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-present 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.netflix.eureka; + +import java.util.HashMap; +import java.util.Map; + +import com.netflix.appinfo.InstanceInfo.InstanceStatus; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("eureka.client.status.mapping") +public class EurekaClientStatusMappingProperties { + + private Map mapping = new HashMap<>(); + + public Map getMapping() { + return mapping; + } + + public void setMapping(Map mapping) { + this.mapping = mapping; + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandler.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandler.java index ff89ef7f94..fbb2106abf 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandler.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandler.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -83,6 +84,8 @@ public class EurekaHealthCheckHandler private final Map healthContributors = new HashMap<>(); + private final EurekaClientStatusMappingProperties properties; + /** * {@code true} until the context is stopped. */ @@ -91,9 +94,14 @@ public class EurekaHealthCheckHandler private final Map reactiveHealthContributors = new HashMap<>(); public EurekaHealthCheckHandler(StatusAggregator statusAggregator) { + this(statusAggregator, new EurekaClientStatusMappingProperties()); + } + + public EurekaHealthCheckHandler(StatusAggregator statusAggregator, EurekaClientStatusMappingProperties properties) { + this.statusAggregator = statusAggregator; + this.properties = properties; Assert.notNull(statusAggregator, "StatusAggregator must not be null"); - } @Override @@ -179,9 +187,27 @@ else if (contributor instanceof ReactiveHealthIndicator) { } protected InstanceStatus mapToInstanceStatus(Status status) { + + if (status == null) { + return InstanceStatus.UNKNOWN; + } + + String statusCode = status.getCode().toLowerCase(Locale.ROOT); + + // 🔥 Custom mapping (case-insensitive) + if (properties != null && properties.getMapping() != null) { + for (Map.Entry entry : properties.getMapping().entrySet()) { + if (entry.getKey().equalsIgnoreCase(statusCode)) { + return entry.getValue(); + } + } + } + + // 🔁 Default mapping if (!STATUS_MAPPING.containsKey(status)) { return InstanceStatus.UNKNOWN; } + return STATUS_MAPPING.get(status); } diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandlerTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandlerTests.java index f4eacdee14..4a64b14e06 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandlerTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaHealthCheckHandlerTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.health.contributor.HealthContributors; import org.springframework.boot.health.contributor.HealthIndicator; import org.springframework.boot.health.contributor.ReactiveHealthIndicator; +import org.springframework.boot.health.contributor.Status; import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator; import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthContributor; import org.springframework.cloud.client.discovery.health.DiscoveryHealthIndicator; @@ -161,6 +162,31 @@ void testCompositeComponentsOneDown() { assertThat(status).isEqualTo(InstanceStatus.DOWN); } + @Test + void testCustomStatusMapping() { + + EurekaClientStatusMappingProperties props = new EurekaClientStatusMappingProperties(); + props.getMapping().put("fatal", InstanceStatus.OUT_OF_SERVICE); + + EurekaHealthCheckHandler handler = new EurekaHealthCheckHandler(new SimpleStatusAggregator(), props); + + Status status = new Status("fatal"); + + InstanceStatus result = handler.mapToInstanceStatus(status); + + assertThat(result).isEqualTo(InstanceStatus.OUT_OF_SERVICE); + } + + @Test + void testNullStatusReturnsUnknown() { + + EurekaHealthCheckHandler handler = new EurekaHealthCheckHandler(new SimpleStatusAggregator()); + + InstanceStatus result = handler.mapToInstanceStatus(null); + + assertThat(result).isEqualTo(InstanceStatus.UNKNOWN); + } + private void initialize(Class... configurations) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(configurations); healthCheckHandler.setApplicationContext(applicationContext);