Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve query params in uri KeyValue with HTTP interface client #34176

Closed
cj-florian-akos-szabo opened this issue Dec 29, 2024 · 4 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: observability An issue related to observability and tracing type: enhancement A general enhancement
Milestone

Comments

@cj-florian-akos-szabo
Copy link

cj-florian-akos-szabo commented Dec 29, 2024

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));
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 29, 2024
@bclozel bclozel added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Dec 29, 2024
@v-perfilev
Copy link
Contributor

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.

@bclozel bclozel self-assigned this Jan 2, 2025
@bclozel bclozel added this to the 6.2.2 milestone Jan 2, 2025
@bclozel bclozel added type: enhancement A general enhancement theme: observability An issue related to observability and tracing and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jan 2, 2025
@bclozel bclozel changed the title RestClient observation uri tag handling of query parameters Improve query params in uri KeyValue with HTTP interface client Jan 2, 2025
@bclozel bclozel closed this as completed in d927d64 Jan 2, 2025
@bclozel
Copy link
Member

bclozel commented Jan 2, 2025

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:

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.

quaff pushed a commit to quaff/spring-framework that referenced this issue Jan 3, 2025
Prior to this commit, the HTTP interface client would create URI
templates and name query params like so:
"?{queryParam0}={queryParam0[0]}".
While technically correct, the URI template is further used in
observations as a KeyValue. This means that several service methods
could result in having the exact same URI template even if they produce
a different set of query params.

This commit improves the naming of query params in the generated URI
templates for better observability integration.

Closes spring-projectsgh-34176
@cj-florian-akos-szabo
Copy link
Author

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?

@bclozel
Copy link
Member

bclozel commented Jan 3, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) theme: observability An issue related to observability and tracing type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

4 participants