diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayApplication.java b/src/main/java/org/springframework/cloud/gateway/GatewayApplication.java index 747804d6..e12adb3e 100644 --- a/src/main/java/org/springframework/cloud/gateway/GatewayApplication.java +++ b/src/main/java/org/springframework/cloud/gateway/GatewayApplication.java @@ -20,10 +20,9 @@ public class GatewayApplication { private static final Log log = LogFactory.getLog(GatewayApplication.class); - // TODO: only apply filters to zuul? @Bean @Order(501) - public WebFilter modifyResponseFilter() { + public GatewayFilter modifyResponseFilter() { return (exchange, chain) -> { log.info("modifyResponseFilter start"); exchange.getResponse().getHeaders().add("X-My-Custom", "MyCustomValue"); @@ -33,7 +32,7 @@ public class GatewayApplication { @Bean @Order(502) - public WebFilter postFilter() { + public GatewayFilter postFilter() { return (exchange, chain) -> { log.info("postFilter start"); return chain.filter(exchange).then(postFilterWork(exchange)); @@ -49,7 +48,6 @@ public class GatewayApplication { public static void main(String[] args) { new SpringApplicationBuilder() .sources(GatewayApplication.class) - //TODO: howto do programatically .run(args); } } diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayConfiguration.java b/src/main/java/org/springframework/cloud/gateway/GatewayConfiguration.java index 463f3c8a..807e5459 100644 --- a/src/main/java/org/springframework/cloud/gateway/GatewayConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/GatewayConfiguration.java @@ -2,7 +2,7 @@ package org.springframework.cloud.gateway; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.gateway.filters.FindRouteFilter; +import org.springframework.cloud.gateway.filters.RouteToUrlFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -22,8 +22,8 @@ public class GatewayConfiguration { } @Bean - public FindRouteFilter findRouteFilter(GatewayProperties properties) { - return new FindRouteFilter(properties); + public RouteToUrlFilter findRouteFilter(GatewayProperties properties) { + return new RouteToUrlFilter(properties); } @Bean @@ -37,18 +37,18 @@ public class GatewayConfiguration { } @Bean - public GatewayHandlerMapping gatewayHandlerMapping(GatewayProperties properties, GatewayWebHandler gatewayWebHandler) { - return new GatewayHandlerMapping(properties, gatewayWebHandler); + public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, GatewayFilter[] filters) { + return new GatewayFilteringWebHandler(gatewayWebHandler, filters); } - /*@Bean - public GatewayWebReactiveConfigurer gatewayWebReactiveConfiguration() { - return new GatewayWebReactiveConfigurer(); + @Bean + public GatewayUrlHandlerMapping gatewayUrlHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) { + return new GatewayUrlHandlerMapping(properties, webHandler); + } + + @Bean + public GatewayHostHandlerMapping gatewayHostHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) { + return new GatewayHostHandlerMapping(properties, webHandler); } - public static class GatewayWebReactiveConfigurer implements WebReactiveConfigurer { - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - } - }*/ } diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayFilter.java b/src/main/java/org/springframework/cloud/gateway/GatewayFilter.java new file mode 100644 index 00000000..237725c3 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/GatewayFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.gateway; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; + +import reactor.core.publisher.Mono; + +/** + * Contract for interception-style, chained processing of Web requests that may + * be used to implement cross-cutting, application-agnostic requirements such + * as security, timeouts, and others. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public interface GatewayFilter { + + default boolean shouldFilter(ServerWebExchange exchange) { + return true; + } + + /** + * Process the Web request and (optionally) delegate to the next + * {@code WebFilter} through the given {@link WebFilterChain}. + * @param exchange the current server exchange + * @param chain provides a way to delegate to the next filter + * @return {@code Mono} to indicate when request processing is complete + */ + Mono filter(ServerWebExchange exchange, WebFilterChain chain); + +} diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayFilteringWebHandler.java b/src/main/java/org/springframework/cloud/gateway/GatewayFilteringWebHandler.java new file mode 100644 index 00000000..4cf4a3d6 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/GatewayFilteringWebHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.gateway; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.handler.WebHandlerDecorator; + +import reactor.core.publisher.Mono; + +/** + * WebHandler that delegates to a chain of {@link GatewayFilter} instances and then + * to the target {@link WebHandler}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class GatewayFilteringWebHandler extends WebHandlerDecorator { + + private final List filters; + + + public GatewayFilteringWebHandler(WebHandler targetHandler, GatewayFilter... filters) { + super(targetHandler); + this.filters = initList(filters); + } + + private static List initList(GatewayFilter[] list) { + return (list != null ? Collections.unmodifiableList(Arrays.asList(list)) : Collections.emptyList()); + } + + + /** + * Return a read-only list of the configured filters. + */ + public List getFilters() { + return this.filters; + } + + @Override + public Mono handle(ServerWebExchange exchange) { + return new DefaultWebFilterChain().filter(exchange); + } + + + private class DefaultWebFilterChain implements WebFilterChain { + + private int index; + + @Override + public Mono filter(ServerWebExchange exchange) { + if (this.index < filters.size()) { + GatewayFilter filter = filters.get(this.index++); + if (filter.shouldFilter(exchange)) { + return filter.filter(exchange, this); + } else { + return this.filter(exchange); + } + } + else { + return getDelegate().handle(exchange); + } + } + } + +} diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/GatewayHandlerMapping.java deleted file mode 100644 index bf1b5076..00000000 --- a/src/main/java/org/springframework/cloud/gateway/GatewayHandlerMapping.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.springframework.cloud.gateway; - -import org.springframework.beans.BeansException; -import org.springframework.cloud.gateway.GatewayProperties.Route; -import org.springframework.util.StringUtils; -import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping; -import org.springframework.web.server.ServerWebExchange; - -import java.util.Map; - -/** - * @author Spencer Gibb - */ -public class GatewayHandlerMapping extends AbstractUrlHandlerMapping { - - private GatewayProperties properties; - private GatewayWebHandler gatewayWebHandler; - - public GatewayHandlerMapping(GatewayProperties properties, GatewayWebHandler gatewayWebHandler) { - this.properties = properties; - this.gatewayWebHandler = gatewayWebHandler; - } - - @Override - protected void initApplicationContext() throws BeansException { - super.initApplicationContext(); - registerHandlers(this.properties.getRoutes()); - } - - protected void registerHandlers(Map routes) { - for (Route route : routes.values()) { - if (StringUtils.hasText(route.getRequestPath())) { - registerHandler(route.getRequestPath(), this.gatewayWebHandler); - } - } - } - -} diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayHostHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/GatewayHostHandlerMapping.java new file mode 100644 index 00000000..822a001e --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/GatewayHostHandlerMapping.java @@ -0,0 +1,218 @@ +package org.springframework.cloud.gateway; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.cloud.gateway.GatewayProperties.Route; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; + +import reactor.core.publisher.Mono; + +/** + * @author Spencer Gibb + */ +public class GatewayHostHandlerMapping extends AbstractHandlerMapping { + + private GatewayProperties properties; + private WebHandler webHandler; + + private final Map handlerMap = new LinkedHashMap<>(); + + public GatewayHostHandlerMapping(GatewayProperties properties, WebHandler webHandler) { + this.properties = properties; + this.webHandler = webHandler; + } + + @Override + protected void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.properties.getRoutes()); + setPathMatcher(new AntPathMatcher(".")); + } + + @Override + protected Mono getHandlerInternal(ServerWebExchange exchange) { + String host = exchange.getRequest().getHeaders().getFirst("Host"); + Object handler; + try { + handler = lookupHandler(host, exchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Mapping [" + host + "] to " + handler); + } + else if (handler == null && logger.isTraceEnabled()) { + logger.trace("No handler mapping found for [" + host + "]"); + } + + return Mono.justOrEmpty(handler); + } + + + /** + * Look up a handler instance for the given URL path. + * + *

Supports direct matches, e.g. a registered "/test" matches "/test", + * and various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher class. + * + *

Looks for the most exact pattern, where most exact is defined as + * the longest path pattern. + * + * @param host URL the bean is mapped to + * @param exchange the current exchange + * @return the associated handler instance, or {@code null} if not found + * @see org.springframework.util.AntPathMatcher + */ + protected Object lookupHandler(String host, ServerWebExchange exchange) throws Exception { + // Direct match? + Object handler = this.handlerMap.get(host); + if (handler != null) { + return handleMatch(handler, host, exchange); + } + // Pattern match? + List matches = new ArrayList<>(); + for (String pattern : this.handlerMap.keySet()) { + if (getPathMatcher().match(pattern, host)) { + matches.add(pattern); + } + } + String bestMatch = null; + Comparator comparator = getPathMatcher().getPatternComparator(host); + if (!matches.isEmpty()) { + Collections.sort(matches, comparator); + if (logger.isDebugEnabled()) { + logger.debug("Matching patterns for request [" + host + "] are " + matches); + } + bestMatch = matches.get(0); + } + if (bestMatch != null) { + handler = this.handlerMap.get(bestMatch); + if (handler == null) { + Assert.isTrue(bestMatch.endsWith("/")); + handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); + } + return handleMatch(handler, bestMatch, exchange); + } + // No handler found... + return null; + } + + + private Object handleMatch(Object handler, String bestMatch, + ServerWebExchange exchange) throws Exception { + + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + + if (handler instanceof RouteHolder) { + RouteHolder holder = (RouteHolder) handler; + exchange.getAttributes().put("gatewayRoute", holder.route); + return holder.webHandler; + } + + validateHandler(handler, exchange); + + exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch); + + return handler; + } + + /** + * Validate the given handler against the current request. + *

The default implementation is empty. Can be overridden in subclasses, + * for example to enforce specific preconditions expressed in URL mappings. + * @param handler the handler object to validate + * @param exchange current exchange + * @throws Exception if validation failed + */ + @SuppressWarnings("UnusedParameters") + protected void validateHandler(Object handler, ServerWebExchange exchange) throws Exception { + } + + + protected void registerHandlers(Map routes) { + for (Route route : routes.values()) { + if (StringUtils.hasText(route.getRequestHost())) { + registerHandler(route.getRequestHost(), new RouteHolder(route, this.webHandler)); + } + } + } + + /** + * Register the specified handler for the given host path. + * @param hostPath the host the bean should be mapped to + * @param handler the handler instance or handler bean name String + * (a bean name will automatically be resolved into the corresponding handler bean) + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(String hostPath, Object handler) throws BeansException, IllegalStateException { + Assert.notNull(hostPath, "host path must not be null"); + Assert.notNull(handler, "Handler object must not be null"); + Object resolvedHandler = handler; + + // Eagerly resolve handler if referencing singleton via name. + /*if (!this.lazyInitHandlers && handler instanceof String) { + String handlerName = (String) handler; + if (getApplicationContext().isSingleton(handlerName)) { + resolvedHandler = getApplicationContext().getBean(handlerName); + } + }*/ + + Object mappedHandler = this.handlerMap.get(hostPath); + if (mappedHandler != null) { + if (mappedHandler != resolvedHandler) { + throw new IllegalStateException( + "Cannot map " + getHandlerDescription(handler) + " to host path [" + hostPath + + "]: There is already " + getHandlerDescription(mappedHandler) + " mapped."); + } + } + else { + this.handlerMap.put(hostPath, resolvedHandler); + if (logger.isInfoEnabled()) { + logger.info("Mapped host path [" + hostPath + "] onto " + getHandlerDescription(handler)); + } + } + } + + + private String getHandlerDescription(Object handler) { + String desc; + if (handler instanceof String) { + desc = "'" + handler + "'"; + } else if (handler instanceof RouteHolder) { + desc = "of type [" + ((RouteHolder) handler).webHandler.getClass() + "]"; + } else { + desc = "of type [" + handler.getClass() + "]"; + } + return "handler " + desc; + } + + private class RouteHolder { + private final Route route; + private final WebHandler webHandler; + + public RouteHolder(Route route, WebHandler webHandler) { + this.route = route; + this.webHandler = webHandler; + } + } + +} diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayUrlHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/GatewayUrlHandlerMapping.java new file mode 100644 index 00000000..697b49d8 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/GatewayUrlHandlerMapping.java @@ -0,0 +1,62 @@ +package org.springframework.cloud.gateway; + +import org.springframework.beans.BeansException; +import org.springframework.cloud.gateway.GatewayProperties.Route; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * @author Spencer Gibb + */ +public class GatewayUrlHandlerMapping extends AbstractUrlHandlerMapping { + + private GatewayProperties properties; + private WebHandler webHandler; + + public GatewayUrlHandlerMapping(GatewayProperties properties, WebHandler webHandler) { + this.properties = properties; + this.webHandler = webHandler; + } + + @Override + protected void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.properties.getRoutes()); + } + + @Override + public Mono getHandler(ServerWebExchange exchange) { + return super.getHandler(exchange).map(o -> { + if (o instanceof RouteHolder) { + RouteHolder holder = (RouteHolder) o; + exchange.getAttributes().put("gatewayRoute", holder.route); + return holder.webHandler; + } + return o; + }); + } + + protected void registerHandlers(Map routes) { + for (Route route : routes.values()) { + if (StringUtils.hasText(route.getRequestPath())) { + registerHandler(route.getRequestPath(), new RouteHolder(route, this.webHandler)); + } + } + } + + private class RouteHolder { + private final Route route; + private final WebHandler webHandler; + + public RouteHolder(Route route, WebHandler webHandler) { + this.route = route; + this.webHandler = webHandler; + } + } + +} diff --git a/src/main/java/org/springframework/cloud/gateway/GatewayWebHandler.java b/src/main/java/org/springframework/cloud/gateway/GatewayWebHandler.java index e8a485dc..f605e244 100644 --- a/src/main/java/org/springframework/cloud/gateway/GatewayWebHandler.java +++ b/src/main/java/org/springframework/cloud/gateway/GatewayWebHandler.java @@ -28,6 +28,7 @@ public class GatewayWebHandler implements WebHandler { @Override public Mono handle(ServerWebExchange exchange) { + //TODO: move to constant Optional requestUrl = exchange.getAttribute("requestUrl"); ServerHttpRequest request = exchange.getRequest(); ClientRequest clientRequest = ClientRequest diff --git a/src/main/java/org/springframework/cloud/gateway/filters/FindRouteFilter.java b/src/main/java/org/springframework/cloud/gateway/filters/FindRouteFilter.java deleted file mode 100644 index 5b455726..00000000 --- a/src/main/java/org/springframework/cloud/gateway/filters/FindRouteFilter.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.springframework.cloud.gateway.filters; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.cloud.gateway.GatewayProperties; -import org.springframework.cloud.gateway.GatewayProperties.Route; -import org.springframework.cloud.gateway.GatewayApplication; -import org.springframework.core.Ordered; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.util.AntPathMatcher; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; -import org.springframework.web.util.UriComponentsBuilder; -import reactor.core.publisher.Mono; - -import java.net.URI; - -import static org.springframework.util.StringUtils.hasText; - -/** - * @author Spencer Gibb - */ -public class FindRouteFilter implements WebFilter, Ordered { - - private static final Log log = LogFactory.getLog(GatewayApplication.class); - - private final GatewayProperties properties; - private final AntPathMatcher pathMatcher = new AntPathMatcher(); - private final AntPathMatcher hostMatcher = new AntPathMatcher("."); - - public FindRouteFilter(GatewayProperties properties) { - this.properties = properties; - } - - @Override - public int getOrder() { - return 500; - } - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - log.info("FindRouteFilter start"); - //TODO: - ServerHttpRequest request = exchange.getRequest(); - URI uri = request.getURI(); - String path = uri.getPath(); - String host = uri.getHost(); - for (Route route : this.properties.getRoutes().values()) { - if (hasText(route.getRequestPath()) - && this.pathMatcher.match(route.getRequestPath(), path)) { - populateRequestUrl(exchange, request, route); - // TODO: this stuff needs to move into GatewayHandlerMapping - // otherwise, only path based routing works - } else if (hasText(route.getRequestHost()) - && this.hostMatcher.match(route.getRequestHost(), host)) { - populateRequestUrl(exchange, request, route); - } - } - return chain.filter(exchange); - } - - private void populateRequestUrl(ServerWebExchange exchange, ServerHttpRequest request, Route route) { - URI requestUrl = UriComponentsBuilder.fromHttpRequest(request) - .uri(route.getDownstreamUrl()) - .build(true) - .toUri(); - exchange.getAttributes().put("requestUrl", requestUrl); - } -} diff --git a/src/main/java/org/springframework/cloud/gateway/filters/RouteToUrlFilter.java b/src/main/java/org/springframework/cloud/gateway/filters/RouteToUrlFilter.java new file mode 100644 index 00000000..db820852 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filters/RouteToUrlFilter.java @@ -0,0 +1,62 @@ +package org.springframework.cloud.gateway.filters; + +import java.net.URI; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.gateway.GatewayFilter; +import org.springframework.cloud.gateway.GatewayProperties; +import org.springframework.cloud.gateway.GatewayProperties.Route; +import org.springframework.core.Ordered; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.util.UriComponentsBuilder; + +import reactor.core.publisher.Mono; + +/** + * @author Spencer Gibb + */ +public class RouteToUrlFilter implements GatewayFilter, Ordered { + + private static final Log log = LogFactory.getLog(RouteToUrlFilter.class); + + private final GatewayProperties properties; + + public RouteToUrlFilter(GatewayProperties properties) { + this.properties = properties; + } + + @Override + public int getOrder() { + // TODO: move to constant + return 500; + } + + // TODO: do we really need shouldFilter or just move the if into filter? + @Override + public boolean shouldFilter(ServerWebExchange exchange) { + //TODO: move to constant + return exchange.getAttributes().containsKey("gatewayRoute"); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + log.info("RouteToUrlFilter start"); + Object gatewayRoute = exchange.getAttributes().get("gatewayRoute"); + if (!(gatewayRoute instanceof Route)) { + return Mono.error(new IllegalStateException("gatewayRoute" + + " not an instance of " + Route.class.getSimpleName() + + ", is " + gatewayRoute.getClass())); + } + Route route = (Route) gatewayRoute; + URI requestUrl = UriComponentsBuilder.fromHttpRequest(exchange.getRequest()) + .uri(route.getDownstreamUrl()) + .build(true) + .toUri(); + //TODO: move to constant + exchange.getAttributes().put("requestUrl", requestUrl); + return chain.filter(exchange); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9e73c77c..136d45a8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,4 @@ + spring: resources: # TODO: how to add this programmatically @@ -5,7 +6,12 @@ spring: cloud: gateway: routes: - test1: - requestPath: /** -# requestHost: '**.example.org' +# test1: +# requestPath: /** +# downstreamUrl: http://httpbin.org:80 + test2: + requestHost: '**.example.org' downstreamUrl: http://httpbin.org:80 +logging: + level: + org.springframework.cloud.gateway: TRACE