Skip to content

Instantly share code, notes, and snippets.

@gberche-orange
Last active June 2, 2023 14:22
Show Gist options
  • Save gberche-orange/06c26477a313df9d19d20a4e115f079f to your computer and use it in GitHub Desktop.
Save gberche-orange/06c26477a313df9d19d20a4e115f079f to your computer and use it in GitHub Desktop.
@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;
	}

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment