Spring Boot Micrometer e Prometheus: metriche Actuator in produzione

Integrazione di Spring Boot Micrometer e Prometheus su Spring Boot 4: modello di accesso Actuator, scrape config, metriche HTTP, health probe e regole di alerting.

Monitoraggio Spring Boot Actuator con Micrometer e Prometheus

Spring Boot Actuator espone endpoint di salute, metriche e diagnostica, e Micrometer è la facciata per le metriche che sta dietro. Aggiungendo la dipendenza micrometer-registry-prometheus si ottiene un endpoint /actuator/prometheus che Prometheus interroga a intervalli fissi. Gli esempi seguenti si basano su Spring Boot 4.1 (release GA corrente, con la 4.2 in release candidate), Micrometer 1.16 e il client Java di Prometheus 1.x.

La risposta breve

L'integrazione di Spring Boot Micrometer con Prometheus richiede tre passaggi: aggiungere spring-boot-starter-actuator e micrometer-registry-prometheus, inserire prometheus in management.endpoints.web.exposure.include, quindi puntare una scrape_config di Prometheus su /actuator/prometheus. Le metriche JVM, HTTP, HikariCP e di sistema vengono pubblicate senza codice aggiuntivo.

Configurare Spring Boot Micrometer: Actuator e il registry Prometheus

Dipendenze Maven Necessarie

Lo starter Actuator abilita gli endpoint e porta con sé Micrometer. Il registry Prometheus formatta ogni meter nel formato di esposizione di Prometheus. Lo starter AspectJ serve solo per le annotazioni @Timed e @Counted.

xml
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
    <!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
    <!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AspectJ for @Timed and @Counted -->
    <!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>
</dependencies>

Nei tutorial più vecchi compaiono due trappole di versione. Da Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus è costruito sul client Java di Prometheus 1.x; il vecchio codice 0.x vive nel modulo deprecato micrometer-registry-prometheus-simpleclient, che la documentazione Micrometer su Prometheus presenta solo come soluzione temporanea. E con Spring Boot 4 un pom.xml che dichiara ancora spring-boot-starter-aop non viene più risolto: la guida di migrazione a Spring Boot 4.0 lo rinomina in spring-boot-starter-aspectj.

Configurazione degli Endpoint Actuator

Per impostazione predefinita, solo l'endpoint health è esposto via HTTP. L'endpoint /actuator/prometheus esiste non appena il registry è nel classpath, ma restituisce 404 finché non viene aggiunto alla lista di esposizione.

Spring Boot 3.4 ha sostituito gli interruttori on/off (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) con un modello di accesso, e le vecchie proprietà sono deprecate. Lo schema opt-in imposta l'accesso predefinito a none e concede read-only endpoint per endpoint:

yaml
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
  endpoints:
    # Every endpoint is inaccessible unless granted below
    access:
      default: none
    web:
      exposure:
        # Endpoints exposed over HTTP
        # health, info, prometheus are the minimum for monitoring
        include: health,info,prometheus,metrics,env,loggers
      base-path: /actuator
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      show-components: when-authorized
    info:
      access: read-only
    prometheus:
      access: read-only
    metrics:
      access: read-only
    env:
      access: read-only
      # Mask sensitive values for anonymous callers
      show-values: when-authorized
    loggers:
      # unrestricted allows POST to change a log level at runtime
      access: unrestricted

Esposizione e accesso sono due controlli distinti. exposure.include decide se un endpoint viene mappato via HTTP; access decide cosa l'endpoint consente una volta mappato. Un file di proprietà che usa ancora enabled: true continua a funzionare con un avviso di deprecazione, ed è quello il momento di migrare.

L'opzione show-details: when-authorized mostra i dettagli di salute solo agli utenti autenticati con il ruolo appropriato. Gli endpoint stessi vengono protetti con una SecurityFilterChain dedicata:

ActuatorSecurityConfig.javajava
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;

import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {

    @Bean
    SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Health and info public for load balancers
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                // Prometheus accessible from the internal network only
                .requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
                // Other endpoints restricted to admins
                .anyRequest().hasRole("ACTUATOR_ADMIN")
            )
            .httpBasic(basic -> {})
            .build();
    }
}

Con Spring Boot 4 EndpointRequest si trova in org.springframework.boot.security.autoconfigure.actuate.web.servlet, come mostra il riferimento degli endpoint Actuator. La DSL Java AuthorizedUrl di authorizeHttpRequests non ha un metodo hasIpAddress: la regola sull'IP passa per IpAddressAuthorizationManager.hasIpAddress, disponibile da Spring Security 6.3.

