HTTPS redirection issue with Spring application behind a load balancer on Weblogic - spring

I have a Spring application running behind a load balancer. The application is running on a Weblogic server. The X-Forwarded-Proto header is coming as HTTPS from the load balancer but the application is redirecting as HTTP. I need HTTPS redirection. The project uses Spring 3.1.2 and JSF 2.1.6. How can I solve this problem?
The flow I expect is as outlined in the diagram below.
I extended HttpServletRequestWrapper to set desired URL scheme if x-forwarded-proto header is set.
package com.example.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class FixSchemeRequest extends HttpServletRequestWrapper {
private static final String X_FORWARDED_PROTO_HEADER = "x-forwarded-proto";
private static final String HTTPS_PREFIX = "https";
private HttpServletRequest request;
public FixSchemeRequest(HttpServletRequest request) {
super(request);
this.request = request;
}
/**
* If request contains x-forwarded-proto
* then assing scheme as dedined is the
* header
* #return http/https
*/
#Override
public String getScheme(){
String fixedSchema = "http";
String protocol = request.getHeader(X_FORWARDED_PROTO_HEADER);
if (protocol != null && !protocol.isEmpty()) {
if(protocol.equals(HTTPS_PREFIX)) {
fixedSchema = "https";
}
}
return fixedSchema;
}
}
Then I added a filter to keep the flow with the scheme assigned for the request. Here, I had to check the ports in the address, because while accessing the application with ip and port in the test environment, it is accessed with the domain address in the product environment. So I tried to produce a dirty solution by adding conditions for path. But after all these processes, I am constantly redirecting to the same address by getting too many redirection errors and at the same time Spring Security still sets the schema as HTTP.
package com.example.filter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class HttpsFilter implements Filter {
private static final String HTTP = "http";
private static final String DEFAULT_HTTP_PORT = "80";
private static final String HTTPS = "https";
private static final String DEFAULT_HTTPS_PORT = "443";
private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = new FixSchemeRequest((HttpServletRequest) request);
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (!httpRequest.getRequestURI().isEmpty()) {
String uri = httpRequest.getRequestURI();
String protocolHeader = getRedirectUriSchemeByXForwardedProtoHeader(httpRequest);
String domain = request.getServerName();
String port = Integer.toString(request.getServerPort());
if (protocolHeader.equalsIgnoreCase(HTTPS) && !request.getScheme().equalsIgnoreCase(HTTPS)) {
String path = HTTPS + "://" + domain + uri;
if(!port.isEmpty() && !DEFAULT_HTTP_PORT.equals(port) && !DEFAULT_HTTPS_PORT.equals(port)){
path = HTTPS + "://" + domain + ":" + port + uri;
}
httpResponse.sendRedirect(path);
} else {
chain.doFilter(request, response);
}
}
}
public String getRedirectUriSchemeByXForwardedProtoHeader(HttpServletRequest request) {
return request.getHeader(X_FORWARDED_PROTO) != null && request.getHeader(X_FORWARDED_PROTO).equalsIgnoreCase(HTTPS) ? HTTPS : HTTP;
}
#Override
public void destroy() {
}
}
The filter definition I added to web.xml is as follows.
<filter>
<filter-name>httpsFilter</filter-name>
<filter-class>com.example.filter.HttpsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Related

Spring Zuul Header based routing hangs when downstream is not responsive for some time

