From e8de5a6d95ffb45cd532c2a863e6a726e159b7e3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Sep 2019 20:54:35 +0100 Subject: [PATCH] Provide a public API for determining a request's outcome Closes gh-18150 --- .../boot/actuate/metrics/http/Outcome.java | 97 +++++++++++++++++++ .../actuate/metrics/http/package-info.java | 20 ++++ .../web/client/RestTemplateExchangeTags.java | 38 +------- .../client/WebClientExchangeTags.java | 44 +-------- .../web/reactive/server/WebFluxTags.java | 31 +----- .../metrics/web/servlet/WebMvcTags.java | 33 +------ .../actuate/metrics/http/OutcomeTests.java | 75 ++++++++++++++ .../client/RestTemplateExchangeTagsTests.java | 8 -- 8 files changed, 205 insertions(+), 141 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/Outcome.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/Outcome.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/Outcome.java new file mode 100644 index 0000000000..dcbc759eba --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/Outcome.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2019 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.boot.actuate.metrics.http; + +import io.micrometer.core.instrument.Tag; + +/** + * The outcome of an HTTP request. + * + * @author Andy Wilkinson + * @since 2.2.0 + */ +public enum Outcome { + + /** + * Outcome of the request was informational. + */ + INFORMATIONAL, + + /** + * Outcome of the request was success. + */ + SUCCESS, + + /** + * Outcome of the request was redirection. + */ + REDIRECTION, + + /** + * Outcome of the request was client error. + */ + CLIENT_ERROR, + + /** + * Outcome of the request was server error. + */ + SERVER_ERROR, + + /** + * Outcome of the request was unknown. + */ + UNKNOWN; + + private final Tag tag; + + Outcome() { + this.tag = Tag.of("outcome", name()); + } + + /** + * Returns the {@code Outcome} as a {@link Tag} named {@code outcome}. + * @return the {@code outcome} {@code Tag} + */ + public Tag asTag() { + return this.tag; + } + + /** + * Return the @code Outcome} for the given HTTP {@code status} code. + * @param status the HTTP status code + * @return the matching Outcome + */ + public static Outcome forStatus(int status) { + if (status >= 100 && status < 200) { + return INFORMATIONAL; + } + else if (status >= 200 && status < 300) { + return SUCCESS; + } + else if (status >= 300 && status < 400) { + return REDIRECTION; + } + else if (status >= 400 && status < 500) { + return CLIENT_ERROR; + } + else if (status >= 500 && status < 600) { + return SERVER_ERROR; + } + return UNKNOWN; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java new file mode 100644 index 0000000000..3675c4bbe7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2019 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. + */ + +/** + * Support classes HTTP-related metrics. + */ +package org.springframework.boot.actuate.metrics.http; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java index e729c1c398..df7c74a50c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java @@ -18,15 +18,12 @@ package org.springframework.boot.actuate.metrics.web.client; import java.io.IOException; import java.net.URI; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Pattern; import io.micrometer.core.instrument.Tag; +import org.springframework.boot.actuate.metrics.http.Outcome; import org.springframework.http.HttpRequest; -import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; @@ -45,30 +42,6 @@ public final class RestTemplateExchangeTags { private static final Pattern STRIP_URI_PATTERN = Pattern.compile("^https?://[^/]+/"); - private static final Tag OUTCOME_UNKNOWN = Tag.of("outcome", "UNKNOWN"); - - private static final Tag OUTCOME_INFORMATIONAL = Tag.of("outcome", "INFORMATIONAL"); - - private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS"); - - private static final Tag OUTCOME_REDIRECTION = Tag.of("outcome", "REDIRECTION"); - - private static final Tag OUTCOME_CLIENT_ERROR = Tag.of("outcome", "CLIENT_ERROR"); - - private static final Tag OUTCOME_SERVER_ERROR = Tag.of("outcome", "SERVER_ERROR"); - - private static final Map SERIES_OUTCOMES; - - static { - Map seriesOutcomes = new HashMap<>(); - seriesOutcomes.put(HttpStatus.Series.INFORMATIONAL, OUTCOME_INFORMATIONAL); - seriesOutcomes.put(HttpStatus.Series.SUCCESSFUL, OUTCOME_SUCCESS); - seriesOutcomes.put(HttpStatus.Series.REDIRECTION, OUTCOME_REDIRECTION); - seriesOutcomes.put(HttpStatus.Series.CLIENT_ERROR, OUTCOME_CLIENT_ERROR); - seriesOutcomes.put(HttpStatus.Series.SERVER_ERROR, OUTCOME_SERVER_ERROR); - SERIES_OUTCOMES = Collections.unmodifiableMap(seriesOutcomes); - } - private RestTemplateExchangeTags() { } @@ -155,16 +128,13 @@ public final class RestTemplateExchangeTags { public static Tag outcome(ClientHttpResponse response) { try { if (response != null) { - HttpStatus.Series series = HttpStatus.Series.resolve(response.getRawStatusCode()); - if (series != null) { - return SERIES_OUTCOMES.getOrDefault(series, OUTCOME_UNKNOWN); - } + return Outcome.forStatus(response.getRawStatusCode()).asTag(); } } - catch (IOException | IllegalArgumentException ex) { + catch (IOException ex) { // Continue } - return OUTCOME_UNKNOWN; + return Outcome.UNKNOWN.asTag(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java index 13ee23871e..b6c0469a08 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java @@ -17,15 +17,11 @@ package org.springframework.boot.actuate.metrics.web.reactive.client; import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Pattern; import io.micrometer.core.instrument.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatus.Series; +import org.springframework.boot.actuate.metrics.http.Outcome; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; @@ -51,30 +47,6 @@ public final class WebClientExchangeTags { private static final Tag CLIENT_NAME_NONE = Tag.of("clientName", "none"); - private static final Tag OUTCOME_UNKNOWN = Tag.of("outcome", "UNKNOWN"); - - private static final Tag OUTCOME_INFORMATIONAL = Tag.of("outcome", "INFORMATIONAL"); - - private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS"); - - private static final Tag OUTCOME_REDIRECTION = Tag.of("outcome", "REDIRECTION"); - - private static final Tag OUTCOME_CLIENT_ERROR = Tag.of("outcome", "CLIENT_ERROR"); - - private static final Tag OUTCOME_SERVER_ERROR = Tag.of("outcome", "SERVER_ERROR"); - - private static final Map SERIES_OUTCOMES; - - static { - Map seriesOutcomes = new HashMap<>(); - seriesOutcomes.put(Series.INFORMATIONAL, OUTCOME_INFORMATIONAL); - seriesOutcomes.put(Series.SUCCESSFUL, OUTCOME_SUCCESS); - seriesOutcomes.put(Series.REDIRECTION, OUTCOME_REDIRECTION); - seriesOutcomes.put(Series.CLIENT_ERROR, OUTCOME_CLIENT_ERROR); - seriesOutcomes.put(Series.SERVER_ERROR, OUTCOME_SERVER_ERROR); - SERIES_OUTCOMES = Collections.unmodifiableMap(seriesOutcomes); - } - private WebClientExchangeTags() { } @@ -146,18 +118,8 @@ public final class WebClientExchangeTags { * @since 2.2.0 */ public static Tag outcome(ClientResponse response) { - try { - if (response != null) { - Series series = HttpStatus.Series.resolve(response.rawStatusCode()); - if (series != null) { - return SERIES_OUTCOMES.getOrDefault(series, OUTCOME_UNKNOWN); - } - } - } - catch (IllegalArgumentException ex) { - // Continue - } - return OUTCOME_UNKNOWN; + Outcome outcome = (response != null) ? Outcome.forStatus(response.rawStatusCode()) : Outcome.UNKNOWN; + return outcome.asTag(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java index aafee89951..1e26db8033 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; import io.micrometer.core.instrument.Tag; +import org.springframework.boot.actuate.metrics.http.Outcome; import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; import org.springframework.web.reactive.HandlerMapping; @@ -45,18 +46,6 @@ public final class WebFluxTags { private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); - private static final Tag OUTCOME_UNKNOWN = Tag.of("outcome", "UNKNOWN"); - - private static final Tag OUTCOME_INFORMATIONAL = Tag.of("outcome", "INFORMATIONAL"); - - private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS"); - - private static final Tag OUTCOME_REDIRECTION = Tag.of("outcome", "REDIRECTION"); - - private static final Tag OUTCOME_CLIENT_ERROR = Tag.of("outcome", "CLIENT_ERROR"); - - private static final Tag OUTCOME_SERVER_ERROR = Tag.of("outcome", "SERVER_ERROR"); - private WebFluxTags() { } @@ -145,22 +134,8 @@ public final class WebFluxTags { */ public static Tag outcome(ServerWebExchange exchange) { HttpStatus status = exchange.getResponse().getStatusCode(); - if (status != null) { - if (status.is1xxInformational()) { - return OUTCOME_INFORMATIONAL; - } - if (status.is2xxSuccessful()) { - return OUTCOME_SUCCESS; - } - if (status.is3xxRedirection()) { - return OUTCOME_REDIRECTION; - } - if (status.is4xxClientError()) { - return OUTCOME_CLIENT_ERROR; - } - return OUTCOME_SERVER_ERROR; - } - return OUTCOME_UNKNOWN; + Outcome outcome = (status != null) ? Outcome.forStatus(status.value()) : Outcome.UNKNOWN; + return outcome.asTag(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java index 89c6d1ba3f..78c4c0b970 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java @@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse; import io.micrometer.core.instrument.Tag; +import org.springframework.boot.actuate.metrics.http.Outcome; import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerMapping; @@ -54,18 +55,6 @@ public final class WebMvcTags { private static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN"); - private static final Tag OUTCOME_UNKNOWN = Tag.of("outcome", "UNKNOWN"); - - private static final Tag OUTCOME_INFORMATIONAL = Tag.of("outcome", "INFORMATIONAL"); - - private static final Tag OUTCOME_SUCCESS = Tag.of("outcome", "SUCCESS"); - - private static final Tag OUTCOME_REDIRECTION = Tag.of("outcome", "REDIRECTION"); - - private static final Tag OUTCOME_CLIENT_ERROR = Tag.of("outcome", "CLIENT_ERROR"); - - private static final Tag OUTCOME_SERVER_ERROR = Tag.of("outcome", "SERVER_ERROR"); - private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); @@ -174,24 +163,8 @@ public final class WebMvcTags { * @since 2.1.0 */ public static Tag outcome(HttpServletResponse response) { - if (response != null) { - HttpStatus.Series series = HttpStatus.Series.resolve(response.getStatus()); - if (series != null) { - switch (series) { - case INFORMATIONAL: - return OUTCOME_INFORMATIONAL; - case SUCCESSFUL: - return OUTCOME_SUCCESS; - case REDIRECTION: - return OUTCOME_REDIRECTION; - case CLIENT_ERROR: - return OUTCOME_CLIENT_ERROR; - case SERVER_ERROR: - return OUTCOME_SERVER_ERROR; - } - } - } - return OUTCOME_UNKNOWN; + Outcome outcome = (response != null) ? Outcome.forStatus(response.getStatus()) : Outcome.UNKNOWN; + return outcome.asTag(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java new file mode 100644 index 0000000000..a07d325073 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2019 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.boot.actuate.metrics.http; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Outcome}. + * + * @author Andy Wilkinson + */ +public class OutcomeTests { + + @Test + void outcomeForInformationalStatusIsInformational() { + for (int status = 100; status < 200; status++) { + assertThat(Outcome.forStatus(status)).isEqualTo(Outcome.INFORMATIONAL); + } + } + + @Test + void outcomeForSuccessStatusIsSuccess() { + for (int status = 200; status < 300; status++) { + assertThat(Outcome.forStatus(status)).isEqualTo(Outcome.SUCCESS); + } + } + + @Test + void outcomeForRedirectionStatusIsRedirection() { + for (int status = 300; status < 400; status++) { + assertThat(Outcome.forStatus(status)).isEqualTo(Outcome.REDIRECTION); + } + } + + @Test + void outcomeForClientErrorStatusIsClientError() { + for (int status = 400; status < 500; status++) { + assertThat(Outcome.forStatus(status)).isEqualTo(Outcome.CLIENT_ERROR); + } + } + + @Test + void outcomeForServerErrorStatusIsServerError() { + for (int status = 500; status < 600; status++) { + assertThat(Outcome.forStatus(status)).isEqualTo(Outcome.SERVER_ERROR); + } + } + + @Test + void outcomeForStatusBelowLowestKnownSeriesIsUnknown() { + assertThat(Outcome.forStatus(99)).isEqualTo(Outcome.UNKNOWN); + } + + @Test + void outcomeForStatusAboveHighestKnownSeriesIsUnknown() { + assertThat(Outcome.forStatus(600)).isEqualTo(Outcome.UNKNOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java index ebaa05c2a5..17be58c703 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java @@ -86,14 +86,6 @@ class RestTemplateExchangeTagsTests { assertThat(tag.getValue()).isEqualTo("UNKNOWN"); } - @Test - void outcomeTagIsUnknownForCustomResponseStatus() throws Exception { - ClientHttpResponse response = mock(ClientHttpResponse.class); - given(response.getRawStatusCode()).willThrow(IllegalArgumentException.class); - Tag tag = RestTemplateExchangeTags.outcome(response); - assertThat(tag.getValue()).isEqualTo("UNKNOWN"); - } - @Test void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() throws IOException { ClientHttpResponse response = mock(ClientHttpResponse.class);