Spring Boot 4 observability with Actuator, Micrometer, and OpenTelemetry. Use when configuring health indicators, custom metrics, distributed tracing, production endpoint exposure, or Kubernetes/Cloud Run probes. Covers Actuator security, Micrometer Timer/Counter/Gauge patterns, and OpenTelemetry span customization.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/actuator.mdreferences/metrics.mdreferences/tracing.mdProduction observability with Actuator endpoints, Micrometer metrics, and OpenTelemetry tracing.
| Component | Purpose |
|---|---|
| Actuator | Health checks, info, metrics exposure, operational endpoints |
| Micrometer | Metrics abstraction (Timer, Counter, Gauge, DistributionSummary) |
| OpenTelemetry | Distributed tracing (default in Spring Boot 4) |
actuator, micrometer-registry-*, opentelemetryserver:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
management:
server:
port: 8081 # Separate management port
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
base-path: /manage
access:
default: none
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
group:
liveness:
include: livenessState,ping
readiness:
include: readinessState,db,redis,diskSpace
health:
defaults:
enabled: true
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
private final ExternalApiClient apiClient;
private static final Duration TIMEOUT = Duration.ofSeconds(5);
@Override
public Health health() {
try {
long start = System.currentTimeMillis();
apiClient.ping();
long latency = System.currentTimeMillis() - start;
if (latency > 3000) {
return Health.down()
.withDetail("latency_ms", latency)
.withDetail("reason", "Response time exceeded threshold")
.build();
}
return Health.up()
.withDetail("latency_ms", latency)
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("error", e.getMessage())
.build();
}
}
}
@Component
class ExternalApiHealthIndicator(private val apiClient: ExternalApiClient) : HealthIndicator {
override fun health(): Health = runCatching {
val start = System.currentTimeMillis()
apiClient.ping()
val latency = System.currentTimeMillis() - start
if (latency > 3000) {
Health.down()
.withDetail("latency_ms", latency)
.withDetail("reason", "Response time exceeded threshold")
.build()
} else {
Health.up().withDetail("latency_ms", latency).build()
}
}.getOrElse { Health.down(it).build() }
}
@Component
public class OrderMetrics {
private final Counter ordersCreated;
private final Timer orderProcessingTime;
private final AtomicInteger activeOrders = new AtomicInteger(0);
public OrderMetrics(MeterRegistry registry) {
this.ordersCreated = Counter.builder("orders.created.total")
.description("Total orders created")
.tag("channel", "web")
.register(registry);
this.orderProcessingTime = Timer.builder("orders.processing.duration")
.description("Order processing duration")
.publishPercentiles(0.5, 0.95, 0.99)
.publishPercentileHistogram()
.serviceLevelObjectives(
Duration.ofMillis(100),
Duration.ofMillis(500),
Duration.ofSeconds(1)
)
.register(registry);
Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
.description("Currently active orders")
.register(registry);
}
public void recordOrderCreated() {
ordersCreated.increment();
activeOrders.incrementAndGet();
}
public <T> T recordProcessing(Supplier<T> operation) {
return orderProcessingTime.record(operation);
}
public void orderCompleted() {
activeOrders.decrementAndGet();
}
}
@Component
public class PaymentProcessor {
private final ObservationRegistry observationRegistry;
public PaymentResult process(PaymentRequest request) {
return Observation.createNotStarted("payment.processing", observationRegistry)
.lowCardinalityKeyValue("payment.method", request.method().name())
.lowCardinalityKeyValue("currency", request.currency())
.highCardinalityKeyValue("merchant.id", request.merchantId())
.observe(() -> executePayment(request));
}
}
management:
tracing:
sampling:
probability: 0.1 # 10% in production
opentelemetry:
resource-attributes:
service.name: my-service
deployment.environment: production
tracing:
export:
otlp:
endpoint: http://otel-collector:4318/v1/traces
// Spring Boot 4 imports
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
management:
endpoints:
access:
default: none # Deny by default
web:
exposure:
include: health,info,prometheus
endpoint:
health:
access: unrestricted
prometheus:
access: read-only
| Anti-Pattern | Fix |
|---|---|
| DB checks in liveness probe | Move to readiness group only |
| 100% trace sampling in production | Use 10% or less |
| Exposing all endpoints publicly | Separate management port + auth |
| High-cardinality metric tags | Use low-cardinality tags only |
| Missing graceful shutdown | Add server.shutdown=graceful |
| No health probe groups | Separate liveness and readiness |