From a456e4758c07f92a4cfabad80049d5a4c2d49e95 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 20 Jul 2023 16:11:14 +0200 Subject: [PATCH] Added ObservedValve for Tomcat to have that working we've added additional convention and contexts around javax and jakarta classes --- config/checkstyle/checkstyle-suppressions.xml | 1 + ...ttpClientRequestObservationConvention.java | 147 ++++++++ ...ttpServerRequestObservationConvention.java | 157 ++++++++ ...rtaClientRequestObservationConvention.java | 80 +++++ ...rtaServerRequestObservationConvention.java | 74 ++++ ...ttpServerRequestObservationConvention.java | 77 ++++ ...akartaClientRequestObservationContext.java | 73 ++++ ...rtaClientRequestObservationConvention.java | 36 ++ ...akartaServerRequestObservationContext.java | 55 +++ ...rtaServerRequestObservationConvention.java | 36 ++ .../http/HttpObservationDocumentation.java | 339 ++++++++++++++++++ .../HttpServerRequestObservationContext.java | 56 +++ ...ttpServerRequestObservationConvention.java | 36 ++ .../binder/tomcat/ObservedValve.java | 97 +++++ .../binder/tomcat/ObservedValveTest.java | 138 +++++++ 15 files changed, 1402 insertions(+) create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpClientRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/AbstractDefaultHttpServerRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaClientRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpJakartaServerRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServerRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationContext.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaClientRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationContext.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpJakartaServerRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpObservationDocumentation.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationContext.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServerRequestObservationConvention.java create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/ObservedValve.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/tomcat/ObservedValveTest.java 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> 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> 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> 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) { + + } + + } + +}