Pular para o conteúdo principal

TP11.1 - Configuração do Actuator

Objetivos do TP​

Neste TP, você irá:

  1. ✅ Configurar o Actuator para uma aplicação de e-commerce
  2. ✅ Proteger os endpoints
  3. ✅ Criar health checks personalizados
  4. ✅ Adicionar métricas de negócio
  5. ✅ Criar um endpoint personalizado

Duração estimada: 1h30


Contexto​

Você está configurando o monitoramento para uma aplicação de e-commerce. A aplicação deve estar pronta para Kubernetes e monitoramento com 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 =====

# Expor os endpoints necessários
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,env

# Health com detalhes
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 comuns para as métricas
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=development

Parte 2: Entidades e Repositórios​

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
}

Repositórios​

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 a verificação de um serviço de pagamento externo:

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

private final Random random = new Random();

@Override
public Health health() {
// Simulação: 90% do tempo 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();
}
}
}

Configuração dos Grupos​

Atualize o application.properties:

# Grupo readiness inclui inventário e payment gateway
management.endpoint.health.group.readiness.include=readinessState,db,inventory,paymentGateway

Parte 4: Métricas de Negócio​

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 baseados no banco de dados
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 com 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 o processamento
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()
);
}
}

Exponha o endpoint:

management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,business-stats

Parte 6: Testes​

Testando os Endpoints​

# Health geral
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 da aplicação
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

Criar Dados de Teste​

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

private final ProductRepository productRepository;
private final OrderService orderService;

@Override
public void run(String... args) {
// Produtos
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));

// Pedidos
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: Verificação​

Checklist​

  • /actuator lista todos os endpoints
  • /actuator/health mostra UP com components
  • /actuator/health/liveness retorna 200
  • /actuator/health/readiness inclui db, inventory, paymentGateway
  • /actuator/info mostra as informações do app
  • /actuator/prometheus expõe as métricas
  • /actuator/business-stats retorna as estatísticas de negócio
  • Métricas business.* visíveis em /actuator/metrics

Resposta 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 Passo​

➡️ TP11.2: Prometheus & Grafana


⬅️ Voltar ao Módulo 11