Micrometer และ Prometheus ใน Spring Boot: เมตริก Actuator บนโปรดักชัน
การเชื่อม Micrometer กับ Prometheus บน Spring Boot 4: โมเดลการเข้าถึงของ Actuator, การตั้งค่า scrape, เมตริก HTTP, health probe และกฎแจ้งเตือน

Spring Boot Actuator เปิด endpoint ด้าน health, เมตริก และการวินิจฉัย โดยมี Micrometer เป็น facade ของเมตริกอยู่เบื้องหลัง การเพิ่ม dependency micrometer-registry-prometheus จะได้ endpoint /actuator/prometheus ที่ Prometheus เข้ามา scrape ตามรอบเวลาที่กำหนด ตัวอย่างในบทความนี้อ้างอิง Spring Boot 4.1 (เวอร์ชัน GA ปัจจุบัน โดย 4.2 อยู่ในขั้น release candidate), Micrometer 1.16 และ Prometheus Java client 1.x
การเชื่อม Micrometer ของ Spring Boot เข้ากับ Prometheus มีสามขั้นตอน: เพิ่ม spring-boot-starter-actuator และ micrometer-registry-prometheus, เพิ่ม prometheus ลงใน management.endpoints.web.exposure.include จากนั้นตั้ง scrape_config ของ Prometheus ให้ชี้ไปที่ /actuator/prometheus เมตริกของ JVM, HTTP, HikariCP และระบบจะถูกเผยแพร่โดยไม่ต้องเขียนโค้ดเพิ่ม
การตั้งค่า Micrometer ใน Spring Boot: Actuator และ Registry ของ Prometheus
Dependency Maven ที่ต้องใช้
starter ของ Actuator เปิดใช้งาน endpoint และดึง Micrometer เข้ามาด้วย registry ของ Prometheus จัดรูปแบบ meter ทุกตัวตาม exposition format ของ Prometheus ส่วน starter ของ AspectJ จำเป็นเฉพาะเมื่อใช้แอนโนเทชัน @Timed และ @Counted
<!-- 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>บทแนะนำรุ่นเก่ายังมีกับดักเรื่องเวอร์ชันอยู่สองจุด ตั้งแต่ Micrometer 1.13 (Spring Boot 3.3) micrometer-registry-prometheus สร้างอยู่บน Prometheus Java client 1.x ส่วนโค้ด 0.x เดิมย้ายไปอยู่ในโมดูล micrometer-registry-prometheus-simpleclient ซึ่งถูก deprecate แล้ว และเอกสาร Micrometer Prometheus ระบุว่าเป็นเพียงทางออกชั่วคราว อีกจุดคือบน Spring Boot 4 ไฟล์ pom.xml ที่ยังประกาศ spring-boot-starter-aop จะ resolve ไม่ได้อีกต่อไป เพราะคู่มือการย้ายไป Spring Boot 4.0 เปลี่ยนชื่อเป็น spring-boot-starter-aspectj
การตั้งค่า Endpoint ของ Actuator
ค่าเริ่มต้นจะเปิดเฉพาะ endpoint health ผ่าน HTTP เท่านั้น endpoint /actuator/prometheus มีอยู่ทันทีที่ registry อยู่ใน classpath แต่จะตอบกลับ 404 จนกว่าจะถูกเพิ่มเข้าไปในรายการ exposure
Spring Boot 3.4 แทนที่สวิตช์เปิด/ปิด (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) ด้วยโมเดลการเข้าถึง (access model) และ property เดิมถูก deprecate แล้ว รูปแบบ opt-in คือกำหนดสิทธิ์เริ่มต้นเป็น none แล้วให้สิทธิ์ read-only ทีละ endpoint:
# 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: unrestrictedExposure และ access เป็นการควบคุมสองชั้นที่แยกจากกัน exposure.include กำหนดว่า endpoint จะถูก map ผ่าน HTTP หรือไม่ ส่วน access กำหนดว่า endpoint อนุญาตให้ทำอะไรได้บ้างเมื่อถูก map แล้ว ไฟล์ property ที่ยังใช้ enabled: true จะยังทำงานได้พร้อมคำเตือน deprecation ซึ่งเป็นจังหวะที่ควรย้ายไปใช้รูปแบบใหม่
ตัวเลือก show-details: when-authorized จะแสดงรายละเอียดสุขภาพเฉพาะกับผู้ใช้ที่ผ่านการยืนยันตัวตนและมีบทบาทเหมาะสมเท่านั้น ตัว endpoint เองได้รับการป้องกันด้วย SecurityFilterChain เฉพาะ:
// 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();
}
}ใน Spring Boot 4 EndpointRequest ย้ายไปอยู่ที่ org.springframework.boot.security.autoconfigure.actuate.web.servlet ตามที่แสดงในเอกสารอ้างอิง endpoint ของ Actuator DSL แบบ Java AuthorizedUrl ของ authorizeHttpRequests ไม่มีเมธอด hasIpAddress กฎเรื่อง IP จึงต้องผ่าน IpAddressAuthorizationManager.hasIpAddress ซึ่งมีให้ใช้ตั้งแต่ Spring Security 6.3
เมตริกแบบกำหนดเองด้วย Micrometer
Counter, Gauge และ Timer ระดับแอปพลิเคชัน
Micrometer มีเมตริกหลายชนิดให้เลือกใช้ตามสถานการณ์ Counter วัดเหตุการณ์สะสม gauge วัดค่าชั่วขณะ ส่วน timer วัดระยะเวลาของการดำเนินการ
// 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);
}
}เมื่อใช้ registry ของ Prometheus มีรายละเอียดเรื่องการตั้งชื่ออยู่สองข้อ ชื่อ meter ของ Micrometer ใช้จุดคั่นและจะถูกแปลงเป็น snake case (orders.processing.duration กลายเป็น orders_processing_duration_seconds) Counter จะได้รับคำต่อท้าย _total โดยอัตโนมัติ การตั้งชื่อ counter ว่า orders.created.total จึงเพิ่มแต่ความรกโดยไม่จำเป็น
publishPercentiles(0.5, 0.95, 0.99) คำนวณ percentile ภายในแต่ละ JVM และค่าเหล่านี้นำมารวมข้าม instance ไม่ได้ ส่วน publishPercentileHistogram() เผยแพร่ bucket ที่ histogram_quantile() นำมารวมได้ทั้งคลัสเตอร์ จึงเป็นตัวเลือกที่ควรใช้เมื่อทำงานร่วมกับ Prometheus
แอนโนเทชัน @Timed และ @Counted
เพื่อหลีกเลี่ยงโค้ดซ้ำซาก Micrometer มีแอนโนเทชันที่ทำการ instrument เมธอดผ่าน AspectJ
// 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
}
}# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
observations:
annotations:
enabled: trueตั้งแต่ Spring Boot 3.2 ไม่จำเป็นต้องประกาศ bean TimedAspect และ CountedAspect เองอีกต่อไป การตั้ง management.observations.annotations.enabled=true พร้อมมี spring-boot-starter-aspectj อยู่ใน classpath จะ auto-configure ให้ทันที ค่าเริ่มต้นของ property นี้คือ false ซึ่งเป็นสาเหตุของรายงานส่วนใหญ่ประเภท "@Timed ไม่ทำงาน"
แอนโนเทชัน @Timed และ @Counted ใช้ได้เฉพาะกับเมธอด public ของ bean ของ Spring ที่ถูกเรียกผ่าน proxy การเรียกจากเมธอดหนึ่งไปอีกเมธอดในคลาสเดียวกันจะข้าม proxy จึงไม่ถูก instrument และแอนโนเทชันบนเมธอด private จะถูกละเลย การใส่แอนโนเทชันบน controller ของ Spring MVC ที่ถูก instrument อยู่แล้วจะทำให้เกิด observation ซ้ำซ้อน
พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Endpoint สุขภาพแบบกำหนดเอง
Health Indicator เชิงธุรกิจ
Health Indicator ตรวจสอบสถานะของบริการภายนอกและองค์ประกอบทางธุรกิจที่สำคัญ Spring Boot มี indicator มาตรฐานสำหรับฐานข้อมูล Redis และบริการที่พบบ่อยอื่น ๆ ใน Spring Boot 4 API ด้าน health แยกไปอยู่ในโมดูล spring-boot-health ของตัวเอง และ Health กับ HealthIndicator ย้ายจาก org.springframework.boot.actuate.health ไปที่ org.springframework.boot.health.contributor
// 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 นี้จะปรากฏใน /actuator/health ภายใต้ชื่อ paymentGateway ซึ่งก็คือชื่อ bean ที่ตัดคำต่อท้าย HealthIndicator ออก
กลุ่มสุขภาพสำหรับ Kubernetes
Spring Boot 4 เปิดใช้ probe liveness และ readiness เป็นค่าเริ่มต้นบนทุกแพลตฟอร์ม ไม่ใช่เฉพาะเมื่อตรวจพบ Kubernetes กลุ่ม /actuator/health/liveness และ /actuator/health/readiness มีให้ใช้โดยไม่ต้องตั้งค่าใด ๆ และไม่จำเป็นต้องประกาศ bean LivenessStateHealthIndicator หรือ ReadinessStateHealthIndicator เอง การตั้งค่าจำเป็นเฉพาะเมื่อต้องการเพิ่มองค์ประกอบเข้าไปในกลุ่ม หรือกำหนดกลุ่มขึ้นเอง:
# 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กลุ่ม liveness ควรจำกัดไว้ที่ livenessState เท่านั้น การใส่ฐานข้อมูลหรือ gateway ภายนอกเข้าไปใน liveness จะทำให้ Kubernetes รีสตาร์ททุก pod เมื่อ dependency นั้นล่ม ทำให้ปัญหาที่กระทบบางส่วนกลายเป็นระบบล่มทั้งหมด dependency ภายนอกควรอยู่ใน readiness และเฉพาะกรณีที่แอปพลิเคชันไม่สามารถให้บริการคำขอใดได้เลยหากขาดมัน
จากนั้น probe ของ Kubernetes ก็ชี้ไปยัง endpoint เฉพาะได้:
# 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เมื่อตั้ง add-additional-paths: true probe จะยังอยู่บนพอร์ตของแอปพลิเคชันแม้ Actuator จะรันบนพอร์ต management แยก ทำให้ probe ตรวจพบได้เมื่อเซิร์ฟเวอร์หลักมีปัญหา
การเชื่อม Prometheus กับ Spring Boot และ Grafana
การตั้งค่า Scraping ของ Prometheus
Prometheus เก็บเมตริกโดยเรียกดู endpoint /actuator/prometheus เป็นระยะ การตั้งค่า scrape จะระบุเป้าหมายและ path ของเมตริก เนื่องจากค่าเริ่มต้นของ Prometheus คือ /metrics
# 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: (.+)เป้าหมายใช้พอร์ต 9090 เพราะการตั้งค่าโปรดักชันในส่วนถัดไปย้าย Actuator ไปไว้บนพอร์ต management แยก หากไม่มี management.server.port เป้าหมายของการ scrape คือพอร์ตของแอปพลิเคชัน
เมตริกของ Spring Boot Actuator ที่เผยแพร่เป็นค่าเริ่มต้น
Actuator ที่ทำงานคู่กับ Micrometer เปิดเผยเมตริกของ JVM, process, HikariCP และ HTTP โดยไม่ต้องเขียนโค้ด คิวรี 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 * 100jvm_memory_used_bytes{area="heap"} คืนค่าหนึ่ง series ต่อหนึ่ง memory pool (Eden, Survivor, Old Gen เมื่อใช้ G1) การหารด้วยค่า max โดยไม่มี sum by (instance) จะได้อัตราส่วนแยกตาม pool ซึ่งดูผิดเพี้ยนบนแดชบอร์ด
bean MeterBinder ใช้เพิ่ม gauge ระดับแอปพลิเคชันที่เมตริกเริ่มต้นไม่ได้ครอบคลุม:
// 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 และ process.uptime ถูกเผยแพร่โดย binder ที่มีมาในตัวของ Micrometer อยู่แล้ว ตัวอย่างข้างต้นแสดงรูปแบบการเขียน ส่วน binder ที่ใช้งานจริงควรลงทะเบียนเฉพาะสิ่งที่ค่าเริ่มต้นยังไม่มี
แดชบอร์ด Grafana ที่พร้อมใช้งาน
Grafana มีแดชบอร์ดจากชุมชนสำหรับ Spring Boot ที่ทำงานกับเมตริกมาตรฐานของ Actuator แดชบอร์ดที่เขียนเองใช้ panel ชนิด timeseries และ stat:
{
"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"
}
]
}
]
}แดชบอร์ดจำนวนมากที่คัดลอกมาจากบทแนะนำรุ่นเก่ายังใช้ panel ชนิด graph และ singlestat Singlestat ถูกถอดออกใน Grafana 8 และGrafana 12 ยกเลิกการรองรับ AngularJS ทั้งหมด panel เหล่านั้นจึงแสดงผลได้ก็ต่อเมื่อย้ายไปใช้ timeseries และ stat แล้ว
วิธีนำเข้าแดชบอร์ดจากชุมชน: Grafana > Dashboards > New > Import แล้วใส่ ID 4701 (JVM Micrometer) หรือ 12900 (SpringBoot APM) ควรตรวจสอบชนิดของ panel หลังนำเข้า เพราะแดชบอร์ดชุมชนรุ่นเก่าอาจยังมี panel แบบ Angular ที่ Grafana 12 ต้องแปลงให้
การแจ้งเตือนด้วย Prometheus
กฎแจ้งเตือนที่จำเป็น
กฎแจ้งเตือนของ Prometheus จะทริกเกอร์การแจ้งเตือนเมื่อเมตริกเกินเกณฑ์วิกฤต
# 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 }}"การแจ้งเตือนเหล่านี้ครอบคลุมปัญหาที่พบบ่อยที่สุดในโปรดักชัน ทั้งความพร้อมใช้งาน ประสิทธิภาพ และทรัพยากร กฎ HighLatency ต้องมี sum by (le, ...) เพราะหากไม่มี histogram_quantile จะคำนวณ P99 แยกต่อ series (ต่อ URI, method และ status) และ endpoint ที่ช้าเพียงตัวเดียวซึ่งถูกเรียกแค่สามครั้งก็ทำให้การแจ้งเตือนทำงานได้
เมตริก HTTP และเมตริกฐานข้อมูลของ Spring Boot
การ Instrument คำขอ HTTP โดยอัตโนมัติ
Spring Boot ทำการ instrument คำขอ Spring MVC และ WebFlux ทุกรายการที่เข้ามาผ่าน Observation API และเผยแพร่ timer http.server.requests พร้อม tag method, uri, status, outcome และ exception
# 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 และ WebMvcTagsProvider ที่ปรากฏในตัวอย่างจำนวนมากถูกถอดออกไปตั้งแต่ Spring Boot 3.0 ปัจจุบันการเพิ่ม tag แบบกำหนดเองทำได้ด้วยการ extend DefaultServerRequestObservationConvention ตามที่เอกสารอ้างอิงเมตริกของ Spring Boot อธิบายไว้:
// 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")
);
}
}เฉพาะ key-value ที่มี cardinality ต่ำเท่านั้นที่กลายเป็น label ของ Prometheus tag tenant ยังรับได้เมื่อมี tenant หลักสิบ แต่จะกลายเป็นปัญหา cardinality เมื่อมีหลักพัน ค่าที่มี cardinality สูง (ID ผู้ใช้, ID คำสั่งซื้อ) ควรอยู่ใน getHighCardinalityKeyValues ซึ่งส่งไปยัง trace เท่านั้น ฝั่ง tracing ของ Observation API อธิบายไว้อย่างละเอียดใน observability ของ Spring Boot ด้วย OpenTelemetry
เมตริก HikariCP และคิวรี SQL
เมตริกของ pool HikariCP ถูกเปิดเผยให้โดยอัตโนมัติด้วย prefix hikaricp ติด tag ตามชื่อ pool และอยู่คู่กับ gauge ทั่วไป jdbc.connections การตั้งชื่อ pool ทำให้ tag อ่านเข้าใจง่าย:
# 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: 1800000Spring Boot ไม่ได้วัดเวลาในระดับคิวรี โปรเจกต์ datasource-micrometer ครอบ DataSource และสร้าง observation หนึ่งรายการต่อการเชื่อมต่อ คิวรี และ result set ซึ่งให้ทั้งเมตริกและ span ของ trace สาย 2.x รองรับ Spring Boot 4 ส่วนสาย 1.x รองรับ Spring Boot 3:
<!-- 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>วิธีนี้แทนที่แนวทางแบบเขียนเองที่ครอบ DataSource ด้วย ProxyDataSourceBuilder และ listener ที่เขียนขึ้นเอง ซึ่งข้าม auto-configuration ของ DataSource ใน Spring Boot และต้องคอยปรับตามทุกครั้งที่อัปเกรด Boot คิวรีที่ช้ามักบ่งบอกถึงรูปแบบ N+1 ซึ่งอธิบายไว้ใน วิธีแก้ปัญหาคิวรี N+1 ของ Spring Data JPA
แนวทางปฏิบัติที่ดีบนโปรดักชัน
Cardinality ของเมตริก
Cardinality ที่มากเกินไปจะลดประสิทธิภาพของ Prometheus เพราะแต่ละการรวม tag ที่ไม่ซ้ำกันจะสร้าง time series ใหม่หนึ่งชุด
// 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();
}
}// 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 ที่มี bucket แบบ histogram ตอบคำถามเรื่อง latency ได้ดีกว่า counter ที่ใส่ tag bucket ขึ้นเอง เพราะ PromQL คำนวณ percentile ใดก็ได้จาก bucket และขอบเขตของ bucket ก็สอดคล้องกันในทุกเซอร์วิส
การตั้งค่าที่พร้อมใช้งานบนโปรดักชัน
# 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}เมื่อใช้ access.default: none endpoint อย่าง env, beans, configprops และ mappings จะเข้าถึงไม่ได้โดยไม่ต้องไล่ระบุทีละตัว management.prometheus.metrics.export.enabled มีค่าเริ่มต้นเป็น true และการตั้งค่า step ไม่มีผลกับ Prometheus เพราะเป็น registry แบบ pull ซึ่งรอบการ scrape กำหนดไว้ใน prometheus.yml
ข้อผิดพลาดที่พบบ่อยกับ Actuator และ Prometheus
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
/actuator/prometheus ตอบกลับ 404 | endpoint ไม่อยู่ในรายการ exposure หรือไม่มี registry | เพิ่ม prometheus ลงใน management.endpoints.web.exposure.include และตรวจสอบว่า micrometer-registry-prometheus อยู่ใน classpath |
| เป้าหมายใน Prometheus แสดง "server returned HTTP status 401" | Spring Security ป้องกันทุก endpoint ยกเว้น health | เปิด prometheus ให้เครือข่ายของ scraper หรือตั้งค่า basic auth ใน scrape config |
@Timed ไม่สร้างเมตริก | management.observations.annotations.enabled เป็น false โดยค่าเริ่มต้น หรือไม่มี AspectJ | ตั้ง property เป็น true และเพิ่ม spring-boot-starter-aspectj |
หา spring-boot-starter-aop ไม่พบหลังอัปเกรด | ถูกเปลี่ยนชื่อใน Spring Boot 4 | ใช้ spring-boot-starter-aspectj |
import Health หรือ HealthIndicator ล้มเหลวบน Boot 4 | package ย้ายไปที่ org.springframework.boot.health.contributor | แก้ไข import |
คำเตือน deprecation บน management.endpoint.*.enabled | โมเดลเปิด/ปิดถูกแทนที่ใน Spring Boot 3.4 | ใช้ management.endpoints.access.default และ management.endpoint.<id>.access |
| percentile บนแดชบอร์ดต่างจากความเป็นจริงมาก | ค่า publishPercentiles ถูกนำมาเฉลี่ยข้าม pod | ใช้ publishPercentileHistogram() และ histogram_quantile() |
คำถามสัมภาษณ์งานเกี่ยวกับ Micrometer ใน Spring Boot
Actuator ถูกถามในการสัมภาษณ์ Spring Boot ส่วนใหญ่สำหรับตำแหน่งระดับกลางและอาวุโส คำตอบที่แยกผู้มีประสบการณ์ออกจากระดับจูเนียร์มีดังนี้:
- ความต่างระหว่าง liveness และ readiness: liveness บอกให้ Kubernetes รีสตาร์ท pod ส่วน readiness บอกให้หยุดส่ง traffic เข้ามา ผู้สมัครที่ใส่ฐานข้อมูลไว้ในกลุ่ม liveness คือผู้ที่ยังไม่เคยผ่านเหตุการณ์ระบบล่มกับการตั้งค่านั้นจริง
- ทำไมใช้ Micrometer แทนการใช้ client ของ Prometheus โดยตรง: Micrometer เป็น facade โค้ด
CounterและTimerชุดเดียวกันส่งออกไปยัง Prometheus, Datadog หรือ OTLP ได้เพียงเปลี่ยน dependency และการ instrument ที่มีมากับ Spring เอง (HTTP, pool ของ JDBC, cache) ก็เขียนอยู่บน Micrometer - percentile ฝั่ง client เทียบกับ histogram: percentile ฝั่ง client นำมารวมข้าม instance ไม่ได้ แต่ histogram ทำได้ นี่คือคำถามหลอกที่พบบ่อยที่สุดในหัวข้อนี้
- cardinality คืออะไรและทำให้ Prometheus พังได้อย่างไร: หนึ่ง time series ต่อหนึ่งการรวม label ที่ไม่ซ้ำกัน tag ที่เป็น ID ผู้ใช้จะทำให้การใช้หน่วยความจำเพิ่มขึ้นเป็นทวีคูณตามจำนวนผู้ใช้
- วิธีรักษาความปลอดภัยของ Actuator: จำกัด exposure, กำหนดระดับการเข้าถึง, รัน management บนพอร์ตแยก และป้องกันด้วย
SecurityFilterChainเฉพาะ
คำถามในลักษณะเดียวกันรวบรวมไว้ในโมดูลคำถามสัมภาษณ์ Spring Boot Actuator
แหล่งอ้างอิง
- Spring Boot 4.0 Migration Guide:
spring-boot-starter-aopเปลี่ยนชื่อเป็นspring-boot-starter-aspectj, probe เปิดเป็นค่าเริ่มต้น, โมดูลใหม่spring-boot-health - Spring Boot 3.4 Release Notes:
enabled-by-defaultและendpoint.<id>.enabledถูก deprecate และแทนที่ด้วยโมเดลการเข้าถึง - เอกสารอ้างอิง endpoint ของ Spring Boot Actuator: property ด้าน access, package ของ
EndpointRequest, health indicator ในorg.springframework.boot.health.contributor, กลุ่ม probe และ path เพิ่มเติม - เอกสารอ้างอิงเมตริกของ Spring Boot:
DefaultServerRequestObservationConvention, เมตริก HikariCP และjdbc.connections, การ deprecate simpleclient - เอกสารอ้างอิง observability ของ Spring Boot:
management.observations.annotations.enabledและความจำเป็นของ AspectJ - เอกสาร Micrometer Prometheus: registry บน Prometheus Java client 1.x และโมดูล simpleclient รุ่นเก่า
- datasource-micrometer บน GitHub: 2.x สำหรับ Spring Boot 4, 1.x สำหรับ Spring Boot 3
- Grafana: การยกเลิก AngularJS ใน Grafana 12: panel Graph และ Singlestat ถูกแทนที่ด้วย Time series และ Stat
- รายการ release ของ Spring Boot: 4.1.2 เป็นรุ่น GA ปัจจุบัน, 4.2.0-RC1 อยู่ในขั้น pre-release
สิ่งที่ควรจำเกี่ยวกับ Micrometer ของ Spring Boot และ Prometheus
- สามขั้นตอนสู่การ scrape ที่ใช้งานได้: starter ของ Actuator,
micrometer-registry-prometheus,prometheusในรายการ exposure - โมเดลการเข้าถึงตั้งแต่ 3.4:
management.endpoints.access.default: noneร่วมกับread-onlyราย endpoint แทนที่แฟล็กenabledที่ถูก deprecate - การเปลี่ยนแปลงใน Spring Boot 4:
spring-boot-starter-aspectj, API ด้าน health ในorg.springframework.boot.health.contributor,EndpointRequestในorg.springframework.boot.security.autoconfigure.actuate.web.servlet, probe เปิดเป็นค่าเริ่มต้น - แอนโนเทชันต้องเปิดสวิตช์:
management.observations.annotations.enabled=trueโดยไม่ต้องสร้าง beanTimedAspectเอง - tag ของ HTTP ผ่าน Observation API: extend
DefaultServerRequestObservationConventionและรักษา label ให้มี cardinality ต่ำ - ใช้ histogram แทน percentile ฝั่ง client:
publishPercentileHistogram()และsum by (le)ในทุกhistogram_quantile - liveness ตรวจเฉพาะภายในตัวเอง: dependency ภายนอกอยู่ใน readiness หรือกลุ่มที่กำหนดเอง ไม่ใช่ใน liveness
ค่าเริ่มต้นเหล่านี้ทำให้เซอร์วิส Spring Boot 4 มีเมตริกที่รวมข้าม pod ได้ถูกต้อง มี endpoint ที่ไม่รั่วไหลข้อมูลออกสู่เครือข่ายสาธารณะ และมีการแจ้งเตือนที่ทำงานเมื่อเกิดเหตุจริง ไม่ใช่เพราะ URI ที่ช้าเพียงตัวเดียว
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
คุณหาบั๊กใน Spring Boot เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 26 ตุลาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Observability ใน Spring Boot 2026: OpenTelemetry, Distributed Tracing และคำถามสัมภาษณ์
เรียนรู้ observability ใน Spring Boot ด้วย OpenTelemetry และ Micrometer Tracing คู่มือการตั้งค่า distributed tracing, Observation API, การส่งออก OTLP และการเตรียมตัวสัมภาษณ์งานเทคนิค

Spring Boot Structured Logging ในปี 2026: Log JSON สำหรับ Production ด้วย Logback และ OpenTelemetry
คู่มือฉบับสมบูรณ์สำหรับ structured logging Spring Boot 4.x รองรับ JSON แบบ native, OpenTelemetry starter, MDC tracing และการรวมกับ ELK Stack สำหรับ observability ใน production

งาน Freelance Developer Java Spring Boot 2026: หาสัญญาที่ไหน อัตราค่าจ้างรายวัน และการเตรียมตัว
คู่มือครบถ้วนสำหรับ developer freelance Java Spring Boot ในปี 2026: แพลตฟอร์มค้นหาสัญญา อัตราค่าจ้างรายวันในแต่ละประเทศ และวิธีเตรียมตัวสำหรับสัมภาษณ์เทคนิค