Correlated WebClient log messages

Issue: SPR-16966
This commit is contained in:
Rossen Stoyanchev
2018-07-03 21:39:04 -04:00
parent 82310660fd
commit 5cdc26770e
7 changed files with 65 additions and 22 deletions

View File

@@ -222,7 +222,7 @@ public interface ServerWebExchange {
* Return a log message prefix to use to correlate messages for this exchange.
* The prefix is based on the value of the attribute {@link #LOG_ID_ATTRIBUTE}
* along with some extra formatting so that the prefix can be conveniently
* prepended with no further formatting no separatorns required.
* prepended with no further formatting no separators required.
* @return the log message prefix or an empty String if the
* {@link #LOG_ID_ATTRIBUTE} is not set.
* @since 5.1

View File

@@ -45,6 +45,16 @@ import org.springframework.web.reactive.function.BodyInserter;
*/
public interface ClientRequest {
/**
* Name of {@link #attributes() attribute} whose value can be used to
* correlate log messages for this request. Use {@link #logPrefix()} to
* obtain a consistently formatted prefix based on this attribute.
* @since 5.1
* @see #logPrefix()
*/
String LOG_ID_ATTRIBUTE = ClientRequest.class.getName() + ".LOG_ID";
/**
* Return the HTTP method.
*/
@@ -90,6 +100,17 @@ public interface ClientRequest {
*/
Map<String, Object> attributes();
/**
* Return a log message prefix to use to correlate messages for this request.
* The prefix is based on the value of the attribute {@link #LOG_ID_ATTRIBUTE}
* along with some extra formatting so that the prefix can be conveniently
* prepended with no further formatting no separators required.
* @return the log message prefix or an empty String if the
* {@link #LOG_ID_ATTRIBUTE} is not set.
* @since 5.1
*/
String logPrefix();
/**
* Writes this request to the given {@link ClientHttpRequest}.
*

View File

@@ -39,6 +39,7 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
@@ -181,6 +182,9 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder {
private final Map<String, Object> attributes;
private final String logPrefix;
public BodyInserterRequest(HttpMethod method, URI url, HttpHeaders headers,
MultiValueMap<String, String> cookies, BodyInserter<?, ? super ClientHttpRequest> body,
Map<String, Object> attributes) {
@@ -191,8 +195,12 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder {
this.cookies = CollectionUtils.unmodifiableMultiValueMap(cookies);
this.body = body;
this.attributes = Collections.unmodifiableMap(attributes);
Object id = attributes.computeIfAbsent(LOG_ID_ATTRIBUTE, name -> ObjectUtils.getIdentityHexString(this));
this.logPrefix = "[" + id + "] ";
}
@Override
public HttpMethod method() {
return this.method;
@@ -223,6 +231,11 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder {
return this.attributes;
}
@Override
public String logPrefix() {
return this.logPrefix;
}
@Override
public Mono<Void> writeTo(ClientHttpRequest request, ExchangeStrategies strategies) {
HttpHeaders requestHeaders = request.getHeaders();
@@ -252,7 +265,7 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder {
}
@Override
public Map<String, Object> hints() {
return Hints.none();
return Hints.from(Hints.LOG_PREFIX_HINT, logPrefix());
}
});
}

View File

@@ -55,11 +55,14 @@ class DefaultClientResponse implements ClientResponse {
private final ExchangeStrategies strategies;
private final String logPrefix;
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies) {
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies, String logPrefix) {
this.response = response;
this.strategies = strategies;
this.headers = new DefaultHeaders();
this.logPrefix = logPrefix;
}
@@ -96,7 +99,7 @@ class DefaultClientResponse implements ClientResponse {
}
@Override
public Map<String, Object> hints() {
return Hints.none();
return Hints.from(Hints.LOG_PREFIX_HINT, logPrefix);
}
});
}

View File

@@ -129,9 +129,14 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder {
@Override
public ClientResponse build() {
ClientHttpResponse clientHttpResponse = new BuiltClientHttpResponse(
this.statusCode, this.headers, this.cookies, this.body);
return new DefaultClientResponse(clientHttpResponse, this.strategies);
ClientHttpResponse httpResponse =
new BuiltClientHttpResponse(this.statusCode, this.headers, this.cookies, this.body);
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
// e.g. via ClientResponse.Builder, but this (builder) is not used currently.
return new DefaultClientResponse(httpResponse, this.strategies, "");
}

View File

@@ -91,18 +91,19 @@ public abstract class ExchangeFunctions {
@Override
public Mono<ClientResponse> exchange(ClientRequest request) {
Assert.notNull(request, "ClientRequest must not be null");
HttpMethod httpMethod = request.method();
URI url = request.url();
public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
Assert.notNull(clientRequest, "ClientRequest must not be null");
HttpMethod httpMethod = clientRequest.method();
URI url = clientRequest.url();
String logPrefix = clientRequest.logPrefix();
return this.connector
.connect(httpMethod, url, httpRequest -> request.writeTo(httpRequest, this.strategies))
.doOnRequest(n -> logRequest(request))
.doOnCancel(() -> logger.debug("Cancel signal (to close connection)"))
.map(response -> {
logResponse(response);
return new DefaultClientResponse(response, this.strategies);
.connect(httpMethod, url, httpRequest -> clientRequest.writeTo(httpRequest, this.strategies))
.doOnRequest(n -> logRequest(clientRequest))
.doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)"))
.map(httpResponse -> {
logResponse(httpResponse, logPrefix);
return new DefaultClientResponse(httpResponse, this.strategies, logPrefix);
});
}
@@ -113,21 +114,21 @@ public abstract class ExchangeFunctions {
int index = formatted.indexOf("?");
formatted = (index != -1 ? formatted.substring(0, index) : formatted);
}
logger.debug("HTTP " + request.method() + " " + formatted);
logger.debug(request.logPrefix() + "HTTP " + request.method() + " " + formatted);
}
}
private void logResponse(ClientHttpResponse response) {
private void logResponse(ClientHttpResponse response, String logPrefix) {
if (logger.isDebugEnabled()) {
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
String message = "Response " + (status != null ? status : code);
if (logger.isTraceEnabled()) {
String headers = this.disableLoggingRequestDetails ? "" : ", headers=" + response.getHeaders();
logger.trace(message + headers);
logger.trace(logPrefix + message + headers);
}
else {
logger.debug(message);
logger.debug(logPrefix + message);
}
}
}

View File

@@ -68,7 +68,7 @@ public class DefaultClientResponseTests {
public void createMocks() {
mockResponse = mock(ClientHttpResponse.class);
mockExchangeStrategies = mock(ExchangeStrategies.class);
defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies);
defaultClientResponse = new DefaultClientResponse(mockResponse, mockExchangeStrategies, "");
}