Micrometer dan Prometheus di Spring Boot: Metrik Actuator di Produksi

Integrasi Micrometer dan Prometheus di Spring Boot 4: model akses Actuator, konfigurasi scrape, metrik HTTP, health probe, dan aturan alert.

Monitoring Spring Boot Actuator dengan Micrometer dan Prometheus

Spring Boot Actuator menyediakan endpoint health, metrik, dan diagnostik, sementara Micrometer adalah facade metrik di baliknya. Menambahkan dependensi micrometer-registry-prometheus menghadirkan endpoint /actuator/prometheus yang di-scrape Prometheus pada interval tetap. Contoh-contoh berikut menargetkan Spring Boot 4.1 (GA saat ini, dengan 4.2 dalam tahap release candidate), Micrometer 1.16, dan Prometheus Java client 1.x.

Jawaban singkat

Integrasi Micrometer Spring Boot dengan Prometheus terdiri dari tiga langkah: tambahkan spring-boot-starter-actuator dan micrometer-registry-prometheus, tambahkan prometheus ke management.endpoints.web.exposure.include, lalu arahkan scrape_config Prometheus ke /actuator/prometheus. Metrik JVM, HTTP, HikariCP, dan sistem dipublikasikan tanpa kode tambahan.

Setup Micrometer di Spring Boot: Actuator dan Registry Prometheus

Dependensi Maven yang Diperlukan

Starter Actuator mengaktifkan endpoint dan sekaligus membawa Micrometer. Registry Prometheus memformat setiap meter dalam format eksposisi Prometheus. Starter AspectJ hanya diperlukan untuk anotasi @Timed dan @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>

Dua jebakan versi masih muncul di tutorial lama. Sejak Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus dibangun di atas Prometheus Java client 1.x; kode 0.x yang lama berada di modul micrometer-registry-prometheus-simpleclient yang sudah deprecated, dan dokumentasi Micrometer Prometheus menyebutnya hanya sebagai solusi sementara. Selain itu, di Spring Boot 4, pom.xml yang masih mendeklarasikan spring-boot-starter-aop tidak lagi dapat di-resolve: panduan migrasi Spring Boot 4.0 mengganti namanya menjadi spring-boot-starter-aspectj.

Konfigurasi Endpoint Actuator

Secara default hanya endpoint health yang diekspos via HTTP. Endpoint /actuator/prometheus sudah ada begitu registry berada di classpath, tetapi mengembalikan 404 sampai endpoint tersebut ditambahkan ke daftar exposure.

Spring Boot 3.4 mengganti sakelar on/off (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) dengan model akses, dan properti lama kini deprecated. Pola opt-in menetapkan akses default ke none lalu memberikan read-only 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

Exposure dan akses adalah dua kontrol yang terpisah. exposure.include menentukan apakah sebuah endpoint dipetakan ke HTTP; access menentukan apa yang diizinkan endpoint tersebut setelah dipetakan. File properti yang masih memakai enabled: true tetap berjalan dengan peringatan deprecation, dan itulah saat yang tepat untuk bermigrasi.

Opsi show-details: when-authorized menampilkan detail kesehatan hanya kepada pengguna terautentikasi dengan peran yang sesuai. Endpoint itu sendiri diamankan dengan SecurityFilterChain khusus:

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

Di Spring Boot 4, EndpointRequest berpindah ke org.springframework.boot.security.autoconfigure.actuate.web.servlet, seperti terlihat di referensi endpoint Actuator. DSL Java AuthorizedUrl pada authorizeHttpRequests tidak memiliki metode hasIpAddress: aturan IP melewati IpAddressAuthorizationManager.hasIpAddress, yang tersedia sejak Spring Security 6.3.

Metrik Kustom dengan Micrometer

Counter, Gauge, dan Timer Aplikatif

Micrometer menyediakan beberapa tipe metrik yang sesuai dengan kasus penggunaan berbeda. Counter mengukur peristiwa kumulatif, gauge mengukur nilai sesaat, dan timer mengukur durasi sebuah operasi.

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);
    }
}

Dua detail penamaan penting saat memakai registry Prometheus. Nama meter Micrometer memakai titik dan dikonversi ke snake case (orders.processing.duration menjadi orders_processing_duration_seconds). Counter otomatis mendapat sufiks _total, sehingga menamai counter orders.created.total hanya menambah noise.