Metriche Custom con Micrometer

Counter, Gauge e Timer Applicativi

Micrometer offre diversi tipi di metriche adatti a casi d'uso differenti. I counter misurano eventi cumulativi, i gauge misurano valori istantanei e i timer misurano la durata delle operazioni.

OrderMetricsService.javajava
// Custom business metrics service
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@Service
public class OrderMetricsService {

    // Counter for orders created
    private final Counter ordersCreatedCounter;
    // Timer to measure processing duration
    private final Timer orderProcessingTimer;
    // Atomic value for pending orders gauge
    private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry registry) {
        // Exported as orders_created_total: the _total suffix is added by the registry
        this.ordersCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("channel", "web")
            .register(registry);

        // Timer with histogram buckets: percentiles computed server-side in PromQL
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .publishPercentileHistogram()
            .register(registry);

        // Gauge linked to atomic value
        // Read on each scrape
        Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
            .description("Number of orders pending processing")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreatedCounter.increment();
        pendingOrdersCount.incrementAndGet();
    }

    public void recordOrderProcessed(Runnable processingLogic) {
        // Measures execution duration
        orderProcessingTimer.record(processingLogic);
        pendingOrdersCount.decrementAndGet();
    }

    public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
        return orderProcessingTimer.record(processingLogic);
    }
}

Con il registry Prometheus contano due dettagli di naming. I nomi dei meter Micrometer usano i punti e vengono convertiti in snake case (orders.processing.duration diventa orders_processing_duration_seconds). I counter ricevono automaticamente il suffisso _total, quindi chiamare un counter orders.created.total aggiunge solo rumore.

publishPercentiles(0.5, 0.95, 0.99) calcola i percentili dentro ogni JVM, e quei valori non si possono aggregare tra istanze. publishPercentileHistogram() pubblica bucket che histogram_quantile() aggrega sull'intera flotta, ed è l'opzione da scegliere dietro Prometheus.

Annotazioni @Timed e @Counted

Per evitare codice ripetitivo, Micrometer fornisce annotazioni che strumentano i metodi tramite AspectJ.

PaymentService.javajava
// Automatic instrumentation with annotations
package com.example.monitoring.service;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentValidator paymentValidator;

    public PaymentService(PaymentValidator paymentValidator) {
        this.paymentValidator = paymentValidator;
    }

    // @Timed creates a Timer
    // Measures each call and publishes count, sum, max and histogram buckets
    @Timed(
        value = "payment.process.duration",
        description = "Payment processing duration",
        histogram = true
    )
    public PaymentResult processPayment(PaymentRequest request) {
        // Call through another bean: the proxy intercepts it
        paymentValidator.validate(request);
        return executePayment(request);
    }

    // @Counted increments a counter on each call
    // Useful for discrete events
    @Counted(
        value = "payment.refunds",
        description = "Total number of refunds"
    )
    public void refundPayment(String transactionId) {
        // Refund logic
    }
}
yaml
# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
  observations:
    annotations:
      enabled: true

Da Spring Boot 3.2 non serve più dichiarare a mano i bean TimedAspect e CountedAspect: impostare management.observations.annotations.enabled=true con spring-boot-starter-aspectj nel classpath li configura automaticamente. La proprietà vale false per default, il che spiega la maggior parte delle segnalazioni del tipo "il mio @Timed non fa nulla".

Limite dell'AOP

Le annotazioni @Timed e @Counted funzionano solo sui metodi public dei bean Spring chiamati attraverso il proxy. Una chiamata da un metodo all'altro della stessa classe aggira il proxy e non viene strumentata, e un'annotazione su un metodo private viene ignorata. Annotare un controller Spring MVC già strumentato produce observation duplicate.

Pronto a superare i tuoi colloqui su Spring Boot?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Endpoint di Salute Custom

Health Indicator di Business

Gli Health Indicator verificano lo stato delle dipendenze esterne e dei componenti critici di business. Spring Boot fornisce indicator di default per database, Redis e altri servizi comuni. In Spring Boot 4 l'API di health vive in un modulo dedicato, spring-boot-health, e Health e HealthIndicator sono passati da org.springframework.boot.actuate.health a org.springframework.boot.health.contributor.

