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

@@ -21,8 +21,8 @@ import java.io.IOException;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.observation.HttpOutcome;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
@@ -43,8 +43,6 @@ public class DefaultClientHttpObservationConvention implements ClientHttpObserva
private static final KeyValue EXCEPTION_NONE = KeyValue.of(ClientHttpObservation.LowCardinalityKeyNames.EXCEPTION, "none");
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(ClientHttpObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
private static final KeyValue URI_EXPANDED_NONE = KeyValue.of(ClientHttpObservation.HighCardinalityKeyNames.URI_EXPANDED, "none");
private final String name;
@@ -122,16 +120,14 @@ public class DefaultClientHttpObservationConvention implements ClientHttpObserva
protected static KeyValue outcome(ClientHttpObservationContext context) {
try {
if (context.getResponse() != null) {
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatusCode().value());
if (status != null) {
return KeyValue.of(ClientHttpObservation.LowCardinalityKeyNames.OUTCOME, status.series().name());
}
HttpOutcome httpOutcome = HttpOutcome.forStatus(context.getResponse().getStatusCode());
return httpOutcome.asKeyValue();
}
}
catch (IOException ex) {
// Continue
}
return OUTCOME_UNKNOWN;
return HttpOutcome.UNKNOWN.asKeyValue();
}
@Override

View File

@@ -0,0 +1,101 @@
/*
* 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.springframework.http.HttpStatusCode;
/**
* The outcome of an HTTP request.
* <p>Used as the {@code "outcome} {@link io.micrometer.common.KeyValue}
* for HTTP {@link io.micrometer.observation.Observation observations}.
*
* @author Brian Clozel
* @author Andy Wilkinson
* @since 6.0.0
*/
public enum HttpOutcome {
/**
* 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 KeyValue keyValue;
HttpOutcome() {
this.keyValue = KeyValue.of("outcome", name());
}
/**
* Returns the {@code Outcome} as a {@link KeyValue} named {@code outcome}.
* @return the {@code outcome} {@code KeyValue}
*/
public KeyValue asKeyValue() {
return this.keyValue;
}
/**
* Return the {@code HttpOutcome} for the given HTTP {@code status} code.
* @param status the HTTP status code
* @return the matching HttpOutcome
*/
public static HttpOutcome forStatus(HttpStatusCode status) {
if (status.is1xxInformational()) {
return INFORMATIONAL;
}
else if (status.is2xxSuccessful()) {
return SUCCESS;
}
else if (status.is3xxRedirection()) {
return REDIRECTION;
}
else if (status.is4xxClientError()) {
return CLIENT_ERROR;
}
else if (status.is5xxServerError()) {
return SERVER_ERROR;
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,9 @@
/**
* Base support for HTTP {@link io.micrometer.observation.Observation}.
*/
@NonNullApi
@NonNullFields
package org.springframework.http.observation;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -20,6 +20,8 @@ import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.observation.HttpOutcome;
/**
* Default {@link HttpRequestsObservationConvention}.
@@ -44,8 +46,6 @@ public class DefaultHttpRequestsObservationConvention implements HttpRequestsObs
private static final KeyValue EXCEPTION_NONE = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, "none");
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
private static final KeyValue URI_EXPANDED_UNKNOWN = KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, "UNKNOWN");
private final String name;
@@ -125,12 +125,11 @@ public class DefaultHttpRequestsObservationConvention implements HttpRequestsObs
protected KeyValue outcome(HttpRequestsObservationContext context) {
if (context.getResponse() != null) {
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatus());
if (status != null) {
return KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, status.series().name());
}
HttpStatusCode statusCode = HttpStatusCode.valueOf(context.getResponse().getStatus());
HttpOutcome httpOutcome = HttpOutcome.forStatus(statusCode);
return httpOutcome.asKeyValue();
}
return OUTCOME_UNKNOWN;
return HttpOutcome.UNKNOWN.asKeyValue();
}
protected KeyValue uriExpanded(HttpRequestsObservationContext context) {

View File

@@ -20,6 +20,7 @@ import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.HttpStatus;
import org.springframework.http.observation.HttpOutcome;
import org.springframework.web.util.pattern.PathPattern;
/**
@@ -46,8 +47,6 @@ public class DefaultHttpRequestsObservationConvention implements HttpRequestsObs
private static final KeyValue EXCEPTION_NONE = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.EXCEPTION, "none");
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN");
private static final KeyValue URI_EXPANDED_UNKNOWN = KeyValue.of(HttpRequestsObservation.HighCardinalityKeyNames.URI_EXPANDED, "UNKNOWN");
private final String name;
@@ -128,15 +127,13 @@ public class DefaultHttpRequestsObservationConvention implements HttpRequestsObs
protected KeyValue outcome(HttpRequestsObservationContext context) {
if (context.isConnectionAborted()) {
return OUTCOME_UNKNOWN;
return HttpOutcome.UNKNOWN.asKeyValue();
}
else if (context.getResponse() != null) {
HttpStatus status = HttpStatus.resolve(context.getResponse().getStatusCode().value());
if (status != null) {
return KeyValue.of(HttpRequestsObservation.LowCardinalityKeyNames.OUTCOME, status.series().name());
}
HttpOutcome httpOutcome = HttpOutcome.forStatus(context.getResponse().getStatusCode());
return httpOutcome.asKeyValue();
}
return OUTCOME_UNKNOWN;
return HttpOutcome.UNKNOWN.asKeyValue();
}
protected KeyValue uriExpanded(HttpRequestsObservationContext context) {