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

  1. Metric Collection

    • Use meaningful metric names
    • Add appropriate tags
    • Monitor key business metrics
    • Set appropriate sampling rates
  2. Health Checks

    • Implement custom health indicators
    • Use proper health check endpoints
    • Monitor dependencies
    • Configure proper timeouts
  3. Performance Monitoring

    • Use appropriate time units
    • Monitor resource usage
    • Track response times
    • Monitor throughput
  4. Alerting

    • Define meaningful thresholds
    • Avoid alert fatigue
    • Use proper routing
    • Implement escalation policies

Common Monitoring Patterns

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