How do I monitor performance and latency of Java 11 HTTP requests?

Monitoring the performance and latency of Java 11 HTTP requests is essential when utilizing the java.net.http package introduced with the new Java 11 HttpClient API. It helps identify bottlenecks, optimize network calls, and ensure efficient resource usage for your application.

Here’s how you can do it:

1. Measure Latency Using Timestamps

You can manually measure the time taken to send and receive HTTP requests by recording timestamps before and after executing the HTTP call.

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;

public class HttpPerformanceMonitor {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://example.com"))
                .GET()
                .build();

        // Record start time
        Instant start = Instant.now();

        // Send request and get response
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // Record end time
        Instant end = Instant.now();

        // Calculate latency
        Duration latency = Duration.between(start, end);
        System.out.println("Response time: " + latency.toMillis() + " ms");
        System.out.println("Response status code: " + response.statusCode());
    }
}

This approach calculates the time taken for the entire HTTP operation, including connection establishment, sending the request, and receiving the response.

2. Use a Custom Executor to Monitor Thread Usage

HttpClient allows you to set a custom Executor for handling its asynchronous operations. You can measure how efficiently the threads are being utilized.

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class HttpClientWithCustomExecutor {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(4); // Monitor thread usage

        HttpClient client = HttpClient.newBuilder()
                .executor(executor)
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .GET()
                .build();

        long startTime = System.currentTimeMillis();

        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenAccept(response -> {
                    long endTime = System.currentTimeMillis();
                    System.out.println("Response time: " + (endTime - startTime) + " ms");
                    System.out.println("Response status code: " + response.statusCode());
                })
                .join(); // Wait for the async call to complete

        executor.shutdown();
    }
}

By setting a custom Executor, you can track thread pool utilization and capture latency.

3. Enable Logging for HTTP Headers and Debugging

Java 11 supports configuring logging for the HTTP Client. You can enable debug-level logging to capture low-level details:

  • Add the following JVM options to enable java.net.http.HttpClient debugging:
-Djdk.httpclient.HttpClient.log=requests,headers,frames:all

This will output detailed log information, including HTTP request/response headers and frames.

4. Monitor HTTP Client Metrics Using Libraries

Libraries that support metrics collection (like Micrometer) can be integrated with Java 11 HttpClient to collect throughput, response times, and errors.
Example: Using Micrometer You can create custom timers and counters to record metrics manually.

package org.kodejava.net.http;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;

