Home

Awesome

<!--- Copyright 2016-2024 Volkan Yazıcı 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 https://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 permits and limitations under the License. -->

Actions Status Maven Central License

HRRS (HTTP Request Record Suite) is a set of tools that you can leverage to record, transform, and replay HTTP requests in your Java EE and Spring web applications written in Java 8 or higher. In essence, HRRS bundles a servlet filter for recording (hrrs-servlet-filter) and standalone command-line Java applications for transforming (hrrs-distiller) and replaying (hrrs-replayer) the requests.

Table of Contents

<a name="rationale"></a>

Rationale

Why would someone want to record HTTP requests as is? There are two major problems that HRRS is aiming to solve:

<a name="overview"></a>

Overview

HRRS Overview

HRRS ships the following artifacts:

These artifacts provide interfaces for the potential concrete implementations. Fortunately, we provide one for you: File-based Base64 implementation. That is, HTTP request records are encoded in Base64 and stored in a plain text file. Following artifacts provide this functionality:

HRRS is designed with extensibility in mind. As of now, it only supports file sourced/targeted Base64 readers/writers. But all you need is a few lines of code to introduce your own serialization schemes powered by a storage backend (RDBMS, NoSQL, etc.) of your preference.

Source code also contains the following modules to exemplify the usage of HRRS with certain Java web frameworks:

<a name="getting-started"></a>

Getting Started

In order to start recording HTTP requests, all you need is to plug the HRRS servlet filter into your Java web application. Below, we will use Base64 serialization for recording HTTP requests in a Spring web application. (See examples directory for the actual sources and the JAX-RS example.)

Add the HRRS servlet filter Maven dependency to your pom.xml:

<dependency>
    <groupId>com.vlkan.hrrs</groupId>
    <artifactId>hrrs-servlet-filter-base64</artifactId>
    <version>${hrrs.version}</version>
</dependency>

In the second and last step, you expose the HRRS servlet filter as beans so that Spring can inject them as interceptors:

@Configuration
public class HrrsConfig {

    @Bean
    public HrrsFilter provideHrrsFilter() throws IOException {
        String tmpPathname = System.getProperty("java.io.tmpdir");
        String file = new File(tmpPathname, "hrrs-spring-records.csv").getAbsolutePath();
        String filePattern = new File(tmpPathname, "hrrs-spring-records-%d{yyyyMMdd-HHmmss-SSS}.csv").getAbsolutePath();
        RotationConfig rotationConfig = RotationConfig
                .builder()
                .file(file)
                .filePattern(filePattern)
                .policy(new ByteMatchingRotationPolicy((byte) '\n', 50_000))
                .build();
        return new Base64HrrsFilter(rotationConfig);
    }

    @Bean
    public ServletRegistrationBean provideHrrsServlet() {
        HrrsServlet hrrsServlet = new HrrsServlet();
        return new ServletRegistrationBean(hrrsServlet, "/hrrs");
    }

}

And that's it! The incoming HTTP requests will be recorded into writerTargetFile. (You can also run HelloApplication of examples/spring in your IDE to see it in action.) All you need to do is instructing the HRRS servlet to enable the recorder:

$ curl http://localhost:8080/hrrs
{"enabled": false}

$ curl -X PUT http://localhost:8080/hrrs?enabled=true

After a couple of GET /hello?name=<name> queries, let's take a quick look at the contents of the Base64-serialized HTTP request records:

$ zcat records.csv.gz | head -n 3
iz4mjlt9_8f89s  20170213-224106.477+0100  hello  POST  ABYvaGVsbG8/bmFtZT1UZXN0TmFtZS0xAAAABQAEaG9zdAAObG9jYWxob3N0OjgwODAACnVzZXItYWdlbnQAC2N1cmwvNy40Ny4wAAZhY2NlcHQAAyovKgAMY29udGVudC10eXBlAAp0ZXh0L3BsYWluAA5jb250ZW50LWxlbmd0aAACMTMAAAAAAAAAAAAAAA9yYW5kb20tZGF0YS0x//8=
iz4mjlui_1l3bw  20170213-224106.522+0100  hello  POST  ABYvaGVsbG8/bmFtZT1UZXN0TmFtZS0zAAAABQAEaG9zdAAObG9jYWxob3N0OjgwODAACnVzZXItYWdlbnQAC2N1cmwvNy40Ny4wAAZhY2NlcHQAAyovKgAMY29udGVudC10eXBlAAp0ZXh0L3BsYWluAA5jb250ZW50LWxlbmd0aAACMTMAAAAAAAAAAAAAAA9yYW5kb20tZGF0YS0z//8=
iz4mjlty_sicli  20170213-224106.502+0100  hello  POST  ABYvaGVsbG8/bmFtZT1UZXN0TmFtZS0yAAAABQAEaG9zdAAObG9jYWxob3N0OjgwODAACnVzZXItYWdlbnQAC2N1cmwvNy40Ny4wAAZhY2NlcHQAAyovKgAMY29udGVudC10eXBlAAp0ZXh0L3BsYWluAA5jb250ZW50LWxlbmd0aAACMTMAAAAAAAAAAAAAAA9yYW5kb20tZGF0YS0y//8=

