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

الفصل 3: المقاييس مع Micrometer

مقدمة​

Micrometer هي مكتبة المقاييس في Spring Boot. توفر واجهة تتكيف مع أنظمة المراقبة المختلفة (Prometheus، Datadog، InfluxDB، إلخ).


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ فهم أنواع المقاييس
  2. ✅ استخدام المقاييس الأصلية
  3. ✅ إنشاء مقاييس مخصصة
  4. ✅ إضافة علامات للتصفية
  5. ✅ تكوين التصدير إلى Prometheus

1. مقدمة إلى Micrometer​

الهندسة المعمارية​

تطبيقك
↓
Micrometer (واجهة)
↓
┌─────┴─────┐
│ السجلات │
├────────────┤
│ Prometheus │
│ Datadog │
│ InfluxDB │
│ CloudWatch │
└────────────┘

تبعية Prometheus​

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

2. أنواع المقاييس​

Counter (عداد)​

قيمة لا تزيد إلا:

@Service
@RequiredArgsConstructor
public class OrderService {

private final Counter ordersCreated;

public OrderService(MeterRegistry registry) {
this.ordersCreated = Counter.builder("orders.created")
.description("Number of orders created")
.register(registry);
}

public Order createOrder(OrderRequest request) {
Order order = processOrder(request);
ordersCreated.increment(); // +1
return order;
}
}

الاستخدامات: عدد الطلبات، الأخطاء، الأحداث.


Gauge (مقياس)​

قيمة فورية يمكن أن ترتفع أو تنخفض:

@Component
public class QueueMetrics {

private final Queue<Task> taskQueue;

public QueueMetrics(MeterRegistry registry, Queue<Task> taskQueue) {
this.taskQueue = taskQueue;

Gauge.builder("tasks.queue.size", taskQueue, Queue::size)
.description("Current number of tasks in queue")
.register(registry);
}
}

الاستخدامات: الذاكرة المستخدمة، الاتصالات النشطة، حجم الطابور.


Timer (مؤقت)​

يقيس المدة وعدد المكالمات:

@Service
public class PaymentService {

private final Timer paymentTimer;

public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.process.time")
.description("Time to process payments")
.register(registry);
}

public Payment process(PaymentRequest request) {
return paymentTimer.record(() -> {
// يتم قياس الوقت تلقائيًا
return executePayment(request);
});
}
}

مع التعليق التوضيحي @Timed:

@Service
public class ProductService {

@Timed(value = "product.search.time", description = "Time to search products")
public List<Product> search(String query) {
return productRepository.search(query);
}
}

تفعيل @Timed:

@Configuration
public class MetricsConfig {

@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}

Summary (ملخص)​

إحصائيات التوزيع (النسب المئوية):

@Service
public class OrderService {

private final DistributionSummary orderAmount;

public OrderService(MeterRegistry registry) {
this.orderAmount = DistributionSummary.builder("order.amount")
.description("Order amounts")
.baseUnit("dollars")
.publishPercentiles(0.5, 0.95, 0.99) // الوسيط، P95، P99
.register(registry);
}

public Order createOrder(Order order) {
orderAmount.record(order.getTotal().doubleValue());
return orderRepository.save(order);
}
}

3. المقاييس الأصلية​

يقوم Spring Boot بتعريض العديد من المقاييس تلقائيًا:

مقاييس JVM​

# الذاكرة
curl http://localhost:8080/actuator/metrics/jvm.memory.used
curl http://localhost:8080/actuator/metrics/jvm.memory.max

# الخيوط
curl http://localhost:8080/actuator/metrics/jvm.threads.live

# جمع القمامة
curl http://localhost:8080/actuator/metrics/jvm.gc.pause

مقاييس HTTP​

# طلبات HTTP
curl http://localhost:8080/actuator/metrics/http.server.requests

# مع التصفية بواسطة العلامة
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/api/users"
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=status:200"

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

# HikariCP
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl http://localhost:8080/actuator/metrics/hikaricp.connections.idle
curl http://localhost:8080/actuator/metrics/hikaricp.connections.pending

القائمة الكاملة​

curl http://localhost:8080/actuator/metrics
{
"names": [
"jvm.memory.used",
"jvm.memory.max",
"jvm.threads.live",
"jvm.gc.pause",
"http.server.requests",
"hikaricp.connections.active",
"logback.events",
"process.cpu.usage",
"system.cpu.usage"
]
}

4. المقاييس المخصصة​

مع MeterRegistry​

@Service
@RequiredArgsConstructor
public class UserService {

private final MeterRegistry meterRegistry;
private final UserRepository userRepository;

public User register(UserRequest request) {
User user = createUser(request);

// زيادة عداد مع العلامات
meterRegistry.counter("users.registered",
"type", request.getType(),
"country", request.getCountry()
).increment();

return user;
}

public void login(String username) {
// مؤقت داخلي
meterRegistry.timer("user.login.time").record(() -> {
performLogin(username);
});

meterRegistry.counter("user.logins").increment();
}
}

