Monitoring Spring Boot Applications
Learn how to monitor Spring Boot applications using Spring Boot Actuator, Prometheus, Grafana, and other monitoring tools
Monitoring Spring Boot Applications
This guide covers comprehensive monitoring strategies for Spring Boot applications using Spring Boot Actuator, Prometheus, Grafana, and other monitoring tools.
Video Tutorial
Learn more about Spring Boot monitoring in this comprehensive video tutorial:
Prerequisites
<dependencies>
<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-aop</artifactId>
</dependency>
</dependencies>
Spring Boot Actuator
Basic Configuration
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,info,env
endpoint:
health:
show-details: always
probes:
enabled: true
metrics:
enabled: true
prometheus:
enabled: true
Custom Health Indicators
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
PreparedStatement ps = conn.prepareStatement("SELECT 1");
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("version", conn.getMetaData().getDatabaseProductVersion())
.build();
}
} catch (SQLException ex) {
return Health.down()
.withException(ex)
.build();
}
return Health.down().build();
}
}
Custom Metrics
@Component
public class OrderMetrics {
private final MeterRegistry registry;
private final Counter orderCounter;
private final Timer orderProcessingTimer;
public OrderMetrics(MeterRegistry registry) {
this.registry = registry;
this.orderCounter = Counter.builder("orders.created")
.description("Number of orders created")
.register(registry);
this.orderProcessingTimer = Timer.builder("orders.processing.time")
.description("Order processing time")
.register(registry);
}
public void recordOrderCreation() {
orderCounter.increment();
}
public void recordOrderProcessingTime(long milliseconds) {
orderProcessingTimer.record(milliseconds, TimeUnit.MILLISECONDS);
}
}
Prometheus Integration
Prometheus Configuration
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
Custom Prometheus Metrics
@Configuration
public class PrometheusConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "my-app")
.commonTags("environment", "production");
}
}
@Component
public class CustomMetrics {
private final Counter customCounter;
private final Gauge customGauge;
private final Summary customSummary;
public CustomMetrics(MeterRegistry registry) {
this.customCounter = Counter.builder("custom.counter")
.description("A custom counter")
.tags("type", "example")
.register(registry);
this.customGauge = Gauge.builder("custom.gauge", this, this::calculateValue)
.description("A custom gauge")
.register(registry);
this.customSummary = Summary.builder("custom.summary")
.description("A custom summary")
.quantiles(0.5, 0.75, 0.95)
.register(registry);
}
private double calculateValue() {
// Custom calculation logic
return Math.random() * 100;
}
}
Grafana Dashboard
Dashboard JSON
{
"dashboard": {
"id": null,
"title": "Spring Boot Metrics",
"panels": [
{
"title": "HTTP Request Rate",
"type": "graph",
"datasource": "Prometheus",
"targets": [
{
"expr": "rate(http_server_requests_seconds_count[5m])",
"legendFormat": "{{method}} {{uri}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"datasource": "Prometheus",
"targets": [
{
"expr": "rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m])",
"legendFormat": "{{method}} {{uri}}"
}
]
}
]
}
}
Distributed Tracing
Sleuth Configuration
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
# application.yml
spring:
sleuth:
sampler:
probability: 1.0
zipkin:
baseUrl: http://localhost:9411
Custom Tracing
@Service
public class OrderService {
private final Tracer tracer;
public Order processOrder(Order order) {
Span span = tracer.nextSpan().name("process-order");
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span.start())) {
span.tag("orderId", order.getId().toString());
// Process order
Order processed = processOrderInternal(order);
span.tag("status", "success");
return processed;
} catch (Exception e) {
span.tag("error", e.getMessage());
span.tag("status", "failed");
throw e;
} finally {
span.finish();
}
}
}
Logging Configuration
Logback Configuration
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<customFields>{"app":"${appName}"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.example" level="DEBUG"/>
</configuration>
Performance Monitoring
Micrometer Timer Aspects
@Aspect
@Component
public class PerformanceMonitoringAspect {
private final MeterRegistry registry;
@Around("@annotation(Timed)")
public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable {
Timer.Sample sample = Timer.start(registry);
try {
return pjp.proceed();
} finally {
sample.stop(Timer.builder("method.execution.time")
.tag("class", pjp.getTarget().getClass().getSimpleName())
.tag("method", pjp.getSignature().getName())
.description("Time taken to execute method")
.register(registry));
}
}
}
Alerting
Alert Manager Configuration
# alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'web.hook'
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://127.0.0.1:5001/'
Prometheus Alert Rules
groups:
- name: spring-boot-alerts
rules:
- alert: HighErrorRate
expr: rate(http_server_requests_seconds_count{status="5xx"}[5m]) > 1
for: 5m
labels:
severity: critical
annotations:
summary: High error rate detected
description: "Error rate is {{ $value }} requests per second"
Best Practices
-
Metric Collection
- Use meaningful metric names
- Add appropriate tags
- Monitor key business metrics
- Set appropriate sampling rates
-
Health Checks
- Implement custom health indicators
- Use proper health check endpoints
- Monitor dependencies
- Configure proper timeouts
-
Performance Monitoring
- Use appropriate time units
- Monitor resource usage
- Track response times
- Monitor throughput
-
Alerting
- Define meaningful thresholds
- Avoid alert fatigue
- Use proper routing
- Implement escalation policies
Common Monitoring Patterns
- Circuit Breaker Monitoring
@Component
public class CircuitBreakerMetrics {
private final MeterRegistry registry;
public void recordCircuitBreakerState(String name, State state) {
registry.gauge("circuit.breaker.state",
Tags.of("name", name),
state.ordinal());
}
}
- Cache Monitoring
@Component
public class CacheMetrics {
private final MeterRegistry registry;
public void recordCacheHit(String cacheName) {
registry.counter("cache.hits",
"name", cacheName).increment();
}
public void recordCacheMiss(String cacheName) {
registry.counter("cache.misses",
"name", cacheName).increment();
}
}
Conclusion
Effective monitoring of Spring Boot applications requires:
- Proper metric collection
- Health check implementation
- Performance monitoring
- Distributed tracing
- Alerting configuration
For more Spring Boot topics, check out: