Reorganize HTTP Observation types

Closes gh-29334
This commit is contained in:
Brian Clozel
2022-10-17 21:54:04 +02:00
parent 4d44aaf81b
commit 001b2636d6
42 changed files with 762 additions and 849 deletions

View File

@@ -22,12 +22,13 @@ import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.docs.ObservationDocumentation;
/**
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the {@link WebClient} observations.
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the {@link WebClient HTTP client} observations.
* <p>This class is used by automated tools to document KeyValues attached to the HTTP client observations.
*
* @author Brian Clozel
* @since 6.0
*/
public enum ClientObservationDocumentation implements ObservationDocumentation {
public enum ClientHttpObservationDocumentation implements ObservationDocumentation {
/**
* Observation created for an HTTP client exchange.
@@ -35,17 +36,17 @@ public enum ClientObservationDocumentation implements ObservationDocumentation {
HTTP_REQUEST {
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultClientObservationConvention.class;
return DefaultClientRequestObservationConvention.class;
}
@Override
public KeyName[] getLowCardinalityKeyNames() {
return ClientObservationDocumentation.LowCardinalityKeyNames.values();
return ClientHttpObservationDocumentation.LowCardinalityKeyNames.values();
}
@Override
public KeyName[] getHighCardinalityKeyNames() {
return ClientObservationDocumentation.HighCardinalityKeyNames.values();
return ClientHttpObservationDocumentation.HighCardinalityKeyNames.values();
}
};

View File

@@ -22,12 +22,12 @@ import org.springframework.lang.Nullable;
/**
* Context that holds information for metadata collection
* during the HTTP client observations.
* during the {@link ClientHttpObservationDocumentation#HTTP_REQUEST HTTP client exchange observations}.
*
* @author Brian Clozel
* @since 6.0
*/
public class ClientObservationContext extends RequestReplySenderContext<ClientRequest, ClientResponse> {
public class ClientRequestObservationContext extends RequestReplySenderContext<ClientRequest, ClientResponse> {
@Nullable
private String uriTemplate;
@@ -35,8 +35,8 @@ public class ClientObservationContext extends RequestReplySenderContext<ClientRe
private boolean aborted;
public ClientObservationContext() {
super(ClientObservationContext::setRequestHeader);
public ClientRequestObservationContext() {
super(ClientRequestObservationContext::setRequestHeader);
}
private static void setRequestHeader(@Nullable ClientRequest request, String name, String value) {

View File

@@ -20,15 +20,17 @@ import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
/**
* Interface for an {@link ObservationConvention} related to client HTTP exchanges.
* Interface for an {@link ObservationConvention} related to
* {@link ClientHttpObservationDocumentation#HTTP_REQUEST HTTP client exchange observations}.
*
* @author Brian Clozel
* @since 6.0
*/
public interface ClientObservationConvention extends ObservationConvention<ClientObservationContext> {
public interface ClientRequestObservationConvention extends ObservationConvention<ClientRequestObservationContext> {
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof ClientObservationContext;
return context instanceof ClientRequestObservationContext;
}
}

View File

@@ -1,157 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.function.client;
import java.io.IOException;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import io.micrometer.observation.ObservationConvention;
import org.springframework.http.client.observation.ClientHttpObservationDocumentation;
import org.springframework.http.observation.HttpOutcome;
import org.springframework.util.StringUtils;
/**
* Default implementation for a {@code WebClient} {@link ObservationConvention},
* extracting information from the {@link ClientObservationContext}.
*
* @author Brian Clozel
* @since 6.0
*/
public class DefaultClientObservationConvention implements ClientObservationConvention {
private static final String DEFAULT_NAME = "http.client.requests";
private static final KeyValue URI_NONE = KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.URI, "none");
private static final KeyValue METHOD_NONE = KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.METHOD, "none");
private static final KeyValue STATUS_IO_ERROR = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, "IO_ERROR");
private static final KeyValue STATUS_CLIENT_ERROR = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, "CLIENT_ERROR");
private static final KeyValue EXCEPTION_NONE = KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, "none");
private static final KeyValue HTTP_URL_NONE = KeyValue.of(ClientHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, "none");
private static final KeyValue CLIENT_NAME_NONE = KeyValue.of(ClientHttpObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME, "none");
private final String name;
/**
* Create a convention with the default name {@code "http.client.requests"}.
*/
public DefaultClientObservationConvention() {
this(DEFAULT_NAME);
}
/**
* Create a convention with a custom name.
* @param name the observation name
*/
public DefaultClientObservationConvention(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getContextualName(ClientObservationContext context) {
return "http " + context.getCarrier().method().name().toLowerCase();
}
@Override
public KeyValues getLowCardinalityKeyValues(ClientObservationContext context) {
return KeyValues.of(uri(context), method(context), status(context), exception(context), outcome(context));
}
protected KeyValue uri(ClientObservationContext context) {
if (context.getUriTemplate() != null) {
return KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.URI, context.getUriTemplate());
}
return URI_NONE;
}
protected KeyValue method(ClientObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().method().name());
}
else {
return METHOD_NONE;
}
}
protected KeyValue status(ClientObservationContext context) {
if (context.isAborted()) {
return STATUS_CLIENT_ERROR;
}
ClientResponse response = context.getResponse();
if (response != null) {
return KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(response.statusCode().value()));
}
if (context.getError() != null && context.getError() instanceof IOException) {
return STATUS_IO_ERROR;
}
return STATUS_CLIENT_ERROR;
}
protected KeyValue exception(ClientObservationContext context) {
Throwable error = context.getError();
if (error != null) {
String simpleName = error.getClass().getSimpleName();
return KeyValue.of(ClientObservationDocumentation.LowCardinalityKeyNames.EXCEPTION,
StringUtils.hasText(simpleName) ? simpleName : error.getClass().getName());
}
return EXCEPTION_NONE;
}
protected KeyValue outcome(ClientObservationContext context) {
if (context.isAborted()) {
return HttpOutcome.UNKNOWN.asKeyValue();
}
if (context.getResponse() != null) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(context.getResponse().statusCode());
return httpOutcome.asKeyValue();
}
return HttpOutcome.UNKNOWN.asKeyValue();
}
@Override
public KeyValues getHighCardinalityKeyValues(ClientObservationContext context) {
return KeyValues.of(httpUrl(context), clientName(context));
}
protected KeyValue httpUrl(ClientObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(ClientObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().url().toASCIIString());
}
return HTTP_URL_NONE;
}
protected KeyValue clientName(ClientObservationContext context) {
if (context.getCarrier() != null && context.getCarrier().url().getHost() != null) {
return KeyValue.of(ClientObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME, context.getCarrier().url().getHost());
}
return CLIENT_NAME_NONE;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.function.client;
import java.io.IOException;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.util.StringUtils;
/**
* Default implementation for a {@link ClientRequestObservationConvention},
* extracting information from the {@link ClientRequestObservationContext}.
*
* @author Brian Clozel
* @since 6.0
*/
public class DefaultClientRequestObservationConvention implements ClientRequestObservationConvention {
private static final String DEFAULT_NAME = "http.client.requests";
private static final KeyValue URI_NONE = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.URI, "none");
private static final KeyValue METHOD_NONE = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, "none");
private static final KeyValue STATUS_IO_ERROR = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, "IO_ERROR");
private static final KeyValue STATUS_CLIENT_ERROR = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, "CLIENT_ERROR");
private static final KeyValue HTTP_OUTCOME_SUCCESS = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.LowCardinalityKeyNames.OUTCOME, "SUCCESS");
private static final KeyValue HTTP_OUTCOME_UNKNOWN = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
private static final KeyValue EXCEPTION_NONE = KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, "none");
private static final KeyValue HTTP_URL_NONE = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, "none");
private static final KeyValue CLIENT_NAME_NONE = KeyValue.of(org.springframework.http.client.observation.ClientHttpObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME, "none");
private final String name;
/**
* Create a convention with the default name {@code "http.client.requests"}.
*/
public DefaultClientRequestObservationConvention() {
this(DEFAULT_NAME);
}
/**
* Create a convention with a custom name.
* @param name the observation name
*/
public DefaultClientRequestObservationConvention(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getContextualName(ClientRequestObservationContext context) {
return "http " + context.getCarrier().method().name().toLowerCase();
}
@Override
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
return KeyValues.of(uri(context), method(context), status(context), exception(context), outcome(context));
}
protected KeyValue uri(ClientRequestObservationContext context) {
if (context.getUriTemplate() != null) {
return KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.URI, context.getUriTemplate());
}
return URI_NONE;
}
protected KeyValue method(ClientRequestObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().method().name());
}
else {
return METHOD_NONE;
}
}
protected KeyValue status(ClientRequestObservationContext context) {
if (context.isAborted()) {
return STATUS_CLIENT_ERROR;
}
ClientResponse response = context.getResponse();
if (response != null) {
return KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(response.statusCode().value()));
}
if (context.getError() != null && context.getError() instanceof IOException) {
return STATUS_IO_ERROR;
}
return STATUS_CLIENT_ERROR;
}
protected KeyValue exception(ClientRequestObservationContext context) {
Throwable error = context.getError();
if (error != null) {
String simpleName = error.getClass().getSimpleName();
return KeyValue.of(ClientHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION,
StringUtils.hasText(simpleName) ? simpleName : error.getClass().getName());
}
return EXCEPTION_NONE;
}
protected KeyValue outcome(ClientRequestObservationContext context) {
if (context.isAborted()) {
return HTTP_OUTCOME_UNKNOWN;
}
if (context.getResponse() != null) {
return HttpOutcome.forStatus(context.getResponse().statusCode());
}
return HTTP_OUTCOME_UNKNOWN;
}
@Override
public KeyValues getHighCardinalityKeyValues(ClientRequestObservationContext context) {
return KeyValues.of(httpUrl(context), clientName(context));
}
protected KeyValue httpUrl(ClientRequestObservationContext context) {
if (context.getCarrier() != null) {
return KeyValue.of(ClientHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().url().toASCIIString());
}
return HTTP_URL_NONE;
}
protected KeyValue clientName(ClientRequestObservationContext context) {
if (context.getCarrier() != null && context.getCarrier().url().getHost() != null) {
return KeyValue.of(ClientHttpObservationDocumentation.HighCardinalityKeyNames.CLIENT_NAME, context.getCarrier().url().getHost());
}
return CLIENT_NAME_NONE;
}
static class HttpOutcome {
static KeyValue forStatus(HttpStatusCode statusCode) {
if (statusCode.is2xxSuccessful()) {
return HTTP_OUTCOME_SUCCESS;
}
else if (statusCode instanceof HttpStatus status){
return KeyValue.of("outcome", status.series().name());
}
else {
return HTTP_OUTCOME_UNKNOWN;
}
}
}
}

View File

@@ -73,7 +73,7 @@ class DefaultWebClient implements WebClient {
private static final Mono<ClientResponse> NO_HTTP_CLIENT_RESPONSE_ERROR = Mono.error(
() -> new IllegalStateException("The underlying HTTP client completed without emitting a response."));
private static final DefaultClientObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultClientObservationConvention();
private static final DefaultClientRequestObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultClientRequestObservationConvention();
private final ExchangeFunction exchangeFunction;
@@ -92,7 +92,7 @@ class DefaultWebClient implements WebClient {
private final ObservationRegistry observationRegistry;
private final ClientObservationConvention observationConvention;
private final ClientRequestObservationConvention observationConvention;
private final DefaultWebClientBuilder builder;
@@ -101,7 +101,7 @@ class DefaultWebClient implements WebClient {
@Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap<String, String> defaultCookies,
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap,
ObservationRegistry observationRegistry, ClientObservationConvention observationConvention,
ObservationRegistry observationRegistry, ClientRequestObservationConvention observationConvention,
DefaultWebClientBuilder builder) {
this.exchangeFunction = exchangeFunction;
@@ -449,12 +449,12 @@ class DefaultWebClient implements WebClient {
@Override
@SuppressWarnings("deprecation")
public Mono<ClientResponse> exchange() {
ClientObservationContext observationContext = new ClientObservationContext();
ClientRequestObservationContext observationContext = new ClientRequestObservationContext();
ClientRequest request = (this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build());
return Mono.defer(() -> {
Observation observation = ClientObservationDocumentation.HTTP_REQUEST.observation(observationConvention,
Observation observation = ClientHttpObservationDocumentation.HTTP_REQUEST.observation(observationConvention,
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry).start();
observationContext.setCarrier(request);
observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null));

View File

@@ -113,7 +113,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
@Nullable
private ClientObservationConvention observationConvention;
private ClientRequestObservationConvention observationConvention;
public DefaultWebClientBuilder() {
@@ -288,7 +288,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
}
@Override
public WebClient.Builder observationConvention(ClientObservationConvention observationConvention) {
public WebClient.Builder observationConvention(ClientRequestObservationConvention observationConvention) {
Assert.notNull(observationConvention, "observationConvention must not be null");
this.observationConvention = observationConvention;
return this;

View File

@@ -347,12 +347,12 @@ public interface WebClient {
/**
* Provide an {@link ObservationConvention} to use for collecting
* metadata for the current observation. Will use {@link DefaultClientObservationConvention}
* metadata for the request observation. Will use {@link DefaultClientRequestObservationConvention}
* if none provided.
* @param observationConvention the observation convention to use
* @since 6.0
*/
Builder observationConvention(ClientObservationConvention observationConvention);
Builder observationConvention(ClientRequestObservationConvention observationConvention);
/**
* Apply the given {@code Consumer} to this builder instance.

View File

@@ -28,13 +28,13 @@ import org.springframework.http.HttpStatus;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultClientObservationConvention}.
* Tests for {@link DefaultClientRequestObservationConvention}.
*
* @author Brian Clozel
*/
class DefaultClientObservationConventionTests {
class DefaultClientRequestObservationConventionTests {
private final DefaultClientObservationConvention observationConvention = new DefaultClientObservationConvention();
private final DefaultClientRequestObservationConvention observationConvention = new DefaultClientRequestObservationConvention();
@Test
void shouldHaveName() {
@@ -43,20 +43,20 @@ class DefaultClientObservationConventionTests {
@Test
void shouldHaveContextualName() {
ClientObservationContext context = new ClientObservationContext();
ClientRequestObservationContext context = new ClientRequestObservationContext();
context.setCarrier(ClientRequest.create(HttpMethod.GET, URI.create("/test")).build());
assertThat(this.observationConvention.getContextualName(context)).isEqualTo("http get");
}
@Test
void shouldOnlySupportWebClientObservationContext() {
assertThat(this.observationConvention.supportsContext(new ClientObservationContext())).isTrue();
assertThat(this.observationConvention.supportsContext(new ClientRequestObservationContext())).isTrue();
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
}
@Test
void shouldAddKeyValuesForNullExchange() {
ClientObservationContext context = new ClientObservationContext();
ClientRequestObservationContext context = new ClientRequestObservationContext();
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(5)
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
KeyValue.of("exception", "none"), KeyValue.of("outcome", "UNKNOWN"));
@@ -66,7 +66,7 @@ class DefaultClientObservationConventionTests {
@Test
void shouldAddKeyValuesForExchangeWithException() {
ClientObservationContext context = new ClientObservationContext();
ClientRequestObservationContext context = new ClientRequestObservationContext();
context.setError(new IllegalStateException("Could not create client request"));
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(5)
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"),
@@ -79,7 +79,7 @@ class DefaultClientObservationConventionTests {
void shouldAddKeyValuesForRequestWithUriTemplate() {
ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/resource/42"))
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}").build();
ClientObservationContext context = createContext(request);
ClientRequestObservationContext context = createContext(request);
context.setUriTemplate("/resource/{id}");
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
@@ -90,7 +90,7 @@ class DefaultClientObservationConventionTests {
@Test
void shouldAddKeyValuesForRequestWithoutUriTemplate() {
ClientObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")).build());
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")).build());
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none"));
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2).contains(KeyValue.of("http.url", "/resource/42"));
@@ -98,12 +98,12 @@ class DefaultClientObservationConventionTests {
@Test
void shouldAddClientNameKeyValueForRequestWithHost() {
ClientObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")).build());
ClientRequestObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")).build());
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("client.name", "localhost"));
}
private ClientObservationContext createContext(ClientRequest request) {
ClientObservationContext context = new ClientObservationContext();
private ClientRequestObservationContext createContext(ClientRequest request) {
ClientRequestObservationContext context = new ClientRequestObservationContext();
context.setCarrier(request);
context.setResponse(ClientResponse.create(HttpStatus.OK).build());
return context;