Prometheus Exporter in Spring Boot in Java

Introduction to Prometheus and Spring Boot

Prometheus is a powerful open-source monitoring and alerting toolkit. When integrated with Spring Boot applications, it provides comprehensive metrics collection, monitoring, and alerting capabilities for microservices architectures.

Project Setup and Dependencies

Maven Dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Prometheus Registry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Micrometer Core -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<!-- For JVM metrics -->
<dependency>
<groupId>io.github.mweirauch</groupId>
<artifactId>micrometer-jvm-extras</artifactId>
<version>0.2.2</version>
</dependency>
<!-- For database metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- For cache metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- For testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Application Configuration

# application.properties
# Application
server.port=8080
spring.application.name=prometheus-exporter-demo
# Actuator Configuration
management.server.port=8081
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true
# Metrics Configuration
management.metrics.export.prometheus.enabled=true
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.enable.jvm=true
management.metrics.enable.logback=true
management.metrics.enable.system=true
# Custom metrics tags
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=development
# JPA (if using database)
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
# Logging (for logback metrics)
logging.level.com.example=INFO

Basic Prometheus Exporter Setup

Main Application Class

package com.example.prometheus;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class PrometheusExporterApplication {
public static void main(String[] args) {
SpringApplication.run(PrometheusExporterApplication.class, args);
}
}

Basic Configuration Class

package com.example.prometheus.config;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> {
registry.config().commonTags(
"application", "prometheus-exporter-demo",
"environment", "dev",
"region", "us-east-1"
);
};
}
@Bean
public MeterFilter renameUnhealthyTag() {
return MeterFilter.renameTag("http.server.requests", "outcome", "status");
}
@Bean
public MeterFilter denyHistogramPercentiles() {
return MeterFilter.denyNameStartsWith("jvm.gc.pause");
}
}

Custom Metrics Exporters

Business Metrics Exporter

package com.example.prometheus.metrics;
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.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class BusinessMetricsExporter {
private final MeterRegistry meterRegistry;
private final Counter ordersCreatedCounter;
private final Counter ordersFailedCounter;
private final Timer orderProcessingTimer;
private final AtomicInteger activeUsersGauge;
private final AtomicLong revenueCounter;
private final ConcurrentHashMap<String, AtomicInteger> productStockGauges;
public BusinessMetricsExporter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.productStockGauges = new ConcurrentHashMap<>();
this.activeUsersGauge = new AtomicInteger(0);
this.revenueCounter = new AtomicLong(0);
// Initialize counters
this.ordersCreatedCounter = Counter.builder("orders.created")
.description("Total number of orders created")
.tag("type", "business")
.register(meterRegistry);
this.ordersFailedCounter = Counter.builder("orders.failed")
.description("Total number of failed orders")
.tag("type", "business")
.register(meterRegistry);
// Initialize timer
this.orderProcessingTimer = Timer.builder("orders.processing.time")
.description("Time taken to process orders")
.tag("type", "business")
.publishPercentiles(0.5, 0.95, 0.99) // 50th, 95th, 99th percentiles
.register(meterRegistry);
// Initialize gauge for active users
Gauge.builder("users.active.count", activeUsersGauge, AtomicInteger::get)
.description("Number of currently active users")
.tag("type", "business")
.register(meterRegistry);
// Initialize revenue counter as gauge
Gauge.builder("revenue.total", revenueCounter, AtomicLong::get)
.description("Total revenue generated")
.tag("currency", "USD")
.register(meterRegistry);
}
public void incrementOrdersCreated(String customerType) {
ordersCreatedCounter.increment();
// Also increment with customer type tag
Counter.builder("orders.created")
.tag("customer_type", customerType)
.register(meterRegistry)
.increment();
}
public void incrementOrdersFailed(String failureReason) {
ordersFailedCounter.increment();
Counter.builder("orders.failed")
.tag("reason", failureReason)
.register(meterRegistry)
.increment();
}
public void recordOrderProcessingTime(long duration, TimeUnit unit) {
orderProcessingTimer.record(duration, unit);
}
public Timer.Sample startOrderProcessingTimer() {
return Timer.start(meterRegistry);
}
public void stopOrderProcessingTimer(Timer.Sample sample, String orderType) {
sample.stop(Timer.builder("orders.processing.time")
.tag("order_type", orderType)
.register(meterRegistry));
}
public void setActiveUsers(int count) {
activeUsersGauge.set(count);
}
public void incrementActiveUsers() {
activeUsersGauge.incrementAndGet();
}
public void decrementActiveUsers() {
activeUsersGauge.decrementAndGet();
}
public void addRevenue(double amount) {
revenueCounter.addAndGet((long) (amount * 100)); // Store as cents to avoid floating point
}
public void setProductStock(String productId, int stock) {
String gaugeName = "product.stock";
productStockGauges.computeIfAbsent(productId, id -> {
AtomicInteger gauge = new AtomicInteger(stock);
Gauge.builder(gaugeName, gauge, AtomicInteger::get)
.tag("product_id", id)
.description("Current stock for product")
.register(meterRegistry);
return gauge;
}).set(stock);
}
public void decrementProductStock(String productId) {
AtomicInteger gauge = productStockGauges.get(productId);
if (gauge != null) {
gauge.decrementAndGet();
}
}
public void removeProductStockGauge(String productId) {
AtomicInteger gauge = productStockGauges.remove(productId);
if (gauge != null) {
// Note: Gauges cannot be directly removed in Micrometer
// They will automatically disappear when garbage collected
}
}
}

JVM and System Metrics Exporter

package com.example.prometheus.metrics;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;
import java.lang.management.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class JvmMetricsExporter implements MeterBinder {
private final AtomicInteger threadCount = new AtomicInteger(0);
private final AtomicLong heapUsed = new AtomicLong(0);
private final AtomicLong heapMax = new AtomicLong(0);
private final AtomicInteger loadedClassCount = new AtomicInteger(0);
private final AtomicLong gcCount = new AtomicLong(0);
private final AtomicLong gcTime = new AtomicLong(0);
@Override
public void bindTo(MeterRegistry registry) {
// Thread metrics
Gauge.builder("jvm.threads.live", threadCount, AtomicInteger::get)
.description("Current live threads")
.baseUnit("threads")
.register(registry);
// Memory metrics
Gauge.builder("jvm.memory.heap.used", heapUsed, AtomicLong::get)
.description("Current heap memory used")
.baseUnit("bytes")
.register(registry);
Gauge.builder("jvm.memory.heap.max", heapMax, AtomicLong::get)
.description("Maximum heap memory")
.baseUnit("bytes")
.register(registry);
// Class loading metrics
Gauge.builder("jvm.classes.loaded", loadedClassCount, AtomicInteger::get)
.description("Currently loaded classes")
.baseUnit("classes")
.register(registry);
// GC metrics
Gauge.builder("jvm.gc.count", gcCount, AtomicLong::get)
.description("Total GC count")
.baseUnit("collections")
.register(registry);
Gauge.builder("jvm.gc.time", gcTime, AtomicLong::get)
.description("Total GC time")
.baseUnit("milliseconds")
.register(registry);
// Start monitoring thread
startMonitoringThread();
}
private void startMonitoringThread() {
Thread monitoringThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
updateMetrics();
Thread.sleep(5000); // Update every 5 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
monitoringThread.setDaemon(true);
monitoringThread.setName("jvm-metrics-monitor");
monitoringThread.start();
}
private void updateMetrics() {
// Thread metrics
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
threadCount.set(threadMXBean.getThreadCount());
// Memory metrics
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
heapUsed.set(heapUsage.getUsed());
heapMax.set(heapUsage.getMax());
// Class loading metrics
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
loadedClassCount.set(classLoadingMXBean.getLoadedClassCount());
// GC metrics
GarbageCollectorMXBean[] gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
long totalGcCount = 0;
long totalGcTime = 0;
for (GarbageCollectorMXBean gcBean : gcBeans) {
totalGcCount += gcBean.getCollectionCount();
totalGcTime += gcBean.getCollectionTime();
}
gcCount.set(totalGcCount);
gcTime.set(totalGcTime);
}
}

Database Metrics Exporter

package com.example.prometheus.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class DatabaseMetricsExporter {
private final MeterRegistry meterRegistry;
private final ConcurrentHashMap<String, Counter> queryCounters;
private final ConcurrentHashMap<String, Timer> queryTimers;
private final AtomicInteger activeConnections;
private final AtomicLong totalQueries;
private final AtomicInteger connectionPoolSize;
public DatabaseMetricsExporter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.queryCounters = new ConcurrentHashMap<>();
this.queryTimers = new ConcurrentHashMap<>();
this.activeConnections = new AtomicInteger(0);
this.totalQueries = new AtomicLong(0);
this.connectionPoolSize = new AtomicInteger(10); // Default pool size
initializeGauges();
}
@PostConstruct
public void init() {
// Initialize connection pool gauge
io.micrometer.core.instrument.Gauge.builder("db.connections.pool.size", 
connectionPoolSize, AtomicInteger::get)
.description("Database connection pool size")
.baseUnit("connections")
.register(meterRegistry);
// Initialize active connections gauge
io.micrometer.core.instrument.Gauge.builder("db.connections.active", 
activeConnections, AtomicInteger::get)
.description("Active database connections")
.baseUnit("connections")
.register(meterRegistry);
// Initialize total queries counter as gauge
io.micrometer.core.instrument.Gauge.builder("db.queries.total", 
totalQueries, AtomicLong::get)
.description("Total database queries executed")
.baseUnit("queries")
.register(meterRegistry);
}
private void initializeGauges() {
// Additional database-related gauges can be initialized here
}
public void recordQuery(String queryType, String table, long duration, TimeUnit unit) {
// Increment total queries
totalQueries.incrementAndGet();
// Record query count
String counterKey = queryType + "." + table;
Counter counter = queryCounters.computeIfAbsent(counterKey, key -> 
Counter.builder("db.queries")
.tag("type", queryType)
.tag("table", table)
.description("Database queries by type and table")
.register(meterRegistry)
);
counter.increment();
// Record query duration
String timerKey = queryType + "." + table;
Timer timer = queryTimers.computeIfAbsent(timerKey, key -> 
Timer.builder("db.queries.duration")
.tag("type", queryType)
.tag("table", table)
.description("Database query execution time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
);
timer.record(duration, unit);
}
public Timer.Sample startQueryTimer() {
return Timer.start(meterRegistry);
}
public void stopQueryTimer(Timer.Sample sample, String queryType, String table) {
sample.stop(Timer.builder("db.queries.duration")
.tag("type", queryType)
.tag("table", table)
.register(meterRegistry));
}
public void incrementActiveConnections() {
activeConnections.incrementAndGet();
}
public void decrementActiveConnections() {
activeConnections.decrementAndGet();
}
public void setConnectionPoolSize(int size) {
connectionPoolSize.set(size);
}
public void recordFailedQuery(String queryType, String table, String error) {
Counter.builder("db.queries.failed")
.tag("type", queryType)
.tag("table", table)
.tag("error", error)
.description("Failed database queries")
.register(meterRegistry)
.increment();
}
public void recordSlowQuery(String queryType, String table, long thresholdMs) {
Counter.builder("db.queries.slow")
.tag("type", queryType)
.tag("table", table)
.tag("threshold_ms", String.valueOf(thresholdMs))
.description("Slow database queries")
.register(meterRegistry)
.increment();
}
}

REST Controller with Metrics Integration

Order Controller with Metrics

package com.example.prometheus.controller;
import com.example.prometheus.metrics.BusinessMetricsExporter;
import com.example.prometheus.metrics.DatabaseMetricsExporter;
import io.micrometer.core.instrument.Timer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private BusinessMetricsExporter businessMetrics;
@Autowired
private DatabaseMetricsExporter databaseMetrics;
private final Random random = new Random();
@PostMapping
public ResponseEntity<Map<String, Object>> createOrder(@RequestBody OrderRequest request) {
Timer.Sample orderTimer = businessMetrics.startOrderProcessingTimer();
Timer.Sample dbTimer = databaseMetrics.startQueryTimer();
try {
// Simulate database operation
simulateDatabaseOperation("INSERT", "orders");
// Simulate business logic processing time
simulateProcessingDelay();
// Record successful order
businessMetrics.incrementOrdersCreated(request.getCustomerType());
businessMetrics.addRevenue(request.getAmount());
// Update product stock
request.getItems().forEach(item -> 
businessMetrics.decrementProductStock(item.getProductId()));
Map<String, Object> response = new HashMap<>();
response.put("orderId", generateOrderId());
response.put("status", "created");
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(response);
} catch (Exception e) {
// Record failed order
businessMetrics.incrementOrdersFailed("processing_error");
databaseMetrics.recordFailedQuery("INSERT", "orders", e.getMessage());
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "Order creation failed");
errorResponse.put("message", e.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
} finally {
// Stop timers
businessMetrics.stopOrderProcessingTimer(orderTimer, "standard");
databaseMetrics.stopQueryTimer(dbTimer, "INSERT", "orders");
}
}
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getOrder(@PathVariable String id) {
Timer.Sample dbTimer = databaseMetrics.startQueryTimer();
try {
// Simulate database query
simulateDatabaseOperation("SELECT", "orders");
Map<String, Object> order = new HashMap<>();
order.put("id", id);
order.put("status", "completed");
order.put("amount", 99.99);
return ResponseEntity.ok(order);
} finally {
databaseMetrics.stopQueryTimer(dbTimer, "SELECT", "orders");
}
}
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getOrderStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalOrders", 1500);
stats.put("activeOrders", 25);
stats.put("revenue", 75000.0);
return ResponseEntity.ok(stats);
}
@PostMapping("/bulk")
public ResponseEntity<Map<String, Object>> createBulkOrders(@RequestBody BulkOrderRequest request) {
Timer.Sample bulkTimer = businessMetrics.startOrderProcessingTimer();
try {
int successCount = 0;
int failureCount = 0;
for (int i = 0; i < request.getCount(); i++) {
try {
simulateDatabaseOperation("INSERT", "orders");
businessMetrics.incrementOrdersCreated("bulk");
successCount++;
} catch (Exception e) {
businessMetrics.incrementOrdersFailed("bulk_error");
failureCount++;
}
}
Map<String, Object> response = new HashMap<>();
response.put("successCount", successCount);
response.put("failureCount", failureCount);
response.put("totalProcessed", request.getCount());
return ResponseEntity.ok(response);
} finally {
businessMetrics.stopOrderProcessingTimer(bulkTimer, "bulk");
}
}
private void simulateDatabaseOperation(String operation, String table) {
long startTime = System.currentTimeMillis();
try {
// Simulate database operation delay
Thread.sleep(random.nextInt(50) + 10); // 10-60ms
// Randomly simulate database errors (5% chance)
if (random.nextDouble() < 0.05) {
throw new RuntimeException("Database connection timeout");
}
long duration = System.currentTimeMillis() - startTime;
databaseMetrics.recordQuery(operation, table, duration, TimeUnit.MILLISECONDS);
// Record slow queries (over 30ms)
if (duration > 30) {
databaseMetrics.recordSlowQuery(operation, table, 30);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Database operation interrupted", e);
}
}
private void simulateProcessingDelay() throws InterruptedException {
Thread.sleep(random.nextInt(100) + 50); // 50-150ms
}
private String generateOrderId() {
return "ORD-" + System.currentTimeMillis() + "-" + random.nextInt(1000);
}
// DTO classes
public static class OrderRequest {
private String customerType;
private double amount;
private java.util.List<OrderItem> items;
// Getters and setters
public String getCustomerType() { return customerType; }
public void setCustomerType(String customerType) { this.customerType = customerType; }
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public java.util.List<OrderItem> getItems() { return items; }
public void setItems(java.util.List<OrderItem> items) { this.items = items; }
}
public static class OrderItem {
private String productId;
private int quantity;
// Getters and setters
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
public static class BulkOrderRequest {
private int count;
// Getters and setters
public int getCount() { return count; }
public void setCount(int count) { this.count = count; }
}
}

Scheduled Metrics Updates

Scheduled Metrics Collector

package com.example.prometheus.scheduler;
import com.example.prometheus.metrics.BusinessMetricsExporter;
import com.example.prometheus.metrics.DatabaseMetricsExporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class MetricsScheduler {
@Autowired
private BusinessMetricsExporter businessMetrics;
@Autowired
private DatabaseMetricsExporter databaseMetrics;
private final Random random = new Random();
private int simulatedActiveUsers = 100;
/**
* Update active users count every 30 seconds
*/
@Scheduled(fixedRate = 30000)
public void updateActiveUsers() {
// Simulate user activity fluctuations
int change = random.nextInt(21) - 10; // -10 to +10
simulatedActiveUsers = Math.max(50, simulatedActiveUsers + change);
businessMetrics.setActiveUsers(simulatedActiveUsers);
}
/**
* Simulate random orders every minute
*/
@Scheduled(fixedRate = 60000)
public void simulateOrderCreation() {
int ordersToCreate = random.nextInt(10) + 1; // 1-10 orders
for (int i = 0; i < ordersToCreate; i++) {
String customerType = random.nextBoolean() ? "premium" : "standard";
businessMetrics.incrementOrdersCreated(customerType);
// Simulate order processing time
long processingTime = random.nextInt(200) + 50; // 50-250ms
businessMetrics.recordOrderProcessingTime(processingTime, java.util.concurrent.TimeUnit.MILLISECONDS);
// Add random revenue
double revenue = 10 + (random.nextDouble() * 90); // $10-$100
businessMetrics.addRevenue(revenue);
}
}
/**
* Update database connection metrics every 15 seconds
*/
@Scheduled(fixedRate = 15000)
public void updateDatabaseMetrics() {
// Simulate connection pool changes
int poolSize = 10 + random.nextInt(11); // 10-20 connections
databaseMetrics.setConnectionPoolSize(poolSize);
// Simulate active connections (40-80% of pool size)
int activeConnections = (int) (poolSize * (0.4 + (random.nextDouble() * 0.4)));
// Update active connections (this is simplified - in real app, get from connection pool)
for (int i = 0; i < activeConnections; i++) {
databaseMetrics.incrementActiveConnections();
}
}
/**
* Update product stock metrics every 2 minutes
*/
@Scheduled(fixedRate = 120000)
public void updateProductStock() {
// Update stock for sample products
String[] productIds = {"prod-001", "prod-002", "prod-003", "prod-004", "prod-005"};
for (String productId : productIds) {
int stock = random.nextInt(100) + 10; // 10-109 in stock
businessMetrics.setProductStock(productId, stock);
}
}
/**
* Clean up old metrics every 10 minutes
*/
@Scheduled(fixedRate = 600000)
public void cleanupOldMetrics() {
// In a real application, you might want to clean up old gauges
// or perform other maintenance tasks
}
}

Custom Health Indicators

Database Health Indicator

package com.example.prometheus.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Random;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final Random random = new Random();
@Override
public Health health() {
try {
// Simulate database health check
boolean isHealthy = random.nextDouble() > 0.1; // 90% healthy
if (isHealthy) {
return Health.up()
.withDetail("database", "H2")
.withDetail("status", "connected")
.withDetail("response_time", random.nextInt(50) + 10 + "ms")
.build();
} else {
return Health.down()
.withDetail("database", "H2")
.withDetail("status", "disconnected")
.withDetail("error", "Connection timeout")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}

Custom Business Health Indicator

package com.example.prometheus.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class BusinessServiceHealthIndicator implements HealthIndicator {
private final Random random = new Random();
@Override
public Health health() {
try {
// Simulate various business service health checks
boolean orderServiceHealthy = random.nextDouble() > 0.05; // 95% healthy
boolean paymentServiceHealthy = random.nextDouble() > 0.02; // 98% healthy
boolean inventoryServiceHealthy = random.nextDouble() > 0.08; // 92% healthy
Health.Builder healthBuilder;
if (orderServiceHealthy && paymentServiceHealthy && inventoryServiceHealthy) {
healthBuilder = Health.up();
} else {
healthBuilder = Health.down();
}
return healthBuilder
.withDetail("order_service", orderServiceHealthy ? "UP" : "DOWN")
.withDetail("payment_service", paymentServiceHealthy ? "UP" : "DOWN")
.withDetail("inventory_service", inventoryServiceHealthy ? "UP" : "DOWN")
.withDetail("timestamp", System.currentTimeMillis())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}

Prometheus Configuration

prometheus.yml

# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
scrape_timeout: 5s
static_configs:
- targets: ['localhost:8081']
labels:
application: 'prometheus-exporter-demo'
environment: 'development'
instance: 'app-instance-1'
- job_name: 'spring-boot-app-business'
metrics_path: '/actuator/prometheus'
scrape_interval: 5s
static_configs:
- targets: ['localhost:8081']
metric_relabel_configs:
- source_labels: [__name__]
regex: '(orders_.*|revenue_.*|users_.*)'
action: keep
- job_name: 'spring-boot-app-system'
metrics_path: '/actuator/prometheus'
scrape_interval: 30s
static_configs:
- targets: ['localhost:8081']
metric_relabel_configs:
- source_labels: [__name__]
regex: '(jvm_.*|system_.*|process_.*)'
action: keep
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093

Alert Rules (Optional)

# alerts.yml
groups:
- name: spring-boot-app-alerts
rules:
- alert: HighErrorRate
expr: rate(orders_failed_total[5m]) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "High order failure rate"
description: "Order failure rate is {{ $value }} per second"
- alert: ServiceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.instance }} is down"
description: "Service has been down for more than 1 minute."
- alert: HighMemoryUsage
expr: (jvm_memory_used_bytes / jvm_memory_max_bytes) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is at {{ $value | humanizePercentage }}"
- alert: SlowDatabaseQueries
expr: rate(db_queries_slow_total[5m]) > 0.5
for: 2m
labels:
severity: warning
annotations:
summary: "High number of slow database queries"
description: "Slow query rate is {{ $value }} per second"

Testing the Exporter

Test Controller

package com.example.prometheus.controller;
import com.example.prometheus.metrics.BusinessMetricsExporter;
import com.example.prometheus.metrics.DatabaseMetricsExporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired
private BusinessMetricsExporter businessMetrics;
@Autowired
private DatabaseMetricsExporter databaseMetrics;
@PostMapping("/metrics/order")
public Map<String, Object> testOrderMetrics() {
businessMetrics.incrementOrdersCreated("test");
businessMetrics.addRevenue(99.99);
businessMetrics.incrementActiveUsers();
Map<String, Object> response = new HashMap<>();
response.put("status", "metrics updated");
response.put("action", "order_created");
return response;
}
@PostMapping("/metrics/error")
public Map<String, Object> testErrorMetrics() {
businessMetrics.incrementOrdersFailed("test_error");
databaseMetrics.recordFailedQuery("SELECT", "test_table", "test_error");
Map<String, Object> response = new HashMap<>();
response.put("status", "error metrics updated");
response.put("action", "error_simulated");
return response;
}
@PostMapping("/metrics/database")
public Map<String, Object> testDatabaseMetrics() {
databaseMetrics.incrementActiveConnections();
databaseMetrics.recordQuery("SELECT", "users", 45, java.util.concurrent.TimeUnit.MILLISECONDS);
databaseMetrics.recordSlowQuery("UPDATE", "orders", 100);
Map<String, Object> response = new HashMap<>();
response.put("status", "database metrics updated");
response.put("action", "database_operations");
return response;
}
@GetMapping("/metrics/status")
public Map<String, Object> getMetricsStatus() {
Map<String, Object> status = new HashMap<>();
status.put("application", "prometheus-exporter-demo");
status.put("status", "running");
status.put("timestamp", System.currentTimeMillis());
status.put("endpoints", new String[] {
"/actuator/prometheus",
"/actuator/health",
"/actuator/metrics"
});
return status;
}
}

Integration Test

package com.example.prometheus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class PrometheusExporterApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
void testPrometheusEndpoint() throws Exception {
mockMvc.perform(get("/actuator/prometheus"))
.andExpect(status().isOk())
.andExpect(content().contentType("text/plain;version=0.0.4;charset=utf-8"))
.andExpect(content().string(org.hamcrest.Matchers.containsString("jvm_memory_used_bytes")));
}
@Test
void testHealthEndpoint() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").exists());
}
@Test
void testMetricsEndpoint() throws Exception {
mockMvc.perform(get("/actuator/metrics"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.names").isArray());
}
}

Usage Examples

Accessing Metrics

# View all metrics in Prometheus format
curl http://localhost:8081/actuator/prometheus
# View specific metric
curl http://localhost:8081/actuator/metrics/orders.created
# View health status
curl http://localhost:8081/actuator/health
# View metrics names
curl http://localhost:8081/actuator/metrics

Sample Queries for Prometheus

# Total orders created
sum(orders_created_total)
# Order creation rate per minute
rate(orders_created_total[1m])
# Error rate percentage
(rate(orders_failed_total[5m]) / rate(orders_created_total[5m])) * 100
# 95th percentile of order processing time
histogram_quantile(0.95, rate(orders_processing_time_seconds_bucket[5m]))
# Memory usage percentage
(jvm_memory_used_bytes / jvm_memory_max_bytes) * 100
# Active database connections
db_connections_active
# Slow query rate
rate(db_queries_slow_total[5m])

This comprehensive Prometheus exporter implementation provides robust monitoring capabilities for Spring Boot applications, including business metrics, system metrics, database metrics, and custom health checks. The solution is production-ready and can be easily extended for specific monitoring requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper