Monday, 9 June 2025

How to Track Endpoint Usage with Prometheus Counters in Java?

When building applications, it's important to track how often certain apis/parts are used like how many times users visit the /health or /user-profile endpoints.

Prometheus helps us collect and monitor such metrics, and Counters are perfect for tracking things that only increase, like the total number of requests.

 

1. What is a Counter in Prometheus?

A Counter is a type of metric that:

·      Can only increase (or be reset to 0).

·      Tracks how many times something has happened.

 

Examples of Counters:

·      http_requests_total: Total number of HTTP requests received.

·      jvm_classes_loaded_total: Total number of classes loaded by JVM.

·      process_cpu_seconds_total: Total CPU time consumed.

·      errors_total: Total number of errors occurred.

 

Naming Conventions for Counters

·      Metric name must start with a letter.

·      Use lowercase letters, numbers, and underscores.

·      Metric names must be unique.

·      For counters, it’s a best practice to end with _total.

 

Sample Application

Let’s create an Application using a simple HTTP server in Java that exposes:

 

·      /health: returns "Health OK" and increments a Prometheus counter

·      /user-profile: returns "User Profile OK" and increments another counter

·      /metrics: Prometheus scrapes this to collect metrics

 

Step 1: Define two counters HEALTH_COUNTER, USER_PROFILE_COUNTER to track health and userprofile visiting stats.

private static final Counter HEALTH_COUNTER = Counter.build()
.name("health_requests_total")
.help("Total requests to /health")
.register();

private static final Counter USER_PROFILE_COUNTER = Counter.build()
.name("user_profile_requests_total")
.help("Total requests to /user-profile")
.register();

Step 2: Create an HTTP Server on port 8080.

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

Step 3: Expose /health, /user-profile, /metrics endpoints.

server.createContext("/health", exchange -> {
    HEALTH_COUNTER.inc();
    respond(exchange, "Health OK");
});

server.createContext("/user-profile", exchange -> {
    USER_PROFILE_COUNTER.inc();
    respond(exchange, "User Profile OK");
});

server.createContext("/metrics", exchange -> {
    String response = scrapeMetrics();
    exchange.getResponseHeaders().set("Content-Type", TextFormat.CONTENT_TYPE_004);
    exchange.sendResponseHeaders(200, response.getBytes().length);
    try (OutputStream os = exchange.getResponseBody()) {
        os.write(response.getBytes());
    }
});

Step 4: Expose the metrics   

CollectorRegistry.defaultRegistry.metricFamilySamples() method.

private static String scrapeMetrics() throws java.io.IOException {
    StringWriter writer = new StringWriter();
    TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples());
    return writer.toString();
}

Step 5: You can expose default JVM metrics (optional), by executing following statement in your Java Application.  

 

DefaultExports.initialize();

Let me comment this line in the main application to make it simple.

 

Final Application looks like below.

 

PrometheusCounterDemo.java

package com.sample.app;

import java.io.OutputStream;
import java.io.StringWriter;
import java.net.InetSocketAddress;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Counter;
import io.prometheus.client.exporter.common.TextFormat;

public class PrometheusCounterDemo {

    private static final Counter HEALTH_COUNTER = Counter.build().name("health_requests_total")
            .help("Total requests to /health").register();

    private static final Counter USER_PROFILE_COUNTER = Counter.build().name("user_profile_requests_total")
            .help("Total requests to /user-profile").register();

    public static void main(String[] args) throws Exception {
        // DefaultExports.initialize();

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        server.createContext("/health", exchange -> {
            HEALTH_COUNTER.inc();
            respond(exchange, "Health OK");
        });

        server.createContext("/user-profile", exchange -> {
            USER_PROFILE_COUNTER.inc();
            respond(exchange, "User Profile OK");
        });

        server.createContext("/metrics", exchange -> {
            String response = scrapeMetrics();
            exchange.getResponseHeaders().set("Content-Type", TextFormat.CONTENT_TYPE_004);
            exchange.sendResponseHeaders(200, response.getBytes().length);
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });

        server.setExecutor(null);
        server.start();
        System.out.println("Server running at http://localhost:8080");
    }

