Add HttpOutcome for HTTP observations

Prior to this commit, the HTTP Observations would use
`HttpStatus.Series` as a value source for the "outcome" key value in
recorded observations. This would work for most cases, but would not
align in the 2xx HTTP status cases: the series would provide a
"SUCESSFUL" value whereas the heritage metrics support in Spring Boot
would give "SUCESS".

This commit introduces a dedicated `HttpOutcome` concept for this and
applies it to all HTTP observations.

Fixes gh-29232
This commit is contained in:
Brian Clozel
2022-09-30 18:14:10 +02:00
parent b9070ae752
commit 8c24e8c034
15 changed files with 224 additions and 42 deletions

View File

@@ -88,7 +88,7 @@ class DefaultClientHttpObservationConventionTests {
context.setUriTemplate("/resource/{id}");
assertThat(this.observationConvention.getLowCardinalityKeyValues(context))
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"),
KeyValue.of("status", "200"), KeyValue.of("outcome", "SUCCESSFUL"));
KeyValue.of("status", "200"), KeyValue.of("outcome", "SUCCESS"));
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2)
.contains(KeyValue.of("client.name", "none"), KeyValue.of("uri.expanded", "/resource/42"));
}

View File

@@ -0,0 +1,83 @@
/*
* 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.http.observation;
import io.micrometer.common.KeyValue;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpStatusCode;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link HttpOutcome}.
*
* @author Brian Clozel
*/
class HttpOutcomeTests {
@ParameterizedTest
@ValueSource(ints = {100, 101, 102})
void shouldResolveInformational(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.INFORMATIONAL);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "INFORMATIONAL"));
}
@ParameterizedTest
@ValueSource(ints = {200, 202, 226})
void shouldResolveSuccess(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.SUCCESS);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "SUCCESS"));
}
@ParameterizedTest
@ValueSource(ints = {300, 302, 303})
void shouldResolveRedirection(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.REDIRECTION);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "REDIRECTION"));
}
@ParameterizedTest
@ValueSource(ints = {404, 404, 405})
void shouldResolveClientError(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.CLIENT_ERROR);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "CLIENT_ERROR"));
}
@ParameterizedTest
@ValueSource(ints = {500, 502, 503})
void shouldResolveServerError(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.SERVER_ERROR);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "SERVER_ERROR"));
}
@ParameterizedTest
@ValueSource(ints = {600, 799, 855})
void shouldResolveUnknown(int code) {
HttpOutcome httpOutcome = HttpOutcome.forStatus(HttpStatusCode.valueOf(code));
assertThat(httpOutcome).isEqualTo(HttpOutcome.UNKNOWN);
assertThat(httpOutcome.asKeyValue()).isEqualTo(KeyValue.of("outcome", "UNKNOWN"));
}
}

View File

@@ -109,7 +109,7 @@ public class RestTemplateObservationTests {
template.execute("https://example.org", GET, null, null);
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESSFUL");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
}
@Test

View File

@@ -64,7 +64,7 @@ class DefaultHttpRequestsObservationConventionTests {
assertThat(this.convention.getLowCardinalityKeyValues(this.context)).hasSize(5)
.contains(KeyValue.of("method", "POST"), KeyValue.of("uri", "UNKNOWN"), KeyValue.of("status", "200"),
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESSFUL"));
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESS"));
assertThat(this.convention.getHighCardinalityKeyValues(this.context)).hasSize(1)
.contains(KeyValue.of("uri.expanded", "/test/resource"));
}
@@ -77,7 +77,7 @@ class DefaultHttpRequestsObservationConventionTests {
assertThat(this.convention.getLowCardinalityKeyValues(this.context)).hasSize(5)
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "/test/{name}"), KeyValue.of("status", "200"),
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESSFUL"));
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESS"));
assertThat(this.convention.getHighCardinalityKeyValues(this.context)).hasSize(1)
.contains(KeyValue.of("uri.expanded", "/test/resource"));
}

View File

@@ -57,7 +57,7 @@ public class HttpRequestsObservationFilterTests {
assertThat(context.getCarrier()).isEqualTo(this.request);
assertThat(context.getResponse()).isEqualTo(this.response);
assertThat(context.getPathPattern()).isNull();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESSFUL");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
}
@Test
@@ -84,7 +84,7 @@ public class HttpRequestsObservationFilterTests {
HttpRequestsObservationContext context = (HttpRequestsObservationContext) this.request
.getAttribute(HttpRequestsObservationFilter.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE);
assertThat(context.getError()).get().isEqualTo(customError);
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESSFUL");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
}
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {

View File

@@ -65,7 +65,7 @@ class DefaultHttpRequestsObservationConventionTests {
assertThat(this.convention.getLowCardinalityKeyValues(context)).hasSize(5)
.contains(KeyValue.of("method", "POST"), KeyValue.of("uri", "UNKNOWN"), KeyValue.of("status", "201"),
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESSFUL"));
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESS"));
assertThat(this.convention.getHighCardinalityKeyValues(context)).hasSize(1)
.contains(KeyValue.of("uri.expanded", "/test/resource"));
}
@@ -80,7 +80,7 @@ class DefaultHttpRequestsObservationConventionTests {
assertThat(this.convention.getLowCardinalityKeyValues(context)).hasSize(5)
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "/test/{name}"), KeyValue.of("status", "200"),
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESSFUL"));
KeyValue.of("exception", "none"), KeyValue.of("outcome", "SUCCESS"));
assertThat(this.convention.getHighCardinalityKeyValues(context)).hasSize(1)
.contains(KeyValue.of("uri.expanded", "/test/resource"));
}

View File

@@ -55,7 +55,7 @@ class HttpRequestsObservationWebFilterTests {
assertThat(observationContext.get().getResponse()).isEqualTo(exchange.getResponse());
});
this.filter.filter(exchange, filterChain).block();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESSFUL");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
}
@Test