publishPercentiles(0.5, 0.95, 0.99) menghitung persentil di dalam setiap JVM, dan nilai tersebut tidak dapat diagregasi antar-instance. publishPercentileHistogram() mempublikasikan bucket yang dapat diagregasi histogram_quantile() di seluruh armada instance, sehingga opsi inilah yang dipilih ketika memakai Prometheus.

Anotasi @Timed dan @Counted

Untuk menghindari kode boilerplate, Micrometer menyediakan anotasi yang menginstrumentasi metode melalui 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

Sejak Spring Boot 3.2, bean TimedAspect dan CountedAspect tidak perlu lagi dideklarasikan manual: mengatur management.observations.annotations.enabled=true dengan spring-boot-starter-aspectj di classpath sudah mengonfigurasinya secara otomatis. Nilai default properti ini adalah false, yang menjelaskan sebagian besar laporan "@Timed saya tidak menghasilkan apa-apa".

Batasan AOP

Anotasi @Timed dan @Counted hanya bekerja pada metode public dari bean Spring yang dipanggil melalui proxy. Panggilan dari satu metode ke metode lain di kelas yang sama melewati proxy sehingga tidak terinstrumentasi, dan anotasi pada metode private diabaikan. Menganotasi controller Spring MVC yang sudah terinstrumentasi menghasilkan observasi ganda.

Siap menguasai wawancara Spring Boot Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Endpoint Kesehatan Kustom

Health Indicator Bisnis

Health Indicator memverifikasi status dependensi eksternal dan komponen bisnis kritis. Spring Boot menyediakan indicator default untuk basis data, Redis, dan layanan umum lainnya. Di Spring Boot 4, API health berada di modul tersendiri spring-boot-health, dan Health serta HealthIndicator berpindah dari org.springframework.boot.actuate.health ke 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();
        }
    }
}

Indicator ini muncul di /actuator/health dengan nama paymentGateway, yaitu nama bean tanpa sufiks HealthIndicator.

Grup Kesehatan untuk Kubernetes

Spring Boot 4 mengaktifkan probe liveness dan readiness secara default di semua platform, tidak hanya ketika mendeteksi Kubernetes. Grup /actuator/health/liveness dan /actuator/health/readiness sudah tersedia tanpa konfigurasi apa pun, dan bean LivenessStateHealthIndicator atau ReadinessStateHealthIndicator tidak perlu dideklarasikan manual. Konfigurasi hanya diperlukan untuk menambahkan komponen ke sebuah grup atau mendefinisikan grup kustom:

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

Grup liveness sebaiknya tetap terbatas pada livenessState. Menambahkan basis data atau gateway eksternal ke liveness membuat Kubernetes me-restart semua pod ketika dependensi tersebut mati, sehingga gangguan parsial berubah menjadi gangguan total. Dependensi eksternal masuk ke readiness, dan hanya jika aplikasi tidak dapat melayani permintaan apa pun tanpanya.

Probe Kubernetes kemudian diarahkan ke endpoint khusus:

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

Dengan add-additional-paths: true, probe tetap berada di port aplikasi meskipun Actuator berjalan di port manajemen terpisah, sehingga server utama yang bermasalah tetap terdeteksi oleh probe.

Integrasi Prometheus dengan Spring Boot dan Grafana

Konfigurasi Scraping Prometheus

Prometheus mengumpulkan metrik dengan menanyakan endpoint /actuator/prometheus secara berkala. Konfigurasi scrape menentukan target dan path metrik, karena Prometheus secara default memakai /metrics.

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: (.+)

Target memakai port 9090 karena konfigurasi produksi di bagian selanjutnya memindahkan Actuator ke port manajemen terpisah. Tanpa management.server.port, target scrape adalah port aplikasi.

Metrik Spring Boot Actuator yang Dipublikasikan secara Default

Actuator dengan Micrometer menyajikan metrik JVM, proses, HikariCP, dan HTTP tanpa kode apa pun. Kueri PromQL berikut mencakup metrik yang pertama kali dipantau di produksi:

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"} mengembalikan satu deret per memory pool (Eden, Survivor, Old Gen pada G1). Membaginya dengan nilai max tanpa sum by (instance) menghasilkan rasio per pool yang tampak salah di dashboard.

Bean MeterBinder menambahkan gauge tingkat aplikasi yang tidak dicakup oleh metrik default:

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 dan process.uptime sudah dipublikasikan oleh binder bawaan Micrometer. Contoh di atas memperlihatkan polanya, sedangkan binder nyata sebaiknya hanya mendaftarkan apa yang tidak tersedia secara default.

