I have a project where I noticed a behavior in the http.client.requests
metric coming from RestClient
that's defined via such interface:
public interface ApiServiceClient {
@GetExchange("/some/api/with/{variable}/and/params")
ApiResultPojo makeApiRequest(
@PathVariable String variable,
@RequestParam(required = false, name=foo) String foo,
@RequestParam(required = false, name=bar) String bar
);
}
resulting in http.client.requests
metric with such uri
tags:
{
"tag": "uri",
"values": [
"/some/api/with/{variable}/and/params",
"/some/api/with/{variable}/and/params?{queryParam0}={queryParam0[0]}",
"/some/api/with/{variable}/and/params?{queryParam0}={queryParam0[0]}&{queryParam1}={queryParam1[0]}"
]
},
depending on how many of the optional query params are non-null.
My issue is mainly with the cryptic names of the query params: queryParam0
& queryParam1
. Could it be somehow possible to default these to use the name
or value
passed in the RequestParam
annotation, and only use these basic placeholder names if custom name/value is not available? So in this case I'd expect the uri
tag to include such items:
{
"tag": "uri",
"values": [
"/some/api/with/{variable}/and/params",
"/some/api/with/{variable}/and/params?foo={foo}",
"/some/api/with/{variable}/and/params?foo={foo}&bar={bar}"
]
},
And currently, to work around this issue I am considering adding such a custom ClientRequestObservationConvention when I build the RestClient:
public class ExtendedClientRequestObservationConvention extends DefaultClientRequestObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
return super.getLowCardinalityKeyValues(context).and(additionalTags(context));
}
protected KeyValues additionalTags(ClientRequestObservationContext context) {
String fullUri = Objects.requireNonNull(context.getCarrier()).getURI().toString();
String paramKeys = UriComponentsBuilder
.fromUriString(fullUri).build()
.getQueryParams().keySet()
.stream()
.sorted()
.map(key -> key + "={" + key + "}")
.collect(Collectors.joining("&"));
String uriTemplate = Objects.requireNonNull(context.getUriTemplate()).split("\\?")[0];
String uri = paramKeys.isEmpty() ? uriTemplate : uriTemplate + "?" + paramKeys;
return KeyValues.of(KeyValue.of(URI, uri));
}
}
Comment From: v-perfilev
Hi Florian, nice workaround, it should work! I just believe that the right long-term solution would involve addressing this directly in the uri methods of DefaultRestClient:
@Override
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) {
UriBuilder uriBuilder = uriBuilderFactory.uriString(uriTemplate);
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.toUriString());
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
This is where the URI_TEMPLATE_ATTRIBUTE is set, and the only place it's used is for metrics in observatonContext. I'd suggest introducing new interfaces: TemplateBuilder and TemplateBuilderFactory (in the org.springframework.web.util package), along with their default implementations. These could then be used in DefaultRestClient like:
@Override
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) {
TemplateBuilder templateBuilder = templateBuilderFactory.uriTemplateWithVars(uriTemplate, uriVariables); // new template builder instead of uriBuilder
attribute(URI_TEMPLATE_ATTRIBUTE, templateBuilder.toUriString());
return uri(DefaultRestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables));
}
With this approach the URI_TEMPLATE_ATTRIBUTE would look like /api/response/{id}?bar={bar} instead of /api/response/{id}?{queryParam0}={queryParam0[0]}.
@bclozel Do you think this feature is necessary? If so, I'd be happy to contribute and implement it.
Comment From: bclozel
Thanks for the report @cj-florian-akos-szabo - I have reproduced this successfully with the HTTP interface client support.
This does not happen when using the RestClient
directly, as the 'uri' KeyValue is given directly as the URI template. As long as the RequestBodySpec.uri()
method is given the right URI template, this should show correctly in the observation metadata. Thanks for the offer @v-perfilev , but the problem isn't located in DefaultRestClient
nor in the ClientRequestObservationConvention
.
The HTTP interface client support creates proxies that look at interface method signatures and create the relevant HTTP requests accordingly. In this case, the HttpRequestValues
is responsible for extracting values from @HttpExchange
methods.
There are several key aspects to consider here:
* we must support collection values like "/params?spring=framework&spring=boot"
* param names can be provided as method arguments or in Map
/MultiValueMap
* encoding
I have prepared a commit that will change the generated URI templates to:
* "/some/api/with/{variable}/and/params?{foo}={foo[0]}"
* "/some/api/with/{variable}/and/params?{foo}={foo[0]}&{bar}={bar[0]}"
* "/some/api/with/{variable}/and/params?{foo}={foo[0]}&{foo}={foo[1]}&{bar}={bar[0]}"
This still adds a bit of ceremony but this should work. I have considered changing the implementation to producing foo={foo}
when possible, but again, query params can be provided as maps and one could do Map.of("query param", "value")
(notice the unencoded space). If we encode it before writing it, developers would lose flexibility for encoding options. If we don't encode it, metrics systems and dashboards might get confused. I think this is a good tradeoff.
Comment From: cj-florian-akos-szabo
Thanks @bclozel for the quick turnaround on this issue, and for reproducing the issue locally. In hindsight, I could have provided more originally to help with that part of the effort.
With this change you implemented, do I get it right that it would also affect and fix this for WebClient, since ReactiveHttpRequestValues
extends HttpRequestValues
?
Comment From: bclozel
No worries your issue was complete enough. Yes, this should cover WebClient as well.
You can test SNAPSHOT versions if you would like to ensure that this is working for you before the release.