Application Metrics

  1. Metrics are numbers that describe how an application behaves while it runs, such as memory used, CPU load, request counts and response times.
  2. They are used to spot slow or overloaded applications, plan capacity and raise alerts before users notice a problem.
  3. Spring Boot Actuator collects JVM, system, web and database metrics automatically through Micrometer, and exposes them at /actuator/metrics.
  4. You can also create your own metrics, such as how many students were created, and expose them in Prometheus format for tools like Grafana.

Features

  • Collects JVM metrics: memory, threads, garbage collection and loaded classes.
  • Collects system metrics: CPU usage, uptime and disk space.
  • Records every HTTP request automatically (count, time, status, URL).
  • Reports MySQL connection pool usage through HikariCP metrics.
  • Supports custom business metrics through MeterRegistry.
  • Filters metrics with tags, for example only GET /students.
  • Exports all metrics as Prometheus text at /actuator/prometheus.

Hands-on Experiment: Creating a spring boot application to understand how runtime and application metrics can be exposed for basic monitoring.

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: metricsdemo
  • Name: metricsdemo
  • Package name: com.example.metricsdemo
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Actuator, Prometheus.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-metrics-demo</artifactId>
    <version>1.0.0</version>
    <name>student-metrics-demo</name>
    <description>JVM and application metrics of the Student application through Actuator</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- Actuator + Micrometer: provides /actuator/metrics -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- Exposes the same metrics in Prometheus text format at /actuator/prometheus -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-metrics-demo
server.port=8082

# MySQL (database is created automatically if it does not exist) - change username/password as needed
spring.datasource.url=jdbc:mysql://localhost:3306/studentdb?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# ---------- Metrics settings ----------
# Expose the metrics endpoint and the Prometheus-format endpoint
management.endpoints.web.exposure.include=health,info,metrics,prometheus

# Add the application name as a tag to every metric (useful when many applications are monitored)
management.metrics.tags.application=${spring.application.name}Code language: PHP (php)

Step 5: Create Entity Class

package com.example.metricsdemo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Column(unique = true)
    private String email;

    private String department;

    public Student() {
    }

    public Student(String name, String email, String department) {
        this.name = name;
        this.email = email;
        this.department = department;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }
}

Step 6: Create below Repository Interface

package com.example.metricsdemo.repository;

import com.example.metricsdemo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    boolean existsByEmail(String email);
}

Step 7: Create below Service class

package com.example.metricsdemo.service;

import com.example.metricsdemo.entity.Student;
import com.example.metricsdemo.repository.StudentRepository;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@Service
public class StudentService {

    private final StudentRepository studentRepository;
    private final MeterRegistry meterRegistry;

    // Custom application metrics
    private final Counter studentCreatedCounter;
    private final Counter studentDeletedCounter;
    private final Timer fetchAllTimer;

    public StudentService(StudentRepository studentRepository, MeterRegistry meterRegistry) {
        this.studentRepository = studentRepository;
        this.meterRegistry = meterRegistry;

        // Counter: a value that only goes up
        this.studentCreatedCounter = Counter.builder("student.created")
                .description("Number of students created since the application started")
                .register(meterRegistry);
        this.studentDeletedCounter = Counter.builder("student.deleted")
                .description("Number of students deleted since the application started")
                .register(meterRegistry);

        // Timer: how many times an operation ran and how long it took
        this.fetchAllTimer = Timer.builder("student.fetch.all")
                .description("Time taken to fetch all students")
                .register(meterRegistry);

        // Gauge: a value that can go up and down - read from the database each time it is requested
        Gauge.builder("student.count", studentRepository, StudentRepository::count)
                .description("Current number of students in the database")
                .register(meterRegistry);
    }

    public Student addStudent(Student student) {
        if (studentRepository.existsByEmail(student.getEmail())) {
            throw new ResponseStatusException(HttpStatus.CONFLICT,
                    "Email already registered: " + student.getEmail());
        }
        Student saved = studentRepository.save(student);
        studentCreatedCounter.increment();
        return saved;
    }

    public List<Student> getAllStudents() {
        Timer.Sample sample = Timer.start(meterRegistry);
        List<Student> students = studentRepository.findAll();
        sample.stop(fetchAllTimer);
        return students;
    }

    public Student getStudentById(Long id) {
        return studentRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
                        "Student not found with id: " + id));
    }

    public void deleteStudent(Long id) {
        if (!studentRepository.existsById(id)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,
                    "Student not found with id: " + id);
        }
        studentRepository.deleteById(id);
        studentDeletedCounter.increment();
    }
}

Step 8: Create Controller class

package com.example.metricsdemo.controller;

import com.example.metricsdemo.entity.Student;
import com.example.metricsdemo.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student addStudent(@RequestBody Student student) {
        return studentService.addStudent(student);
    }

    @GetMapping
    public List<Student> getAllStudents() {
        return studentService.getAllStudents();
    }

    @GetMapping("/{id}")
    public Student getStudentById(@PathVariable Long id) {
        return studentService.getStudentById(id);
    }

    @DeleteMapping("/{id}")
    public String deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
        return "Student deleted with id: " + id;
    }
}

Step 9: Main Application

package com.example.metricsdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MetricsDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MetricsDemoApplication.class, args);
    }
}

Step 10: Run the MetricsDemoApplication

Scroll to Top