Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion docs/modules/ROOT/pages/spring-cloud-netflix.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, InstanceStatus> mapping = new HashMap<>();

public Map<String, InstanceStatus> getMapping() {
return mapping;
}

public void setMapping(Map<String, InstanceStatus> mapping) {
this.mapping = mapping;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -83,6 +84,8 @@ public class EurekaHealthCheckHandler

private final Map<String, HealthContributor> healthContributors = new HashMap<>();

private final EurekaClientStatusMappingProperties properties;

/**
* {@code true} until the context is stopped.
*/
Expand All @@ -91,9 +94,14 @@ public class EurekaHealthCheckHandler
private final Map<String, ReactiveHealthContributor> 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
Expand Down Expand Up @@ -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<String, InstanceStatus> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down