PaymentGatewayHealthIndicator.javajava
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.time.Instant;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final String gatewayHealthUrl;

    public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
        this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
    }

    @Override
    public Health health() {
        Instant start = Instant.now();

        try {
            // Call gateway health endpoint
            var response = restClient.get()
                .uri(gatewayHealthUrl)
                .retrieve()
                .toBodilessEntity();

            Duration responseTime = Duration.between(start, Instant.now());

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime.toMillis() + "ms")
                    .withDetail("statusCode", response.getStatusCode().value())
                    .build();
            } else {
                return Health.down()
                    .withDetail("statusCode", response.getStatusCode().value())
                    .withDetail("reason", "Unexpected status code")
                    .build();
            }
        } catch (Exception e) {
            Duration responseTime = Duration.between(start, Instant.now());

            return Health.down()
                .withDetail("error", e.getClass().getSimpleName())
                .withDetail("message", e.getMessage())
                .withDetail("responseTime", responseTime.toMillis() + "ms")
                .build();
        }
    }
}

Questo indicator compare in /actuator/health con il nome paymentGateway: il nome del bean senza il suffisso HealthIndicator.

Health Group per Kubernetes

Spring Boot 4 abilita le probe liveness e readiness per default, su qualsiasi piattaforma e non solo quando rileva Kubernetes. I gruppi /actuator/health/liveness e /actuator/health/readiness esistono senza alcuna configurazione, e dichiarare a mano i bean LivenessStateHealthIndicator o ReadinessStateHealthIndicator è superfluo. La configurazione serve solo per aggiungere componenti a un gruppo o per definire gruppi personalizzati:

yaml
# application.yml
# Health groups configuration for Kubernetes
management:
  endpoint:
    health:
      probes:
        # true by default in Spring Boot 4
        enabled: true
        # Also serve the groups at /livez and /readyz on the main server port
        add-additional-paths: true
      group:
        # Readiness: can the application receive traffic?
        readiness:
          include: readinessState,db
        # Custom group for critical dependencies
        critical:
          include: db,paymentGateway
          show-details: when-authorized

Il gruppo liveness dovrebbe restare limitato a livenessState. Aggiungere un database o un gateway esterno alla liveness porta Kubernetes a riavviare ogni pod quando quella dipendenza cade, trasformando un'interruzione parziale in un'interruzione totale. Le dipendenze esterne vanno nella readiness, e solo quando l'applicazione non può servire alcuna richiesta senza di esse.

Le probe Kubernetes puntano poi a endpoint dedicati:

yaml
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /livez
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3

Con add-additional-paths: true le probe restano sulla porta dell'applicazione anche quando Actuator gira su una porta di management separata, così un server principale in errore viene rilevato dalla probe.

Integrazione di Spring Boot con Prometheus e Grafana

Configurazione dello Scraping Prometheus

Prometheus raccoglie le metriche interrogando periodicamente l'endpoint /actuator/prometheus. La configurazione di scraping definisce i target e il percorso delle metriche, dato che Prometheus usa /metrics per default.

yaml
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets:
          - 'order-service:9090'
          - 'payment-service:9090'
          - 'inventory-service:9090'
    # Relabeling to strip the port from the instance label
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # Kubernetes service discovery
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # Only scrape pods with annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)

I target usano la porta 9090 perché la configurazione di produzione più avanti sposta Actuator su una porta di management separata. Senza management.server.port, il target di scraping è la porta dell'applicazione.

Metriche Spring Boot Actuator Pubblicate per Default

Actuator con Micrometer espone metriche JVM, di processo, HikariCP e HTTP senza alcun codice. Queste query PromQL coprono quelle da osservare per prime in produzione:

promql
# PromQL queries for JVM monitoring

# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})

# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
  / sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100

# Live threads
jvm_threads_live_threads

# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])

# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60

# CPU used by the JVM process
process_cpu_usage

# Active database connections
hikaricp_connections_active

# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100

jvm_memory_used_bytes{area="heap"} restituisce una serie per ogni pool di memoria (Eden, Survivor, Old Gen con G1). Dividerla per il massimo senza sum by (instance) produce rapporti per pool che in una dashboard sembrano sbagliati.

Un bean MeterBinder aggiunge gauge a livello applicativo che le metriche predefinite non coprono:

CustomJvmMetrics.javajava
// Additional JVM metrics
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

@Component
public class CustomJvmMetrics implements MeterBinder {

    @Override
    public void bindTo(MeterRegistry registry) {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        // System load average
        Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
            .description("System load average over 1 minute")
            .register(registry);

        // Available processors count
        Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
            .description("Number of available processors")
            .register(registry);

        // Application uptime
        Gauge.builder("application.uptime.seconds",
                ManagementFactory.getRuntimeMXBean(),
                bean -> bean.getUptime() / 1000.0)
            .description("Application uptime in seconds")
            .register(registry);
    }
}

system.cpu.count e process.uptime sono già pubblicati dai binder integrati di Micrometer. L'esempio mostra lo schema, e un binder reale dovrebbe registrare solo ciò che manca alle metriche predefinite.

Dashboard Grafana Pronte all'Uso

Grafana offre dashboard della community per Spring Boot che funzionano con le metriche Actuator standard. Una dashboard scritta a mano usa i tipi di pannello timeseries e stat:

json
{
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "title": "Request Rate",
      "type": "timeseries",
      "targets": [
        {
          "expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
          "legendFormat": "{{method}} {{uri}} - {{status}}"
        }
      ]
    },
    {
      "title": "Response Time P99",
      "type": "timeseries",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
          "legendFormat": "{{method}} {{uri}}"
        }
      ]
    },
    {
      "title": "Error Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
        }
      ]
    }
  ]
}

Molte dashboard copiate da tutorial più vecchi usano ancora i tipi di pannello graph e singlestat. Singlestat è stato rimosso in Grafana 8, e Grafana 12 ha eliminato del tutto il supporto ad AngularJS, quindi quei pannelli vengono visualizzati solo dopo la migrazione a timeseries e stat.

Import in Grafana

Per importare una dashboard della community: Grafana > Dashboards > New > Import, poi inserire l'ID 4701 (JVM Micrometer) oppure 12900 (SpringBoot APM). Dopo l'import conviene controllare i tipi di pannello: le revisioni più vecchie delle dashboard della community possono contenere ancora pannelli Angular che Grafana 12 deve migrare.

Alerting con Prometheus

Regole di Alerting Essenziali

Le regole di alerting di Prometheus generano notifiche quando le metriche superano soglie critiche.

yaml
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
  - name: spring-boot-alerts
    rules:
      # Alert if application is down
      - alert: ApplicationDown
        expr: up{job="spring-boot-apps"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Application {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # Alert on HTTP error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
          /
          sum(rate(http_server_requests_seconds_count[5m])) by (application)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High error rate on {{ $labels.application }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Alert on P99 latency, aggregated per application
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.application }}"
          description: "P99 latency is {{ $value | humanizeDuration }}"

      # Heap memory alert
      - alert: HighHeapUsage
        expr: |
          sum by (instance) (jvm_memory_used_bytes{area="heap"})
          / sum by (instance) (jvm_memory_max_bytes{area="heap"})
          > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High heap memory usage on {{ $labels.instance }}"
          description: "Heap usage is at {{ $value | humanizePercentage }}"

      # Database connection pool exhausted alert
      - alert: DatabaseConnectionPoolExhausted
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max
          > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Database connection pool nearly exhausted"
          description: "{{ $value | humanizePercentage }} of connections in use"

      # Excessive GC alert
      - alert: HighGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m])
          > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High GC pause time"
          description: "Average GC pause is {{ $value | humanizeDuration }}"

Questi alert coprono i problemi di produzione più comuni: disponibilità, performance e risorse. La regola HighLatency richiede sum by (le, ...): senza, histogram_quantile calcola un P99 per ogni serie (per URI, metodo e status), e un solo endpoint lento con tre chiamate fa scattare l'alert.

Metriche HTTP e Metriche Database in Spring Boot

Strumentazione Automatica delle Richieste HTTP

Spring Boot strumenta ogni richiesta Spring MVC e WebFlux in ingresso tramite l'Observation API e pubblica il timer http.server.requests, con i tag method, uri, status, outcome ed exception.

yaml
# application.yml
# HTTP metrics configuration
management:
  metrics:
    distribution:
      # Histogram buckets for server-side percentiles
      percentiles-histogram:
        http.server.requests: true
      # SLO buckets: count requests faster than each boundary
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s
    tags:
      # Common tags added to every meter
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

Le interfacce WebMvcTagsContributor e WebMvcTagsProvider presenti in molti esempi sono state rimosse in Spring Boot 3.0. I tag personalizzati si aggiungono ora estendendo DefaultServerRequestObservationConvention, come documenta il riferimento delle metriche Spring Boot:

TenantObservationConvention.javajava
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        HttpServletRequest request = context.getCarrier();
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");

        // Keep the default tags and add two bounded ones
        return super.getLowCardinalityKeyValues(context).and(
            KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
            KeyValue.of("tenant", tenantId != null ? tenantId : "default")
        );
    }
}

Solo i key-value a bassa cardinalità diventano label Prometheus. Un tag tenant è accettabile con qualche decina di tenant e diventa un problema di cardinalità con migliaia. I valori ad alta cardinalità (ID utente, ID ordine) vanno in getHighCardinalityKeyValues, che alimenta solo le trace. Il lato tracing dell'Observation API è trattato in dettaglio in observability di Spring Boot con OpenTelemetry.

Metriche HikariCP e Query SQL

Le metriche del pool HikariCP sono esposte automaticamente con il prefisso hikaricp, con il nome del pool come tag, accanto ai gauge generici jdbc.connections. Impostare il nome del pool rende il tag leggibile:

yaml
# application.yml
# HikariCP pool configuration
spring:
  datasource:
    hikari:
      # Becomes the "pool" tag on hikaricp_* metrics
      pool-name: OrderServicePool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Spring Boot non misura i tempi delle singole query. Il progetto datasource-micrometer avvolge il DataSource e crea un'observation per connessione, query e result set, ottenendo sia metriche sia span di trace. La linea 2.x è pensata per Spring Boot 4, la 1.x per Spring Boot 3:

xml
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
    <groupId>net.ttddyy.observation</groupId>
    <artifactId>datasource-micrometer-spring-boot</artifactId>
    <version>2.0.1</version>
</dependency>

Questo sostituisce l'approccio manuale di avvolgere il DataSource in un ProxyDataSourceBuilder con un listener scritto a mano, che aggirava l'autoconfigurazione del DataSource di Spring Boot e andava riallineato a ogni aggiornamento di Boot. Le query lente indicano di solito un pattern N+1, trattato in soluzioni al problema N+1 in Spring Data JPA.

Best Practice per la Produzione

Cardinalità delle Metriche

Una cardinalità eccessiva degrada le performance di Prometheus. Ogni combinazione unica di tag genera una serie temporale distinta.

AntiPatternHighCardinality.javajava
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;

@Service
public class AntiPatternHighCardinality {

    private final MeterRegistry registry;

    // BAD: userId creates one series per user
    public void trackUserAction(String userId, String action) {
        Counter.builder("user.actions")
            .tag("userId", userId)  // Millions of possible values
            .tag("action", action)
            .register(registry)
            .increment();
    }
}
GoodPracticeCardinality.javajava
// Controlled cardinality
package com.example.monitoring.bestpractice;

@Service
public class GoodPracticeCardinality {

    private final MeterRegistry registry;

    // GOOD: user category instead of ID
    public void trackUserAction(User user, String action) {
        Counter.builder("user.actions")
            .tag("userType", user.getSubscriptionType())  // FREE, PREMIUM, ENTERPRISE
            .tag("action", action)
            .register(registry)
            .increment();
    }

    // GOOD: let a timer histogram bucket durations
    public void trackResponseTime(long responseTimeMs) {
        Timer.builder("response.time")
            .publishPercentileHistogram()
            .register(registry)
            .record(Duration.ofMillis(responseTimeMs));
    }
}

Un Timer con bucket di istogramma risponde alle domande sulla latenza meglio di un counter con un tag bucket costruito a mano: PromQL può calcolare qualsiasi percentile dai bucket, e i limiti dei bucket restano coerenti tra i servizi.

Configurazione Pronta per la Produzione

yaml
# application-production.yml
# Production configuration (Spring Boot 4)
management:
  server:
    # Separate port for management endpoints, not exposed publicly
    port: 9090
  endpoints:
    access:
      default: none
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      probes:
        add-additional-paths: true
    info:
      access: read-only
    prometheus:
      access: read-only
  observations:
    annotations:
      enabled: true
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 1ms
      maximum-expected-value:
        http.server.requests: 30s
    tags:
      application: ${spring.application.name}
      environment: production
      version: ${app.version:unknown}

