From 1125c67f29e1d4694d88c6d0bd81323cf394d651 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Thu, 2 Apr 2026 16:18:49 +0530 Subject: [PATCH 1/3] Improve documentation for Load Balancing with Eureka and Spring Cloud LoadBalancer Signed-off-by: 98001yash --- .../ROOT/pages/spring-cloud-netflix.adoc | 133 +++++++++++++++++- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 243db5b0cb..5d36820ca8 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -389,15 +389,134 @@ the ability to refresh Eureka clients. To do this set `eureka.client.refresh.en === Using Eureka with Spring Cloud LoadBalancer -We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`. -The `zone` value from the Eureka instance metadata (`eureka.instance.metadataMap.zone`) is used for setting the -value of `spring-cloud-loadbalancer-zone` property that is used to filter service instances by zone. +Spring Cloud Netflix integrates with Spring Cloud LoadBalancer to provide +client-side load balancing for services registered with Eureka. -If that is missing and if the `spring.cloud.loadbalancer.eureka.approximateZoneFromHostname` flag is set to `true`, -it can use the domain name from the server hostname as a proxy for the zone. +When a client makes a request to another service using a logical service ID, +Spring Cloud LoadBalancer selects an appropriate service instance based on +available instances retrieved from Eureka. -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). +==== How It Works + +The load balancing process follows these steps: + +1. A client makes a request using a service ID (for example, `http://user-service`). +2. The `DiscoveryClient` retrieves all available instances of the service from Eureka. +3. Spring Cloud LoadBalancer selects one instance using a load balancing strategy. +4. The request is routed to the selected service instance. + +This mechanism enables service discovery and client-side load balancing across multiple instances. + +==== Configuration Example + +The following example shows a minimal configuration for using Eureka with Spring Cloud LoadBalancer: + +.application.yml +[source,yaml] +---- +spring: + application: + name: user-service + cloud: + loadbalancer: + ribbon: + enabled: false + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ +---- + +NOTE: Ribbon is deprecated and replaced by Spring Cloud LoadBalancer. + +==== Using Load-Balanced Clients + +Spring Cloud LoadBalancer can be used with `RestTemplate` or `WebClient`. + +.Example using RestTemplate +[source,java,indent=0] +---- +@Bean +@LoadBalanced +public RestTemplate restTemplate() { + return new RestTemplate(); +} +---- + +.Example using WebClient +[source,java,indent=0] +---- +@Bean +@LoadBalanced +public WebClient.Builder webClientBuilder() { + return WebClient.builder(); +} +---- + +With these configurations, requests made using a service name are automatically resolved +to an actual service instance registered in Eureka. + +==== Zone-Based Load Balancing + +Spring Cloud LoadBalancer supports zone-based routing by using metadata provided +by Eureka instances. + +To define a zone for a service instance, use the following configuration: + +.application.yml +[source,yaml] +---- +eureka: + instance: + metadataMap: + zone: zone1 +---- + +When multiple instances exist across zones, the load balancer can prioritize instances +in the same zone as the client. + +The `ZonePreferenceServiceInstanceListSupplier` is used to filter instances based on zone. + +==== Zone Resolution + +The zone used by the load balancer can be determined in multiple ways: + +- Explicitly via `eureka.instance.metadataMap.zone` +- From the hostname if the following property is enabled: + +[source] +---- +spring.cloud.loadbalancer.eureka.approximateZoneFromHostname=true +---- + +When this property is enabled, the system attempts to infer the zone from the domain name +of the service instance hostname. + +If no zone information is available, the system falls back to the client configuration, +such as `eureka.client.availabilityZones`. + +==== Troubleshooting + +If load balancing does not behave as expected, consider the following: + +- Ensure that all service instances are properly registered in Eureka +- Verify that `metadataMap.zone` is correctly configured +- Check that the Eureka client is able to fetch registry data +- Enable debug logs to inspect load balancer decisions + +==== Best Practices + +- Always define zone metadata for service instances in multi-zone deployments +- Use consistent naming conventions for zones +- Avoid relying solely on hostname-based zone approximation in production +- Monitor service instance health and registration status + +==== Summary + +Spring Cloud LoadBalancer, when combined with Eureka, provides a flexible and dynamic +approach to client-side load balancing. It enables services to discover and communicate +with each other efficiently while distributing traffic across available instances. === AOT and Native Image Support From 1d026163899a04452dfbb73b14af9302933afb47 Mon Sep 17 00:00:00 2001 From: 98001yash Date: Thu, 2 Apr 2026 17:54:08 +0530 Subject: [PATCH 2/3] Add support for custom actuator health status mapping in EurekaHealthCheckHandler Signed-off-by: 98001yash --- .../EurekaClientStatusMappingProperties.java | 39 +++++++++++++++++++ .../eureka/EurekaHealthCheckHandler.java | 28 ++++++++++++- .../eureka/EurekaHealthCheckHandlerTests.java | 26 +++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientStatusMappingProperties.java 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); From 40b6974882264131a27d5bbf4293f62b832f4f3b Mon Sep 17 00:00:00 2001 From: 98001yash Date: Fri, 3 Apr 2026 07:55:41 +0530 Subject: [PATCH 3/3] Revert unrelated documentation changes Signed-off-by: 98001yash --- .../ROOT/pages/spring-cloud-netflix.adoc | 133 +----------------- 1 file changed, 7 insertions(+), 126 deletions(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 5d36820ca8..243db5b0cb 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -389,134 +389,15 @@ the ability to refresh Eureka clients. To do this set `eureka.client.refresh.en === Using Eureka with Spring Cloud LoadBalancer -Spring Cloud Netflix integrates with Spring Cloud LoadBalancer to provide -client-side load balancing for services registered with Eureka. +We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`. +The `zone` value from the Eureka instance metadata (`eureka.instance.metadataMap.zone`) is used for setting the +value of `spring-cloud-loadbalancer-zone` property that is used to filter service instances by zone. -When a client makes a request to another service using a logical service ID, -Spring Cloud LoadBalancer selects an appropriate service instance based on -available instances retrieved from Eureka. +If that is missing and if the `spring.cloud.loadbalancer.eureka.approximateZoneFromHostname` flag is set to `true`, +it can use the domain name from the server hostname as a proxy for the zone. -==== How It Works - -The load balancing process follows these steps: - -1. A client makes a request using a service ID (for example, `http://user-service`). -2. The `DiscoveryClient` retrieves all available instances of the service from Eureka. -3. Spring Cloud LoadBalancer selects one instance using a load balancing strategy. -4. The request is routed to the selected service instance. - -This mechanism enables service discovery and client-side load balancing across multiple instances. - -==== Configuration Example - -The following example shows a minimal configuration for using Eureka with Spring Cloud LoadBalancer: - -.application.yml -[source,yaml] ----- -spring: - application: - name: user-service - cloud: - loadbalancer: - ribbon: - enabled: false - -eureka: - client: - serviceUrl: - defaultZone: http://localhost:8761/eureka/ ----- - -NOTE: Ribbon is deprecated and replaced by Spring Cloud LoadBalancer. - -==== Using Load-Balanced Clients - -Spring Cloud LoadBalancer can be used with `RestTemplate` or `WebClient`. - -.Example using RestTemplate -[source,java,indent=0] ----- -@Bean -@LoadBalanced -public RestTemplate restTemplate() { - return new RestTemplate(); -} ----- - -.Example using WebClient -[source,java,indent=0] ----- -@Bean -@LoadBalanced -public WebClient.Builder webClientBuilder() { - return WebClient.builder(); -} ----- - -With these configurations, requests made using a service name are automatically resolved -to an actual service instance registered in Eureka. - -==== Zone-Based Load Balancing - -Spring Cloud LoadBalancer supports zone-based routing by using metadata provided -by Eureka instances. - -To define a zone for a service instance, use the following configuration: - -.application.yml -[source,yaml] ----- -eureka: - instance: - metadataMap: - zone: zone1 ----- - -When multiple instances exist across zones, the load balancer can prioritize instances -in the same zone as the client. - -The `ZonePreferenceServiceInstanceListSupplier` is used to filter instances based on zone. - -==== Zone Resolution - -The zone used by the load balancer can be determined in multiple ways: - -- Explicitly via `eureka.instance.metadataMap.zone` -- From the hostname if the following property is enabled: - -[source] ----- -spring.cloud.loadbalancer.eureka.approximateZoneFromHostname=true ----- - -When this property is enabled, the system attempts to infer the zone from the domain name -of the service instance hostname. - -If no zone information is available, the system falls back to the client configuration, -such as `eureka.client.availabilityZones`. - -==== Troubleshooting - -If load balancing does not behave as expected, consider the following: - -- Ensure that all service instances are properly registered in Eureka -- Verify that `metadataMap.zone` is correctly configured -- Check that the Eureka client is able to fetch registry data -- Enable debug logs to inspect load balancer decisions - -==== Best Practices - -- Always define zone metadata for service instances in multi-zone deployments -- Use consistent naming conventions for zones -- Avoid relying solely on hostname-based zone approximation in production -- Monitor service instance health and registration status - -==== Summary - -Spring Cloud LoadBalancer, when combined with Eureka, provides a flexible and dynamic -approach to client-side load balancing. It enables services to discover and communicate -with each other efficiently while distributing traffic across available instances. +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). === AOT and Native Image Support