انتقل إلى المحتوى الرئيسي

TP11.1 - إعداد Actuator

أهداف التمرين​

في هذا التمرين، سوف تقوم بـ:

  1. ✅ إعداد Actuator لتطبيق التجارة الإلكترونية
  2. ✅ تأمين النقاط النهائية
  3. ✅ إنشاء فحوصات صحية مخصصة
  4. ✅ إضافة مقاييس الأعمال
  5. ✅ إنشاء نقطة نهاية مخصصة

المدة المقدرة: 1 ساعة و30 دقيقة


السياق​

أنت تقوم بإعداد المراقبة لتطبيق التجارة الإلكترونية. يجب أن يكون التطبيق جاهزًا لـ Kubernetes والمراقبة باستخدام Prometheus.


الجزء 1: الإعداد الأساسي​

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 =====

# Exposer les endpoints nécessaires
management.endpoints.web.exposure.include=health,info,metrics,prometheus,loggers,env

# Health avec détails
management.endpoint.health.show-details=always
management.endpoint.health.show-components=always

# Probes Kubernetes
management.endpoint.health.probes.enabled=true

# Groupes de santé
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 communs pour les métriques
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=development

الجزء 2: الكيانات والمستودعات​

كيان المنتج​

@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;
}

كيان الطلب​

@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
}

المستودعات​

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();
}

الجزء 3: الفحوصات الصحية المخصصة​

مؤشر الصحة للمخزون​

@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();
}
}

مؤشر الصحة لبوابة الدفع​

محاكاة التحقق من خدمة دفع خارجية:

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

private final Random random = new Random();

@Override
public Health health() {
// Simulation: 90% du temps 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();
}
}
}

إعداد المجموعات​

قم بتحديث application.properties:

# Groupe readiness inclut l'inventaire et le payment gateway
management.endpoint.health.group.readiness.include=readinessState,db,inventory,paymentGateway

الجزء 4: مقاييس الأعمال​

مكون مقاييس الأعمال​

@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 basés sur 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);
}
}

خدمة الطلبات مع المقاييس​

@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(() -> {
// Simuler le traitement
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;
}
}

الجزء 5: نقطة نهاية مخصصة​

Endpoint الإحصائيات​

@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()
);
}
}

عرض نقطة النهاية:

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

الجزء 6: الاختبارات​

اختبار النقاط النهائية​

# Health général
curl http://localhost:8080/actuator/health

# Health liveness (pour K8s)
curl http://localhost:8080/actuator/health/liveness

# Health readiness (pour K8s)
curl http://localhost:8080/actuator/health/readiness

# Info application
curl http://localhost:8080/actuator/info

# Liste des métriques
curl http://localhost:8080/actuator/metrics

# Métriques spécifiques
curl http://localhost:8080/actuator/metrics/business.orders.pending
curl http://localhost:8080/actuator/metrics/jvm.memory.used

# Format Prometheus
curl http://localhost:8080/actuator/prometheus

# Endpoint personnalisé
curl http://localhost:8080/actuator/business-stats

إنشاء بيانات اختبار​

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

private final ProductRepository productRepository;
private final OrderService orderService;

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

// Commandes
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());
}
}

الجزء 7: التحقق​

قائمة التحقق​

  • /actuator يعرض جميع النقاط النهائية
  • /actuator/health يظهر UP مع المكونات
  • /actuator/health/liveness يعيد 200
  • /actuator/health/readiness يشمل db، inventory، paymentGateway
  • /actuator/info يظهر معلومات التطبيق
  • /actuator/prometheus يعرض المقاييس
  • /actuator/business-stats يعيد إحصائيات الأعمال
  • مقاييس business.* مرئية في /actuator/metrics

الاستجابة المتوقعة لـ /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"}
}
}

🎯 الخطوة التالية​

➡️ TP11.2: Prometheus & Grafana


⬅️ العودة إلى الوحدة 11