Gather HTTP metrics for a Java app using Open Telemetry and Prometheus.

  • Context Propagation: Ensure proper propagation of the trace context across threads and asynchronous operations. Error Handling: Implement proper error handling to capture and report exceptions. Sampling: Adjust the sampling rate based on your monitoring needs and performance requirements. Security: Securely configure your OTLP collector and protect sensitive data. Monitoring: Use Prometheus and Grafana to visualize and analyze the collected metrics.

Prerequisites

  • AWS Account with EC2 Instance.

  • Open JDK, Maven, Docker, and Prometheus must be installed.

Set Up Ubuntu EC2 Instance

Update the package list.

sudo apt update

Install the default JDK for java.

sudo apt install -y default-jdk

check its version to verify its installation.

java -version

Install Maven.

sudo apt install -y maven

check its version to verify its installation.

mvn -version

Install Docker.

sudo apt install -y docker.io

start and enable the docker.

sudo systemctl start docker
sudo systemctl enable docker

Pull the Prometheus Docker image.

sudo docker pull prom/prometheus

Create the Maven Roll Dice Application to Collect http Metrics

Run the following command to create a new Maven project.

mvn archetype:generate -DgroupId=com.example -DartifactId=roll-dice -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Navigate to the Project Directory.

cd roll-dice

Edit the pom.xml to include the following dependencies for OpenTelemetry and Prometheus.

vi pom.xml

Replace its content with the following code.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>helloworld</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>helloworld</name>
  <url>http://maven.apache.org</url>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.0</version>
    <relativePath />
  </parent>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- OpenTelemetry API -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-api</artifactId>
        <version>1.44.1</version>
    </dependency>

    <!-- OpenTelemetry SDK (for exporting metrics/traces) -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk</artifactId>
        <version>1.44.1</version>
    </dependency>

    <!-- Optional: Prometheus Exporter -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-prometheus</artifactId>
        <version>1.44.1-alpha</version>
    </dependency>

    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-logging</artifactId>
        <version>1.44.1</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient</artifactId>
        <version>0.16.0</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient_httpserver</artifactId>
        <version>0.16.0</version>
    </dependency>
    <dependency>
        <groupId>io.prometheus</groupId>
        <artifactId>simpleclient_hotspot</artifactId>
        <version>0.16.0</version>
    </dependency>
    <dependency>
        <groupId>org.nanohttpd</groupId>
        <artifactId>nanohttpd</artifactId>
        <version>2.3.1</version>
    </dependency>

    <!-- SLF4J for Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.1.0-alpha1</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

Collect HTTP Metrics for Java App using Opentelemetry

Navigate to the src/main/java/com/example.

cd src/main/java/com/example

Open the application file.

vi App.java

Replace its content with the following code.

package com.example;

import fi.iki.elonen.NanoHTTPD;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
import io.prometheus.client.exporter.common.TextFormat;

import java.io.StringWriter;
import java.util.Map;

public class App extends NanoHTTPD {

    // Counters for total requests and errors by HTTP status codes
    private static final Counter requestCounter = Counter.build()
            .name("http_requests_total")
            .help("Total HTTP requests.")
            .labelNames("method", "endpoint", "status")
            .register();

    // Histogram for request duration (latency)
    private static final Histogram requestLatency = Histogram.build()
            .name("http_request_duration_seconds")
            .help("Request latency in seconds.")
            .labelNames("method", "endpoint", "status")
            .register();

    public App() throws Exception {
        super(8080); // Start HTTP server on port 8080
        start(SOCKET_READ_TIMEOUT, false);
        System.out.println("Server running at http://localhost:8080");
    }

    public static void main(String[] args) {
        try {
            // Enable JVM default metrics
            io.prometheus.client.hotspot.DefaultExports.initialize();
            new App();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Response serve(IHTTPSession session) {
        String uri = session.getUri();
        String method = session.getMethod().name();

        long startTime = System.nanoTime(); // Record start time for latency calculation
        Response response;

        try {
            if ("/rolldice".equalsIgnoreCase(uri)) {
                // Simulate dice rolling logic
                int diceRoll = (int) (Math.random() * 6) + 1;
                response = newFixedLengthResponse(Response.Status.OK, "text/plain", "Dice rolled: " + diceRoll);
            } else if ("/metrics".equalsIgnoreCase(uri)) {
                // Expose Prometheus metrics
                StringWriter metricsWriter = new StringWriter();
                TextFormat.write004(metricsWriter, CollectorRegistry.defaultRegistry.metricFamilySamples());
                response = newFixedLengthResponse(Response.Status.OK, TextFormat.CONTENT_TYPE_004, metricsWriter.toString());
            } else {
                // 404 Not Found for unrecognized endpoints
                response = newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Endpoint not found");
            }
        } catch (Exception e) {
            e.printStackTrace();
            response = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Internal server error");
        }

        // Capture metrics after processing the request
        long duration = System.nanoTime() - startTime; // Calculate latency
        String status = String.valueOf(response.getStatus().getRequestStatus()); // HTTP status code

        // Record metrics
        requestCounter.labels(method, uri, status).inc();
        requestLatency.labels(method, uri, status).observe(duration / 1.0e9); // Convert nanoseconds to seconds

        return response;
    }
}

Configure Prometheus for Java Application

Go back to the root directory.

cd ../../../../..

Create a configuration file for Prometheus.

nano prometheus.yml

Add the following content into it.

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "roll-dice"
    scrape_interval: 5s
    static_configs:
      - targets: ['<EC2-instance-IP>:8080']

Replace <EC2-instance-IP> with your EC2 instance's public IP address.

Build and Run the Application

Build the application using the following command.

mvn clean install

Execute the Application using following command.

mvn spring-boot:run

Output be like this:

Open your browser to view the Application and Prometheus metrics.

Roll Dice Endpoint: Go to http://<EC2-instance-IP>:8080/rolldice to roll the dice and see the result.

Replace <EC2-instance-IP> with your EC2 instance's public IP address.

Metrics Endpoint: Visit http://<EC2-instance-IP>:8080/metrics to view the HTTP metrics.

Replace <EC2-instance-IP> with your EC2 instance's public IP address.

Start the Prometheus

Open a new tab and go to the root directory.

cd roll-dice

Use the following command to run the Prometheus container.

sudo docker run -p 9090:9090 -v /home/ubuntu/roll-dice/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

Visit http://<EC2-instance-IP>:9090 to view the Prometheus UI.

To verify that Prometheus is scraping metrics from your Java application, go to the "Targets" page in the Prometheus UI, click on "Status" in the menu bar, then "Targets," and ensure your job roll-dice is listed and marked as "UP."