Hi,
Here are the versions this discussion is related to :
- Spring 5.1.4.RELEASE
- SpringBoot 2.1.2.RELEASE
- Micrometer 1.1.2
Using reactive WebClient with micrometer give the ability to expose a lot of interesting metrics (such as number of requests, duration, ...). As describe in the official documentation, metrics generated by an instrumented client are tagged with the request’s URI template prior to variable substitution, if possible (for example, /api/person/{id}).
Indeed, it appears that the URI template cannot be used when specifying uri using UriBuilder
. So doing this :
webclient.get().uri("/api/person/{id}", "1234")...
will generate a metric with the expected uri template (eg : /api/person/{id}), but using uri builder like this :
webclient.get().uri(uriBuilder -> uriBuilder.path("/api/person/{id}").build("1234"))
will generate metrics using the substituted uri (eg : /api/person/1234).
This is annoying cause we can have lot of possible uris and we loose precious information (and I think the uri builder is the smartest way to forge uris).
Looking closer, it appears that there is no URI_TEMPLATE_ATTRIBUTE
attribute set when using UriBuilder
with WebClient
.
Digging deeper, I discovered that the current implementation does not take care about baseUri
set on the WebClient
. So doing this :
webClientBuilder().baseUri("https://foo.com/api").build().get().uri("/person/{id}", "1234")...
Will generate a metric using /person/{id}
instead of /api/person/{id}
.
I suggest to make few changes on UriBuilder
to be able to get access to the original uri template (add a getUriTemplate
method). This way we could adapt the DefaultWebClient
like this :
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
UriBuilder uriBuilder = uriBuilderFactory.uriString(uriTemplate);
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.getUriTemplate());
return uri(uriBuilder.build(uriVariables));
}
@Override
public RequestBodySpec uri(String uriTemplate, Map<String, ?> uriVariables) {
UriBuilder uriBuilder = uriBuilderFactory.uriString(uriTemplate);
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.getUriTemplate());
return uri(uriBuilder.build(uriVariables));
}
@Override
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
UriBuilder uriBuilder = uriBuilderFactory.builder();
RequestBodySpec uri = uri(uriFunction.apply(uriBuilder));
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.getUriTemplate());
return uri;
}
This solution seems very simple but I would like to know if you think this is a good approach and if it make sense for you ?
If you want I can make a pull request (around 10 lines of code).
Keep me in touch.
Comment From: rstoyanchev
Digging deeper, I discovered that the current implementation does not take care about
baseUri
...webClientBuilder().baseUri("https://foo.com/api").build().get().uri("/person/{id}", "1234")
The code snippet works for me. Provide a sample that demonstrates the issue.
I would like to know if you think this is a good approach and if it make sense for you ?
Thanks for asking but the approach is not clear. This does not compile:
@Override
public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) {
UriBuilder uriBuilder = uriBuilderFactory.builder();
RequestBodySpec uri = uri(uriFunction.apply(uriBuilder));
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.getUriTemplate()); // Cannot resolve `getUriTemplate'
return uri;
}
Comment From: jhaeyaert
Hi,
I don't know what I can provide to demonstrate that the current implementation does not take care about baseUri
.
If you take a look at the current implementation of the uri(String uriTemplate, Object... uriVariables)
method on DefaultWebClient
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables));
}
You can see, the that attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
only take the uriTemplate
value. This basically demonstrate that if you do this webClientBuilder().baseUri("https://foo.com/api").build().get().uri("/person/{id}", "1234")
you will have URI_TEMPLATE_ATTRIBUTE
set to /person/{id}
instead of /api/person/{id}
right ?
About the suggested implementation you noticed :
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
UriBuilder uriBuilder = uriBuilderFactory.uriString(uriTemplate);
attribute(URI_TEMPLATE_ATTRIBUTE, uriBuilder.getUriTemplate());
return uri(uriBuilder.build(uriVariables));
}
It is normal this code does not compile cause it was just an example of what the code could be if the UriBuilder
interface were modified with a new getUriTemplate
method to give access to the not expanded uri.
May be a complete pull request could be more clear ?
Comment From: rstoyanchev
Okay, I took "does not take care about baseUri set on the WebClient" to mean the baseUri doesn't work in general. I understand now you meant it for the URI_TEMPLATE_ATTRIBUTE
only.
I'm not keen on automatically creating as many objects on every request for an attribute that may or may not be needed. It would also require an accessor method on UriBuilder
which is an interface for building. And we'd have to somehow implement it on UriComponentsBuilder
which may have been created from other inputs besides a URI template (e.g. URI, ServerHttpRequest).
The URI template vs the builder options are somewhat mutually exclusive I would say. Can you comment on your use of the builder with a URI template? The given example could also be done as a URI template or with builder methods, e.g. path("/api/person").pathSegment("1234")
. One reason to ask is we could consider adding uri(String uriTemplate, Function<UriBuilder, URI>)
where you provide the URI template and then finish up with the builder but it'd be useful to have something concrete in mind for that.
As for the baseUrl issue can be addressed independently in any case by extracting the path from the baseUrl up front and then prepending it later to the uriTemplate value.
Comment From: jhaeyaert
Hi,
I used to use the UriBuilder
cause I think this is the most elegant and safe solution to prepand an uri with all dynamic path segments and query params (whatever the type of the parameters).
For example, using pathSegment
with a value other than String is not possible. So doing this is not possible path("/api/person").pathSegment(1234)
Also, using UriBuilder
is always better if I want to automatically manage query parameters of different types (String, Int, List, Array, null values, ...) instead of doing it by myself.
That's why I always prefer construct the uri this way (even for simple cases) :
Integer anInteger = 12345;
double anDouble = 0.2;
List<String> aListOfValues = ...;
webclient.get().uri(uriBuilder -> uriBuilder.path("/api/person/{personId}")
.queryParam("param1", aDouble)
.queryParam("param2", "A string value with spaces")
.queryParam("param3", aListOfValues)
.queryParam("param4", null)
.build(anInteger))
As explain on my first post, doing like this will not add the URI_TEMPLATE_ATTRIBUTE (cause the uriTemplate is inaccessible) and will unfortunately generate metrics with /api/person/12345
instead of /api/person/{personId}
. When you have a lot of possibilities such as a person identifier, this could lead to generate a lot of useless metrics.
I can only agree that modifying the UriBuilder
interface is not a good idea. I think your suggestion about adding uri(String uriTemplate, Function<UriBuilder, URI>)
is a very interesting track to explore and could be effectively a simple solution to add the URI_TEMPLATE_ATTRIBUTE attribute.
A possible implementation could be :
@Override
public RequestBodySpec uri(String uriTemplate, Function<UriBuilder, URI>) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriFunction.apply(uriBuilderFactory.uriString(uriTemplate)));
}
Do you think it could be interesting to go further and try to provide a working solution based on uri(String uriTemplate, Function<UriBuilder, URI>)
suggestion ?
Comment From: jhaeyaert
Hi,
I make a little up
if you want me to provide a working solution.
Comment From: rstoyanchev
Sure give it a go.
Comment From: rstoyanchev
This is now superseded by the #22705.
Comment From: nhmarujo
@rstoyanchev can this be reopened? I see you superseded this ticket with itself (maybe some mistake?). I also still observe the issues described on this ticket. Thanks
Comment From: bclozel
@nhmarujo it's been superseded by the PR, which has been merged in 5.2. See #22705 If you're still experiencing the problem, please create a new issue with a sample project reproducing the problem.
Comment From: nhmarujo
Thanks @bclozel. I opened https://github.com/spring-projects/spring-boot/issues/22832 and referenced this one.