    private static void respond(HttpExchange exchange, String response) throws java.io.IOException {
        exchange.sendResponseHeaders(200, response.length());
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(response.getBytes());
        }
    }

    private static String scrapeMetrics() throws java.io.IOException {
        StringWriter writer = new StringWriter();
        TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples());
        return writer.toString();
    }

}

Run the Application.

 

You can see output like below.

 

Server running at http://localhost:8080

 

Open the url ‘http://localhost:8080/metrics’ in browser.

 


 

Understanding the Output

When you go to http://localhost:8080/metrics, you see output like this:

 

# HELP user_profile_requests_total Total requests to /user-profile
# TYPE user_profile_requests_total counter
user_profile_requests_total 0.0

# HELP health_requests_total Total requests to /health
# TYPE health_requests_total counter
health_requests_total 0.0

# HELP health_requests_created Total requests to /health
# TYPE health_requests_created gauge
health_requests_created 1.744523757043E9

# HELP user_profile_requests_created Total requests to /user-profile
# TYPE user_profile_requests_created gauge
user_profile_requests_created 1.744523757044E9

 

# HELP lines

These are comments (metadata) that describe what the metric is about. They help humans and monitoring tools understand the purpose of the metric.

 

# HELP user_profile_requests_total Total requests to /user-profile

 

This tells us that user_profile_requests_total is a metric that counts the total number of requests made to the /user-profile endpoint.

 

# TYPE lines

These lines tell Prometheus what kind of metric it is. In this case:

 

# TYPE user_profile_requests_total counter

 

Above line tells us that this metric is a counter (i.e., it only goes up).

 

The actual metric values

user_profile_requests_total 0.0

 

This line gives the current value of the counter. In this case, 0.0 means no requests have been made yet.

 

What Are These _created Metrics?

# HELP health_requests_created Total requests to /health

# TYPE health_requests_created gauge

health_requests_created 1.744523757043E9

 

# HELP user_profile_requests_created Total requests to /user-profile

# TYPE user_profile_requests_created gauge

user_profile_requests_created 1.744523757044E9

 

This is where things may feel confusing. We added two counter metrics like health_requests_total, user_profile_requests_total only, then from where the metrics health_requests_created, user_profile_requests_created of type Gauge are coming.

 

These are automatically added by Prometheus client when you create a Counter. Whenever you create a Counter metric, Prometheus automatically adds a _created gauge.

 

What does *_created mean?

It's a timestamp specifies the time (in seconds since Unix Epoch) when the counter was created (i.e., registered in the code).

 

The number is in scientific notation:

1.744523757043E9 = 1,744,523,757.043 seconds =  Sunday, April 13, 2025 11:25:57.043 AM GMT+05:30

 

Why is this useful?

·      Monitoring tools (like Grafana or Prometheus itself) can use the *_created timestamp to:

·      Detect when a metric was first seen.

·      Help to troubleshoot if counters reset unexpectedly (e.g., after a restart).

·      Calculate metric age.

 

Let’s hit the http://localhost:8080/health endpoint and see whether the counter is increasing or not

As you see following image, we can observe that the health endpoint counter is set to 1 now.

 


Start hitting /health, /user-profile endpoints, you can see these counter are increasing.

 

http://localhost:8080/user-profile

http://localhost:8080/health


 

 

How to see this metrics in prometheus UI?

Let’s attach this target to Prometheus instance by updating prometheus.yml file

 

prometheus.yml

 

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'node_exporter'
    static_configs:
      - targets: ['localhost:9100']
  - job_name: 'java_app'
    static_configs:
      - targets: ['localhost:8080']

 

Start Prometheus application by executing following command.

prometheus --config.file=./prometheus.yml

 

Upon successful starting of Prometheus server, execute following metric.

 

user_profile_requests_total

 

You can see the total visits to the /user-profile url

 


 

Let’s hit user-profile endpoint couple of times.

 

Wait for 15 seconds until Prometheus rescrap the metrics, and click on Graph tab to see the metrics in Graph view.

 

You can see Graph something like below.

 


You can use the rate() function in Prometheus queries to see the rate at which requests are coming in to the /user-profile API.

 

rate(user_profile_requests_total[5m])

 

How many requests per second, on average, came to the /user-profile endpoint in the last 5 minutes?

 


 

That’s it, Happy leaning……

 

References

https://prometheus.io/docs/practices/naming/

 

Previous                                                    Next                                                    Home

No comments:

Post a Comment