Skip to content

Commit ac59d13

Browse files
Use uriTemplate attribute in metrics (#1422)
1 parent 2890f5f commit ac59d13

File tree

7 files changed

+189
-30
lines changed

7 files changed

+189
-30
lines changed

docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,12 @@ set the value of the `spring.cloud.loadbalancer.stats.micrometer.enabled` to `tr
506506

507507
Additional information regarding the service instances, request data, and response data is added to metrics via tags whenever available.
508508

509+
NOTE: For `WebClient` and `RestClient`-backed load-balancing, we use `uriTemplate` for the `uri` tag whenever available.
510+
511+
TIP: It is possible to disable adding `path` to `uri` tag by setting `spring.cloud.loadbalancer.stats.include-path` to `false`.
512+
513+
WARNING: As with `RestTemplate`-backed load-balancing, we don't have access to `uriTemplate`, full path is always used in the `uri` tag. In order to avoid high cardinality issues, if path is a high cardinality value (for example, `/orders/\{id\}`, where `id` takes a big number of values), it is strongly recommended to disable adding path to `uri` tag by setting `spring.cloud.loadbalancer.stats.include-path` to `false`.
514+
509515
NOTE: For some implementations, such as `BlockingLoadBalancerClient`, request and response data might not be available, as we establish generic types from arguments and might not be able to determine the types and read the data.
510516

511517
NOTE: The meters are registered in the registry when at least one record is added for a given meter.

docs/modules/ROOT/partials/_configprops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
|spring.cloud.loadbalancer.retry.retryable-exceptions | `+++{}+++` | A `Set` of `Throwable` classes that should trigger a retry.
6767
|spring.cloud.loadbalancer.retry.retryable-status-codes | `+++{}+++` | A `Set` of status codes that should trigger a retry.
6868
|spring.cloud.loadbalancer.service-discovery.timeout | | String representation of Duration of the timeout for calls to service discovery.
69+
|spring.cloud.loadbalancer.stats.include-path | `+++true+++` | Indicates whether the {@code path} should be added to {@code uri} tag in metrics. When {@link RestTemplate} is used to execute load-balanced requests with high cardinality paths, setting it to {@code false} is recommended.
6970
|spring.cloud.loadbalancer.stats.micrometer.enabled | `+++false+++` | Enables Spring Cloud LoadBalancer Micrometer stats.
7071
|spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie | `+++false+++` | Indicates whether a cookie with the newly selected instance should be added by LoadBalancer.
7172
|spring.cloud.loadbalancer.sticky-session.instance-id-cookie-name | `+++sc-lb-instance-id+++` | The name of the cookie holding the preferred instance id.

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
3333
import org.springframework.core.env.PropertyResolver;
3434
import org.springframework.http.HttpMethod;
3535
import org.springframework.util.LinkedCaseInsensitiveMap;
36+
import org.springframework.web.client.RestTemplate;
3637

3738
/**
3839
* The base configuration bean for Spring Cloud LoadBalancer.
@@ -90,10 +91,20 @@ public class LoadBalancerProperties {
9091

9192
/**
9293
* Properties for
93-
* {@link org.springframework.cloud.loadbalancer.core.SubsetServiceInstanceListSupplier}.
94+
* {@code org.springframework.cloud.loadbalancer.core.SubsetServiceInstanceListSupplier}.
9495
*/
9596
private Subset subset = new Subset();
9697

98+
/**
99+
* Enabling X-Forwarded Host and Proto Headers.
100+
*/
101+
private XForwarded xForwarded = new XForwarded();
102+
103+
/**
104+
* Properties for LoadBalancer metrics.
105+
*/
106+
private Stats stats = new Stats();
107+
97108
public HealthCheck getHealthCheck() {
98109
return healthCheck;
99110
}
@@ -134,11 +145,6 @@ public void setHintHeaderName(String hintHeaderName) {
134145
this.hintHeaderName = hintHeaderName;
135146
}
136147

137-
/**
138-
* Enabling X-Forwarded Host and Proto Headers.
139-
*/
140-
private XForwarded xForwarded = new XForwarded();
141-
142148
// TODO: fix spelling in a major release
143149
public void setxForwarded(XForwarded xForwarded) {
144150
this.xForwarded = xForwarded;
@@ -164,6 +170,14 @@ public void setCallGetWithRequestOnDelegates(boolean callGetWithRequestOnDelegat
164170
this.callGetWithRequestOnDelegates = callGetWithRequestOnDelegates;
165171
}
166172

173+
public Stats getStats() {
174+
return stats;
175+
}
176+
177+
public void setStats(Stats stats) {
178+
this.stats = stats;
179+
}
180+
167181
public static class StickySession {
168182

169183
/**
@@ -539,4 +553,23 @@ public void setSize(int size) {
539553

540554
}
541555

556+
public static class Stats {
557+
558+
/**
559+
* Indicates whether the {@code path} should be added to {@code uri} tag in
560+
* metrics. When {@link RestTemplate} is used to execute load-balanced requests
561+
* with high cardinality paths, setting it to {@code false} is recommended.
562+
*/
563+
private boolean includePath = true;
564+
565+
public boolean isIncludePath() {
566+
return includePath;
567+
}
568+
569+
public void setIncludePath(boolean includePath) {
570+
this.includePath = includePath;
571+
}
572+
573+
}
574+
542575
}

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerStatsAutoConfiguration.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
2121
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2222
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2323
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
24+
import org.springframework.cloud.client.ServiceInstance;
25+
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
2426
import org.springframework.cloud.loadbalancer.stats.MicrometerStatsLoadBalancerLifecycle;
2527
import org.springframework.context.annotation.Bean;
2628
import org.springframework.context.annotation.Configuration;
@@ -38,8 +40,9 @@ public class LoadBalancerStatsAutoConfiguration {
3840

3941
@Bean
4042
@ConditionalOnBean(MeterRegistry.class)
41-
public MicrometerStatsLoadBalancerLifecycle micrometerStatsLifecycle(MeterRegistry meterRegistry) {
42-
return new MicrometerStatsLoadBalancerLifecycle(meterRegistry);
43+
public MicrometerStatsLoadBalancerLifecycle micrometerStatsLifecycle(MeterRegistry meterRegistry,
44+
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
45+
return new MicrometerStatsLoadBalancerLifecycle(meterRegistry, loadBalancerFactory);
4346
}
4447

4548
}

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/LoadBalancerTags.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,17 @@
1616

1717
package org.springframework.cloud.loadbalancer.stats;
1818

19+
import java.util.Collections;
20+
import java.util.Objects;
21+
import java.util.Optional;
22+
import java.util.Set;
23+
1924
import io.micrometer.core.instrument.Tag;
2025
import io.micrometer.core.instrument.Tags;
2126

2227
import org.springframework.cloud.client.ServiceInstance;
2328
import org.springframework.cloud.client.loadbalancer.CompletionContext;
29+
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
2430
import org.springframework.cloud.client.loadbalancer.RequestData;
2531
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
2632
import org.springframework.cloud.client.loadbalancer.ResponseData;
@@ -30,17 +36,25 @@
3036
* Utility class for building metrics tags for load-balanced calls.
3137
*
3238
* @author Olga Maciaszek-Sharma
39+
* @author Jaroslaw Dembek
3340
* @since 3.0.0
3441
*/
35-
final class LoadBalancerTags {
42+
class LoadBalancerTags {
3643

3744
static final String UNKNOWN = "UNKNOWN";
3845

39-
private LoadBalancerTags() {
40-
throw new UnsupportedOperationException("Cannot instantiate utility class");
46+
private final LoadBalancerProperties properties;
47+
48+
// Not using class references in case not in classpath
49+
private static final Set<String> URI_TEMPLATE_ATTRIBUTES = Set.of(
50+
"org.springframework.web.reactive.function.client.WebClient.uriTemplate",
51+
"org.springframework.web.client.RestClient.uriTemplate");
52+
53+
LoadBalancerTags(LoadBalancerProperties properties) {
54+
this.properties = properties;
4155
}
4256

43-
static Iterable<Tag> buildSuccessRequestTags(CompletionContext<Object, ServiceInstance, Object> completionContext) {
57+
Iterable<Tag> buildSuccessRequestTags(CompletionContext<Object, ServiceInstance, Object> completionContext) {
4458
ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer();
4559
Tags tags = Tags.of(buildServiceInstanceTags(serviceInstance));
4660
Object clientResponse = completionContext.getClientResponse();
@@ -69,12 +83,23 @@ private static int statusValue(ResponseData responseData) {
6983
return responseData.getHttpStatus() != null ? responseData.getHttpStatus().value() : 200;
7084
}
7185

72-
private static String getPath(RequestData requestData) {
73-
return requestData.getUrl() != null ? requestData.getUrl().getPath() : UNKNOWN;
86+
private String getPath(RequestData requestData) {
87+
if (!properties.getStats().isIncludePath()) {
88+
return UNKNOWN;
89+
}
90+
Optional<Object> uriTemplateValue = Optional.ofNullable(requestData.getAttributes())
91+
.orElse(Collections.emptyMap())
92+
.keySet()
93+
.stream()
94+
.filter(URI_TEMPLATE_ATTRIBUTES::contains)
95+
.map(key -> requestData.getAttributes().get(key))
96+
.filter(Objects::nonNull)
97+
.findAny();
98+
return uriTemplateValue.map(uriTemplate -> (String) uriTemplate)
99+
.orElseGet(() -> (requestData.getUrl() != null) ? requestData.getUrl().getPath() : UNKNOWN);
74100
}
75101

76-
static Iterable<Tag> buildDiscardedRequestTags(
77-
CompletionContext<Object, ServiceInstance, Object> completionContext) {
102+
Iterable<Tag> buildDiscardedRequestTags(CompletionContext<Object, ServiceInstance, Object> completionContext) {
78103
if (completionContext.getLoadBalancerRequest().getContext() instanceof RequestDataContext) {
79104
RequestData requestData = ((RequestDataContext) completionContext.getLoadBalancerRequest().getContext())
80105
.getClientRequest();
@@ -92,7 +117,7 @@ private static String getHost(RequestData requestData) {
92117
return requestData.getUrl() != null ? requestData.getUrl().getHost() : UNKNOWN;
93118
}
94119

95-
static Iterable<Tag> buildFailedRequestTags(CompletionContext<Object, ServiceInstance, Object> completionContext) {
120+
Iterable<Tag> buildFailedRequestTags(CompletionContext<Object, ServiceInstance, Object> completionContext) {
96121
ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer();
97122
Tags tags = Tags.of(buildServiceInstanceTags(serviceInstance)).and(exception(completionContext.getThrowable()));
98123
if (completionContext.getLoadBalancerRequest().getContext() instanceof RequestDataContext) {

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,31 +27,50 @@
2727

2828
import org.springframework.cloud.client.ServiceInstance;
2929
import org.springframework.cloud.client.loadbalancer.CompletionContext;
30+
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
3031
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
32+
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
3133
import org.springframework.cloud.client.loadbalancer.Request;
3234
import org.springframework.cloud.client.loadbalancer.Response;
3335
import org.springframework.cloud.client.loadbalancer.TimedRequestContext;
36+
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
37+
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
3438

35-
import static org.springframework.cloud.loadbalancer.stats.LoadBalancerTags.buildDiscardedRequestTags;
36-
import static org.springframework.cloud.loadbalancer.stats.LoadBalancerTags.buildFailedRequestTags;
3739
import static org.springframework.cloud.loadbalancer.stats.LoadBalancerTags.buildServiceInstanceTags;
38-
import static org.springframework.cloud.loadbalancer.stats.LoadBalancerTags.buildSuccessRequestTags;
3940

4041
/**
4142
* An implementation of {@link LoadBalancerLifecycle} that records metrics for
4243
* load-balanced calls.
4344
*
4445
* @author Olga Maciaszek-Sharma
46+
* @author Jaroslaw Dembek
4547
* @since 3.0.0
4648
*/
4749
public class MicrometerStatsLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
4850

4951
private final MeterRegistry meterRegistry;
5052

53+
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
54+
5155
private final ConcurrentHashMap<ServiceInstance, AtomicLong> activeRequestsPerInstance = new ConcurrentHashMap<>();
5256

53-
public MicrometerStatsLoadBalancerLifecycle(MeterRegistry meterRegistry) {
57+
public MicrometerStatsLoadBalancerLifecycle(MeterRegistry meterRegistry,
58+
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
5459
this.meterRegistry = meterRegistry;
60+
this.loadBalancerFactory = loadBalancerFactory;
61+
}
62+
63+
/**
64+
* Creates a MicrometerStatsLoadBalancerLifecycle instance based on the provided
65+
* {@link MeterRegistry}.
66+
* @param meterRegistry {@link MeterRegistry} to use for Micrometer metrics.
67+
* @deprecated in favour of
68+
* {@link MicrometerStatsLoadBalancerLifecycle#MicrometerStatsLoadBalancerLifecycle(MeterRegistry, ReactiveLoadBalancer.Factory)}
69+
*/
70+
@Deprecated(forRemoval = true)
71+
public MicrometerStatsLoadBalancerLifecycle(MeterRegistry meterRegistry) {
72+
// use default properties when calling deprecated constructor
73+
this(meterRegistry, new LoadBalancerClientFactory(new LoadBalancerClientsProperties()));
5574
}
5675

5776
@Override
@@ -85,15 +104,19 @@ public void onStartRequest(Request<Object> request, Response<ServiceInstance> lb
85104

86105
@Override
87106
public void onComplete(CompletionContext<Object, ServiceInstance, Object> completionContext) {
107+
ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer();
108+
LoadBalancerProperties properties = serviceInstance != null
109+
? loadBalancerFactory.getProperties(serviceInstance.getServiceId())
110+
: loadBalancerFactory.getProperties(null);
111+
LoadBalancerTags loadBalancerTags = new LoadBalancerTags(properties);
88112
long requestFinishedTimestamp = System.nanoTime();
89113
if (CompletionContext.Status.DISCARD.equals(completionContext.status())) {
90114
Counter.builder("loadbalancer.requests.discard")
91-
.tags(buildDiscardedRequestTags(completionContext))
115+
.tags(loadBalancerTags.buildDiscardedRequestTags(completionContext))
92116
.register(meterRegistry)
93117
.increment();
94118
return;
95119
}
96-
ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer();
97120
AtomicLong activeRequestsCounter = activeRequestsPerInstance.get(serviceInstance);
98121
if (activeRequestsCounter != null) {
99122
activeRequestsCounter.decrementAndGet();
@@ -102,15 +125,15 @@ public void onComplete(CompletionContext<Object, ServiceInstance, Object> comple
102125
if (requestHasBeenTimed(loadBalancerRequestContext)) {
103126
if (CompletionContext.Status.FAILED.equals(completionContext.status())) {
104127
Timer.builder("loadbalancer.requests.failed")
105-
.tags(buildFailedRequestTags(completionContext))
128+
.tags(loadBalancerTags.buildFailedRequestTags(completionContext))
106129
.register(meterRegistry)
107130
.record(requestFinishedTimestamp
108131
- ((TimedRequestContext) loadBalancerRequestContext).getRequestStartTime(),
109132
TimeUnit.NANOSECONDS);
110133
return;
111134
}
112135
Timer.builder("loadbalancer.requests.success")
113-
.tags(buildSuccessRequestTags(completionContext))
136+
.tags(loadBalancerTags.buildSuccessRequestTags(completionContext))
114137
.register(meterRegistry)
115138
.record(requestFinishedTimestamp
116139
- ((TimedRequestContext) loadBalancerRequestContext).getRequestStartTime(),

0 commit comments

Comments
 (0)