@Configuration
@EnableConfigurationProperties(OsbReverseProxyProperties.class)
@EnableAutoConfiguration // necessary for unit tests not starting a springboot app but merely a spring context
public class ReverseProxyRouteConfiguration {
@Bean
public RouteLocator osbApiRoute(RouteLocatorBuilder builder,
OsbReverseProxyProperties osbReverseProxyProperties) {
return builder.routes()
.route("osb-api",
p -> p
.path("/v2/**", "**/v2/**")
.filters(f -> f
.modifyResponseBody(String.class, String.class,
(webExchange, originalBody) -> {
if (originalBody != null) {
//See https://stackoverflow.com/a/19975149/1484823 for abbreviation
String abbreviatedBody = StringUtils.abbreviate(originalBody, 10000);
webExchange.getAttributes().put("cachedResponseBodyObject", abbreviatedBody);
return Mono.just(originalBody);
} else {
return Mono.empty();
}
})
.modifyRequestBody(String.class, String.class,
(webExchange, originalBody) -> {
if (originalBody != null) {
//See https://stackoverflow.com/a/19975149/1484823 for abbreviation
String abbreviatedBody = StringUtils.abbreviate(originalBody, 10000);
webExchange.getAttributes().put("cachedRequestBodyObject", abbreviatedBody);
return Mono.just(originalBody);
} else {
return Mono.empty();
}
})
)
.uri(osbReverseProxyProperties.getBackendBrokerUri())
)
.build();
}
//Forked from https://github.com/spring-projects/spring-boot/blob/7df18d9a91f1cee8f0f5a4e0a17d56c85ca75835/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/trace/reactive/HttpTraceWebFilter.java
//
//Overriden to instanciate ExtendedServerWebExchangeTraceableRequest and
//ExtendedTraceableServerHttpResponse
//
// Must extend HttpTraceWebFilter for HttpTraceAutoConfiguration ConditionalOnMissingBean to create duplicate
// HttpTraceWebFilter resulting in duplicate http trace.
///**
// * A {@link WebFilter} for tracing HTTP requests.
// *
// * @author Andy Wilkinson
// * @since 2.0.0
// */
public class ExtendedHttpTraceWebFilter extends HttpTraceWebFilter implements WebFilter, Ordered {
private static final Object NONE = new Object();
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final HttpTraceRepository repository;
private final HttpExchangeTracer tracer;
private final Set<Include> includes;
public ExtendedHttpTraceWebFilter(HttpTraceRepository repository, HttpExchangeTracer tracer, Set<Include> includes) {
super(repository, tracer, includes);
this.repository = repository;
this.tracer = tracer;
this.includes = includes;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<?> principal = (this.includes.contains(Include.PRINCIPAL)
? exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE) : Mono.just(NONE));
Mono<?> session = (this.includes.contains(Include.SESSION_ID) ? exchange.getSession() : Mono.just(NONE));
return Mono.zip(principal, session).flatMap((tuple) -> filter(exchange, chain,
asType(tuple.getT1(), Principal.class), asType(tuple.getT2(), WebSession.class)));
}
private <T> T asType(Object object, Class<T> type) {
if (type.isInstance(object)) {
return type.cast(object);
}
return null;
}
private Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain, Principal principal,
WebSession session) {
exchange.getResponse().beforeCommit(() -> {
//Request needs to be read after other filters including spring cloud gateway
//so that the request body gets cached as exchange attribute
//We therefore run both request and response after response is received
ExtendedServerWebExchangeTraceableRequest request = new ExtendedServerWebExchangeTraceableRequest(exchange);
HttpTrace trace = this.tracer.receivedRequest(request);
ExtendedTraceableServerHttpResponse response = new ExtendedTraceableServerHttpResponse(exchange);
this.tracer.sendingResponse(trace, response, () -> principal, () -> getStartedSessionId(session));
this.repository.add(trace);
return Mono.empty();
});
return chain.filter(exchange);
}
private String getStartedSessionId(WebSession session) {
return (session != null && session.isStarted()) ? session.getId() : null;
}
}
/**
* A {@link TraceableRequest} backed by a {@link ServerWebExchange}.
*
* @author Andy Wilkinson
*/
class ExtendedServerWebExchangeTraceableRequest implements TraceableRequest {
private final String method;
private final Map<String, List<String>> headers;
private final URI uri;
private final String remoteAddress;
ExtendedServerWebExchangeTraceableRequest(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
this.method = request.getMethodValue();
this.headers = new HashMap(request.getHeaders());
//Remove the cached body to ease garbage collection in case of large response bodies
Object cachedRequestBodyObject = exchange.getAttributes().remove("cachedRequestBodyObject");
if (cachedRequestBodyObject != null) {
this.headers.put("request_body", singletonList(cachedRequestBodyObject.toString()));
}
this.uri = request.getURI();
this.remoteAddress = getRemoteAddress(request);
}
private static String getRemoteAddress(ServerHttpRequest request) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
InetAddress address = (remoteAddress != null) ? remoteAddress.getAddress() : null;
return (address != null) ? address.toString() : null;
}
@Override
public String getMethod() {
return this.method;
}
@Override
public URI getUri() {
return this.uri;
}
@Override
public Map<String, List<String>> getHeaders() {
return new LinkedHashMap<>(this.headers);
}
@Override
public String getRemoteAddress() {
return this.remoteAddress;
}
}
/**
* An adapter that exposes a {@link ServerHttpResponse} as a {@link TraceableResponse}.
*
* @author Andy Wilkinson
*/
class ExtendedTraceableServerHttpResponse implements TraceableResponse {
private final int status;
private final Map<String, List<String>> headers;
ExtendedTraceableServerHttpResponse(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
this.status = (response.getStatusCode() != null) ? response.getStatusCode().value() : HttpStatus.OK.value();
this.headers = new LinkedHashMap<>(response.getHeaders());
//Remove the cached body to ease garbage collection in case of large response bodies
Object cachedResponseBodyObject = exchange.getAttributes().remove("cachedResponseBodyObject");
if (cachedResponseBodyObject != null) {
this.headers.put("response_body", singletonList(cachedResponseBodyObject.toString()));
}
}
@Override
public int getStatus() {
return this.status;
}
@Override
public Map<String, List<String>> getHeaders() {
return this.headers;
}
}
/**
* A {@link TraceableRequest} backed by a {@link ServerWebExchange}.
*
* @author Andy Wilkinson
*/
class ServerWebExchangeTraceableRequest implements TraceableRequest {
private final String method;
private final Map<String, List<String>> headers;
private final URI uri;
private final String remoteAddress;
ServerWebExchangeTraceableRequest(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
this.method = request.getMethodValue();
this.headers = request.getHeaders();
this.uri = request.getURI();
this.remoteAddress = getRemoteAddress(request);
}
private static String getRemoteAddress(ServerHttpRequest request) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
InetAddress address = (remoteAddress != null) ? remoteAddress.getAddress() : null;
return (address != null) ? address.toString() : null;
}
@Override
public String getMethod() {
return this.method;
}
@Override
public URI getUri() {
return this.uri;
}
@Override
public Map<String, List<String>> getHeaders() {
return new LinkedHashMap<>(this.headers);
}
@Override
public String getRemoteAddress() {
return this.remoteAddress;
}
}