public class HttpClientWithMetrics {
    public static void main(String[] args) throws Exception {
        MeterRegistry registry = new SimpleMeterRegistry(); // Replace with a distributed registry like Prometheus

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://example.com"))
                .GET()
                .build();

        registry.timer("http.requests").record(() -> {
            try {
                Instant start = Instant.now();
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                Instant end = Instant.now();

                System.out.println("Response time: " + Duration.between(start, end).toMillis() + " ms");
                System.out.println("Response status code: " + response.statusCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        registry.get("http.requests").timers().forEach(timer -> {
            System.out.println("Timer count: " + timer.count());
            System.out.println("Total time: " + timer.totalTime(TimeUnit.MILLISECONDS) + " ms");
        });
    }
}

5. Use Dependency Injection Frameworks

Combine the Java 11 HttpClient with Spring Boot’s actuator (if you’re already using it). Actuator provides built-in metrics and HTTP tracing capabilities.

6. Profiling with External Tools

You can observe Java application performance (including HTTP requests) using APM tools, such as:

  • Java Flight Recorder (JFR)
    • Use JFR to monitor detailed HTTP request timings and underlying JVM performance.
  • Java VisualVM
    • Profile application threads and network usage.
  • Third-Party APMs
    • Tools like New Relic, AppDynamics, or Datadog can report on HTTP client usage.

Summary of Approaches

Approach Tools/Techniques Notes
Manual Timing Instant + Duration Tracks latency at the request level.
Custom Executors ExecutorService Measures thread usage and async execution.
Logging JVM Debugging (HttpClient.log) Debugs detailed network activity.
Metrics Libraries Micrometer (manual instrumentation) Can feed metrics into observability tools.
Framework Integration Spring Boot Actuator Collects trace and performance metrics.
External Profiling/Monitoring JFR, APM Tools, VisualVM Monitors JVM, HTTP, and app performance.

Choose the approach based on your application’s complexity and monitoring needs!


Maven Dependencies

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
    <version>1.15.0</version>
</dependency>

Maven Central

How do I use java.time.Instant class of Java Date-Time API?

The java.time.Instant class in the Java Date-Time API is an immutable representation of a point in time. It stores a long count of seconds from the epoch of the first moment of 1970 UTC, plus a number of nanoseconds for the further precision within that second.

The java.time.LocalDate class represents a date without a time or time zone. It is used to represent just a date as year-month-day (e.g., 2023-03-27) in the ISO-8601 calendar system.

The java.time.LocalTime class represents a time without a date or time zone. It is used to represent just a time as hour-minute-second (e.g., 13:45:20).

It’s also worth noting that Instant class is part of Java 8’s new date and time API which was brought in to address the shortcomings of the old java.util.Date and java.util.Calendar API.

Here’s a quick example of how to use the Instant class:

package org.kodejava.datetime;

import java.time.Instant;

public class InstantExample {
    public static void main(String[] args) {
        // Get the current point in time
        Instant now = Instant.now();
        System.out.println("Current time: " + now);

        // Add duration of 500 seconds from now
        Instant later = now.plusSeconds(500);
        System.out.println("500 seconds later: " + later);

        // Subtract duration of 500 seconds from now
        Instant earlier = now.minusSeconds(500);
        System.out.println("500 seconds earlier: " + earlier);

        // Compare two Instants
        int comparison = now.compareTo(later);
        if (comparison < 0) {
            System.out.println("Now is earlier than later");
        } else if (comparison > 0) {
            System.out.println("Now is later than later");
        } else {
            System.out.println("Now and later are at the same time");
        }
    }
}

Output:

Current time: 2024-01-18T09:26:56.152268Z
500 seconds later: 2024-01-18T09:35:16.152268Z
500 seconds earlier: 2024-01-18T09:18:36.152268Z
Now is earlier than later

In this example, Instant.now() is used to get the current Instant. Various methods like plusSeconds(), minusSeconds(), and compareTo() are used to manipulate and compare the Instant.

LocalDate and LocalTime are local in the sense that they represent date and time from the context of the observer, without a time zone.

To connect Instant with LocalDate and LocalTime, you need a time zone. This is because Instant is in UTC and LocalDate/LocalTime are in a local time zone, so you need to explicitly provide a conversion between them.

Here’s how you convert an Instant to a LocalDate and a LocalTime:

package org.kodejava.datetime;

import java.time.*;

public class InstantConvertExample {
    public static void main(String[] args) {
        Instant now = Instant.now();
        System.out.println("Instant: " + now);

        // Get the system default timezone
        ZoneId zoneId = ZoneId.systemDefault(); 

        LocalDate localDate = now.atZone(zoneId).toLocalDate();
        System.out.println("LocalDate: " + localDate);

        LocalTime localTime = now.atZone(zoneId).toLocalTime();
        System.out.println("LocalTime: " + localTime);
    }
}

Here Instant.now() gives the current timestamp. .atZone(ZoneId.systemDefault()) converts it to ZonedDateTime which is then converted to LocalDate and LocalTime by using .toLocalDate() and .toLocalTime() respectively.

You can also go from LocalDate and LocalTime back to Instant. Here’s how:

package org.kodejava.datetime;

import java.time.*;

public class ToInstantExample {
    public static void main(String[] args) {
        LocalDate localDate = LocalDate.now();
        LocalTime localTime = LocalTime.now();
        ZoneId zoneId = ZoneId.systemDefault();
        Instant instantFromDateAndTime = LocalDateTime.of(localDate, localTime).atZone(zoneId).toInstant();

        System.out.println("Instant from LocalDate and LocalTime: " + instantFromDateAndTime);
    }
}

How do I use java.time.Duration class?

java.time.Duration is another useful class in Java for dealing with time. It measures time in seconds and nanoseconds and is most suitable for smaller amounts of time, like “20 seconds” or “3 hours”, and not for larger units like “3 days” or “4 months”. Here’s a guide on how to use it:

1. Creating a Duration instance

You can create an instance of Duration using one of its static factory methods that best suits your needs, such as ofSeconds(), ofMinutes(), ofHours(), or ofMillis().

//create a duration of 60 seconds
Duration duration = Duration.ofSeconds(60);

//create a duration of 2 hours
Duration twoHours = Duration.ofHours(2);

2. Creating a Duration between two Instants

Duration also provides a static method between() that can be used to find the duration between two points in time.

Instant start = Instant.now();
// Do some time consuming task...
Instant end = Instant.now();

Duration duration = Duration.between(start, end);

3. Retrieving the Duration

You can retrieve the number of days, hours, minutes, or seconds in a Duration using methods like toDays(), toHours(), toMinutes(), or getSeconds().

long hours = twoHours.toHours();  // returns 2

4. Adding and Subtracting from a Duration

Duration can be added or subtracted from another using the plus() and minus() methods or the more specific plus / minus methods such as plusHours(), minusMinutes(), etc.

// Adding
Duration additionalDuration = duration.plusHours(4);

// Subtracting
Duration lessDuration = additionalDuration.minusMinutes(50);

5. Comparing Durations

The Duration class provides compareTo(), equals(), negated(), and abs() methods for comparison:

Duration duration1 = Duration.ofHours(4);
Duration duration2 = Duration.ofHours(2);

// Returns a negative number, zero, or positive number if less than, 
// equal to, or greater than the other.
int comparison = duration1.compareTo(duration2);

boolean equals = duration1.equals(duration2); // false

// Returns a duration with the new duration being negative of this 
// duration.
Duration negated = duration1.negated(); 

// Returns a duration with the new duration being absolute of 
// this duration, effectively, it returns the same as duration1.
Duration abs = negated.abs();