diff --git a/framework-docs/modules/ROOT/pages/integration/observability.adoc b/framework-docs/modules/ROOT/pages/integration/observability.adoc index 3d3f0679ba..398416379b 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -2,8 +2,8 @@ = 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. +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 holistic 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. @@ -39,16 +39,16 @@ https://micrometer.io/docs/concepts#_naming_meters[to the format preferred by th [[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. +If you are not familiar with Micrometer Observation, here's a quick summary of the 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 a server HTTP observation, this could be the HTTP request method, the HTTP response status... + In the case of an HTTP server observation, the context implementation could hold the HTTP request, the HTTP response, any exception thrown during processing, and so forth. +* Each `Observation` holds `KeyValues` metadata. In the case of an HTTP server observation, this could be the HTTP request method, the HTTP response status, and so forth. 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 method is a good example). Low cardinality values are contributed to metrics only. - High cardinality values are on the other hand unbounded (for example, HTTP request URIs) and are only contributed to Traces. + Conversely, "high cardinality" values are 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. @@ -66,16 +66,16 @@ Each instrumented component will provide two extension points: === 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. +This observation uses 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: +If you want full control, you can implement the entire convention contract for the observation you're interested in: include-code::./CustomServerRequestObservationConvention[] -You can also achieve similar goals using a custom `ObservationFilter` - adding or removing key values for an observation. +You can also achieve 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[] @@ -111,22 +111,22 @@ By default, the following `KeyValues` are created: [[observability.http-server]] == HTTP Server instrumentation -HTTP server exchanges observations are created with the name `"http.server.requests"` for Servlet and Reactive applications. +HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications. [[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`. +It uses the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`. -This will only record an observation as an error if the `Exception` has not been handled by the web Framework and has bubbled up to the Servlet filter. +This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the Servlet filter. Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation. You can, at any point during request processing, set the error field on the `ObservationContext` yourself: include-code::./UserController[] NOTE: Because the instrumentation is done at the Servlet Filter level, the observation scope only covers the filters ordered after this one as well as the handling of the request. -Typically, the Servlet container error handling is done at a lower level and won't have any active observation nor span. +Typically, Servlet container error handling is performed at a lower level and won't have any active observation or span. For this use case, a container-specific implementation is required, such as a `org.apache.catalina.Valve` for Tomcat; this is outside of the scope of this project. By default, the following `KeyValues` are created: @@ -189,9 +189,9 @@ By default, the following `KeyValues` are created: [[observability.http-client]] -== HTTP Client instrumentation +== HTTP Client Instrumentation -HTTP client exchanges observations are created with the name `"http.client.requests"` for blocking and reactive clients. +HTTP client exchange 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. [[observability.http-client.resttemplate]] @@ -200,7 +200,7 @@ Unlike their server counterparts, the instrumentation is implemented directly in Applications must configure an `ObservationRegistry` on `RestTemplate` instances to enable the instrumentation; without that, observations are "no-ops". Spring Boot will auto-configure `RestTemplateBuilder` beans with the observation registry already set. -Instrumentation is using the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. +Instrumentation uses the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. .Low cardinality Keys [cols="a,a"] @@ -229,7 +229,7 @@ Instrumentation is using the `org.springframework.http.client.observation.Client Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops". Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set. -Instrumentation is using the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. +Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`. .Low cardinality Keys [cols="a,a"] diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index fba1806bea..fbca876f29 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -54,6 +54,7 @@ import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; /** * Unit tests for {@link DefaultConversionService}. @@ -324,8 +325,8 @@ class DefaultConversionServiceTests { @Test void numberToNumberNotSupportedNumber() { - assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> - conversionService.convert(1, CustomNumber.class)); + assertThatExceptionOfType(ConversionFailedException.class) + .isThrownBy(() -> conversionService.convert(1, CustomNumber.class)); } @Test @@ -342,28 +343,32 @@ class DefaultConversionServiceTests { @Test void convertArrayToCollectionInterface() { - Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Collection.class); + @SuppressWarnings("unchecked") + Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Collection.class); assertThat(result).isEqualTo(List.of("1", "2", "3")); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test void convertArrayToSetInterface() { - Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Set.class); - assertThat(result).isEqualTo(Set.of("1", "2", "3")); + @SuppressWarnings("unchecked") + Collection result = conversionService.convert(new String[] {"1", "2", "3"}, Set.class); + assertThat(result).isExactlyInstanceOf(LinkedHashSet.class).containsExactly("1", "2", "3"); } @Test void convertArrayToListInterface() { - List result = conversionService.convert(new String[] {"1", "2", "3"}, List.class); - assertThat(result).isEqualTo(List.of("1", "2", "3")); + @SuppressWarnings("unchecked") + List result = conversionService.convert(new String[] {"1", "2", "3"}, List.class); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test void convertArrayToCollectionGenericTypeConversion() throws Exception { @SuppressWarnings("unchecked") - List result = (List) conversionService.convert(new String[] {"1", "2", "3"}, TypeDescriptor - .valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); - assertThat(result).isEqualTo(List.of(1, 2, 3)); + List result = (List) conversionService.convert(new String[] {"1", "2", "3"}, + TypeDescriptor.valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly(1, 2, 3); } @Test @@ -371,15 +376,13 @@ class DefaultConversionServiceTests { String[] source = {"1", "3", "4"}; @SuppressWarnings("unchecked") Stream result = (Stream) this.conversionService.convert(source, - TypeDescriptor.valueOf(String[].class), - new TypeDescriptor(getClass().getDeclaredField("genericStream"))); + TypeDescriptor.valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericStream"))); assertThat(result).containsExactly(1, 3, 4); } @Test void spr7766() throws Exception { - ConverterRegistry registry = (conversionService); - registry.addConverter(new ColorConverter()); + conversionService.addConverter(new ColorConverter()); @SuppressWarnings("unchecked") List colors = (List) conversionService.convert(new String[] {"ffffff", "#000000"}, TypeDescriptor.valueOf(String[].class), @@ -389,14 +392,15 @@ class DefaultConversionServiceTests { @Test void convertArrayToCollectionImpl() { - ArrayList result = conversionService.convert(new String[] {"1", "2", "3"}, ArrayList.class); - assertThat(result).isEqualTo(List.of("1", "2", "3")); + @SuppressWarnings("unchecked") + ArrayList result = conversionService.convert(new String[] {"1", "2", "3"}, ArrayList.class); + assertThat(result).isExactlyInstanceOf(ArrayList.class).containsExactly("1", "2", "3"); } @Test void convertArrayToAbstractCollection() { - assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> - conversionService.convert(new String[]{"1", "2", "3"}, AbstractList.class)); + assertThatExceptionOfType(ConversionFailedException.class) + .isThrownBy(() -> conversionService.convert(new String[]{"1", "2", "3"}, AbstractList.class)); } @Test @@ -465,8 +469,7 @@ class DefaultConversionServiceTests { @Test void convertObjectToArray() { Object[] result = conversionService.convert(3L, Object[].class); - assertThat(result).hasSize(1); - assertThat(result[0]).isEqualTo(3L); + assertThat(result).containsExactly(3L); } @Test @@ -506,15 +509,17 @@ class DefaultConversionServiceTests { @Test void convertStringToCollection() { - List result = conversionService.convert("1,2,3", List.class); - assertThat(result).isEqualTo(List.of("1", "2", "3")); + @SuppressWarnings("unchecked") + List result = conversionService.convert("1,2,3", List.class); + assertThat(result).containsExactly("1", "2", "3"); } @Test void convertStringToCollectionWithElementConversion() throws Exception { - List result = (List) conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), + @SuppressWarnings("unchecked") + List result = (List) conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericList"))); - assertThat(result).isEqualTo(List.of(1, 2, 3)); + assertThat(result).containsExactly(1, 2, 3); } @Test @@ -539,17 +544,14 @@ class DefaultConversionServiceTests { @Test void convertCollectionToObjectAssignableTarget() throws Exception { - Collection source = new ArrayList<>(); - source.add("foo"); + Collection source = List.of("foo"); Object result = conversionService.convert(source, new TypeDescriptor(getClass().getField("assignableTarget"))); - assertThat(result).isEqualTo(source); + assertThat(result).isSameAs(source); } @Test void convertCollectionToObjectWithCustomConverter() { - List source = new ArrayList<>(); - source.add("A"); - source.add("B"); + List source = List.of("A", "B"); conversionService.addConverter(List.class, ListWrapper.class, ListWrapper::new); ListWrapper result = conversionService.convert(source, ListWrapper.class); assertThat(result.getList()).isSameAs(source); @@ -557,8 +559,9 @@ class DefaultConversionServiceTests { @Test void convertObjectToCollection() { - List result = conversionService.convert(3L, List.class); - assertThat(result).isEqualTo(List.of(3L)); + @SuppressWarnings("unchecked") + List result = conversionService.convert(3L, List.class); + assertThat(result).containsExactly(3L); } @Test @@ -607,7 +610,7 @@ class DefaultConversionServiceTests { @Test void convertByteArrayToWrapperArray() { - byte[] byteArray = new byte[] {1, 2, 3}; + byte[] byteArray = {1, 2, 3}; Byte[] converted = conversionService.convert(byteArray, Byte[].class); assertThat(converted).isEqualTo(new Byte[]{1, 2, 3}); } @@ -667,21 +670,18 @@ class DefaultConversionServiceTests { @SuppressWarnings("unchecked") List bar = (List) conversionService.convert(null, TypeDescriptor.valueOf(LinkedHashSet.class), new TypeDescriptor(getClass().getField("genericList"))); - assertThat((Object) bar).isNull(); + assertThat(bar).isNull(); } @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "unchecked" }) void convertCollectionToCollectionNotGeneric() { Set foo = new LinkedHashSet<>(); foo.add("1"); foo.add("2"); foo.add("3"); - List bar = (List) conversionService.convert(foo, TypeDescriptor.valueOf(LinkedHashSet.class), TypeDescriptor - .valueOf(List.class)); - assertThat(bar.get(0)).isEqualTo("1"); - assertThat(bar.get(1)).isEqualTo("2"); - assertThat(bar.get(2)).isEqualTo("3"); + List bar = (List) conversionService.convert(foo, TypeDescriptor.valueOf(LinkedHashSet.class), TypeDescriptor.valueOf(List.class)); + assertThat(bar).containsExactly("1", "2", "3"); } @Test @@ -694,34 +694,25 @@ class DefaultConversionServiceTests { Collection values = map.values(); List bar = (List) conversionService.convert(values, TypeDescriptor.forObject(values), new TypeDescriptor(getClass().getField("genericList"))); - assertThat(bar).hasSize(3); - assertThat(bar.get(0)).isEqualTo(1); - assertThat(bar.get(1)).isEqualTo(2); - assertThat(bar.get(2)).isEqualTo(3); + assertThat(bar).containsExactly(1, 2, 3); } @Test void collection() { - List strings = new ArrayList<>(); - strings.add("3"); - strings.add("9"); + List strings = List.of("3", "9"); @SuppressWarnings("unchecked") List integers = (List) conversionService.convert(strings, TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class))); - assertThat(integers.get(0)).isEqualTo(3); - assertThat(integers.get(1)).isEqualTo(9); + assertThat(integers).containsExactly(3, 9); } @Test void convertMapToMap() throws Exception { - Map foo = new HashMap<>(); - foo.put("1", "BAR"); - foo.put("2", "BAZ"); + Map foo = Map.of("1", "BAR", "2", "BAZ"); @SuppressWarnings("unchecked") Map map = (Map) conversionService.convert(foo, TypeDescriptor.forObject(foo), new TypeDescriptor(getClass().getField("genericMap"))); - assertThat(map.get(1)).isEqualTo(Foo.BAR); - assertThat(map.get(2)).isEqualTo(Foo.BAZ); + assertThat(map).contains(entry(1, Foo.BAR), entry(2, Foo.BAZ)); } @Test @@ -729,8 +720,9 @@ class DefaultConversionServiceTests { Map hashMap = new LinkedHashMap<>(); hashMap.put("1", 1); hashMap.put("2", 2); - List converted = conversionService.convert(hashMap.values(), List.class); - assertThat(converted).isEqualTo(List.of(1, 2)); + @SuppressWarnings("unchecked") + List converted = conversionService.convert(hashMap.values(), List.class); + assertThat(converted).containsExactly(1, 2); } @Test @@ -741,8 +733,7 @@ class DefaultConversionServiceTests { @SuppressWarnings("unchecked") Map integers = (Map) conversionService.convert(strings, TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(Integer.class))); - assertThat(integers.get(3)).isEqualTo(9); - assertThat(integers.get(6)).isEqualTo(31); + assertThat(integers).contains(entry(3, 9), entry(6, 31)); } @Test @@ -751,25 +742,25 @@ class DefaultConversionServiceTests { foo.setProperty("1", "BAR"); foo.setProperty("2", "BAZ"); String result = conversionService.convert(foo, String.class); - assertThat(result).contains("1=BAR"); - assertThat(result).contains("2=BAZ"); + assertThat(result).contains("1=BAR", "2=BAZ"); } @Test void convertStringToProperties() { - Properties result = conversionService.convert("a=b\nc=2\nd=", Properties.class); - assertThat(result).hasSize(3); - assertThat(result.getProperty("a")).isEqualTo("b"); - assertThat(result.getProperty("c")).isEqualTo("2"); - assertThat(result.getProperty("d")).isEmpty(); + Properties result = conversionService.convert(""" + a=b + c=2 + d=""", Properties.class); + assertThat(result).contains(entry("a", "b"), entry("c", "2"), entry("d", "")); } @Test - void convertStringToPropertiesWithSpaces() { - Properties result = conversionService.convert(" foo=bar\n bar=baz\n baz=boop", Properties.class); - assertThat(result.get("foo")).isEqualTo("bar"); - assertThat(result.get("bar")).isEqualTo("baz"); - assertThat(result.get("baz")).isEqualTo("boop"); + void convertStringToPropertiesWithLeadingSpaces() { + Properties result = conversionService.convert(""" + \s foo=bar + \s bar=baz + \s baz=boo""", Properties.class); + assertThat(result).contains(entry("foo", "bar"), entry("bar", "baz"), entry("baz", "boo")); } // generic object conversion @@ -838,8 +829,8 @@ class DefaultConversionServiceTests { @Test void convertObjectToObjectNoValueOfMethodOrConstructor() { - assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> - conversionService.convert(3L, SSN.class)); + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert(3L, SSN.class)); } @Test @@ -870,21 +861,20 @@ class DefaultConversionServiceTests { @Test void convertStringToCharArray() { char[] converted = conversionService.convert("a,b,c", char[].class); - assertThat(converted).isEqualTo(new char[]{'a', 'b', 'c'}); + assertThat(converted).containsExactly('a', 'b', 'c'); } @Test void convertStringToCustomCharArray() { conversionService.addConverter(String.class, char[].class, String::toCharArray); char[] converted = conversionService.convert("abc", char[].class); - assertThat(converted).isEqualTo(new char[] {'a', 'b', 'c'}); + assertThat(converted).containsExactly('a', 'b', 'c'); } @Test @SuppressWarnings("unchecked") void multidimensionalArrayToListConversionShouldConvertEntriesCorrectly() { - String[][] grid = new String[][] {new String[] {"1", "2", "3", "4"}, new String[] {"5", "6", "7", "8"}, - new String[] {"9", "10", "11", "12"}}; + String[][] grid = new String[][] {{"1", "2", "3", "4"}, {"5", "6", "7", "8"}, {"9", "10", "11", "12"}}; List converted = conversionService.convert(grid, List.class); String[][] convertedBack = conversionService.convert(converted, String[][].class); assertThat(convertedBack).isEqualTo(grid); @@ -893,10 +883,10 @@ class DefaultConversionServiceTests { @Test void convertCannotOptimizeArray() { conversionService.addConverter(Byte.class, Byte.class, source -> (byte) (source + 1)); - byte[] byteArray = new byte[] {1, 2, 3}; + byte[] byteArray = {1, 2, 3}; byte[] converted = conversionService.convert(byteArray, byte[].class); assertThat(converted).isNotSameAs(byteArray); - assertThat(converted).isEqualTo(new byte[]{2, 3, 4}); + assertThat(converted).containsExactly(2, 3, 4); } @Test