I have implemented the below code to route requests based on headers to the respective downstreams.
When the downstream are unresponsive for sometime Zuul stops forwarding the requests, it does not forward the request when the downstream is up again.
There are no errors in the logs.
package com.uk.proxy.filter;
import ai.cuddle.sift.dataapi.proxy.Application;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE;
#Component
public class RoutingFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
private static final String DEFAULT_DOWNSTREAM_GROUP = "SYSTEM";
public static final String HEADER_ORIGIN = "";
#Autowired
#Qualifier(value = "downstreamConfig")
private Map<String, String> downstreamMap;
#Override
public String filterType() {
return ROUTE_TYPE;
}
#Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 100;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String inputURI = request.getRequestURI();
String header = request.getHeader(HEADER_ORIGIN);
try {
String downstreamHost = getDownstreamHost(header);
LOGGER.info(" Header "+header+ " Downstream Host "+downstreamHost);
ctx.setRouteHost(new URL(downstreamHost));
ctx.put("requestURI", inputURI);
} catch (MalformedURLException e) {
LOGGER.error(e.getMessage(), e);
}
return null;
}
private String getDownstreamHost(String header) {
if(header == null || header.isEmpty()){
LOGGER.warn("Header is null or empty");
}
String downstreamGroup = (header == null || header.isEmpty()) ? DEFAULT_DOWNSTREAM_GROUP : header.toUpperCase();
String downstreamHost;
if (downstreamMap.containsKey(downstreamGroup)) {
downstreamHost = downstreamMap.get(downstreamGroup);
} else {
LOGGER.error("Header "+header+" not found in config. DownstreamMap "+downstreamMap);
downstreamHost = downstreamMap.get(DEFAULT_DOWNSTREAM_GROUP);
}
return downstreamHost;
}
}
application.yaml
zuul:
ignoredPatterns:
- /manage/**
routes:
yourService:
path: /**
stripPrefix: false
serviceId: customServiceId
host:
connect-timeout-millis: 300000
socket-timeout-millis: 300000
ribbon:
eureka:
enabled: false
Spring cloud version: Hoxton.SR8.
Please let me know if anyone has faced this issue.

How to by default execute the latest version of endpoint in Spring Boot REST?

I'm developing Spring Boot v2.2.2.RELEASE and API versioning using Custom Header. By default I want latest version of endpoint to be executed.
Here in below code its X-API-VERSION=2. Before executing the endpoint I need to check mapping and then set the latest version in for the Custom Header and then execute it.
#GetMapping(value = "/student/header", headers = {"X-API-VERSION=1"})
public StudentV1 headerV1() {
return serviceImpl.headerV1();
}
#GetMapping(value = "/student/header", headers = {"X-API-VERSION=2"})
public StudentV1 headerV2() {
return serviceImpl.headerV2();
}
If I got your question right, you want that the request sent without X-API-VERSION header will automatically be routed to headerV2() method
This can be done:
Conceptually you'll need a Web Filter that gets executed before the Spring MVC Dispatch Servlet and you'll have to check whether the request contains a header and if it doesn't add a header of the version that you think should be the latest one (from configuration or something).
One caveat here is that the HttpServletRequest doesn't allow adding Headers so you'll have to create a HttpServletRequestWrapper and implement the logic of header addition there.
Here is an example (I've borrowed the implementation from this question):
// The web filter
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
#Component
#Order(1)
public class LatestVersionFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if(req.getHeader("X-API-VERSION")== null) {
HeaderMapRequestWrapper wrappedRequest = new HeaderMapRequestWrapper(req);
wrappedRequest.addHeader("X-API-VERSION", resolveLastVersion());
filterChain.doFilter(wrappedRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
private String resolveLastVersion() {
// read from configuration or something
return "2";
}
}
Note that the request is wrapped to add the header as I've explained:
class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
/**
* construct a wrapper for this request
*
* #param request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
private Map<String, String> headerMap = new HashMap<String, String>();
/**
* add a header with given name and value
*
* #param name
* #param value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
#Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* get the Header names
*/
#Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
#Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
With this implementation (make sure you put the servlet in the package next to controller or something so that the spring boot will recognize it as a component) you'll see:
GET with header X-API-VERSION=1 will be routed to headerV1()
GET with header X-API-VERSION=2 will be routed to headerV2()
GET without X-API-VERSION header will be routed to headerV2()
You can add default method without header, like that:
#GetMapping(value = "/student/header")
public StudentV1 headerDefault() {
return headerV2();
}
Or just remove headers = {"X-API-VERSION=2"} from method that should be current.

