Custom Health Indicator
Giriş
Bir hastanenin acil servis tabelasını düşünün. "Açık" yazıyorsa hastaneye girebilirsiniz, "Kapalı" yazıyorsa başka hastane aramanız gerekir. Ama bu tabela sadece hastanenin binası hakkında bilgi verir — içerideki röntgen cihazı, laboratuvar veya ameliyathane çalışıyor mu? Bunu bilmek için her departmanın kendi "durum tabelası" olmalı. Spring Boot Actuator'daki Custom Health Indicator tam olarak budur — uygulamanızın bağımlı olduğu her bileşenin sağlık durumunu /actuator/health çıktısına dahil etmenizi sağlar.
Spring Boot Actuator yerleşik olarak veritabanı, disk alanı, Redis, Elasticsearch gibi bileşenler için sağlık kontrolü sunar. Ancak gerçek dünyada uygulamalarınız bunların ötesinde harici servislere de bağımlıdır: bir ödeme gateway'i, bir e-posta servisi, bir SMS API, bir dosya depolama sistemi, bir CRM, bir ERP. Bu harici bağımlılıkların sağlık durumunu izlemek için custom health indicator yazarsınız.
Bu derste HealthIndicator arayüzünü, Health builder pattern'ını, CompositeHealthContributor ile gruplamayı, health group'ları ve Kubernetes probe entegrasyonunu, InfoContributor ile uygulama bilgisi eklemeyi ve production best practice'lerini derinlemesine öğreneceğiz.
HealthIndicator Arayüzü
Spring Boot, sağlık kontrolü mekanizmasını HealthIndicator fonksiyonel arayüzü üzerinden soyutlar:
@FunctionalInterface
public interface HealthIndicator {
Health health();
}Health nesnesi builder pattern ile oluşturulur ve dört temel durumdan birini taşır:
// UP — bileşen sağlıklı, her şey yolunda
Health.up()
.withDetail("version", "2.4.1")
.withDetail("responseTime", "45ms")
.build();
// DOWN — bileşen çalışmıyor, kritik sorun
Health.down()
.withDetail("error", "Connection refused")
.withException(exception) // Stack trace eklenir
.build();
// OUT_OF_SERVICE — geçici olarak hizmet dışı (planlı bakım vb.)
Health.outOfService()
.withDetail("reason", "Scheduled maintenance until 03:00 UTC")
.build();
// UNKNOWN — durum belirlenemiyor
Health.unknown()
.withDetail("reason", "Timeout after 5000ms — unable to determine status")
.build();İlk Custom Health Indicator
Diyelim ki uygulamanız bir harici ödeme API'sine bağımlı. Bu API'nin erişilebilir olup olmadığını sağlık kontrolüne dahil edelim:
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final RestClient restClient;
public PaymentGatewayHealthIndicator(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("https://api.payment-provider.com")
.build();
}
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
// Basit bir ping/health endpoint'i çağır
restClient.get()
.uri("/health")
.retrieve()
.toBodilessEntity();
long duration = System.currentTimeMillis() - start;
// Yanıt süresi 1 saniyeyi aşarsa uyarı
if (duration > 1000) {
return Health.up()
.withDetail("provider", "PaymentProvider")
.withDetail("responseTime", duration + "ms")
.withDetail("warning", "Response time is above threshold")
.build();
}
return Health.up()
.withDetail("provider", "PaymentProvider")
.withDetail("responseTime", duration + "ms")
.build();
} catch (Exception ex) {
return Health.down()
.withDetail("provider", "PaymentProvider")
.withDetail("error", ex.getMessage())
.withException(ex)
.build();
}
}
}Adlandırma Kuralı
Bean adındaki HealthIndicator son eki otomatik olarak kaldırılır:
PaymentGatewayHealthIndicator→/healthçıktısındapaymentGatewayanahtarıEmailServiceHealthIndicator→emailServiceanahtarıSmsApiHealthIndicator→smsApianahtarı
GET /actuator/health
{
"status": "UP",
"components": {
"paymentGateway": {
"status": "UP",
"details": {
"provider": "PaymentProvider",
"responseTime": "45ms"
}
},
"db": { "status": "UP" },
"diskSpace": { "status": "UP" }
}
}Detaylı bilginin görünmesi için:
management.endpoint.health.show-details=always
# Seçenekler: never (varsayılan), when-authorized, alwaysFarklı Senaryo Örnekleri
E-posta Servisi Health Indicator
@Component
public class EmailServiceHealthIndicator implements HealthIndicator {
private final JavaMailSender mailSender;
@Override
public Health health() {
try {
// SMTP bağlantısını test et
((JavaMailSenderImpl) mailSender).testConnection();
return Health.up()
.withDetail("smtpHost", ((JavaMailSenderImpl) mailSender).getHost())
.withDetail("smtpPort", ((JavaMailSenderImpl) mailSender).getPort())
.build();
} catch (MessagingException e) {
return Health.down()
.withDetail("error", "SMTP connection failed: " + e.getMessage())
.build();
}
}
}Disk Alanı Kontrolü (Özelleştirilmiş)
@Component
public class StorageHealthIndicator implements HealthIndicator {
@Value("${app.storage.path:/data/uploads}")
private String storagePath;
@Value("${app.storage.threshold-gb:5}")
private long thresholdGb;
@Override
public Health health() {
File storage = new File(storagePath);
if (!storage.exists()) {
return Health.down()
.withDetail("path", storagePath)
.withDetail("error", "Storage path does not exist")
.build();
}
long freeSpaceGb = storage.getFreeSpace() / (1024 * 1024 * 1024);
long totalSpaceGb = storage.getTotalSpace() / (1024 * 1024 * 1024);
long usedPercent = ((totalSpaceGb - freeSpaceGb) * 100) / totalSpaceGb;
Health.Builder builder = (freeSpaceGb < thresholdGb) ? Health.down() : Health.up();
return builder
.withDetail("path", storagePath)
.withDetail("totalSpaceGb", totalSpaceGb)
.withDetail("freeSpaceGb", freeSpaceGb)
.withDetail("usedPercent", usedPercent + "%")
.withDetail("threshold", thresholdGb + " GB")
.build();
}
}Dış API Bağlantı Kontrolü
@Component
public class CrmApiHealthIndicator implements HealthIndicator {
private final RestClient crmClient;
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
ResponseEntity<Void> response = crmClient.get()
.uri("/api/v1/ping")
.retrieve()
.toBodilessEntity();
long duration = System.currentTimeMillis() - start;
return Health.up()
.withDetail("endpoint", "CRM API")
.withDetail("statusCode", response.getStatusCode().value())
.withDetail("responseTime", duration + "ms")
.build();
} catch (RestClientResponseException e) {
return Health.down()
.withDetail("endpoint", "CRM API")
.withDetail("statusCode", e.getStatusCode().value())
.withDetail("error", e.getStatusText())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("endpoint", "CRM API")
.withDetail("error", e.getMessage())
.build();
}
}
}CompositeHealthContributor — Alt Bileşen Gruplama
Bazı durumlarda tek bir mantıksal bileşen birden fazla alt kontrolden oluşur. Örneğin "Messaging" bileşeni hem RabbitMQ hem Kafka sağlığını kapsayabilir:
@Component("messaging")
public class MessagingHealthContributor implements CompositeHealthContributor {
private final Map<String, HealthContributor> contributors;
public MessagingHealthContributor(
RabbitHealthIndicator rabbitHealth,
KafkaHealthIndicator kafkaHealth) {
this.contributors = Map.of(
"rabbitmq", rabbitHealth,
"kafka", kafkaHealth
);
}
@Override
public HealthContributor getContributor(String name) {
return contributors.get(name);
}
@Override
public Iterator<NamedContributor<HealthContributor>> iterator() {
return contributors.entrySet().stream()
.map(e -> NamedContributor.of(e.getKey(), e.getValue()))
.iterator();
}
}{
"status": "UP",
"components": {
"messaging": {
"status": "UP",
"components": {
"rabbitmq": { "status": "UP", "details": { "version": "3.12.0" } },
"kafka": { "status": "UP", "details": { "clusterId": "abc123" } }
}
}
}
}Health Groups ve Kubernetes Probe'ları
Spring Boot 2.2+ ile gelen health groups, endpoint'leri Kubernetes liveness ve readiness probe'larına eşlemenizi sağlar:
# Liveness: Uygulama çalışıyor mu? (deadlock yok mu?)
management.endpoint.health.group.liveness.include=livenessState
management.endpoint.health.group.liveness.show-details=always
# Readiness: Uygulama trafik almaya hazır mı?
management.endpoint.health.group.readiness.include=readinessState,db,redis
management.endpoint.health.group.readiness.show-details=always
# Custom group: Sadece harici servisler
management.endpoint.health.group.external.include=paymentGateway,emailService,crmApi
management.endpoint.health.group.external.show-details=alwaysHer grup ayrı endpoint olarak erişilebilir:
GET /actuator/health/liveness → { "status": "UP" }
GET /actuator/health/readiness → { "status": "UP", "components": { "db": ... } }
GET /actuator/health/external → { "status": "DOWN", "components": { "crmApi": ... } }⚠️ Dikkat: Readiness probe'una ağır dış servis kontrolü (ödeme gateway, CRM) koymayın! Dış servis çöktüğünde TÜM pod'lar readiness'ı kaybeder → cascade failure. Dış servisler için ayrı bir group oluşturun ve ayrı monitoring ile izleyin.
InfoContributor — Uygulama Bilgisi
InfoContributor arayüzü, /actuator/info endpoint'ine programatik olarak bilgi eklemenizi sağlar:
@Component
public class AppInfoContributor implements InfoContributor {
private final UserRepository userRepository;
private final OrderRepository orderRepository;
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("statistics", Map.of(
"totalUsers", userRepository.count(),
"totalOrders", orderRepository.count(),
"activeUsers", userRepository.countByActiveTrue()
));
builder.withDetail("features", Map.of(
"emailNotifications", true,
"smsNotifications", false,
"twoFactorAuth", true,
"darkMode", true
));
builder.withDetail("runtime", Map.of(
"javaVersion", System.getProperty("java.version"),
"processors", Runtime.getRuntime().availableProcessors(),
"maxMemoryMB", Runtime.getRuntime().maxMemory() / (1024 * 1024)
));
}
}Cache'li Health Indicator (Best Practice)
Harici servis health check'leri her çağrıda ağ isteği yapar. Yoğun monitoring ortamlarında bu, dış servise gereksiz yük bindirir. Cache ile çözün:
@Component
public class CachedExternalHealthIndicator implements HealthIndicator {
private volatile Health cachedHealth = Health.unknown().build();
private final RestClient client;
public CachedExternalHealthIndicator(RestClient.Builder builder) {
this.client = builder.baseUrl("https://api.external.com").build();
}
// 30 saniyede bir güncelle — her /health çağrısında ağ isteği yapmaz
@Scheduled(fixedRate = 30_000)
public void refreshHealth() {
try {
long start = System.currentTimeMillis();
client.get().uri("/ping").retrieve().toBodilessEntity();
long duration = System.currentTimeMillis() - start;
cachedHealth = Health.up()
.withDetail("responseTime", duration + "ms")
.withDetail("lastChecked", Instant.now().toString())
.build();
} catch (Exception ex) {
cachedHealth = Health.down()
.withDetail("error", ex.getMessage())
.withDetail("lastChecked", Instant.now().toString())
.build();
}
}
@Override
public Health health() {
return cachedHealth; // Cache'ten döndür — anlık, maliyetsiz
}
}Yaygın Hatalar
1. Health Check'te Timeout Koymamak
// ❌ YANLIŞ — dış servis yanıt vermezse health check sonsuza kadar bekler
restClient.get().uri("/ping").retrieve().toBodilessEntity();
// ✅ DOĞRU — timeout ile
RestClient client = RestClient.builder()
.baseUrl("https://api.external.com")
.requestFactory(new SimpleClientHttpRequestFactory() {{
setConnectTimeout(Duration.ofSeconds(3));
setReadTimeout(Duration.ofSeconds(5));
}})
.build();2. Readiness'a Gereksiz Kontrol Eklemek
# ❌ YANLIŞ — dış servis çökerse tüm pod'lar trafik almayı durdurur
management.endpoint.health.group.readiness.include=readinessState,db,redis,paymentGateway,crmApi
# ✅ DOĞRU — sadece kritik iç bağımlılıklar
management.endpoint.health.group.readiness.include=readinessState,db,redis3. Health Check İçinde Ağır İşlem Yapmak
// ❌ YANLIŞ — her health check'te tam veritabanı sayımı
@Override
public Health health() {
long count = orderRepository.count(); // Milyonlarca kayıtlı tabloda yavaş!
return Health.up().withDetail("orderCount", count).build();
}
// ✅ DOĞRU — basit bağlantı testi
@Override
public Health health() {
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
return Health.up().build();
}Özet
HealthIndicator arayüzü ile harici servis bağımlılıklarınızın sağlık durumunu
/actuator/healthçıktısına ekleyin.Health.up(),Health.down(),Health.outOfService(),Health.unknown()durumlarını kullanın.CompositeHealthContributor ile ilişkili alt bileşenleri (RabbitMQ + Kafka = Messaging) gruplayın. Çıktı iç içe yapıda görünür.
Health groups ile Kubernetes liveness/readiness probe'larına farklı bileşenler atayın. Liveness basit (deadlock tespiti), readiness kritik iç bağımlılıklar (DB, Redis).
Cache'li health check kullanarak dış servislere gereksiz yük bindirmeyin.
@Scheduledile periyodik güncelleme,health()metodu cache'ten döndürme.InfoContributor ile
/actuator/infoendpoint'ine uygulama istatistikleri, özellik bayrakları ve runtime bilgileri ekleyin.Health check'lerde mutlaka timeout koyun. Ağır işlemlerden (tam tablo sayımı) kaçının. Readiness probe'una dış servis kontrolü koymayın — cascade failure riski.
Gerçek Dünya Örneği: E-Ticaret Health Dashboard
Bir e-ticaret uygulamasında birden fazla dış bağımlılık vardır:
// Ödeme gateway health
@Component
public class PaymentHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
paymentClient.ping(Duration.ofSeconds(3));
return Health.up().withDetail("gateway", "stripe").build();
} catch (Exception e) {
return Health.down().withDetail("gateway", "stripe")
.withDetail("error", e.getMessage()).build();
}
}
}
// Kargo API health
@Component
public class ShippingHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
shippingClient.healthCheck(Duration.ofSeconds(3));
return Health.up().withDetail("carrier", "ups").build();
} catch (Exception e) {
return Health.down().withDetail("carrier", "ups")
.withDetail("error", e.getMessage()).build();
}
}
}
// Arama motoru (Elasticsearch) health — yerleşik indicator varsa kullanın
// Yoksa custom yazın
@Component
public class SearchHealthIndicator implements HealthIndicator {
private final ElasticsearchClient esClient;
@Override
public Health health() {
try {
HealthResponse esHealth = esClient.cluster().health();
String status = esHealth.status().name();
Health.Builder builder = "GREEN".equals(status) ? Health.up() :
"YELLOW".equals(status) ? Health.up() : Health.down();
return builder
.withDetail("clusterName", esHealth.clusterName())
.withDetail("status", status)
.withDetail("numberOfNodes", esHealth.numberOfNodes())
.withDetail("activeShards", esHealth.activeShards())
.build();
} catch (Exception e) {
return Health.down().withDetail("error", e.getMessage()).build();
}
}
}Health group yapılandırması:
# Kubernetes probe'ları
management.endpoint.health.group.liveness.include=livenessState
management.endpoint.health.group.readiness.include=readinessState,db,redis
# Dış servis monitoring (ayrı dashboard)
management.endpoint.health.group.external.include=payment,shipping,search
management.endpoint.health.group.external.show-details=alwaysBu yapıda /actuator/health/external endpoint'i tüm dış servislerin durumunu toplu olarak gösterir. Grafana veya özel bir dashboard ile bu endpoint'i izleyerek dış servis kesintilerini anında tespit edebilirsiniz.
AI Asistan
Sorularını yanıtlamaya hazır