Extending Spring Boot's Zuul Proxy to Record Requests and Responses
A code snippet that allows the user to extend Spring Boot's Zuul proxy to record requests and responses.
Join the DZone community and get the full member experience.
Join For FreeAn easy way to capture the incoming requests to your server and replay them can be very useful. For example, if you lack an automation team, or simply want to be quick about it, you can record the REST requests of a scenario and play them again after every push to GIT. You can also use the recorded requests to simulate many users. Or, you can analyze the requests to learn useful statistics, etc. You get the idea.
OK, so how can we do it the easy way? First, we’ll exploit spring boot Zuul gateway filters feature – this gateway can activate custom filters for every request and every response. A handler must simply extend ZuulFilter and provide information of:
- The Filter’s type, for example, ‘pre’ means a pre-routing filter. Other values can be ‘post’ for post-routing filters or ‘route’ for routing to an origin.
- The Filter’s order in the chain of filters. A lower number will cause the filter to be executed before a filter with a higher number.
- Should filter — if the filter should be applied for a given route.
- Run — the actual things you want to do.
We’ll use these filters to record the incoming requests (listing 1) and outgoing responses (listing 2) requests to a file. Naturally, you can record them to DB, to S3, or to whatever repository you’ll be comfortable to use later. No, you can analyze this information at your leisure. The ‘global Id’ is a GUID saved on ThreadLocal and helps us to bind together matching requests and responses.
@Component
@Profile("recording")
public class LogRequestFilter extends ZuulFilter {
privatestatic Logger log = LoggerFactory.getLogger(LogRequestFilter.class);
@Value("${recording.file:c:/temp/record.txt}")
private String recordFile;
@Override
public String filterType() {
return "pre";
}
@Override
publicint filterOrder() {
return 2;
}
@Override
publicboolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = new HttpServletRequestWrapper(ctx.getRequest());
String requestData = null;
try {
if (request.getContentLength() > 0) {
requestData = CharStreams.toString(request.getReader());
}
} catch (Exception e) {
log.error("Error parsing request", e);
throw e;
}
try {
String line = String.format("Request, %s, %s,%s,%s \r\n",
getContext().getGlobalId(), request.getRequestURL(),
request.getMethod(), requestData);
BufferedWriter bw = Files.newBufferedWriter(Paths.get(recordFile),
Charset.forName("UTF-8"), StandardOpenOption.APPEND);
bw.write(line);
bw.close();
} catch (IOException e) {
log.error("Error writing request", e);
throw e;
}
returnnull;
}
}
Listing 2– Recording outgoing responses
@Component
@Profile("recording")
publicclass LogResponseFilter extends ZuulFilter {
privatestatic Logger logger = LoggerFactory.getLogger(LogResponseFilter.class);
@Value("${recording.file:c:/temp/record.txt}")
private String recordFile;
@Override
public String filterType() {
return "post";
}
@Override
publicint filterOrder() {
return 2;
}
@Override
publicboolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try (final InputStream responseDataStream = ctx.getResponseDataStream())
{
final String responseData =
CharStreams.toString(new InputStreamReader(responseDataStream,
"UTF-8"));
try {
String line = String.format("Response, %s, %s \r\n",
getContext().getGlobalId(), responseData);
BufferedWriter bw = Files.newBufferedWriter(Paths.get(recordFile),
Charset.forName("UTF-8"), StandardOpenOption.APPEND);
bw.write(line);
bw.close();
} catch (IOException e) {
logger.error("Error writing response", e);
throw e;
}
ctx.setResponseBody(responseData);
} catch (IOException e) {
logger.error("Error reading body", e);
throw e;
}
returnnull;
}
}
Note that both beans are active only when a ‘recording’ profile is declared in application.properties
.
A common use case would be to use this data for ‘automation’, meaning running the recorded scenarios whenever needed (after a code push, nightly, etc.). Request values can be replaced by parameters, that will be populated from excel (for example) and enable running the same test with different values.
Replaying can be done straightforwardly, using Spring’s RestTemplate. The recording file is the input to the player, which simply sends the requests lines as REST
requests, using the recorded data as a request body for POST
requests, where needed.
We can also add a validator that compares the responses to the response lines in the recording. Since some values in the response will be different for sure (such as timestamp fields, or id fields for created objects) we can make the validator smarter and do validations like ‘id must not be null’ instead of a naïve string comparison.
Sending multiple requests in parallel to check load balancing (or simply see the behavior of your app with more than one request at a time) is fairly simple as well, using Java’s Executor to run the same recording multiple times in parallel.
These were just a few examples of usages for such recordings. If you find some other useful usages, please let me know!
Opinions expressed by DZone contributors are their own.
Comments