(If you can't see any content yet, you can enforce flushing via curl -X POST http://localhost:8080/hrrs.)

Here each line corresponds to an HTTP request record and fields are separated by \t character. A line first starts with plain text id, timestamp, group name, and method fields. There it is followed by a Base64-encoded field containing the URL (including request parameters), headers, and payload. This simple representation makes it easy to employ well-known command line tools (grep, sed, awk, etc.) to extract a certain subset of records.

$ zcat records.csv.gz | head -n 1 | awk '{print $5}' | base64 --decode | hd
00000000  00 16 2f 68 65 6c 6c 6f  3f 6e 61 6d 65 3d 54 65  |../hello?name=Te|
00000010  73 74 4e 61 6d 65 2d 31  00 00 00 05 00 04 68 6f  |stName-1......ho|
00000020  73 74 00 0e 6c 6f 63 61  6c 68 6f 73 74 3a 38 30  |st..localhost:80|
00000030  38 30 00 0a 75 73 65 72  2d 61 67 65 6e 74 00 0b  |80..user-agent..|
00000040  63 75 72 6c 2f 37 2e 34  37 2e 30 00 06 61 63 63  |curl/7.47.0..acc|
00000050  65 70 74 00 03 2a 2f 2a  00 0c 63 6f 6e 74 65 6e  |ept..*/*..conten|
00000060  74 2d 74 79 70 65 00 0a  74 65 78 74 2f 70 6c 61  |t-type..text/pla|
00000070  69 6e 00 0e 63 6f 6e 74  65 6e 74 2d 6c 65 6e 67  |in..content-leng|
00000080  74 68 00 02 31 33 00 00  00 00 00 00 00 00 00 00  |th..13..........|
00000090  00 0f 72 61 6e 64 6f 6d  2d 64 61 74 61 2d 31 ff  |..random-data-1.|
000000a0  ff                                                |.|
000000a1

Once you start recording HTTP requests, you can setup logrotate to periodically rotate and compress the record output file. You can even take one step further and schedule a cron job to copy these records to a directory accessible by your test environment. There you can replay HTTP request records using the replayer provided by HRRS:

$ java \
    -jar /path/to/hrrs-replayer-base64-<version>.jar \
    --targetHost localhost \
    --targetPort 8080 \
    --threadCount 10 \
    --maxRequestCountPerSecond 1000 \
    --inputUri file:///path/to/records.csv.gz

Below is the list of parameters supported by the replayer.

ParameterRequiredDefaultDescription
--help, -hNfalsedisplay this help and exit
--inputUri, -iYinput URI for HTTP records (Base64 replayer can accept input URIs with .gz suffix.)
--jtlOutputFile, -ojNApache JMeter JTL output file for test results
--localAddress, -lNaddress to bind to when making outgoing connections
--loggerLevelSpecs, -LN*=warn,com.vlkan.hrrs=infocomma-separated list of loggerName=loggerLevel pairs
--maxRequestCountPerSecond, -rN1number of concurrent requests per second
--metricsOutputFile, -omNoutput file to dump Dropwizard metrics
--metricsOutputPeriodSeconds, -mpN10Dropwizard metrics report frequency in seconds
--rampUpDurationSeconds, -dN1ramp up duration in seconds to reach to the maximum number of requests
--redirectStrategy, -rsNDEFAULTredirect strategy (NONE, DEFAULT, or LAX)
--requestTimeoutSeconds, -tN10HTTP request connect/write/read timeout in seconds
--replayOnce, -1Nfalseexit once all the records are replayed
--targetHost, -thYremote HTTP server host
--targetPort, -tpYremote HTTP server port
--threadCount, -nN2HTTP request worker pool size
--totalDurationSeconds, -DN10total run duration in seconds

It is not always desired to replay recorded HTTP requests as is. One might need to exclude certain HTTP headers, remove promotion codes from the URL, sanitize payload by shadowing sensitive customer information, etc. You can use distiller provided by HRRS for this purpose:

$ java \
    -jar /path/to/hrrs-distiller-base64-<version>.jar
    --inputUri file:///path/to/input-records.csv.gz
    --outputUri file:///path/to/output-records.csv.gz
    --scriptUri file:///path/to/transform.js

Distiller passes each read input record to the transform() function defined in the JavaScript file pointed by --scriptUri parameter. transform() receives a single argument of type HttpRequestRecord and returns an HttpRequestRecord. (Returning null lets the distiller to exclude that record.) Consider the following example:

var formatter = new java.text.SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ");
var loTimestamp = formatter.parse("20170415-204551.527+0200");
var hiTimestamp = formatter.parse("20170415-204551.700+0200");

/**
* Remove `Host` and `Content-Length` headers.
*/
function sanitizeHeaders(oldHeaders) {
    var newHeaders = [];
    for (var oldHeaderIndex = 0; oldHeaderIndex < oldHeaders.length; oldHeaderIndex++) {
        var oldHeader = oldHeaders[oldHeaderIndex];
        var oldHeaderName = oldHeader.getName();
        var allowed =
            !oldHeaderName.equalsIgnoreCase("host") &&
            !oldHeaderName.equalsIgnoreCase("content-length");
        if (allowed) {
            newHeaders.push(oldHeader);
        }
    }
    return newHeaders;
}

function transform(input) {
    var timestamp = input.getTimestamp();
    if (timestamp.after(loTimestamp) && timestamp.before(hiTimestamp)) {    // Check the timestamp.
        var newHeaders = sanitizeHeaders(input.getHeaders());               // Sanitize headers.
        return input.toBuilder().setHeaders(newHeaders).build();            // Reconstruct record with new headers.
    }
    return null;                                                            // Out of time range, ignore the record.
}

Below is the list of parameters supported by the distiller.

ParameterRequiredDefaultDescription
--help, -hNfalsedisplay this help and exit
--inputUri, -iYinput URI for HTTP records
--loggerLevelSpecs, -LN*=warn,com.vlkan.hrrs=infocomma-separated list of loggerName=loggerLevel pairs
--outputUri, -oYoutput URI for HTTP records
--scriptUri, -sYinput URI for script file

For a more detailed walk-through see README.md in examples/spring.

<a name="recorder-configuration"></a>

Recorder Configuration

By default, HRRS servlet filter records every HTTP request along with its payload. This certainly is not a desired option for many applications. For such cases, you can override certain methods of the HrrsFilter to have a more fine-grained control over the recorder.

public abstract class HrrsFilter implements Filter {

    // ...

    public static final long DEFAULT_MAX_RECORDABLE_PAYLOAD_BYTE_COUNT = 10 * 1024 * 1024;

    /**
     * Checks if the given HTTP request is recordable.
     */
    protected boolean isRequestRecordable(HttpServletRequest request) {
        return true;
    }

    /**
     * Maximum amount of bytes that can be recorded per request.
     * Defaults to {@link HrrsFilter#DEFAULT_MAX_RECORDABLE_PAYLOAD_BYTE_COUNT}.
     */
    public long getMaxRecordablePayloadByteCount() {
        return DEFAULT_MAX_RECORDABLE_PAYLOAD_BYTE_COUNT;
    }

    /**
     * Create a group name for the given request.
     *
     * Group names are used to group requests and later on are used
     * as identifiers while reporting statistics in the replayer.
     * It is strongly recommended to use group names similar to Java
     * package names.
     */
    protected String createRequestGroupName(HttpServletRequest request) {
        String requestUri = createRequestUri(request);
        return requestUri
                .replaceFirst("\\?.*", "")      // Replace query parameters.
                .replaceFirst("^/", "")         // Replace the initial slash.
                .replaceAll("/", ".");          // Replace all slashes with dots.
    }

    /**
     * Creates a unique identifier for the given request.
     */
    protected String createRequestId(HttpServletRequest request) {
        return ID_GENERATOR.next();
    }

    /**
     * Filters the given record prior to writing.
     * @return the modified record or null to exclude the record
     */
    protected HttpRequestRecord filterRecord(HttpRequestRecord record) {
        return record;
    }

    // ...

}

<a name="recorder-performance"></a>

Recorder Performance

HRRS provided servlet filter wraps the input stream of the HTTP request model. Whenever user consumes from the input, we store the read bytes in a seperate buffer, which later on gets Base64-encoded at request completion. There are two issues with this approach:

It is possible to use a fixed (thread local?) memory pool to avoid extra memory allocations for each request. Further, encoding and storing can also be performed in a separate thread to not block the request handler thread. These being said, HRRS is successfully deployed on a 6-node Java EE application cluster (each node handles approximately 600 reqs/sec and requests generally contain a payload close to 50KB) without any noticeable memory or processing overhead.

Additionally, you can override isRequestRecordable() and getMaxRecordablePayloadByteCount() methods in HrrsFilter to have a more fine-grained control over the recorded HTTP requests.

<a name="replayer-reports"></a>

Replayer Reports

If you have ever used HTTP benchmarking tools like JMeter or Gatling, then you should be familiar with the reports generated by these tools. Rather than generating its own eye candy reports, HRRS optionally (--jtlOutputFile) dumps a JMeter JTL file with the statistics (timestamp, latency, etc.) of each executed request. A quick peek at the JMeter JTL file looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<testResults version="1.2">
<httpSample t="108" lt="108" ts="1486330510795" s="true" rc="200" lb="hello" tn="RateLimitedExecutor-0"/>
<httpSample t="6" lt="6" ts="1486330510802" s="true" rc="200" lb="hello" tn="RateLimitedExecutor-1"/>
<httpSample t="3" lt="3" ts="1486330510828" s="true" rc="200" lb="hello" tn="RateLimitedExecutor-0"/>
<!-- ... -->
</testResults>

For an overview or to track the progress, you can also command HRRS to periodically dump Dropwizard metrics (--metricsOutputFile and --metricsOutputPeriodSeconds) to a file as well. HRRS uses ConsoleReporter of Dropwizard metrics to dump the statistics, which look as follows:

__all__
             count = 10
         mean rate = 1.01 calls/second
     1-minute rate = 1.00 calls/second
     5-minute rate = 1.00 calls/second
    15-minute rate = 1.00 calls/second
               min = 3.00 milliseconds
               max = 108.00 milliseconds
              mean = 16.15 milliseconds
            stddev = 29.46 milliseconds
            median = 8.00 milliseconds
              75% <= 9.00 milliseconds
              95% <= 108.00 milliseconds
              98% <= 108.00 milliseconds
              99% <= 108.00 milliseconds
            99.9% <= 108.00 milliseconds
__all__.200
             count = 10
         mean rate = 1.01 calls/second
     1-minute rate = 1.00 calls/second
     5-minute rate = 1.00 calls/second
    15-minute rate = 1.00 calls/second
               min = 3.00 milliseconds
               max = 108.00 milliseconds
              mean = 16.15 milliseconds
            stddev = 29.46 milliseconds
            median = 8.00 milliseconds
              75% <= 9.00 milliseconds
              95% <= 108.00 milliseconds
              98% <= 108.00 milliseconds
              99% <= 108.00 milliseconds
            99.9% <= 108.00 milliseconds
hello
             count = 10
         mean rate = 1.01 calls/second
     1-minute rate = 1.00 calls/second
     5-minute rate = 1.00 calls/second
    15-minute rate = 1.00 calls/second
               min = 3.00 milliseconds
               max = 108.00 milliseconds
              mean = 16.15 milliseconds
            stddev = 29.46 milliseconds
            median = 8.00 milliseconds
              75% <= 9.00 milliseconds
              95% <= 108.00 milliseconds
              98% <= 108.00 milliseconds
              99% <= 108.00 milliseconds
            99.9% <= 108.00 milliseconds
hello.200
             count = 10
         mean rate = 1.01 calls/second
     1-minute rate = 1.00 calls/second
     5-minute rate = 1.00 calls/second
    15-minute rate = 1.00 calls/second
               min = 3.00 milliseconds
               max = 108.00 milliseconds
              mean = 16.15 milliseconds
            stddev = 29.46 milliseconds
            median = 8.00 milliseconds
              75% <= 9.00 milliseconds
              95% <= 108.00 milliseconds
              98% <= 108.00 milliseconds
              99% <= 108.00 milliseconds
            99.9% <= 108.00 milliseconds

Here HRRS updates a Dropwizard timer with label <groupName>.<responseCode> for each executed request. It also updates the metrics of a pseudo group, called __all__, which covers all the existing groups.

<a name="debugging"></a>

Distiller & Replayer Debugging

Sometimes it becomes handy to have more insight into the distiller and replayer internals. For such cases, you can increase the logging verbosity of certain packages. As a starting point, adding --loggerLevelSpecs "*=info,com.vlkan.hrrs=trace" to the command line arguments is generally a good idea. Note that, you don't want to have such a level of verbosity while executing the actual performance tests.

<a name="faq"></a>

F.A.Q.

<a name="caveats"></a>

Caveats

<a name="security"></a>

Security policy

If you have encountered an unlisted security vulnerability or other unexpected behaviour that has security impact, please report them privately to the volkan@yazi.ci email address.

<a name="license"></a>

License

Copyright © 2016-2024 Volkan Yazıcı

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

https://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.