Sticky session Ribbon rule in Zuul always has null request

I am attempting to implement a sticky session load balancer rule in a Zuul proxy service. I am using the code from this example: https://github.com/alejandro-du/vaadin-microservices-demo/blob/master/proxy-server/src/main/java/com/example/StickySessionRule.java
I seem to have everything configured correctly, and the rule is triggering in my debugger, but the call to RequestContext.getCurrentContext().getResponse() always returns null, so the cookie is never found, so the rule never takes effect.
The rest of the Zuul config is working 100%. My traffic is proxied and routed and I can use the app fine, only the sticky session rule is not working.
Is there another step I am missing to get the request wired in to this rule correctly?
My route config:
zuul.routes.appname.path=/appname/**
zuul.routes.appname.sensitiveHeaders=
zuul.routes.appname.stripPrefix=false
zuul.routes.appname.retryable=true
zuul.add-host-header=true
zuul.routes.appname.service-id=APP_NAME
hystrix.command.APP_NAME.execution.isolation.strategy=THREAD
hystrix.command.APP_NAME.execution.isolation.thread.timeoutInMilliseconds=125000
APP_NAME.ribbon.ServerListRefreshInterval=10000
APP_NAME.ribbon.retryableStatusCodes=500
APP_NAME.ribbon.MaxAutoRetries=5
APP_NAME.ribbon.MaxAutoRetriesNextServer=1
APP_NAME.ribbon.OkToRetryOnAllOperations=true
APP_NAME.ribbon.ReadTimeout=5000
APP_NAME.ribbon.ConnectTimeout=5000
APP_NAME.ribbon.EnablePrimeConnections=true
APP_NAME.ribbon.NFLoadBalancerRuleClassName=my.package.name.StickySessionRule
The app:
#EnableZuulProxy
#SpringBootApplication
public class ApplicationGateway {
public static void main(String[] args) {
SpringApplication.run(ApplicationGateway.class, args);
}
#Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
EDIT: As requested, the code:
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* #author Alejandro Duarte.
*/
public class StickySessionRule extends ZoneAvoidanceRule {
public static final String COOKIE_NAME_SUFFIX = "-" + StickySessionRule.class.getSimpleName();
#Override
public Server choose(Object key) {
Optional<Cookie> cookie = getCookie(key);
if (cookie.isPresent()) {
Cookie hash = cookie.get();
List<Server> servers = getLoadBalancer().getReachableServers();
Optional<Server> server = servers.stream()
.filter(s -> s.isAlive() && s.isReadyToServe())
.filter(s -> hash.getValue().equals("" + s.hashCode()))
.findFirst();
if (server.isPresent()) {
return server.get();
}
}
return useNewServer(key);
}
private Server useNewServer(Object key) {
Server server = super.choose(key);
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
if (response != null) {
String cookieName = getCookieName(server);
Cookie newCookie = new Cookie(cookieName, "" + server.hashCode());
newCookie.setPath("/");
response.addCookie(newCookie);
}
return server;
}
private Optional<Cookie> getCookie(Object key) {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if (request != null) {
Server server = super.choose(key);
String cookieName = getCookieName(server);
Cookie[] cookies = request.getCookies();
if (cookies != null) {
return Arrays.stream(cookies)
.filter(c -> c.getName().equals(cookieName))
.findFirst();
}
}
return Optional.empty();
}
private String getCookieName(Server server) {
return server.getMetaInfo().getAppName() + COOKIE_NAME_SUFFIX;
}
}
I think you are missing a PreFilter, like this:
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
public class PreFilter extends com.netflix.zuul.ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
RequestContext.getCurrentContext().set(FilterConstants.LOAD_BALANCER_KEY, ctx.getRequest());
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
}
#Override
public String filterType() {
return "pre";
}
}
Mark as Bean
#Bean
public PreFilter preFilter() {
return new PreFilter();
}
And use it in your rule
#Override
public Server choose(Object key) {
javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest) key;
RequestContext not working cause "hystrix.command.APP_NAME.execution.isolation.strategy=THREAD"

