diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 0f85f8228d7f..7c14e7817d61 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -13,6 +13,7 @@ configurations { dependencies { api(project(":spring-context")) api(project(":spring-web")) + api("jakarta.servlet:jakarta.servlet-api") implementation(project(":spring-core-test")) implementation("org.assertj:assertj-core") diff --git a/framework-docs/src/docs/asciidoc/index.adoc b/framework-docs/src/docs/asciidoc/index.adoc index 337af70033a5..42be83404281 100644 --- a/framework-docs/src/docs/asciidoc/index.adoc +++ b/framework-docs/src/docs/asciidoc/index.adoc @@ -16,7 +16,7 @@ STOMP Messaging. <> :: Spring WebFlux, WebClient, WebSocket, RSocket. <> :: REST Clients, JMS, JCA, JMX, -Email, Tasks, Scheduling, Caching. +Email, Tasks, Scheduling, Caching and Observability. <> :: Kotlin, Groovy, Dynamic Languages. <> :: Spring properties. https://github.com/spring-projects/spring-framework/wiki[*Wiki*] :: What's New, diff --git a/framework-docs/src/docs/asciidoc/integration.adoc b/framework-docs/src/docs/asciidoc/integration.adoc index c28f3416997e..2dd9aeb1e193 100644 --- a/framework-docs/src/docs/asciidoc/integration.adoc +++ b/framework-docs/src/docs/asciidoc/integration.adoc @@ -18,4 +18,6 @@ include::integration/scheduling.adoc[leveloffset=+1] include::integration/cache.adoc[leveloffset=+1] +include::integration/observability.adoc[leveloffset=+1] + include::integration/integration-appendix.adoc[leveloffset=+1] diff --git a/framework-docs/src/docs/asciidoc/integration/observability.adoc b/framework-docs/src/docs/asciidoc/integration/observability.adoc new file mode 100644 index 000000000000..9a8436988b9c --- /dev/null +++ b/framework-docs/src/docs/asciidoc/integration/observability.adoc @@ -0,0 +1,175 @@ +[[integration.observability]] += Observability Support + +Micrometer defines an https://micrometer.io/docs/observation[Observation concept that enables both Metrics and Traces] in applications. +Metrics support offers a way to create timers, gauges or counters for collecting statistics about the runtime behavior of your application. +Metrics can help you to track error rates, usage patterns, performance and more. +Traces provide a holisitic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications. + +Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured. +You can learn more about {docs-spring-boot}/html/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot]. + +[[integration.observability.concepts]] +== Micrometer Observation concepts + +If you are not familiar with Micrometer Observation, here's a quick summary of the new concepts you should know about. + +* `Observation` is the actual recording of something happening in your application. This is processed by `ObservationHandler` implementations to produce metrics or traces. +* Each observation has a corresponding `ObservationContext` implementation; this type holds all the relevant information for extracting metadata for it. + In the case of an HTTP server observation, the context implementation could hold the HTTP request, the HTTP response, any Exception thrown during processing... +* Each `Observation` holds `KeyValues` metadata. In the case of an server HTTP observation, this could be the HTTP request method, the HTTP response status... + This metadata is contributed by `ObservationConvention` implementations which should declare the type of `ObservationContext` they support. +* `KeyValues` are said to be "low cardinality" if there is a low, bounded number of possible values for the `KeyValue` tuple (HTTP methods is a good example). + Low cardinality values are contributed to metrics only. + "High cardinality" are on the other hand unbounded (for example, HTTP request URIs) and are only contributed to Traces. +* An `ObservationDocumentation` documents all observations in a particular domain, listing the expected key names and their meaning. + + +[[integration.observability.config]] +== Configuring Observations + +Global configuration options are available at the `ObservationRegistry#observationConfig()` level. +Each instrumented component will provide two extension points: + +* setting the `ObservationRegistry`; if not set, observations will not be recorded and will be no-ops +* providing a custom `ObservationConvention` to change the default observation name and extracted `KeyValues` + + +[[integration.observability.config.conventions]] +=== Using custom Observation conventions + +Let's take the example of the Spring MVC "http.server.requests" metrics instrumentation with the `ServerHttpObservationFilter`. +This observation is using a `ServerRequestObservationConvention` with a `ServerRequestObservationContext`; custom conventions can be configured on the Servlet filter. +If you would like to customize the metadata produced with the observation, you can extend the `DefaultServerRequestObservationConvention` for your requirements: + +include::code:ExtendedServerRequestObservationConvention[] + +If you want full control, you can then implement the entire convention contract for the observation you're interested in: + +include::code:CustomServerRequestObservationConvention[] + +You can also similar goals using a custom `ObservationFilter` - adding or removing key values for an observation. +Filters do not replace the default convention and are used as a post-processing component. + +include::code:ServerRequestObservationFilter[] + +You can configure `ObservationFilter` instances on the `ObservationRegistry`. + + +[[integration.observability.http-server]] +== HTTP Server instrumentation + +HTTP server exchanges observations are created with the name `"http.server.requests"` for Servlet and Reactive applications. + +[[integration.observability.http-server.servlet]] +=== Servlet applications + +Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application. +It is using the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. + +By default, the following `KeyValues` are created: + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE`} if no exception happened. +|`method` _(required)_|Name of HTTP request method or `KeyValue#NONE_VALUE` if the request was not received properly. +|`outcome` _(required)_|Outcome of the HTTP server exchange. +|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created. +|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests. +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`http.url` _(required)_|HTTP request URI. +|=== + + +[[integration.observability.http-server.reactive]] +=== Reactive applications + +Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application. +It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. + +By default, the following `KeyValues` are created: + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE` if no exception happened. +|`method` _(required)_|Name of HTTP request method or `KeyValue#NONE_VALUE` if the request was not received properly. +|`outcome` _(required)_|Outcome of the HTTP server exchange. +|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created. +|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests. +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`http.url` _(required)_|HTTP request URI. +|=== + + + +[[integration.observability.http-client]] +== HTTP Client instrumentation + +HTTP client exchanges observations are created with the name `"http.client.requests"` for blocking and reactive clients. +Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client. + +[[integration.observability.http-server.resttemplate]] +=== RestTemplate + +Instrumentation is using the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE` if no exception happened. +|`method` _(required)_|Name of HTTP request method or `KeyValue#NONE_VALUE` if the request could not be created. +|`outcome` _(required)_|Outcome of the HTTP client exchange. +|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. +|`uri` _(required)_|URI template used for HTTP request, or `KeyValue#NONE_VALUE` if none was provided. +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`client.name` _(required)_|Client name derived from the request URI host. +|`http.url` _(required)_|HTTP request URI. +|=== + + + +[[integration.observability.http-server.webclient]] +=== WebClient + +Instrumentation is using the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE` if no exception happened. +|`method` _(required)_|Name of HTTP request method or `KeyValue#NONE_VALUE` if the request could not be created. +|`outcome` _(required)_|Outcome of the HTTP client exchange. +|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received. +|`uri` _(required)_|URI template used for HTTP request, or `KeyValue#NONE_VALUE` if none was provided. +|=== + +.High cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`client.name` _(required)_|Client name derived from the request URI host. +|`http.url` _(required)_|HTTP request URI. +|=== + + diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java new file mode 100644 index 000000000000..079a83099e1c --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/CustomServerRequestObservationConvention.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.config.conventions; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +import org.springframework.http.server.observation.ServerHttpObservationDocumentation; +import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.http.server.observation.ServerRequestObservationConvention; + +public class CustomServerRequestObservationConvention implements ServerRequestObservationConvention { + + @Override + public String getName() { + // will be used as the metric name + return "http.server.requests"; + } + + @Override + public String getContextualName(ServerRequestObservationContext context) { + // will be used for the trace name + return "http " + context.getCarrier().getMethod().toLowerCase(); + } + + @Override + public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) { + return KeyValues.of(method(context), status(context), exception(context)); + } + + + @Override + public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) { + return KeyValues.of(httpUrl(context)); + } + + + protected KeyValue method(ServerRequestObservationContext context) { + // You should reuse as much as possible the corresponding ObservationDocumentation for key names + return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod()); + } + + // @fold:on // status(), exception(), httpUrl()... + private KeyValue exception(ServerRequestObservationContext context) { + String exception = (context.getError() != null) ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE; + return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception); + } + + private KeyValue status(ServerRequestObservationContext context) { + return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(context.getResponse().getStatus())); + } + + private KeyValue httpUrl(ServerRequestObservationContext context) { + return KeyValue.of(ServerHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().getRequestURI()); + } + // @fold:off + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java new file mode 100644 index 000000000000..de24b8bf2865 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ExtendedServerRequestObservationConvention.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.config.conventions; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationContext; + +public class ExtendedServerRequestObservationConvention extends DefaultServerRequestObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) { + // here, we just want to have an additional KeyValue to the observation, keeping the default values + return super.getLowCardinalityKeyValues(context).and(custom(context)); + } + + protected KeyValue custom(ServerRequestObservationContext context) { + return KeyValue.of("custom.method", context.getCarrier().getMethod()); + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java new file mode 100644 index 000000000000..fabad6d79f90 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/observability/config/conventions/ServerRequestObservationFilter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.integration.observability.config.conventions; + + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationFilter; + +import org.springframework.http.server.observation.ServerRequestObservationContext; + +public class ServerRequestObservationFilter implements ObservationFilter { + + @Override + public Observation.Context map(Observation.Context context) { + if (context instanceof ServerRequestObservationContext serverContext) { + context.setName("custom.observation.name"); + context.addLowCardinalityKeyValue(KeyValue.of("project", "spring")); + String customAttribute = (String) serverContext.getCarrier().getAttribute("customAttribute"); + context.addLowCardinalityKeyValue(KeyValue.of("custom.attribute", customAttribute)); + } + return context; + } +}