مع @Counted و @Timed​

@Service
public class NotificationService {

@Counted(value = "notifications.sent", description = "Number of notifications sent")
public void sendNotification(Notification notification) {
// إرسال...
}

@Timed(value = "notifications.process.time", percentiles = {0.5, 0.95, 0.99})
public void processNotifications() {
// معالجة...
}
}

مثال كامل: مقاييس الأعمال​

@Component
@RequiredArgsConstructor
public class BusinessMetrics {

private final MeterRegistry registry;
private final OrderRepository orderRepository;

private Counter ordersCreated;
private Counter ordersFailed;
private Timer orderProcessingTime;
private AtomicInteger activeOrders;

@PostConstruct
public void init() {
// العدادات
ordersCreated = Counter.builder("business.orders.created")
.description("Total orders created")
.register(registry);

ordersFailed = Counter.builder("business.orders.failed")
.description("Total orders failed")
.register(registry);

// مؤقت
orderProcessingTime = Timer.builder("business.orders.processing.time")
.description("Order processing time")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);

// مقياس (قيمة ديناميكية)
activeOrders = new AtomicInteger(0);
Gauge.builder("business.orders.active", activeOrders, AtomicInteger::get)
.description("Currently active orders")
.register(registry);

// مقياس مبني على استعلام قاعدة بيانات
Gauge.builder("business.orders.pending", orderRepository,
repo -> repo.countByStatus(OrderStatus.PENDING))
.description("Pending orders in database")
.register(registry);
}

public void recordOrderCreated(String type) {
ordersCreated.increment();
registry.counter("business.orders.created.by.type", "type", type).increment();
}

public void recordOrderFailed(String reason) {
ordersFailed.increment();
registry.counter("business.orders.failed.by.reason", "reason", reason).increment();
}

public void recordProcessingTime(Runnable operation) {
orderProcessingTime.record(operation);
}

public void incrementActiveOrders() {
activeOrders.incrementAndGet();
}

public void decrementActiveOrders() {
activeOrders.decrementAndGet();
}
}

5. العلامات والأبعاد​

أهمية العلامات​

تسمح العلامات بتصفية وتجميع المقاييس:

// بدون علامات: قيمة واحدة عالمية
meterRegistry.counter("http.requests").increment();

// مع العلامات: قابل للتصفية حسب الطريقة، الحالة، uri
meterRegistry.counter("http.requests",
"method", "GET",
"status", "200",
"uri", "/api/users"
).increment();

العلامات المشتركة (العالمية)​

@Configuration
public class MetricsConfig {

@Bean
public MeterRegistryCustomizer<MeterRegistry> commonTags() {
return registry -> registry.config()
.commonTags(
"application", "my-app",
"environment", "production",
"region", "us-east-1"
);
}
}

ممارسات جيدة للعلامات​

// ✅ جيد: عدد قليل من القيم
meterRegistry.counter("orders", "status", "COMPLETED"); // ~5 قيم ممكنة
meterRegistry.counter("orders", "type", "STANDARD"); // ~3 قيم ممكنة

// ❌ سيء: عدد كبير من القيم (انفجار السلاسل)
meterRegistry.counter("orders", "orderId", order.getId()); // ملايين القيم!
meterRegistry.counter("users", "email", user.getEmail()); // فريد لكل مستخدم!

6. تصدير Prometheus​

التكوين​

# application.properties
management.endpoints.web.exposure.include=prometheus,health,metrics
management.prometheus.metrics.export.enabled=true

تنسيق Prometheus​

curl http://localhost:8080/actuator/prometheus
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 1.048576E7
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 2.097152E7

# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="GET",status="200",uri="/api/users",} 150.0
http_server_requests_seconds_sum{method="GET",status="200",uri="/api/users",} 12.5

# HELP orders_created_total Number of orders created
# TYPE orders_created_total counter
orders_created_total{type="STANDARD",} 1523.0
orders_created_total{type="EXPRESS",} 342.0

7. ملخص​

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

النوعالاستخدامالمثال
Counterعد الأحداثالطلبات، الأخطاء
Gaugeقيمة فوريةالذاكرة، الاتصالات النشطة
Timerالمدة + العدوقت الاستجابة
Summaryالتوزيعالنسب المئوية

إنشاء المقاييس​

// Counter
meterRegistry.counter("my.counter", "tag", "value").increment();

// Timer
meterRegistry.timer("my.timer").record(() -> operation());

// Gauge
Gauge.builder("my.gauge", obj, Object::getValue).register(registry);

التعليقات التوضيحية​

@Timed("method.time")
@Counted("method.calls")
public void myMethod() { }

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

المقاييس هي للقياس. ولكن كيف تعرف إذا كان التطبيق صحيًا؟ حان الوقت لفحوصات الصحة!

➡️ الفصل 4: فحوصات الصحة


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