google-site-verification: google61fe8ba583a51912.html
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