diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml
index de6300e345..f8e879b90f 100644
--- a/config/checkstyle/checkstyle-suppressions.xml
+++ b/config/checkstyle/checkstyle-suppressions.xml
@@ -10,6 +10,7 @@
+
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpClientRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpClientRequestObservationConvention.java
new file mode 100644
index 0000000000..4826cd9710
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpClientRequestObservationConvention.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+import io.micrometer.common.lang.Nullable;
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation.ClientLowCardinalityKeys;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation.CommonHighCardinalityKeys;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation.CommonLowCardinalityKeys;
+
+import java.net.URI;
+import java.util.regex.Pattern;
+
+class AbstractDefaultHttpClientRequestObservationConvention {
+
+ private static final Pattern PATTERN_BEFORE_PATH = Pattern.compile("^https?://[^/]+/");
+
+ private static final KeyValue URI_NONE = KeyValue.of(CommonLowCardinalityKeys.URI, KeyValue.NONE_VALUE);
+
+ private static final KeyValue METHOD_NONE = KeyValue.of(CommonLowCardinalityKeys.METHOD, KeyValue.NONE_VALUE);
+
+ private static final KeyValue STATUS_CLIENT_ERROR = KeyValue.of(CommonLowCardinalityKeys.STATUS, "CLIENT_ERROR");
+
+ private static final KeyValue HTTP_OUTCOME_SUCCESS = KeyValue.of(CommonLowCardinalityKeys.OUTCOME, "SUCCESS");
+
+ private static final KeyValue HTTP_OUTCOME_UNKNOWN = KeyValue.of(CommonLowCardinalityKeys.OUTCOME, "UNKNOWN");
+
+ private static final KeyValue CLIENT_NAME_NONE = KeyValue.of(ClientLowCardinalityKeys.CLIENT_NAME,
+ KeyValue.NONE_VALUE);
+
+ private static final KeyValue EXCEPTION_NONE = KeyValue.of(CommonLowCardinalityKeys.EXCEPTION, KeyValue.NONE_VALUE);
+
+ private static final KeyValue HTTP_URL_NONE = KeyValue.of(CommonHighCardinalityKeys.HTTP_URL, KeyValue.NONE_VALUE);
+
+ private static final KeyValue USER_AGENT_NONE = KeyValue.of(CommonHighCardinalityKeys.USER_AGENT_ORIGINAL,
+ KeyValue.NONE_VALUE);
+
+ protected String getContextualName(String lowercaseMethod) {
+ return "http " + lowercaseMethod;
+ }
+
+ protected KeyValues getLowCardinalityKeyValues(@Nullable URI uri, @Nullable Throwable throwable,
+ @Nullable String methodName, @Nullable Integer statusCode, @Nullable String uriPathPattern) {
+ return KeyValues.of(clientName(uri), exception(throwable), method(methodName), outcome(statusCode),
+ status(statusCode), uri(uriPathPattern));
+ }
+
+ private KeyValue uri(@Nullable String uriTemplate) {
+ if (uriTemplate != null) {
+ return KeyValue.of(CommonLowCardinalityKeys.URI, extractPath(uriTemplate));
+ }
+ return URI_NONE;
+ }
+
+ private static String extractPath(String uriTemplate) {
+ String path = PATTERN_BEFORE_PATH.matcher(uriTemplate).replaceFirst("");
+ return (path.startsWith("/") ? path : "/" + path);
+ }
+
+ private KeyValue method(@Nullable String methodName) {
+ if (methodName != null) {
+ return KeyValue.of(CommonLowCardinalityKeys.METHOD, methodName);
+ }
+ else {
+ return METHOD_NONE;
+ }
+ }
+
+ private KeyValue status(@Nullable Integer statusCode) {
+ if (statusCode == null) {
+ return STATUS_CLIENT_ERROR;
+ }
+ return KeyValue.of(CommonLowCardinalityKeys.STATUS, String.valueOf(statusCode));
+ }
+
+ private KeyValue clientName(@Nullable URI uri) {
+ if (uri != null && uri.getHost() != null) {
+ return KeyValue.of(ClientLowCardinalityKeys.CLIENT_NAME, uri.getHost());
+ }
+ return CLIENT_NAME_NONE;
+ }
+
+ private KeyValue exception(@Nullable Throwable error) {
+ if (error != null) {
+ String simpleName = error.getClass().getSimpleName();
+ return KeyValue.of(CommonLowCardinalityKeys.EXCEPTION,
+ StringUtils.isNotBlank(simpleName) ? simpleName : error.getClass().getName());
+ }
+ return EXCEPTION_NONE;
+ }
+
+ private KeyValue outcome(@Nullable Integer statusCode) {
+ if (statusCode != null) {
+ return HttpOutcome.forStatus(statusCode);
+ }
+ return HTTP_OUTCOME_UNKNOWN;
+ }
+
+ protected KeyValues getHighCardinalityKeyValues(@Nullable URI uri, @Nullable String userAgent) {
+ // Make sure that KeyValues entries are already sorted by name for better
+ // performance
+ return KeyValues.of(requestUri(uri), userAgent(userAgent));
+ }
+
+ private KeyValue requestUri(@Nullable URI uri) {
+ if (uri != null) {
+ return KeyValue.of(CommonHighCardinalityKeys.HTTP_URL, uri.toASCIIString());
+ }
+ return HTTP_URL_NONE;
+ }
+
+ private KeyValue userAgent(@Nullable String userAgent) {
+ if (userAgent != null) {
+ return KeyValue.of(CommonHighCardinalityKeys.USER_AGENT_ORIGINAL, userAgent);
+ }
+ return USER_AGENT_NONE;
+ }
+
+ static class HttpOutcome {
+
+ static KeyValue forStatus(int statusCode) {
+ if (statusCode >= 200 && statusCode < 300) {
+ return HTTP_OUTCOME_SUCCESS;
+ }
+ else {
+ return HTTP_OUTCOME_UNKNOWN;
+ }
+ }
+
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpServerRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpServerRequestObservationConvention.java
new file mode 100644
index 0000000000..bbad20611f
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpServerRequestObservationConvention.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import java.net.URI;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+import io.micrometer.common.lang.Nullable;
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation.CommonHighCardinalityKeys;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation.CommonLowCardinalityKeys;
+
+class AbstractDefaultHttpServerRequestObservationConvention {
+
+ protected static final String DEFAULT_NAME = "http.server.requests";
+
+ private static final KeyValue METHOD_UNKNOWN = KeyValue.of(CommonLowCardinalityKeys.METHOD, "UNKNOWN");
+
+ private static final KeyValue STATUS_UNKNOWN = KeyValue.of(CommonLowCardinalityKeys.STATUS, "UNKNOWN");
+
+ private static final KeyValue HTTP_OUTCOME_SUCCESS = KeyValue.of(CommonLowCardinalityKeys.OUTCOME, "SUCCESS");
+
+ private static final KeyValue HTTP_OUTCOME_UNKNOWN = KeyValue.of(CommonLowCardinalityKeys.OUTCOME, "UNKNOWN");
+
+ private static final KeyValue URI_UNKNOWN = KeyValue.of(CommonLowCardinalityKeys.URI, "UNKNOWN");
+
+ private static final KeyValue URI_ROOT = KeyValue.of(CommonLowCardinalityKeys.URI, "root");
+
+ private static final KeyValue URI_NOT_FOUND = KeyValue.of(CommonLowCardinalityKeys.URI, "NOT_FOUND");
+
+ private static final KeyValue URI_REDIRECTION = KeyValue.of(CommonLowCardinalityKeys.URI, "REDIRECTION");
+
+ private static final KeyValue EXCEPTION_NONE = KeyValue.of(CommonLowCardinalityKeys.EXCEPTION, KeyValue.NONE_VALUE);
+
+ private static final KeyValue HTTP_URL_UNKNOWN = KeyValue.of(CommonHighCardinalityKeys.HTTP_URL, "UNKNOWN");
+
+ protected String getContextualName(String lowercaseHttpMethod, @Nullable String pathPattern) {
+ if (pathPattern != null) {
+ return "http " + lowercaseHttpMethod + " " + pathPattern;
+ }
+ return "http " + lowercaseHttpMethod;
+ }
+
+ protected KeyValues getLowCardinalityKeyValues(@Nullable Throwable error, @Nullable String method,
+ @Nullable Integer status, @Nullable String pathPattern, @Nullable String requestUri) {
+ // Make sure that KeyValues entries are already sorted by name for better
+ // performance
+ KeyValues micrometerKeyValues = KeyValues.of(exception(error), method(method), outcome(status), status(status),
+ uri(pathPattern, status));
+ if (method == null) {
+ return micrometerKeyValues;
+ }
+ return micrometerKeyValues.and(lowCardinalityKeyValues(method, requestUri, status));
+ }
+
+ private KeyValues lowCardinalityKeyValues(String method, String requestUri, @Nullable Integer responseStatus) {
+ try {
+ URI uri = URI.create(requestUri);
+ KeyValue requestMethod = CommonLowCardinalityKeys.HTTP_REQUEST_METHOD.withValue(method);
+ KeyValue network = CommonLowCardinalityKeys.NETWORK_PROTOCOL_NAME.withValue("http");
+ KeyValue serverAddress = CommonLowCardinalityKeys.SERVER_ADDRESS.withValue(uri.getHost());
+ KeyValue serverPort = CommonLowCardinalityKeys.SERVER_PORT.withValue(String.valueOf(uri.getPort()));
+ KeyValue urlScheme = CommonLowCardinalityKeys.URL_SCHEME.withValue(String.valueOf(uri.getScheme()));
+ KeyValues keyValues = KeyValues.of(requestMethod, network, serverAddress, serverPort, urlScheme);
+ if (responseStatus != null) {
+ keyValues = keyValues
+ .and(CommonLowCardinalityKeys.HTTP_RESPONSE_STATUS_CODE.withValue(String.valueOf(responseStatus)));
+ }
+ return keyValues;
+ }
+ catch (Exception ex) {
+ return KeyValues.empty();
+ }
+ }
+
+ protected KeyValues getHighCardinalityKeyValues(String requestUri) {
+ return KeyValues.of(httpUrl(requestUri));
+ }
+
+ protected KeyValue method(@Nullable String method) {
+ return (method != null) ? KeyValue.of(CommonLowCardinalityKeys.METHOD, method) : METHOD_UNKNOWN;
+ }
+
+ private KeyValue status(@Nullable Integer status) {
+ return (status != null) ? KeyValue.of(CommonLowCardinalityKeys.STATUS, Integer.toString(status))
+ : STATUS_UNKNOWN;
+ }
+
+ private KeyValue uri(@Nullable String pattern, @Nullable Integer status) {
+ if (pattern != null) {
+ if (pattern.isEmpty()) {
+ return URI_ROOT;
+ }
+ return KeyValue.of(CommonLowCardinalityKeys.URI, pattern);
+ }
+ if (status != null) {
+ if (status >= 300 && status < 400) {
+ return URI_REDIRECTION;
+ }
+ if (status == 404) {
+ return URI_NOT_FOUND;
+ }
+ }
+ return URI_UNKNOWN;
+ }
+
+ private KeyValue exception(@Nullable Throwable error) {
+ if (error != null) {
+ String simpleName = error.getClass().getSimpleName();
+ return KeyValue.of(CommonLowCardinalityKeys.EXCEPTION,
+ StringUtils.isNotBlank(simpleName) ? simpleName : error.getClass().getName());
+ }
+ return EXCEPTION_NONE;
+ }
+
+ private KeyValue outcome(@Nullable Integer status) {
+ if (status != null) {
+ return HttpOutcome.forStatus(status);
+ }
+ return HTTP_OUTCOME_UNKNOWN;
+ }
+
+ private KeyValue httpUrl(@Nullable String requestUri) {
+ if (requestUri != null) {
+ return KeyValue.of(CommonHighCardinalityKeys.HTTP_URL, requestUri);
+ }
+ return HTTP_URL_UNKNOWN;
+ }
+
+ static class HttpOutcome {
+
+ static KeyValue forStatus(int statusCode) {
+ if (statusCode >= 200 && statusCode < 300) {
+ return HTTP_OUTCOME_SUCCESS;
+ }
+ else {
+ return HTTP_OUTCOME_UNKNOWN;
+ }
+ }
+
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaClientRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaClientRequestObservationConvention.java
new file mode 100644
index 0000000000..5a2c46b9cf
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaClientRequestObservationConvention.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import java.net.URI;
+
+import io.micrometer.common.KeyValues;
+
+/**
+ * Default {@link HttpJakartaServerRequestObservationConvention}.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public class DefaultHttpJakartaClientRequestObservationConvention extends
+ AbstractDefaultHttpClientRequestObservationConvention implements HttpJakartaClientRequestObservationConvention {
+
+ private static final String DEFAULT_NAME = "http.client.requests";
+
+ private final String name;
+
+ /**
+ * Create a convention with the default name {@code "http.client.requests"}.
+ */
+ public DefaultHttpJakartaClientRequestObservationConvention() {
+ this(DEFAULT_NAME);
+ }
+
+ /**
+ * Create a convention with a custom name.
+ * @param name the observation name
+ */
+ public DefaultHttpJakartaClientRequestObservationConvention(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String getContextualName(HttpJakartaClientRequestObservationContext context) {
+ String method = context.getCarrier() != null
+ ? (context.getCarrier().getMethod() != null ? context.getCarrier().getMethod() : null) : null;
+ return getContextualName(method);
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(HttpJakartaClientRequestObservationContext context) {
+ URI uri = context.getCarrier() != null ? context.getCarrier().getUriInfo().getRequestUri() : null;
+ Throwable throwable = context.getError();
+ String methodName = context.getCarrier() != null ? context.getCarrier().getMethod() : null;
+ Integer statusCode = context.getResponse() != null ? context.getResponse().getStatus() : null;
+ String uriPathPattern = context.getUriTemplate();
+ return getLowCardinalityKeyValues(uri, throwable, methodName, statusCode, uriPathPattern);
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(HttpJakartaClientRequestObservationContext context) {
+ URI uri = context.getCarrier() != null ? context.getCarrier().getUriInfo().getRequestUri() : null;
+ String userAgent = context.getCarrier() != null ? context.getCarrier().getHeaderString("User-Agent") : null;
+ return getHighCardinalityKeyValues(uri, userAgent);
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaServerRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaServerRequestObservationConvention.java
new file mode 100644
index 0000000000..a65dc4fa3a
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaServerRequestObservationConvention.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.KeyValues;
+
+/**
+ * Default {@link HttpJakartaServerRequestObservationConvention}.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public class DefaultHttpJakartaServerRequestObservationConvention extends
+ AbstractDefaultHttpServerRequestObservationConvention implements HttpJakartaServerRequestObservationConvention {
+
+ private static final String DEFAULT_NAME = "http.server.requests";
+
+ private final String name;
+
+ /**
+ * Create a convention with the default name {@code "http.server.requests"}.
+ */
+ public DefaultHttpJakartaServerRequestObservationConvention() {
+ this(DEFAULT_NAME);
+ }
+
+ /**
+ * Create a convention with a custom name.
+ * @param name the observation name
+ */
+ public DefaultHttpJakartaServerRequestObservationConvention(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String getContextualName(HttpJakartaServerRequestObservationContext context) {
+ return getContextualName(context.getCarrier().getMethod().toLowerCase(), context.getPathPattern());
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(HttpJakartaServerRequestObservationContext context) {
+ String method = context.getCarrier() != null ? context.getCarrier().getMethod() : null;
+ Integer status = context.getResponse() != null ? context.getResponse().getStatus() : null;
+ String pathPattern = context.getPathPattern();
+ String requestUri = context.getCarrier() != null ? context.getCarrier().getRequestURI() : null;
+ return getLowCardinalityKeyValues(context.getError(), method, status, pathPattern, requestUri);
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(HttpJakartaServerRequestObservationContext context) {
+ String requestUri = context.getCarrier() != null ? context.getCarrier().getRequestURI() : null;
+ return getHighCardinalityKeyValues(requestUri);
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServerRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServerRequestObservationConvention.java
new file mode 100644
index 0000000000..6536b4b211
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServerRequestObservationConvention.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.KeyValues;
+
+/**
+ * Default {@link HttpJakartaServerRequestObservationConvention}.
+ *
+ * @author Brian Clozel
+ * @since 1.12.0
+ */
+public class DefaultHttpServerRequestObservationConvention extends AbstractDefaultHttpServerRequestObservationConvention
+ implements HttpServerRequestObservationConvention {
+
+ private static final String DEFAULT_NAME = "http.server.requests";
+
+ private final String name;
+
+ /**
+ * Create a convention with the default name {@code "http.server.requests"}.
+ */
+ public DefaultHttpServerRequestObservationConvention() {
+ this(DEFAULT_NAME);
+ }
+
+ /**
+ * Create a convention with a custom name.
+ * @param name the observation name
+ */
+ public DefaultHttpServerRequestObservationConvention(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String getContextualName(HttpServerRequestObservationContext context) {
+ String method = context.getCarrier().getMethod();
+ if (method == null) {
+ return null;
+ }
+ return getContextualName(method.toLowerCase(), context.getPathPattern());
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(HttpServerRequestObservationContext context) {
+ String method = context.getCarrier() != null ? context.getCarrier().getMethod() : null;
+ Integer status = context.getResponse() != null ? context.getResponse().getStatus() : null;
+ String pathPattern = context.getPathPattern();
+ String requestUri = context.getCarrier() != null ? context.getCarrier().getRequestURI() : null;
+ return getLowCardinalityKeyValues(context.getError(), method, status, pathPattern, requestUri);
+ }
+
+ @Override
+ public KeyValues getHighCardinalityKeyValues(HttpServerRequestObservationContext context) {
+ String requestUri = context.getCarrier() != null ? context.getCarrier().getRequestURI() : null;
+ return getHighCardinalityKeyValues(requestUri);
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationContext.java
new file mode 100644
index 0000000000..42bf4a8dcf
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationContext.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.lang.Nullable;
+import io.micrometer.observation.transport.RequestReplySenderContext;
+
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+
+/**
+ * Context that holds information for metadata collection during the client HTTP exchanges
+ * observations.
+ *
+ * This context also extends {@link RequestReplySenderContext} for propagating tracing
+ * information with the HTTP client exchange.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public class HttpJakartaClientRequestObservationContext
+ extends RequestReplySenderContext {
+
+ @Nullable
+ private String uriTemplate;
+
+ /**
+ * Create an observation context for HTTP client observations.
+ * @param containerRequestContext the context for a {@link ContainerRequestFilter}
+ */
+ public HttpJakartaClientRequestObservationContext(ContainerRequestContext containerRequestContext) {
+ super(HttpJakartaClientRequestObservationContext::setRequestHeader);
+ this.setCarrier(containerRequestContext);
+ }
+
+ private static void setRequestHeader(@Nullable ContainerRequestContext context, String name, String value) {
+ if (context != null) {
+ context.getHeaders().add(name, value);
+ }
+ }
+
+ /**
+ * Return the URI template used for the current client exchange, {@code null} if none
+ * was used.
+ */
+ @Nullable
+ public String getUriTemplate() {
+ return this.uriTemplate;
+ }
+
+ /**
+ * Set the URI template used for the current client exchange.
+ */
+ public void setUriTemplate(@Nullable String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationConvention.java
new file mode 100644
index 0000000000..4a9959efde
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationConvention.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * Interface for an {@link ObservationConvention} for Servlet HTTP requests.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public interface HttpJakartaClientRequestObservationConvention
+ extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof HttpJakartaClientRequestObservationContext;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationContext.java
new file mode 100644
index 0000000000..77bd259504
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationContext.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.lang.Nullable;
+import io.micrometer.observation.transport.RequestReplyReceiverContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * Context that holds information for metadata collection regarding Servlet HTTP requests
+ * observations.
+ *
+ * This context also extends {@link RequestReplyReceiverContext} for propagating tracing
+ * information during HTTP request processing.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public class HttpJakartaServerRequestObservationContext
+ extends RequestReplyReceiverContext {
+
+ @Nullable
+ private String pathPattern;
+
+ public HttpJakartaServerRequestObservationContext(HttpServletRequest request, HttpServletResponse response) {
+ super(HttpServletRequest::getHeader);
+ setCarrier(request);
+ setResponse(response);
+ }
+
+ @Nullable
+ public String getPathPattern() {
+ return this.pathPattern;
+ }
+
+ public void setPathPattern(@Nullable String pathPattern) {
+ this.pathPattern = pathPattern;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationConvention.java
new file mode 100644
index 0000000000..d0bb68782c
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationConvention.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * Interface for an {@link ObservationConvention} for Servlet HTTP requests.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public interface HttpJakartaServerRequestObservationConvention
+ extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof HttpJakartaServerRequestObservationContext;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpObservationDocumentation.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpObservationDocumentation.java
new file mode 100644
index 0000000000..5d493ff538
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpObservationDocumentation.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.Observation.Context;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * Documentation of HTTP based observations.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public enum HttpObservationDocumentation implements ObservationDocumentation {
+
+ /**
+ * Observation created when a request is sent out via Jakarta API.
+ */
+ JAKARTA_CLIENT_OBSERVATION {
+ @Override
+ public Class extends ObservationConvention extends Context>> getDefaultConvention() {
+ return DefaultHttpJakartaClientRequestObservationConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return KeyName.merge(CommonLowCardinalityKeys.values(), ClientLowCardinalityKeys.values());
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return KeyName.merge(CommonHighCardinalityKeys.values(), ClientHighCardinalityKeys.values());
+ }
+ },
+
+ /**
+ * Observation created when a request is received with Jakarta Http.
+ */
+ JAKARTA_SERVER_OBSERVATION {
+ @Override
+ public Class extends ObservationConvention extends Context>> getDefaultConvention() {
+ return DefaultHttpJakartaServerRequestObservationConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+
+ return KeyName.merge(CommonLowCardinalityKeys.values(), ServerLowCardinalityKeys.values());
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return CommonHighCardinalityKeys.values();
+ }
+
+ },
+
+ /**
+ * Observation created when a request is received with Javax Http.
+ */
+ SERVER_OBSERVATION {
+ @Override
+ public Class extends ObservationConvention extends Context>> getDefaultConvention() {
+ return DefaultHttpServerRequestObservationConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return KeyName.merge(CommonLowCardinalityKeys.values(), ServerLowCardinalityKeys.values());
+ }
+
+ @Override
+ public KeyName[] getHighCardinalityKeyNames() {
+ return CommonHighCardinalityKeys.values();
+ }
+
+ };
+
+ enum CommonLowCardinalityKeys implements KeyName {
+
+ /**
+ * HTTP request method.
+ */
+ HTTP_REQUEST_METHOD {
+ @Override
+ public String asString() {
+ return "http.request.method";
+ }
+ },
+
+ /**
+ * HTTP response status code.
+ */
+ HTTP_RESPONSE_STATUS_CODE {
+ @Override
+ public boolean isRequired() {
+ return false;
+ }
+
+ @Override
+ public String asString() {
+ return "http.response.status_code";
+ }
+ },
+
+ /**
+ * OSI Application Layer or non-OSI equivalent. The value SHOULD be normalized to
+ * lowercase.
+ */
+ NETWORK_PROTOCOL_NAME {
+ @Override
+ public String asString() {
+ return "network.protocol.name";
+ }
+ },
+
+ /**
+ * Name of the local HTTP server that received the request.
+ */
+ SERVER_ADDRESS {
+ @Override
+ public String asString() {
+ return "server.address";
+ }
+
+ },
+
+ /**
+ * Port of the local HTTP server that received the request.
+ */
+ SERVER_PORT {
+ @Override
+ public String asString() {
+ return "server.port";
+ }
+
+ },
+
+ /**
+ * The URI scheme component identifying the used protocol.
+ */
+ URL_SCHEME {
+ @Override
+ public String asString() {
+ return "url.scheme";
+ }
+ },
+
+ /**
+ * Name of HTTP request method or {@value KeyValue#NONE_VALUE} if the request was
+ * not received properly.
+ */
+ METHOD {
+ @Override
+ public String asString() {
+ return "method";
+ }
+
+ },
+
+ /**
+ * HTTP response raw status code, or {@code "UNKNOWN"} if no response was created.
+ */
+ STATUS {
+ @Override
+ public String asString() {
+ return "status";
+ }
+ },
+
+ /**
+ * URI pattern for the matching handler if available, falling back to
+ * {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 responses,
+ * {@code root} for requests with no path info, and {@code UNKNOWN} for all other
+ * requests.
+ */
+ URI {
+ @Override
+ public String asString() {
+ return "uri";
+ }
+ },
+
+ /**
+ * Name of the exception thrown during the exchange, or
+ * {@value KeyValue#NONE_VALUE}} if no exception happened.
+ */
+ EXCEPTION {
+ @Override
+ public String asString() {
+ return "exception";
+ }
+ },
+
+ /**
+ * Outcome of the HTTP server exchange.
+ */
+ OUTCOME {
+ @Override
+ public String asString() {
+ return "outcome";
+ }
+ }
+
+ }
+
+ enum ServerLowCardinalityKeys implements KeyName {
+
+ /**
+ * The matched route (path template in the format used by the respective server
+ * framework).
+ */
+ HTTP_ROUTE {
+ @Override
+ public boolean isRequired() {
+ return false;
+ }
+
+ @Override
+ public String asString() {
+ return "http.route";
+ }
+ }
+
+ }
+
+ enum ClientLowCardinalityKeys implements KeyName {
+
+ /**
+ * Client name derived from the request URI host.
+ */
+ CLIENT_NAME {
+ @Override
+ public String asString() {
+ return "client.name";
+ }
+ }
+
+ }
+
+ enum CommonHighCardinalityKeys implements KeyName {
+
+ /**
+ * The size of the request payload body in bytes. This is the number of bytes
+ * transferred excluding headers and is often, but not always, present as the
+ * Content-Length header. For requests using transport encoding, this should be
+ * the compressed size.
+ */
+ REQUEST_BODY_SIZE {
+ @Override
+ public boolean isRequired() {
+ return false;
+ }
+
+ @Override
+ public String asString() {
+ return "http.request.body.size";
+ }
+ },
+
+ /**
+ * The size of the response payload body in bytes. This is the number of bytes
+ * transferred excluding headers and is often, but not always, present as the
+ * Content-Length header. For requests using transport encoding, this should be
+ * the compressed size.
+ */
+ RESPONSE_BODY_SIZE {
+ @Override
+ public boolean isRequired() {
+ return false;
+ }
+
+ @Override
+ public String asString() {
+ return "http.response.body.size";
+ }
+
+ },
+
+ /**
+ * Value of the HTTP User-Agent header sent by the client.
+ */
+ USER_AGENT_ORIGINAL {
+ @Override
+ public boolean isRequired() {
+ return false;
+ }
+
+ @Override
+ public String asString() {
+ return "user_agent.original";
+ }
+
+ },
+
+ /**
+ * HTTP request URI.
+ */
+ HTTP_URL {
+ @Override
+ public String asString() {
+ return "http.url";
+ }
+ }
+
+ }
+
+ enum ClientHighCardinalityKeys implements KeyName {
+
+ /**
+ * Absolute URL describing a network resource according to RFC3986
+ */
+ URL_FULL {
+ @Override
+ public String asString() {
+ return "url.full";
+ }
+ }
+
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationContext.java
new file mode 100644
index 0000000000..afa3d5414a
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationContext.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import io.micrometer.common.lang.Nullable;
+import io.micrometer.observation.transport.RequestReplyReceiverContext;
+
+/**
+ * Context that holds information for metadata collection regarding Servlet HTTP requests
+ * observations.
+ *
+ * This context also extends {@link RequestReplyReceiverContext} for propagating tracing
+ * information during HTTP request processing.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public class HttpServerRequestObservationContext
+ extends RequestReplyReceiverContext {
+
+ @Nullable
+ private String pathPattern;
+
+ public HttpServerRequestObservationContext(HttpServletRequest request, HttpServletResponse response) {
+ super(HttpServletRequest::getHeader);
+ setCarrier(request);
+ setResponse(response);
+ }
+
+ @Nullable
+ public String getPathPattern() {
+ return this.pathPattern;
+ }
+
+ public void setPathPattern(@Nullable String pathPattern) {
+ this.pathPattern = pathPattern;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationConvention.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationConvention.java
new file mode 100644
index 0000000000..1a7d96394c
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationConvention.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.http;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * Interface for an {@link ObservationConvention} for Servlet HTTP requests.
+ *
+ * @author Brian Clozel
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ */
+public interface HttpServerRequestObservationConvention
+ extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof HttpServerRequestObservationContext;
+ }
+
+}
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/ObservedValve.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/ObservedValve.java
new file mode 100644
index 0000000000..c4d5513eb6
--- /dev/null
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/ObservedValve.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.tomcat;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Valve;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.valves.ValveBase;
+import io.micrometer.core.instrument.binder.http.DefaultHttpServerRequestObservationConvention;
+import io.micrometer.core.instrument.binder.http.HttpObservationDocumentation;
+import io.micrometer.core.instrument.binder.http.HttpServerRequestObservationContext;
+import io.micrometer.core.instrument.binder.http.HttpServerRequestObservationConvention;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+/**
+ * A {@link Valve} that creates {@link Observation}.
+ *
+ * @author Marcin Grzejszczak
+ * @since 1.12.0
+ * @see HttpObservationDocumentation
+ */
+public class ObservedValve extends ValveBase {
+
+ private static final HttpServerRequestObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultHttpServerRequestObservationConvention();
+
+ private final ObservationRegistry observationRegistry;
+
+ private final HttpServerRequestObservationConvention observationConvention;
+
+ public ObservedValve(ObservationRegistry observationRegistry,
+ HttpServerRequestObservationConvention observationConvention) {
+ this.observationRegistry = observationRegistry;
+ this.observationConvention = observationConvention;
+ setAsyncSupported(true);
+ }
+
+ public ObservedValve(ObservationRegistry observationRegistry) {
+ this(observationRegistry, null);
+ }
+
+ @Override
+ public void invoke(Request request, Response response) throws IOException, ServletException {
+ Observation observation = (Observation) request.getAttribute(Observation.class.getName());
+ if (observation != null) {
+ // this could happen for async dispatch
+ try (Observation.Scope scope = observation.openScope()) {
+ Valve next = getNext();
+ if (null == next) {
+ // no next valve
+ return;
+ }
+ next.invoke(request, response);
+ return;
+ }
+ }
+ HttpServerRequestObservationContext context = new HttpServerRequestObservationContext(request, response);
+ observation = HttpObservationDocumentation.SERVER_OBSERVATION
+ .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> context,
+ this.observationRegistry)
+ .start();
+ request.setAttribute(Observation.class.getName(), observation);
+ try (Observation.Scope scope = observation.openScope()) {
+ Valve next = getNext();
+ if (null == next) {
+ // no next valve
+ return;
+ }
+ next.invoke(request, response);
+ }
+ catch (Exception exception) {
+ observation.error(exception);
+ throw exception;
+ }
+ finally {
+ observation.stop();
+ }
+ }
+
+}
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/tomcat/ObservedValveTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/tomcat/ObservedValveTest.java
new file mode 100644
index 0000000000..56bb88f187
--- /dev/null
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/tomcat/ObservedValveTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 VMware, Inc.
+ *
+ * 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 io.micrometer.core.instrument.binder.tomcat;
+
+import static io.micrometer.observation.tck.TestObservationRegistryAssert.then;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Valve;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.valves.ValveBase;
+import org.assertj.core.api.BDDAssertions;
+import org.junit.jupiter.api.Test;
+
+import io.micrometer.core.instrument.binder.http.DefaultHttpServerRequestObservationConvention;
+import io.micrometer.core.instrument.binder.http.HttpServerRequestObservationContext;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert;
+
+class ObservedValveTest {
+
+ TestObservationRegistry observationRegistry = TestObservationRegistry.create();
+
+ ObservedValve observedValve = new ObservedValve(observationRegistry);
+
+ @Test
+ void shouldPopulateObservationAttributeForObservationFilterToReuse() throws ServletException, IOException {
+ Request request = request();
+
+ this.observedValve.invoke(request, response());
+
+ then(request.getAttribute(Observation.class.getName())).isNotNull();
+ thenObservationIsStartedAndStopped().hasHighCardinalityKeyValue("http.url", "UNKNOWN")
+ .hasLowCardinalityKeyValue("exception", "none");
+ then(observationRegistry).doesNotHaveAnyRemainingCurrentObservation();
+ }
+
+ @Test
+ void shouldOverrideDefaultObservationConvention() throws ServletException, IOException {
+ Request request = request();
+
+ new ObservedValve(observationRegistry, new DefaultHttpServerRequestObservationConvention() {
+ @Override
+ public String getContextualName(HttpServerRequestObservationContext context) {
+ return "HELLO";
+ }
+ }).invoke(request, response());
+
+ thenObservationIsStartedAndStopped().hasContextualNameEqualTo("HELLO");
+ }
+
+ @Test
+ void shouldHaveAsyncSupportedByDefault() {
+ ObservedValve observedValve = new ObservedValve(observationRegistry);
+
+ BDDAssertions.then(observedValve.isAsyncSupported()).isTrue();
+ }
+
+ private TestObservationRegistryAssertReturningObservationContextAssert thenObservationIsStartedAndStopped() {
+ return then(observationRegistry).hasSingleObservationThat().hasBeenStarted().hasBeenStopped();
+ }
+
+ @Test
+ void shouldPopulateObservationAttributeForObservationFilterToReuseWhenThereIsAnotherValveInChain()
+ throws ServletException, IOException {
+ Request request = request();
+
+ new ObservedValve(observationRegistry) {
+ @Override
+ public Valve getNext() {
+ return new MyValve();
+ }
+ }.invoke(request, response());
+
+ then(request.getAttribute(Observation.class.getName())).isNotNull();
+ thenObservationIsStartedAndStopped();
+ then(observationRegistry).doesNotHaveAnyRemainingCurrentObservation();
+ }
+
+ @Test
+ void shouldNotGenerateANewObservationWhenOneAlreadyPresent() throws ServletException, IOException {
+ Request request = request();
+
+ new ObservedValve(observationRegistry) {
+ @Override
+ public Valve getNext() {
+ return new ObservedValve(observationRegistry);
+ }
+ }.invoke(request, response());
+
+ then(request.getAttribute(Observation.class.getName())).isNotNull();
+ thenObservationIsStartedAndStopped();
+ then(observationRegistry).doesNotHaveAnyRemainingCurrentObservation();
+ }
+
+ private Request request() {
+ Request request = new Request();
+ request.setConnector(new Connector());
+ request.setCoyoteRequest(new org.apache.coyote.Request());
+ return request;
+ }
+
+ private Response response() {
+ Response response = new Response();
+ response.setConnector(new Connector());
+ response.setCoyoteResponse(new org.apache.coyote.Response());
+ return response;
+ }
+
+ static class MyValve extends ValveBase {
+
+ @Override
+ public void invoke(Request request, Response response) {
+
+ }
+
+ }
+
+}