Skip to content

Lb micrometer stats #871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8ebe1fb
Add stats lifecycle bean. Add onStartRequest method to LoadBalancerLi…
OlgaMaciaszek Dec 15, 2020
e63caac
Add TimedRequestContext interface. Make RetryableRequestContext exten…
OlgaMaciaszek Dec 16, 2020
cfdc347
Add separate meters depending on CompletionContext.Status.
OlgaMaciaszek Dec 16, 2020
101db04
Modify registered metrics. Add adapter for BlockingLoadBalancerClient…
OlgaMaciaszek Dec 16, 2020
3c6d30c
Make new config conditional on MeterRegistry class.
OlgaMaciaszek Dec 17, 2020
28ad929
Rename lifecycle bean. Do not log request if 0 timestamp.
OlgaMaciaszek Dec 17, 2020
05edd79
Fix onStartRequest call arguments for BlockingLoadBalancerClient.
OlgaMaciaszek Dec 17, 2020
3f20e7e
Fix onStartRequest and onComplete calls for RetryLoadBalancerIntercep…
OlgaMaciaszek Dec 17, 2020
3cc7b8a
Only register timed request once. Add tests.
OlgaMaciaszek Dec 17, 2020
9b1ff91
Adjust tags logic. Add more tests.
OlgaMaciaszek Dec 17, 2020
9318a06
Add more tests.
OlgaMaciaszek Dec 17, 2020
b089d9f
Refactor. Add javadocs.
OlgaMaciaszek Dec 17, 2020
b73341d
Refactor.
OlgaMaciaszek Dec 17, 2020
af8d2c6
Refactor.
OlgaMaciaszek Dec 17, 2020
8fba4d4
Retrieve client response data if possible in BlockingLoadBalancerClient.
OlgaMaciaszek Dec 17, 2020
0960c6a
Refactor.
OlgaMaciaszek Dec 17, 2020
6df4398
Fix docs after review.
OlgaMaciaszek Dec 18, 2020
d89252b
Make previousServiceInstanceMutable.
OlgaMaciaszek Dec 18, 2020
5d1ec8c
Change argument order for CompletionContext constructors. Remove dupl…
OlgaMaciaszek Dec 18, 2020
80da0a8
Merge remote-tracking branch 'origin/master' into lb-micrometer-stats
OlgaMaciaszek Dec 18, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions docs/src/main/asciidoc/spring-cloud-commons.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1118,15 +1118,38 @@ public class MyConfiguration {

One type of bean that it may be useful to register using <<custom-loadbalancer-configuration,Custom LoadBalancer configuration>> is `LoadBalancerLifecycle`.

The LoadBalancerLifecycle beans provide callback methods, named `onStart(Request<RC> request)` and `onComplete(CompletionContext<RES, T> completionContext)`, that you should implement to specify what actions should take place before and after load-balancing.
The `LoadBalancerLifecycle` beans provide callback methods, named `onStart(Request<RC> request)`, `onStartRequest(Request<RC> request, Response<T> lbResponse)` and `onComplete(CompletionContext<RES, T, RC> completionContext)`, that you should implement to specify what actions should take place before and after load-balancing.

`onStart(Request<RC> request)` takes a `Request` object as a parameter. It contains data that is used to select an appropriate instance, including the downstream client request and <<spring-cloud-loadbalancer-hints,hint>>. On the other hand, a `CompletionContext` object is provided to the `onComplete(CompletionContext<RES, T> completionContext)` method. It contains the LoadBalancer `Response`, including the selected service instance, the `Status` of the request executed against that service instance and (if available) the response returned to the downstream client, and (if an exception has occurred) the corresponding `Throwable`.
`onStart(Request<RC> request)` takes a `Request` object as a parameter. It contains data that is used to select an appropriate instance, including the downstream client request and <<spring-cloud-loadbalancer-hints,hint>>. `onStartRequest` also takes the `Request` object and, additionally, the `Response<T>` object as parameters. On the other hand, a `CompletionContext` object is provided to the `onComplete(CompletionContext<RES, T, RC> completionContext)` method. It contains the LoadBalancer `Response`, including the selected service instance, the `Status` of the request executed against that service instance and (if available) the response returned to the downstream client, and (if an exception has occurred) the corresponding `Throwable`.

The `supports(Class requestContextClass, Class responseClass,
Class serverTypeClass)` method can be used to determine whether the processor in question handles objects of provided types. If not overridden by the user, it returns `true`.

NOTE: In the preceding method calls, `RC` means `RequestContext` type, `RES` means client response type, and `T` means returned server type.

[[loadbalancer-micrometer-stats-lifecycle]]
=== Spring Cloud LoadBalancer Statistics

We provide a `LoadBalancerLifecycle` bean called `MicrometerStatsLoadBalancerLifecycle`, which uses Micrometer to provide statistics for load-balanced calls.

In order to get this bean added to your application context,
set the value of the `spring.cloud.loadbalancer.stats.micrometer.enabled` to `true` and have a `MeterRegistry` available (for example, by adding https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html[Spring Boot Actuator] to your project).

`MicrometerStatsLoadBalancerLifecycle` registers the following meters in `MeterRegistry`:

* `loadbalancer.requests.active`: A gauge that allows you to monitor the number of currently active requests for any service instance (service instance data available via tags);
* `loadbalancer.requests.success`: A timer that measures the time of execution of any load-balanced requests that have ended in passing a response on to the underlying client;
* `loadbalancer.requests.failed`: A timer that measures the time of execution of any load-balanced requests that have ended with an exception;
* `loadbalancer.requests.discard`: A counter that measures the number of discarded load-balanced requests, i.e. requests where a service instance to run the request on has not been retrieved by the LoadBalancer.

Additional information regarding the service instances, request data, and response data is added to metrics via tags whenever available.

NOTE: For some implementations, such as `BlockingLoadBalancerClient`, request and response data might not be available, as we establish generic types from arguments and might not be able to determine the types and read the data.

NOTE: The meters are registered in the registry when at least one record is added for a given meter.

TIP: You can further configure the behavior of those metrics (for example, add https://micrometer.io/docs/concepts#_histograms_and_percentiles[publishing percentiles and histograms]) by https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-per-meter-properties[adding `MeterFilters`].

== Spring Cloud Circuit Breaker

include::spring-cloud-circuitbreaker.adoc[leveloffset=+1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.io.IOException;
import java.net.URI;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.AsyncClientHttpRequestExecution;
import org.springframework.http.client.AsyncClientHttpRequestInterceptor;
Expand All @@ -42,14 +41,10 @@ public ListenableFuture<ClientHttpResponse> intercept(final HttpRequest request,
final AsyncClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
return this.loadBalancer.execute(serviceName, new LoadBalancerRequest<ListenableFuture<ClientHttpResponse>>() {
@Override
public ListenableFuture<ClientHttpResponse> apply(final ServiceInstance instance) throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance,
AsyncLoadBalancerInterceptor.this.loadBalancer);
return execution.executeAsync(serviceRequest, body);
}

return this.loadBalancer.execute(serviceName, instance -> {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance,
AsyncLoadBalancerInterceptor.this.loadBalancer);
return execution.executeAsync(serviceRequest, body);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* @author Olga Maciaszek-Sharma
* @since 3.0.0
*/
public class CompletionContext<RES, T> {
public class CompletionContext<RES, T, C> {

private final Status status;

Expand All @@ -35,25 +35,31 @@ public class CompletionContext<RES, T> {

private final RES clientResponse;

public CompletionContext(Status status) {
this(status, null, null, null);
private final Request<C> loadBalancerRequest;

public CompletionContext(Status status, Request<C> loadBalancerRequest) {
this(status, null, loadBalancerRequest, null, null);
}

public CompletionContext(Status status, Response<T> response) {
this(status, null, response, null);
public CompletionContext(Status status, Request<C> loadBalancerRequest, Response<T> response) {
this(status, null, loadBalancerRequest, response, null);
}

public CompletionContext(Status status, Throwable throwable, Response<T> loadBalancerResponse) {
this(status, throwable, loadBalancerResponse, null);
public CompletionContext(Status status, Throwable throwable, Request<C> loadBalancerRequest,
Response<T> loadBalancerResponse) {
this(status, throwable, loadBalancerRequest, loadBalancerResponse, null);
}

public CompletionContext(Status status, Response<T> loadBalancerResponse, RES clientResponse) {
this(status, null, loadBalancerResponse, clientResponse);
public CompletionContext(Status status, Request<C> loadBalancerRequest, Response<T> loadBalancerResponse,
RES clientResponse) {
this(status, null, loadBalancerRequest, loadBalancerResponse, clientResponse);
}

public CompletionContext(Status status, Throwable throwable, Response<T> loadBalancerResponse, RES clientResponse) {
public CompletionContext(Status status, Throwable throwable, Request<C> loadBalancerRequest,
Response<T> loadBalancerResponse, RES clientResponse) {
this.status = status;
this.throwable = throwable;
this.loadBalancerRequest = loadBalancerRequest;
this.loadBalancerResponse = loadBalancerResponse;
this.clientResponse = clientResponse;
}
Expand All @@ -74,6 +80,10 @@ public RES getClientResponse() {
return clientResponse;
}

public Request<C> getLoadBalancerRequest() {
return loadBalancerRequest;
}

@Override
public String toString() {
ToStringCreator to = new ToStringCreator(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@
*
* @author Olga Maciaszek-Sharma
*/
public class HintRequestContext {
public class HintRequestContext implements TimedRequestContext {

/**
* A {@link String} value of hint that can be used to choose the correct service
* instance.
*/
private String hint = "default";

private long requestStartTime;

public HintRequestContext() {
}

Expand All @@ -48,6 +50,16 @@ public void setHint(String hint) {
this.hint = hint;
}

@Override
public long getRequestStartTime() {
return requestStartTime;
}

@Override
public void setRequestStartTime(long requestStartTime) {
this.requestStartTime = requestStartTime;
}

@Override
public String toString() {
ToStringCreator to = new ToStringCreator(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,20 @@ default boolean supports(Class requestContextClass, Class responseClass, Class s
*/
void onStart(Request<RC> request);

/**
* A callback method executed after a service instance has been selected, before
* executing the actual load-balanced request.
* @param request the {@link Request} that has been used by the LoadBalancer to select
* a service instance
* @param lbResponse the {@link Response} returned by the LoadBalancer
*/
void onStartRequest(Request<RC> request, Response<T> lbResponse);

/**
* A callback method executed after load-balancing.
* @param completionContext the {@link CompletionContext} containing data relevant to
* the load-balancing and the response returned from the selected service instance
*/
void onComplete(CompletionContext<RES, T> completionContext);
void onComplete(CompletionContext<RES, T, RC> completionContext);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2012-2020 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.cloud.client.loadbalancer;

import org.springframework.cloud.client.ServiceInstance;

/**
* An adapter class that allows creating {@link Request} objects from previously
* {@link LoadBalancerRequest} objects.
*
* @author Olga Maciaszek-Sharma
* @since 3.0.0
*/
public class LoadBalancerRequestAdapter<T, RC> extends DefaultRequest<RC> implements LoadBalancerRequest<T> {

private final LoadBalancerRequest<T> delegate;

public LoadBalancerRequestAdapter(LoadBalancerRequest<T> delegate) {
this.delegate = delegate;
}

public LoadBalancerRequestAdapter(LoadBalancerRequest<T> delegate, RC context) {
super(context);
this.delegate = delegate;
}

@Override
public T apply(ServiceInstance instance) throws Exception {
return delegate.apply(instance);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
*/
public class RequestDataContext extends DefaultRequestContext {

public RequestDataContext() {
super();
}

public RequestDataContext(RequestData requestData) {
this(requestData, "default");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

package org.springframework.cloud.client.loadbalancer;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.ClientResponse;

Expand Down Expand Up @@ -59,6 +64,11 @@ public ResponseData(ServerHttpResponse response, RequestData requestData) {
this(response.getStatusCode(), response.getHeaders(), response.getCookies(), requestData);
}

public ResponseData(ClientHttpResponse clientHttpResponse, RequestData requestData) throws IOException {
this(clientHttpResponse.getStatusCode(), clientHttpResponse.getHeaders(),
buildCookiesFromHeaders(clientHttpResponse.getHeaders()), requestData);
}

public HttpStatus getHttpStatus() {
return httpStatus;
}
Expand All @@ -82,6 +92,30 @@ public String toString() {
return to.toString();
}

static MultiValueMap<String, ResponseCookie> buildCookiesFromHeaders(HttpHeaders headers) {
LinkedMultiValueMap<String, ResponseCookie> newCookies = new LinkedMultiValueMap<>();
if (headers == null) {
return newCookies;
}
List<String> cookiesFromHeaders = headers.get(HttpHeaders.COOKIE);
if (cookiesFromHeaders != null) {
cookiesFromHeaders.forEach(cookie -> {
String[] splitCookie = cookie.split("=");
if (splitCookie.length < 2) {
return;
}
newCookies.put(splitCookie[0],
Collections.singletonList(ResponseCookie.from(splitCookie[0], splitCookie[1]).build()));
});
}
return newCookies;
}

@Override
public int hashCode() {
return Objects.hash(httpStatus, headers, cookies, requestData);
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -95,9 +129,4 @@ public boolean equals(Object o) {
&& Objects.equals(cookies, that.cookies) && Objects.equals(requestData, that.requestData);
}

@Override
public int hashCode() {
return Objects.hash(httpStatus, headers, cookies, requestData);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerFactory.getInstances(serviceName, LoadBalancerLifecycle.class),
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
String hint = getHint(serviceName);
if (serviceInstance == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Service instance retrieved from LoadBalancedRetryContext: was null. "
Expand All @@ -100,7 +101,6 @@ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
previousServiceInstance = lbContext.getPreviousServiceInstance();
}
String hint = getHint(serviceName);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
new RetryableRequestContext(previousServiceInstance, new RequestData(request), hint));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
Expand All @@ -112,15 +112,22 @@ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
lbContext.setServiceInstance(serviceInstance);
}
Response<ServiceInstance> lbResponse = new DefaultResponse(serviceInstance);
if (serviceInstance == null) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>(
CompletionContext.Status.DISCARD,
new DefaultRequest<>(
new RetryableRequestContext(null, new RequestData(request), hint)),
lbResponse)));
}
}
Response<ServiceInstance> lbResponse = new DefaultResponse(serviceInstance);
if (serviceInstance == null) {
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<ResponseData, ServiceInstance>(
CompletionContext.Status.DISCARD, lbResponse)));
}
LoadBalancerRequestAdapter<ClientHttpResponse, RetryableRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(
requestFactory.createRequest(request, body, execution),
new RetryableRequestContext(null, new RequestData(request), hint));
ServiceInstance finalServiceInstance = serviceInstance;
ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(serviceName,
serviceInstance, requestFactory.createRequest(request, body, execution));
finalServiceInstance, lbRequest);
int statusCode = response.getRawStatusCode();
if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
if (LOG.isDebugEnabled()) {
Expand Down
Loading