Con access.default: none, endpoint come env, beans, configprops e mappings sono inaccessibili senza doverli elencare uno per uno. management.prometheus.metrics.export.enabled vale true per default, e un'impostazione step non ha effetto su Prometheus: è un registry di tipo pull, e l'intervallo di scraping si definisce in prometheus.yml.

Errori Comuni con Actuator e Prometheus

SintomoCausaSoluzione
/actuator/prometheus restituisce 404Endpoint assente dalla lista di esposizione, oppure registry mancanteAggiungere prometheus a management.endpoints.web.exposure.include e verificare che micrometer-registry-prometheus sia nel classpath
Il target Prometheus mostra "server returned HTTP status 401"Spring Security protegge tutti gli endpoint tranne healthAprire prometheus alla rete dello scraper o configurare la basic auth nella scrape config
@Timed non produce alcuna metricamanagement.observations.annotations.enabled vale false per default, oppure AspectJ mancaImpostare la proprietà a true e aggiungere spring-boot-starter-aspectj
spring-boot-starter-aop non trovato dopo l'aggiornamentoRinominato in Spring Boot 4Usare spring-boot-starter-aspectj
L'import di Health o HealthIndicator fallisce su Boot 4Package spostato in org.springframework.boot.health.contributorAggiornare gli import
Avvisi di deprecazione su management.endpoint.*.enabledModello on/off sostituito in Spring Boot 3.4Usare management.endpoints.access.default e management.endpoint.<id>.access
I percentili in dashboard divergono molto dalla realtàValori di publishPercentiles mediati tra i podUsare publishPercentileHistogram() e histogram_quantile()

Domande da Colloquio su Spring Boot Micrometer

Actuator emerge nella maggior parte dei colloqui Spring Boot per ruoli mid-level e senior. Le risposte che distinguono un profilo esperto da uno junior:

  • Differenza tra liveness e readiness: la liveness dice a Kubernetes di riavviare il pod, la readiness gli dice di smettere di instradare traffico. Un candidato che mette il database nel gruppo liveness non l'ha mai visto attraversare un'interruzione.
  • Perché Micrometer e non direttamente il client Prometheus: Micrometer è una facciata. Lo stesso codice con Counter e Timer esporta verso Prometheus, Datadog o OTLP cambiando una dipendenza, e la strumentazione di Spring stesso (HTTP, pool JDBC, cache) è scritta su Micrometer.
  • Percentili lato client o istogrammi: i percentili lato client non si aggregano tra istanze, gli istogrammi sì. È la domanda trabocchetto più frequente sull'argomento.
  • Cosa significa cardinalità e come manda in crisi Prometheus: una serie temporale per ogni combinazione unica di label; un tag con l'ID utente moltiplica l'uso di memoria per il numero di utenti.
  • Come proteggere Actuator: limitare l'esposizione, impostare i livelli di accesso, eseguire il management su una porta separata e proteggerlo con una SecurityFilterChain dedicata.

Altre domande di questo tipo sono raccolte nel modulo domande da colloquio su Spring Boot Actuator.

Fonti

Cosa Ricordare su Spring Boot Micrometer e Prometheus

  • Tre passaggi per uno scraping funzionante: starter Actuator, micrometer-registry-prometheus, prometheus nella lista di esposizione
  • Modello di accesso dalla 3.4: management.endpoints.access.default: none più read-only per endpoint sostituisce i flag enabled deprecati
  • Cambiamenti di Spring Boot 4: spring-boot-starter-aspectj, API di health in org.springframework.boot.health.contributor, EndpointRequest in org.springframework.boot.security.autoconfigure.actuate.web.servlet, probe attive per default
  • Le annotazioni richiedono un interruttore: management.observations.annotations.enabled=true, nessun bean TimedAspect manuale
  • Tag HTTP tramite l'Observation API: estendere DefaultServerRequestObservationConvention, mantenere le label a bassa cardinalità
  • Istogrammi invece dei percentili lato client: publishPercentileHistogram() e sum by (le) in ogni histogram_quantile
  • La liveness resta locale: le dipendenze esterne vanno nella readiness o in un gruppo personalizzato, mai nella liveness

Con queste impostazioni un servizio Spring Boot 4 produce metriche che si aggregano correttamente tra i pod, endpoint che non espongono nulla alla rete pubblica e alert che scattano su incidenti reali anziché su un singolo URI lento.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in Spring Boot?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 26 ottobre 2026

Tag

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observability

Condividi

Articoli correlati