Dashboard Grafana Siap Pakai

Grafana menyediakan dashboard komunitas untuk Spring Boot yang bekerja dengan metrik Actuator standar. Dashboard yang ditulis manual memakai tipe panel timeseries dan 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"
        }
      ]
    }
  ]
}

Banyak dashboard yang disalin dari tutorial lama masih memakai tipe panel graph dan singlestat. Singlestat dihapus di Grafana 8, dan Grafana 12 menghapus dukungan AngularJS sepenuhnya, sehingga panel tersebut hanya tampil setelah dimigrasikan ke timeseries dan stat.

Impor di Grafana

Untuk mengimpor dashboard komunitas: Grafana > Dashboards > New > Import, lalu masukkan ID 4701 (JVM Micrometer) atau 12900 (SpringBoot APM). Periksa tipe panel setelah impor: revisi lama dashboard komunitas mungkin masih berisi panel Angular yang harus dimigrasikan oleh Grafana 12.

Alerting dengan Prometheus

Aturan Alert Penting

Aturan alert Prometheus memicu notifikasi ketika metrik melewati ambang batas kritis.

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 }}"

Alert tersebut mencakup masalah produksi yang paling umum: ketersediaan, performa, dan sumber daya. Aturan HighLatency membutuhkan sum by (le, ...): tanpanya, histogram_quantile menghitung satu P99 per deret (per URI, metode, dan status), dan satu endpoint lambat dengan tiga panggilan sudah cukup untuk memicu alert.

Metrik HTTP dan Metrik Basis Data di Spring Boot

Instrumentasi Otomatis Permintaan HTTP

Spring Boot menginstrumentasi setiap permintaan Spring MVC dan WebFlux yang masuk melalui Observation API dan mempublikasikan timer http.server.requests, dengan tag method, uri, status, outcome, dan 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}

Interface WebMvcTagsContributor dan WebMvcTagsProvider yang muncul di banyak contoh sudah dihapus di Spring Boot 3.0. Tag kustom kini ditambahkan dengan meng-extend DefaultServerRequestObservationConvention, sebagaimana didokumentasikan di referensi metrik 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")
        );
    }
}

Hanya key-value berkardinalitas rendah yang menjadi label Prometheus. Tag tenant masih wajar dengan beberapa puluh tenant dan menjadi masalah kardinalitas dengan ribuan tenant. Nilai berkardinalitas tinggi (ID pengguna, ID pesanan) ditempatkan di getHighCardinalityKeyValues, yang hanya mengisi trace. Sisi tracing dari Observation API dibahas di observability Spring Boot dengan OpenTelemetry.

Metrik HikariCP dan Kueri SQL

Metrik pool HikariCP diekspos otomatis dengan prefiks hikaricp, diberi tag nama pool, berdampingan dengan gauge generik jdbc.connections. Mengatur nama pool membuat tag tersebut mudah dibaca:

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

Pengukuran waktu per kueri tidak dicakup oleh Spring Boot. Proyek datasource-micrometer membungkus DataSource dan membuat satu observasi per koneksi, kueri, dan result set, yang menghasilkan metrik sekaligus span trace. Lini 2.x menargetkan Spring Boot 4, lini 1.x menargetkan 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>

Cara ini menggantikan pendekatan manual yang membungkus DataSource dengan ProxyDataSourceBuilder dan listener buatan sendiri, yang melewati auto-configuration DataSource milik Spring Boot dan harus disesuaikan setiap kali Boot di-upgrade. Kueri lambat biasanya menunjukkan pola N+1, yang dibahas di solusi kueri N+1 Spring Data JPA.

Praktik Terbaik untuk Produksi

Kardinalitas Metrik

Kardinalitas yang berlebihan menurunkan performa Prometheus. Setiap kombinasi tag yang unik menciptakan deret waktu tersendiri.

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));
    }
}

Timer dengan bucket histogram menjawab pertanyaan latensi lebih baik daripada counter dengan tag bucket buatan sendiri: PromQL dapat menghitung persentil apa pun dari bucket tersebut, dan batas bucket tetap konsisten di semua layanan.

Konfigurasi Siap Produksi

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}

Dengan access.default: none, endpoint seperti env, beans, configprops, dan mappings tidak dapat diakses tanpa perlu dicantumkan satu per satu. management.prometheus.metrics.export.enabled bernilai default true, dan pengaturan step tidak berpengaruh pada Prometheus: registry ini berbasis pull, dan interval scrape diatur di prometheus.yml.

Kesalahan Umum pada Actuator dan Prometheus

GejalaPenyebabSolusi
/actuator/prometheus mengembalikan 404Endpoint tidak ada di daftar exposure, atau registry tidak adaTambahkan prometheus ke management.endpoints.web.exposure.include dan pastikan micrometer-registry-prometheus ada di classpath
Target Prometheus menampilkan "server returned HTTP status 401"Spring Security melindungi semua endpoint kecuali healthBuka prometheus untuk jaringan scraper atau konfigurasikan basic auth di konfigurasi scrape
@Timed tidak menghasilkan metrikmanagement.observations.annotations.enabled bernilai false secara default, atau AspectJ tidak adaAtur properti ke true dan tambahkan spring-boot-starter-aspectj
spring-boot-starter-aop tidak ditemukan setelah upgradeDiganti namanya di Spring Boot 4Gunakan spring-boot-starter-aspectj
Import Health atau HealthIndicator gagal di Boot 4Package berpindah ke org.springframework.boot.health.contributorPerbarui import
Peringatan deprecation pada management.endpoint.*.enabledModel on/off diganti di Spring Boot 3.4Gunakan management.endpoints.access.default dan management.endpoint.<id>.access
Persentil di dashboard jauh berbeda dari kenyataanNilai publishPercentiles dirata-ratakan antar-podGunakan publishPercentileHistogram() dan histogram_quantile()

Pertanyaan Wawancara tentang Micrometer di Spring Boot

Actuator muncul di sebagian besar wawancara Spring Boot untuk posisi mid-level dan senior. Jawaban yang membedakan profil berpengalaman dari junior:

  • Perbedaan liveness dan readiness: liveness memberi tahu Kubernetes untuk me-restart pod, readiness memberi tahu Kubernetes untuk berhenti mengarahkan trafik. Kandidat yang memasukkan basis data ke grup liveness belum pernah mengalaminya saat terjadi gangguan.
  • Mengapa Micrometer dan bukan client Prometheus secara langsung: Micrometer adalah facade. Kode Counter dan Timer yang sama dapat mengekspor ke Prometheus, Datadog, atau OTLP hanya dengan mengganti dependensi, dan instrumentasi bawaan Spring (HTTP, pool JDBC, cache) ditulis di atasnya.
  • Persentil sisi klien versus histogram: persentil sisi klien tidak dapat diagregasi antar-instance, histogram dapat. Ini pertanyaan jebakan yang paling sering muncul untuk topik ini.
  • Arti kardinalitas dan bagaimana ia merusak Prometheus: satu deret waktu per kombinasi label yang unik; tag ID pengguna melipatgandakan penggunaan memori sebanyak jumlah pengguna.
  • Cara mengamankan Actuator: batasi exposure, tetapkan level akses, jalankan manajemen di port terpisah, dan lindungi dengan SecurityFilterChain khusus.

Pertanyaan sejenis lainnya dikumpulkan di modul pertanyaan wawancara Spring Boot Actuator.

Sumber

Hal yang Perlu Diingat tentang Micrometer Spring Boot dan Prometheus

  • Tiga langkah menuju scrape yang berfungsi: starter Actuator, micrometer-registry-prometheus, prometheus di daftar exposure
  • Model akses sejak 3.4: management.endpoints.access.default: none ditambah read-only per endpoint menggantikan flag enabled yang deprecated
  • Perubahan di Spring Boot 4: spring-boot-starter-aspectj, API health di org.springframework.boot.health.contributor, EndpointRequest di org.springframework.boot.security.autoconfigure.actuate.web.servlet, probe aktif secara default
  • Anotasi butuh sakelar: management.observations.annotations.enabled=true, tanpa bean TimedAspect manual
  • Tag HTTP melalui Observation API: extend DefaultServerRequestObservationConvention, jaga label tetap berkardinalitas rendah
  • Histogram, bukan persentil sisi klien: publishPercentileHistogram() dan sum by (le) di setiap histogram_quantile
  • Liveness tetap lokal: dependensi eksternal masuk ke readiness atau grup kustom, tidak pernah ke liveness

Default ini memberi layanan Spring Boot 4 metrik yang teragregasi dengan benar antar-pod, endpoint yang tidak membocorkan apa pun ke jaringan publik, dan alert yang terpicu oleh insiden nyata, bukan oleh satu URI yang lambat.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di Spring Boot?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 26 Oktober 2026

Tag

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

Bagikan

Artikel terkait