TP11.1 - Configuración de Actuator
Objetivos del TP
En este TP, usted va a:
- ✅ Configurar Actuator para una aplicación de e-commerce
- ✅ Proteger los endpoints
- ✅ Crear health checks personalizados
- ✅ Añadir métricas de negocio
- ✅ Crear un endpoint personalizado
Duración estimada: 1h30
Contexto
Usted configura el monitoreo para una aplicación de e-commerce. La aplicación debe estar lista para Kubernetes y el monitoreo con Prometheus.
Parte 1: Setup Básico
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 =====
# Exponer los endpoints necesarios
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,env
# Health con detalles
management.endpoint.health.show-details=always
management.endpoint.health.show-components=always
# Probes Kubernetes
management.endpoint.health.probes.enabled=true
# Grupos de health
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
# Tags comunes para las métricas
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=development
Parte 2: Entidades y Repositorios
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();
}
Parte 3: Health Checks Personalizados
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
Simular la verificación de un servicio de pago externo:
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final Random random = new Random();
@Override
public Health health() {
// Simulación: 90% del tiempo 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();
}
}
}
Configuración de Grupos
Actualice application.properties:
# Grupo readiness incluye inventario y payment gateway
management.endpoint.health.group.readiness.include=readinessState,db,inventory,paymentGateway
Parte 4: Métricas de Negocio
Componente BusinessMetrics
@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 basados en la BDD
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 con Métricas
@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(() -> {
// Simular el procesamiento
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;
}
}
Parte 5: Endpoint Personalizado
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()
);
}
}
Exponer el endpoint:
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,business-stats
Parte 6: Pruebas
Prueba de los Endpoints
# Health general
curl http://localhost:8080/actuator/health
# Health liveness (para K8s)
curl http://localhost:8080/actuator/health/liveness
# Health readiness (para K8s)
curl http://localhost:8080/actuator/health/readiness
# Info aplicación
curl http://localhost:8080/actuator/info
# Lista de métricas
curl http://localhost:8080/actuator/metrics
# Métricas específicas
curl http://localhost:8080/actuator/metrics/business.orders.pending
curl http://localhost:8080/actuator/metrics/jvm.memory.used
# Formato Prometheus
curl http://localhost:8080/actuator/prometheus
# Endpoint personalizado
curl http://localhost:8080/actuator/business-stats
Crear Datos de Prueba
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final ProductRepository productRepository;
private final OrderService orderService;
@Override
public void run(String... args) {
// Productos
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));
// Órdenes
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());
}
}
Parte 7: Verificación
Lista de Verificación
-
/actuatorlista todos los endpoints -
/actuator/healthmuestra UP con componentes -
/actuator/health/livenessretorna 200 -
/actuator/health/readinessincluye db, inventory, paymentGateway -
/actuator/infomuestra la información de la app -
/actuator/prometheusexpone las métricas -
/actuator/business-statsretorna las estadísticas de negocio - Métricas
business.*visibles en/actuator/metrics
Respuesta Esperada para /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"}
}
}
🎯 Próximo Paso
➡️ TP11.2: Prometheus & Grafana