TP11.1 - Actuator Configuration
Objectives of the Lab
In this lab, you will:
- ✅ Configure Actuator for an e-commerce application
- ✅ Secure the endpoints
- ✅ Create custom health checks
- ✅ Add business metrics
- ✅ Create a custom endpoint
Estimated duration: 1h30
Context
You are configuring monitoring for an e-commerce application. The application must be ready for Kubernetes and monitoring with Prometheus.
Part 1: Basic Setup
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
application.properties
spring.application.name=ecommerce-app
# Database
spring.datasource.url=jdbc:h2:mem:testdb
spring.h2.console.enabled=true
# ===== ACTUATOR CONFIGURATION =====
# Expose necessary endpoints
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,env
# Health with details
management.endpoint.health.show-details=always
management.endpoint.health.show-components=always
# Kubernetes Probes
management.endpoint.health.probes.enabled=true
# Health groups
management.endpoint.health.group.liveness.include=ping,livenessState
management.endpoint.health.group.readiness.include=readinessState,db,diskSpace
# Info
info.app.name=${spring.application.name}
info.app.version=1.0.0
info.app.description=E-Commerce Application
info.contact.team=Backend Team
info.contact.email=backend@company.com
# Common tags for metrics
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=development
Part 2: Entities and Repositories
Product Entity
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private boolean active = true;
}
Order Entity
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerEmail;
private BigDecimal total;
@Enumerated(EnumType.STRING)
private OrderStatus status = OrderStatus.PENDING;
private LocalDateTime createdAt = LocalDateTime.now();
}
public enum OrderStatus {
PENDING, PROCESSING, COMPLETED, CANCELLED
}
Repositories
public interface ProductRepository extends JpaRepository<Product, Long> {
long countByActiveTrue();
long countByStockLessThan(int threshold);
}
public interface OrderRepository extends JpaRepository<Order, Long> {
long countByStatus(OrderStatus status);
long countByCreatedAtAfter(LocalDateTime date);
@Query("SELECT COALESCE(SUM(o.total), 0) FROM Order o WHERE o.status = 'COMPLETED'")
BigDecimal sumCompletedOrdersTotal();
}
Part 3: Custom Health Checks
InventoryHealthIndicator
@Component
@RequiredArgsConstructor
public class InventoryHealthIndicator implements HealthIndicator {
private final ProductRepository productRepository;
private static final int LOW_STOCK_THRESHOLD = 5;
private static final int CRITICAL_LOW_STOCK_COUNT = 10;
@Override
public Health health() {
long lowStockCount = productRepository.countByStockLessThan(LOW_STOCK_THRESHOLD);
long totalProducts = productRepository.countByActiveTrue();
Health.Builder builder = Health.up()
.withDetail("totalActiveProducts", totalProducts)
.withDetail("lowStockProducts", lowStockCount)
.withDetail("lowStockThreshold", LOW_STOCK_THRESHOLD);
if (lowStockCount >= CRITICAL_LOW_STOCK_COUNT) {
return builder
.status("WARNING")
.withDetail("message", "Too many products with low stock!")
.build();
}
return builder
.withDetail("message", "Inventory levels are healthy")
.build();
}
}
PaymentGatewayHealthIndicator
Simulate checking an external payment service:
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final Random random = new Random();
@Override
public Health health() {
// Simulation: 90% of the time UP
boolean isUp = random.nextInt(100) < 90;
long responseTime = 50 + random.nextInt(200); // 50-250ms
if (isUp) {
return Health.up()
.withDetail("gateway", "stripe")
.withDetail("responseTime", responseTime + "ms")
.withDetail("status", "connected")
.build();
} else {
return Health.down()
.withDetail("gateway", "stripe")
.withDetail("error", "Connection timeout")
.withDetail("lastAttempt", LocalDateTime.now())
.build();
}
}
}
Group Configuration
Update application.properties:
# Readiness group includes inventory and payment gateway
management.endpoint.health.group.readiness.include=readinessState,db,inventory,paymentGateway
Part 4: Business Metrics
BusinessMetrics Component
@Component
@RequiredArgsConstructor
@Slf4j
public class BusinessMetrics {
private final MeterRegistry meterRegistry;
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private Counter ordersCreated;
private Counter ordersCompleted;
private Counter ordersCancelled;
private Timer orderProcessingTime;
@PostConstruct
public void init() {
// Counters
ordersCreated = Counter.builder("business.orders.created")
.description("Total orders created")
.register(meterRegistry);
ordersCompleted = Counter.builder("business.orders.completed")
.description("Total orders completed")
.register(meterRegistry);
ordersCancelled = Counter.builder("business.orders.cancelled")
.description("Total orders cancelled")
.register(meterRegistry);
// Timer
orderProcessingTime = Timer.builder("business.orders.processing.time")
.description("Order processing time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
// Gauges based on the database
Gauge.builder("business.orders.pending", orderRepository,
repo -> repo.countByStatus(OrderStatus.PENDING))
.description("Current pending orders")
.register(meterRegistry);
Gauge.builder("business.products.low.stock", productRepository,
repo -> repo.countByStockLessThan(5))
.description("Products with low stock")
.register(meterRegistry);
Gauge.builder("business.revenue.total", orderRepository,
repo -> repo.sumCompletedOrdersTotal().doubleValue())
.description("Total revenue from completed orders")
.baseUnit("dollars")
.register(meterRegistry);
log.info("Business metrics initialized");
}
public void recordOrderCreated() {
ordersCreated.increment();
}
public void recordOrderCompleted() {
ordersCompleted.increment();
}
public void recordOrderCancelled() {
ordersCancelled.increment();
}
public void recordProcessingTime(Runnable operation) {
orderProcessingTime.record(operation);
}
}
OrderService with Metrics
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
private final BusinessMetrics businessMetrics;
public Order createOrder(String customerEmail, BigDecimal total) {
log.info("Creating order for customer: {}", customerEmail);
Order order = new Order();
order.setCustomerEmail(customerEmail);
order.setTotal(total);
order.setStatus(OrderStatus.PENDING);
Order saved = orderRepository.save(order);
businessMetrics.recordOrderCreated();
log.info("Order {} created", saved.getId());
return saved;
}
public Order completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
businessMetrics.recordProcessingTime(() -> {
// Simulate processing
try {
Thread.sleep(100 + new Random().nextInt(400));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
order.setStatus(OrderStatus.COMPLETED);
Order saved = orderRepository.save(order);
businessMetrics.recordOrderCompleted();
return saved;
}
}
Part 5: Custom Endpoint
StatsEndpoint
@Component
@Endpoint(id = "business-stats")
@RequiredArgsConstructor
public class BusinessStatsEndpoint {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
@ReadOperation
public Map<String, Object> getStats() {
LocalDateTime today = LocalDate.now().atStartOfDay();
return Map.of(
"orders", Map.of(
"pending", orderRepository.countByStatus(OrderStatus.PENDING),
"processing", orderRepository.countByStatus(OrderStatus.PROCESSING),
"completed", orderRepository.countByStatus(OrderStatus.COMPLETED),
"cancelled", orderRepository.countByStatus(OrderStatus.CANCELLED),
"today", orderRepository.countByCreatedAtAfter(today)
),
"products", Map.of(
"total", productRepository.count(),
"active", productRepository.countByActiveTrue(),
"lowStock", productRepository.countByStockLessThan(5)
),
"revenue", Map.of(
"total", orderRepository.sumCompletedOrdersTotal()
),
"timestamp", LocalDateTime.now()
);
}
}
Expose the endpoint:
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,business-stats
Part 6: Tests
Endpoint Testing
# General health
curl http://localhost:8080/actuator/health
# Liveness health (for K8s)
curl http://localhost:8080/actuator/health/liveness
# Readiness health (for K8s)
curl http://localhost:8080/actuator/health/readiness
# Application info
curl http://localhost:8080/actuator/info
# List of metrics
curl http://localhost:8080/actuator/metrics
# Specific metrics
curl http://localhost:8080/actuator/metrics/business.orders.pending
curl http://localhost:8080/actuator/metrics/jvm.memory.used
# Prometheus format
curl http://localhost:8080/actuator/prometheus
# Custom endpoint
curl http://localhost:8080/actuator/business-stats
Create Test Data
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final ProductRepository productRepository;
private final OrderService orderService;
@Override
public void run(String... args) {
// Products
productRepository.save(new Product(null, "Laptop", new BigDecimal("999.99"), 50, true));
productRepository.save(new Product(null, "Phone", new BigDecimal("699.99"), 3, true)); // Low stock
productRepository.save(new Product(null, "Tablet", new BigDecimal("499.99"), 2, true)); // Low stock
productRepository.save(new Product(null, "Headphones", new BigDecimal("199.99"), 100, true));
// Orders
orderService.createOrder("john@test.com", new BigDecimal("999.99"));
orderService.createOrder("jane@test.com", new BigDecimal("699.99"));
Order order = orderService.createOrder("bob@test.com", new BigDecimal("199.99"));
orderService.completeOrder(order.getId());
}
}
Part 7: Verification
Checklist
-
/actuatorlists all endpoints -
/actuator/healthshows UP with components -
/actuator/health/livenessreturns 200 -
/actuator/health/readinessincludes db, inventory, paymentGateway -
/actuator/infoshows app info -
/actuator/prometheusexposes metrics -
/actuator/business-statsreturns business stats -
business.*metrics visible in/actuator/metrics
Expected Response for /actuator/health
{
"status": "UP",
"components": {
"db": {"status": "UP", "details": {"database": "H2"}},
"diskSpace": {"status": "UP"},
"inventory": {
"status": "UP",
"details": {
"totalActiveProducts": 4,
"lowStockProducts": 2,
"message": "Inventory levels are healthy"
}
},
"paymentGateway": {
"status": "UP",
"details": {"gateway": "stripe", "responseTime": "125ms"}
},
"ping": {"status": "UP"}
}
}
🎯 Next Step
➡️ TP11.2: Prometheus & Grafana