Skip to content

Commit 5dfa61e

Browse files
committed
Restrict "uri" KeyValue for client observations
Prior to this commit, the "uri" KeyValue for low cardinality metadata would contain the entire uri template given to the HTTP client when creating the request. This was a breaking change for existing metrics dashboards, as previous support was removing the protocol, host and port parts of the URI. Indeed, this information is available in the "client.name" and "http.uri" KayValue. This commit parses and removes the protocol+host+port information from the uri template for the "uri" KeyValue. Fixes gh-29885
1 parent b267547 commit 5dfa61e

File tree

8 files changed

+45
-7
lines changed

8 files changed

+45
-7
lines changed

Diff for: framework-docs/src/docs/asciidoc/integration/observability.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Instrumentation is using the `org.springframework.http.client.observation.Client
132132
|===
133133
|Name | Description
134134
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
135-
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided.
135+
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
136136
|`client.name` _(required)_|Client name derived from the request URI host.
137137
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
138138
|`outcome` _(required)_|Outcome of the HTTP client exchange.
@@ -158,7 +158,7 @@ Instrumentation is using the `org.springframework.web.reactive.function.client.C
158158
|===
159159
|Name | Description
160160
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
161-
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided.
161+
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
162162
|`client.name` _(required)_|Client name derived from the request URI host.
163163
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
164164
|`outcome` _(required)_|Outcome of the HTTP client exchange.

Diff for: spring-web/src/main/java/org/springframework/http/client/observation/ClientHttpObservationDocumentation.java

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public String asString() {
7070

7171
/**
7272
* URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if none was provided.
73+
* Only the path part of the URI is considered.
7374
*/
7475
URI {
7576
@Override

Diff for: spring-web/src/main/java/org/springframework/http/client/observation/DefaultClientRequestObservationConvention.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.http.client.observation;
1818

1919
import java.io.IOException;
20+
import java.util.regex.Pattern;
2021

2122
import io.micrometer.common.KeyValue;
2223
import io.micrometer.common.KeyValues;
@@ -39,6 +40,8 @@ public class DefaultClientRequestObservationConvention implements ClientRequestO
3940

4041
private static final String DEFAULT_NAME = "http.client.requests";
4142

43+
private static final Pattern PATTERN_BEFORE_PATH = Pattern.compile("^https?://[^/]+/");
44+
4245
private static final KeyValue URI_NONE = KeyValue.of(LowCardinalityKeyNames.URI, KeyValue.NONE_VALUE);
4346

4447
private static final KeyValue METHOD_NONE = KeyValue.of(LowCardinalityKeyNames.METHOD, KeyValue.NONE_VALUE);
@@ -92,11 +95,16 @@ public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext cont
9295

9396
protected KeyValue uri(ClientRequestObservationContext context) {
9497
if (context.getUriTemplate() != null) {
95-
return KeyValue.of(LowCardinalityKeyNames.URI, context.getUriTemplate());
98+
return KeyValue.of(LowCardinalityKeyNames.URI, extractPath(context.getUriTemplate()));
9699
}
97100
return URI_NONE;
98101
}
99102

103+
private static String extractPath(String uriTemplate) {
104+
String path = PATTERN_BEFORE_PATH.matcher(uriTemplate).replaceFirst("");
105+
return (path.startsWith("/") ? path : "/" + path);
106+
}
107+
100108
protected KeyValue method(ClientRequestObservationContext context) {
101109
if (context.getCarrier() != null) {
102110
return KeyValue.of(LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod().name());

Diff for: spring-web/src/test/java/org/springframework/http/client/observation/DefaultClientRequestObservationConventionTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ void addsKeyValuesForRequestWithUriTemplate() {
7373
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("http.url", "/resource/42"));
7474
}
7575

76+
@Test
77+
void addsKeyValuesForRequestWithUriTemplateWithHost() {
78+
ClientRequestObservationContext context = createContext(
79+
new MockClientHttpRequest(HttpMethod.GET, "https://example.org/resource/{id}", 42), new MockClientHttpResponse());
80+
context.setUriTemplate("https://example.org/resource/{id}");
81+
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
82+
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
83+
KeyValue.of("status", "200"), KeyValue.of("client.name", "example.org"), KeyValue.of("outcome", "SUCCESS"));
84+
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("http.url", "https://example.org/resource/42"));
85+
}
86+
87+
7688
@Test
7789
void addsKeyValuesForRequestWithoutUriTemplate() {
7890
ClientRequestObservationContext context = createContext(

Diff for: spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void executeVarArgsAddsUriTemplateAsKeyValue() throws Exception {
8888
template.execute("https://example.com/hotels/{hotel}/bookings/{booking}", GET,
8989
null, null, "42", "21");
9090

91-
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "https://example.com/hotels/{hotel}/bookings/{booking}");
91+
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "/hotels/{hotel}/bookings/{booking}");
9292
}
9393

9494
@Test
@@ -101,7 +101,7 @@ void executeArgsMapAddsUriTemplateAsKeyValue() throws Exception {
101101
template.execute("https://example.com/hotels/{hotel}/bookings/{booking}", GET,
102102
null, null, vars);
103103

104-
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "https://example.com/hotels/{hotel}/bookings/{booking}");
104+
assertThatHttpObservation().hasLowCardinalityKeyValue("uri", "/hotels/{hotel}/bookings/{booking}");
105105
}
106106

107107

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientHttpObservationDocumentation.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public String asString() {
6767

6868
/**
6969
* URI template used for HTTP request, or {@value KeyValue#NONE_VALUE} if none was provided.
70+
* Only the path part of the URI is considered.
7071
*/
7172
URI {
7273
@Override
@@ -123,7 +124,7 @@ public String asString() {
123124
public enum HighCardinalityKeyNames implements KeyName {
124125

125126
/**
126-
* HTTP request URI.
127+
* The full HTTP request URI.
127128
*/
128129
HTTP_URL {
129130
@Override

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestObservationConvention.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.function.client;
1818

1919
import java.io.IOException;
20+
import java.util.regex.Pattern;
2021

2122
import io.micrometer.common.KeyValue;
2223
import io.micrometer.common.KeyValues;
@@ -40,6 +41,8 @@ public class DefaultClientRequestObservationConvention implements ClientRequestO
4041

4142
private static final String ROOT_PATH = "/";
4243

44+
private static final Pattern PATTERN_BEFORE_PATH = Pattern.compile("^https?://[^/]+/");
45+
4346
private static final KeyValue URI_NONE = KeyValue.of(LowCardinalityKeyNames.URI, KeyValue.NONE_VALUE);
4447

4548
private static final KeyValue METHOD_NONE = KeyValue.of(LowCardinalityKeyNames.METHOD, KeyValue.NONE_VALUE);
@@ -94,7 +97,7 @@ public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext cont
9497

9598
protected KeyValue uri(ClientRequestObservationContext context) {
9699
if (context.getUriTemplate() != null) {
97-
return KeyValue.of(LowCardinalityKeyNames.URI, context.getUriTemplate());
100+
return KeyValue.of(LowCardinalityKeyNames.URI, extractPath(context.getUriTemplate()));
98101
}
99102
ClientRequest request = context.getRequest();
100103
if (request != null && ROOT_PATH.equals(request.url().getPath())) {
@@ -103,6 +106,11 @@ protected KeyValue uri(ClientRequestObservationContext context) {
103106
return URI_NONE;
104107
}
105108

109+
private static String extractPath(String uriTemplate) {
110+
String path = PATTERN_BEFORE_PATH.matcher(uriTemplate).replaceFirst("");
111+
return (path.startsWith("/") ? path : "/" + path);
112+
}
113+
106114
protected KeyValue method(ClientRequestObservationContext context) {
107115
if (context.getRequest() != null) {
108116
return KeyValue.of(LowCardinalityKeyNames.METHOD, context.getRequest().method().name());

Diff for: spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestObservationConventionTests.java

+8
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ void shouldAddRootUriEvenIfTemplateMissing() {
114114
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/"));
115115
}
116116

117+
@Test
118+
void shouldOnlyConsiderPathForUriKeyValue() {
119+
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/resource/42")));
120+
context.setUriTemplate("https://example.org/resource/{id}");
121+
context.setRequest(context.getCarrier().build());
122+
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("uri", "/resource/{id}"));
123+
}
124+
117125
private ClientRequestObservationContext createContext(ClientRequest.Builder request) {
118126
ClientRequestObservationContext context = new ClientRequestObservationContext();
119127
context.setCarrier(request);

0 commit comments

Comments
 (0)