Is there a way to automatically propagate an incoming HTTP header in a JAX-RS request to an outgoing JAX-RS request?

I'm looking for the proper way—in a Jersey application—to read a header from an incoming request and automatically install it in any outgoing requests that might be made by a JAX-RS client that my application is using.
Ideally I'd like to do this without polluting any of my classes' inner logic at all, so via various filters and interceptors.
For simple use cases, I can do this: I have a ClientRequestFilter implementation that I register on my ClientBuilder, and that filter implementation has:
#Context
private HttpHeaders headers;
...which is a context-sensitive proxy (by definition), so in its filter method it can refer to headers that were present on the inbound request that's driving all this, and install them on the outgoing request. For straightforward cases, this appears to work OK.
However, this fails in the case of asynchronicity: if I use the JAX-RS asynchronous client APIs to spawn a bunch of GETs, the filter is still invoked, but can no longer invoke methods on that headers instance variable; Jersey complains that as far as it knows we're no longer in request scope. This makes sense if request scope is defined to be per-thread: the spawned GETs are running in some Jersey-managed thread pool somewhere, not on the same thread as the one with which the headers proxy is associated, so that proxy throws IllegalStateExceptions all over the place when my filter tries to talk to it.
I feel like there's some combination of ContainerRequestFilter and ClientRequestFilter that should be able to get the job done even in asynchronous cases, but I'm not seeing it.
What I would do is make a WebTarget injectable that is preconfigured with a ClientRequestFilter to add the headers. It's better to configure the WebTarget this way, as opposed to the Client, since the Client is an expensive object to create.
We can make the WebTarget injectable using a custom annotation and an InjectionResolver. In the InjectionResolver, we can get the ContainerRequest and get the headers from that, which we will pass to the ClientRequestFilter.
Here it is in action
Create the custom annotation
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface WithHeadersTarget {
String baseUri();
String[] headerNames() default {};
}
Make the InjectionResolver with the custom ClientRequestFilter
private static class WithHeadersTargetInjectionResolver
implements InjectionResolver<WithHeadersTarget> {
private final Provider<ContainerRequest> requestProvider;
private final Client client;
#Inject
public WithHeadersTargetInjectionResolver(Provider<ContainerRequest> requestProvider) {
this.requestProvider = requestProvider;
this.client = ClientBuilder.newClient();
}
#Override
public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
if (injectee.getRequiredType() == WebTarget.class
&& injectee.getParent().isAnnotationPresent(WithHeadersTarget.class)) {
WithHeadersTarget anno = injectee.getParent().getAnnotation(WithHeadersTarget.class);
String uri = anno.baseUri();
String[] headersNames = anno.headerNames();
MultivaluedMap<String, String> requestHeaders = requestProvider.get().getRequestHeaders();
return client.target(uri)
.register(new HeadersFilter(requestHeaders, headersNames));
}
return null;
}
#Override
public boolean isConstructorParameterIndicator() {
return false;
}
#Override
public boolean isMethodParameterIndicator() {
return false;
}
private class HeadersFilter implements ClientRequestFilter {
private final MultivaluedMap<String, String> headers;
private final String[] headerNames;
private HeadersFilter(MultivaluedMap<String, String> headers, String[] headerNames) {
this.headers = headers;
this.headerNames = headerNames;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
// if headers names is empty, add all headers
if (this.headerNames.length == 0) {
for (Map.Entry<String, List<String>> entry: this.headers.entrySet()) {
requestContext.getHeaders().put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
// else just add the headers from the annotation
} else {
for (String header: this.headerNames) {
requestContext.getHeaders().put(header, new ArrayList<>(this.headers.get(header)));
}
}
}
}
}
One thing about this implementation is that it checks for an empty headerNames in the #WithHeadersTarget annotation. If it is empty, then we just forward all headers. If the user specifies some header names, then it will only forward those
Register the InjectionResolver
new ResourceConfig()
.register(new AbstractBinder() {
#Override
protected void configure() {
bind(WithHeadersTargetInjectionResolver.class)
.to(new TypeLiteral<InjectionResolver<WithHeadersTarget>>() {
}).in(Singleton.class);
}
})
Use it
#Path("test")
public static class TestResource {
#WithHeadersTarget(
baseUri = BASE_URI
headerNames = {TEST_HEADER_NAME})
private WebTarget target;
#GET
public String get() {
return target.path("client").request().get(String.class);
}
}
In this example if, the headerNames is left out, then it will default to an empty array, which will cause all the request headers to be forwarded.
Complete test using Jersey Test Framework
import org.glassfish.hk2.api.Injectee;
import org.glassfish.hk2.api.InjectionResolver;
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import static org.assertj.core.api.Assertions.assertThat;
public class ForwardHeadersTest extends JerseyTest {
private static final String BASE_URI = "http://localhost:8000";
private static final String TEST_HEADER_NAME = "X-Test-Header";
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface WithHeadersTarget {
String baseUri();
String[] headerNames() default {};
}
#Path("test")
public static class TestResource {
#WithHeadersTarget(
baseUri = BASE_URI
)
private WebTarget target;
#GET
public String get() {
return target.path("client").request().get(String.class);
}
}
#Path("client")
public static class ClientResource {
#GET
public String getReversedHeader(#HeaderParam(TEST_HEADER_NAME) String header) {
System.out.println(header);
return new StringBuilder(header).reverse().toString();
}
}
private static class WithHeadersTargetInjectionResolver
implements InjectionResolver<WithHeadersTarget> {
private final Provider<ContainerRequest> requestProvider;
private final Client client;
#Inject
public WithHeadersTargetInjectionResolver(Provider<ContainerRequest> requestProvider) {
this.requestProvider = requestProvider;
this.client = ClientBuilder.newClient();
}
#Override
public Object resolve(Injectee injectee, ServiceHandle<?> handle) {
if (injectee.getRequiredType() == WebTarget.class
&& injectee.getParent().isAnnotationPresent(WithHeadersTarget.class)) {
WithHeadersTarget anno = injectee.getParent().getAnnotation(WithHeadersTarget.class);
String uri = anno.baseUri();
String[] headersNames = anno.headerNames();
MultivaluedMap<String, String> requestHeaders = requestProvider.get().getRequestHeaders();
return client.target(uri)
.register(new HeadersFilter(requestHeaders, headersNames));
}
return null;
}
#Override
public boolean isConstructorParameterIndicator() {
return false;
}
#Override
public boolean isMethodParameterIndicator() {
return false;
}
private class HeadersFilter implements ClientRequestFilter {
private final MultivaluedMap<String, String> headers;
private final String[] headerNames;
private HeadersFilter(MultivaluedMap<String, String> headers, String[] headerNames) {
this.headers = headers;
this.headerNames = headerNames;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
// if headers names is empty, add all headers
if (this.headerNames.length == 0) {
for (Map.Entry<String, List<String>> entry: this.headers.entrySet()) {
requestContext.getHeaders().put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
// else just add the headers from the annotation
} else {
for (String header: this.headerNames) {
requestContext.getHeaders().put(header, new ArrayList<>(this.headers.get(header)));
}
}
}
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig()
.register(TestResource.class)
.register(ClientResource.class)
.register(new AbstractBinder() {
#Override
protected void configure() {
bind(WithHeadersTargetInjectionResolver.class)
.to(new TypeLiteral<InjectionResolver<WithHeadersTarget>>() {
}).in(Singleton.class);
}
})
.register(new LoggingFilter(Logger.getAnonymousLogger(), true))
.register(new ExceptionMapper<Throwable>() {
#Override
public Response toResponse(Throwable t) {
t.printStackTrace();
return Response.serverError().entity(t.getMessage()).build();
}
});
}
#Override
public URI getBaseUri() {
return URI.create(BASE_URI);
}
#Test
public void testIt() {
final String response = target("test")
.request()
.header(TEST_HEADER_NAME, "HelloWorld")
.get(String.class);
assertThat(response).isEqualTo("dlroWolleH");
}
}

Using filters to track response time in spring boot

I have implemented an API using spring boot and I want to track the response times of the different API calls (GET, POST, DELETE, PUT).
Currently I've been trying to use the following code as the filter
#Component
public class timeFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(timeFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
// empty
}
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
long time = System.currentTimeMillis();
try {
chain.doFilter(req, resp);
} finally {
time = System.currentTimeMillis() - time;
LOGGER.trace("{}: {} ms ", ((HttpServletRequest) req).getRequestURI(), time);
}
}
#Override
public void destroy() {
// empty
}
}
However, this will only track the response time of the GET call that retrieves all students from my repository.
Is there a way that I can track the response time of the other calls as well as I need to plot the response time of each calls against each other on a graph. Also is there a reason why my first GET call has a response time of around 200-300 MS but any call after that has a response time of between 0-20?
In case any one finds this useful, here is one way of doing this using reactive WebFilter
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Component
public class RequestTimingFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestTimingFilter.class);
private final boolean logParameters;
#Autowired
RequestTimingFilter(#Value("${flags.log.parameters:false}") boolean logParameters) {
this.logParameters = logParameters;
}
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
long start = System.currentTimeMillis();
String path = exchange.getRequest().getPath().toString();
StringBuilder params = new StringBuilder();
if (this.logParameters) {
String pairs = exchange.getRequest().getQueryParams().toSingleValueMap()
.entrySet()
.stream()
.map(em -> String.format("%s=%s", em.getKey(), em.getValue()))
.collect(Collectors.joining(", "));
params.append(pairs.isEmpty() ? "" : ", ").append(pairs);
}
return chain.filter(exchange)
.doOnSuccess(v -> {
long endTime = System.currentTimeMillis();
if (LOGGER.isInfoEnabled()) {
LOGGER.info("tag={}, uri=\"{}\", time={}, unit=ms{}", "request-timing",
path, (endTime - start), params.toString());
}
});
}
}
You should do with spring's OncePerRequestFilter , which should do the work and
Make sure this component is scanned.
Note here i also have dynamic property testproject.logging.includeQueryParams which you can control if you need to include query params and same goes for headers,etc..
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Implementation of Spring's {#link OncePerRequestFilter} to log each request
* including the URI, query String and execution time.
*/
#Component
public class RequestLoggingInterceptor extends OncePerRequestFilter {
/** {#code Logger} instance. */
private final Logger logger = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
/** {#code true} if query parameters should be logged. */
private boolean includeQueryParams = true;
/** {#code true} if client address should be logged. */
private boolean includeClient = true;
/** {#code true} if request headers should be logged. */
private boolean includeHeaders = true;
#Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
final long start = System.nanoTime();
try {
filterChain.doFilter(request, response);
} finally {
if( logger.isInfoEnabled() ) {
final long end = System.nanoTime();
logger.info(buildMessage(request, end - start));
}
}
}
/**
* Builds the message to log from the specified {#code request} including
* the {#code executionTime}.
*
* #param request
* #param executionTime in nanoseconds
* #return log message
*/
private String buildMessage(final HttpServletRequest request, final long executionTime) {
final StringBuilder buffer = new StringBuilder();
buffer.append("method=").append(request.getMethod());
buffer.append(" uri=").append(request.getRequestURI());
if( includeQueryParams && request.getQueryString() != null ) {
buffer.append('?').append(request.getQueryString());
}
buffer.append(" executionTime=").append(executionTime);
return buffer.toString();
}
/**
* Sets whether to {#code include} the query parameter String when logging
* the request URI.
*
* #param include
*/
#Value("${testproject.logging.includeQueryParams:true}")
public void setIncludeQueryParams(final boolean include) {
includeQueryParams = include;
}
}

Resources