Spring Boot 应用 Metrics 埋点实践记录(2026)
目录
为什么 Metrics 是可观测性的基础信号#
在日志、链路追踪(Traces)和 Metrics 三大可观测性信号中,Metrics 往往是最先被告警系统用到的那个。
Metrics 是聚合数据:它告诉你"过去 1 分钟,/api/hello 接口平均延迟 320ms,错误率 0.3%"。日志是个体事件:它记录每一次请求的具体参数。两者不是替代关系,而是分工:Metrics 负责趋势感知和告警触发,日志负责单点定位和细节还原,链路追踪负责调用链拼接。
RED 方法论是 Metrics 最重要的一套设计框架,适合所有面向用户的服务:
| 指标 | 英文 | 含义 |
|---|---|---|
| 请求速率 | Rate | 每秒处理多少请求 |
| 错误率 | Error | 失败请求占比 |
| 请求耗时 | Duration | P50 / P95 / P99 延迟分布 |
本文以 springboot3.5-otel 项目(三服务微服务架构)为参考,整理一次 Spring Boot 应用层 Metrics 埋点实践。虽然该项目通过 OTel Collector 导出 Metrics,但本文大部分应用层代码对直接使用 Prometheus 的团队同样适用——因为埋点 API 是 Micrometer 的抽象层,与后端无关。
一、Spring Boot Metrics 技术栈:后端无关的 Micrometer 抽象#
Micrometer 的核心价值#
Micrometer 是 Spring Boot 的 Metrics 门面(类似 SLF4J 之于日志),它提供统一的埋点 API,在应用层之上屏蔽了不同监控后端的差异:
应用代码(@Observed / ObservationRegistry / MeterRegistry)
↓ Micrometer 抽象层
多注册中心(任选其一或多个):
- PrometheusMeterRegistry → /actuator/prometheus → Prometheus 抓取
- OtlpMeterRegistry → OTLP HTTP → OTel Collector
- DatadogMeterRegistry → Datadog Agent
- CloudWatchMeterRegistry → AWS CloudWatch
- ...
关键结论:应用层的埋点代码(@Observed、ObservationRegistry、Observation API)与最终的导出后端无关。团队现在用 Prometheus,未来切换到 OTel Collector,通常不需要修改业务代码,只需调整依赖和配置。
参考项目的导出链路#
Spring Boot 应用
└─ micrometer-registry-otlp(每 10s push)
└─ OTLP HTTP 4318
└─ OTel Collector
└─ Prometheus(Grafana LGTM 内置)
直接使用 Prometheus 的团队,导出链路更简单:
Spring Boot 应用
└─ micrometer-registry-prometheus(Actuator 暴露)
└─ /actuator/prometheus
└─ Prometheus(主动抓取,scrape_interval: 15s)
二、开箱即用的自动埋点#
Spring Boot Actuator + Micrometer 自动配置覆盖了大量常见场景,无需任何手动代码。
HTTP Server(大多数应用都会有)#
只要引入 spring-boot-starter-actuator,所有 HTTP 请求自动生成 http.server.requests 指标:
http.server.requests{method="GET", uri="/api/hello", status="200", outcome="SUCCESS"}
- 低基数维度(适合作为 Label):
method、uri(路由模板,不是完整 URL)、status、outcome - 自动统计:计数、总耗时、最大耗时、分位数(需额外配置)
Micrometer 对
uri做了路由模板归一化处理:/api/users/123→/api/users/{id},避免高基数爆炸。
HTTP Client(RestClient / RestTemplate)#
// 这里要注入 Spring 管理的 RestClient.Builder,才能获得自动埋点
// Spring Boot 自动配置会在 RestClient.Builder 上挂载 ObservationInterceptor
@Configuration
public class HttpClientConfig {
@Bean
public RestClient userServiceClient(RestClient.Builder builder) {
return builder
.baseUrl("http://user-service")
.build();
}
}
注意:
RestClient.builder()手动 new 出来的 builder 不会经过 Spring Boot 的自动配置,缺少ObservationInterceptor,导致出站请求的http.client.requests指标和 Trace Span 都不会生成。这里更稳妥的做法仍然是注入RestClient.Builder。
自动维度:method、uri、status、client.name(目标服务)。
JDBC#
连接池指标(内置,零配置):Spring Boot 集成 HikariCP 后,自动暴露连接池维度的指标:
| 指标名 | 含义 |
|---|---|
hikaricp.connections.active |
当前活跃连接数 |
hikaricp.connections.idle |
空闲连接数 |
hikaricp.connections.pending |
等待获取连接的线程数 |
hikaricp.connections.creation |
连接创建耗时 |
hikaricp.connections.acquire |
获取连接耗时(排队等待时间) |
per-query Span(需额外依赖):连接池指标无法定位是哪条 SQL 慢。若需要 SQL 级别的 Trace Span,引入 datasource-micrometer:
// user-service/build.gradle.kts
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.6")
引入后,所有 SQL 查询自动生成 db.query Trace Span,包含 SQL 语句和执行耗时,无需修改任何 Repository 代码。
Redis(Lettuce 自动仪表)#
spring-boot-starter-data-redis 默认使用 Lettuce 驱动,Micrometer 对其有开箱即用的 Observation 支持:
db.redis{command="GET", local.address="...", remote.address="..."}
Kafka Producer / Consumer#
在配置文件中开启观测即可:
spring:
kafka:
template:
observation-enabled: true # KafkaTemplate 生产者 metrics
listener:
observation-enabled: true # @KafkaListener 消费者 metrics
自动生成 messaging.publish(生产)和 messaging.process(消费)指标,以及对应 Trace Span,并自动通过 Kafka Header 传播 TraceContext——这是分布式追踪的关键。
JVM 基础指标#
| 指标名 | 含义 |
|---|---|
jvm.memory.used |
JVM 各内存区域已使用量 |
jvm.threads.live |
存活线程数(Java 21+ 包含虚拟线程) |
jvm.gc.pause |
GC 暂停时间 |
jvm.classes.loaded |
已加载类数量 |
process.cpu.usage |
进程 CPU 使用率 |
Virtual Threads 与 Observation 上下文传播(Java 25 / Spring Boot 3.5):
Spring Boot 3.5 启用虚拟线程只需一行配置:
spring:
threads:
virtual:
enabled: true
关键特性:在 Spring Boot 3.5 + Java 25 这组组合里,Micrometer 的 ObservationRegistry 已经能比较自然地配合虚拟线程使用——TraceContext 和 Observation 上下文通常可以自动传播,不需要额外样板代码。这意味着:
- 每个虚拟线程的 Observation 链路自动隔离,互不污染
- 异步任务(
async、Kafka listener)在虚拟线程中执行时,TraceContext 仍正确传播 - 性能指标(如
http.server.requests、greeting.resolve)在虚拟线程环境下统计准确
虚拟线程在 Java 25 上已经是比较稳妥的选择,但吞吐提升幅度仍然高度依赖应用模型、数据库连接池和下游限流条件。对于 I/O 密集型场景,它经常能带来收益;至于具体能提升多少,最好还是以自己的压测结果为准。
Metrics 侧通常不需要为虚拟线程额外写特殊处理,正常的 @Observed 和 Observation 埋点在虚拟线程中运行时一般可以自动适配。详见《Spring Boot 3.5 + OpenTelemetry 实践记录(2026)》中虚拟线程配置和性能基准章节。
三、应用层手动埋点实践#
自动埋点覆盖了基础的 Infrastructure 层面,但业务逻辑的 Metrics 需要手动埋点。
3.1 @Observed 注解:最简单的埋点方式#
@Observed 是 Micrometer 提供的 AOP 注解,标注在方法上,自动记录调用次数、耗时,并创建 Trace Span。
启用前提(Spring Boot 3.5 需显式开启,Spring Boot 4 默认开启):
management:
observations:
annotations:
enabled: true
<!-- pom.xml 或 build.gradle:AOP 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
基本用法:
@Service
public class HelloService {
@Observed(
name = "hello.service.getHello", // Metric 名称(低基数,用于聚合)
contextualName = "getHello" // Span 名称(出现在链路追踪中)
)
public HelloResponse getHello(Long userId, String acceptLanguage) {
// 业务逻辑
}
}
@Observed 自动生成:
- Metric:
hello.service.getHello(计数 + 耗时) - Trace Span:
getHello(与父 Span 自动关联)
附加低基数维度:
@Observed(
name = "greeting.resolve",
contextualName = "resolveGreeting",
lowCardinalityKeyValues = {"cache", "redis"} // 固定值 key-value,适合做 Label
)
public Greeting getGreeting(String language) {
// ...
}
lowCardinalityKeyValues接受固定键值对,值域必须有限(如枚举、布尔、服务名)。不要把 userId、订单号等高基数值放在这里。
3.2 高基数 vs 低基数字段的本质区别#
这是 Metrics 埋点中最重要的设计决策,混淆两者会导致监控系统崩溃。
低基数字段(Low Cardinality):
- 值域有限、可枚举,如:
method=GET、status=200、cache=redis、region=cn-east - 适合作为 Prometheus Label / OTel Metric Attribute
- Prometheus 会为每个 Label 组合创建一个时间序列,低基数才可控
高基数字段(High Cardinality):
- 值域无限或极大,如:
userId=123456、orderId=uuid、traceId=... - 不适合作为 Metric Label,否则时间序列数量爆炸,Prometheus 内存耗尽
- 适合放到 Trace Span 的 Attribute 中,用于单次请求的细节定位
在代码中的区分方式:
@Observed(name = "hello.service.getHello", contextualName = "getHello")
public HelloResponse getHello(Long userId, String acceptLanguage) {
// ✅ 高基数字段 → 放到 Span Attribute(只在追踪系统中,不进 Metrics)
var current = registry.getCurrentObservation();
if (current != null) {
current.highCardinalityKeyValue("user.id", userId.toString());
}
// ❌ 不要这样做:高基数值作为 Metric 维度
// current.lowCardinalityKeyValue("user.id", userId.toString());
}
highCardinalityKeyValue 只写入 Trace Span(Tempo / Jaeger 可查),不会进入 Metrics 的 Label,从根源规避基数爆炸。
3.3 手动 Observation:细粒度控制#
当 @Observed 粒度不够(如需要在方法内部对某段逻辑单独计时),使用 Observation API 手动创建:
@Service
public class GreetingService {
private final ObservationRegistry registry;
private final StringRedisTemplate redis;
public GreetingService(ObservationRegistry registry, StringRedisTemplate redis) {
this.registry = registry;
this.redis = redis;
}
// @Observed 覆盖整个方法,自动处理正常路径
@Observed(
name = "greeting.resolve",
contextualName = "resolveGreeting",
lowCardinalityKeyValues = {"cache", "redis"}
)
public Greeting getGreeting(String language) {
String cacheKey = "greeting:" + language;
// 缓存命中:Lettuce 自动生成 db.redis span
String cachedMessage = redis.opsForValue().get(cacheKey);
if (cachedMessage != null) {
return new Greeting(language, cachedMessage);
}
// 缓存未命中:手动 Observation 单独记录这段逻辑的耗时
return resolveFromSource(language, cacheKey);
}
private Greeting resolveFromSource(String language, String cacheKey) {
// ✅ 推荐:.observe() 函数式 API,自动处理 start/stop/error/scope
return Observation.createNotStarted("greeting.lookup", registry)
.lowCardinalityKeyValue("language", language)
.observe(() -> {
Greeting greeting = GREETINGS.getOrDefault(language, GREETINGS.get("en"));
redis.opsForValue().set(cacheKey, greeting.message(), CACHE_TTL);
return greeting;
});
}
}
.observe(() -> {...}) 是首选写法:它自动处理 start()、openScope()、error(e)、stop() 的完整生命周期,消除了忘记调用 stop() 或 Scope 泄漏的风险。
当需要在 Lambda 内部访问 Observation 对象本身(如动态添加 KeyValue),才退回到手动模式:
// 手动模式:需要在运行时动态附加维度时使用
private Greeting resolveFromSource(String language, String cacheKey) {
Observation lookup = Observation.createNotStarted("greeting.lookup", registry)
.lowCardinalityKeyValue("language", language)
.start();
try (Observation.Scope scope = lookup.openScope()) {
Greeting greeting = GREETINGS.getOrDefault(language, GREETINGS.get("en"));
// 运行时才能确定的维度
lookup.lowCardinalityKeyValue("source", greeting.source());
redis.opsForValue().set(cacheKey, greeting.message(), CACHE_TTL);
return greeting;
} catch (Exception e) {
lookup.error(e);
throw e;
} finally {
lookup.stop(); // finally 确保无论异常与否都记录耗时
}
}
3.4 异常处理:在 GlobalExceptionHandler 中记录#
对于未被捕获的异常,统一在全局异常处理器中通过 ObservationRegistry 记录:
@RestControllerAdvice
public class GlobalExceptionHandler {
private final ObservationRegistry registry;
public GlobalExceptionHandler(ObservationRegistry registry) {
this.registry = registry;
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> handleException(Exception ex) {
// ✅ 通过 Micrometer 抽象层记录异常,后端无关
Observation observation = registry.getCurrentObservation();
if (observation != null) {
observation.error(ex); // 自动写入 Span exception 事件 + 标记 ERROR 状态
// + 在 Metrics 中增加 error tag,Prometheus 可聚合
}
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
problem.setDetail(ex.getMessage());
return ResponseEntity.status(500).body(problem);
}
}
observation.error(ex) 在 OTel 后端等效于 span.recordException(ex) + span.setStatus(ERROR),在 Prometheus 后端则对对应 Metric 增加 error tag,一行代码同时覆盖两个后端,且无需引入任何 OTel SDK 依赖。
与业务代码中用
lookup.error(e)的逻辑相同——始终通过ObservationAPI 操作,永远不直接调用Span.current(),保持应用代码的后端无关性。
四、配置要点#
Actuator 暴露(Prometheus 用户必须配置)#
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus # 按需暴露
metrics:
distribution:
percentiles-histogram:
http.server.requests: true # 开启直方图,支持 Prometheus histogram_quantile()
percentiles:
http.server.requests: 0.5, 0.95, 0.99 # 或使用固定分位数(不支持自定义 range)
直方图(
percentiles-histogram: true)与固定分位数(percentiles)只能选其一:前者数据量更大,但支持在 Grafana 中灵活计算任意分位数;后者数据量小,但只能查预计算好的分位数。
OTel Metrics 导出配置#
management:
otlp:
metrics:
export:
url: http://localhost:4318/v1/metrics
step: 10s # 推送间隔,生产环境建议 30s~60s,减少带宽压力
@Observed 注解启用(Spring Boot 3.5)#
management:
observations:
annotations:
enabled: true # Spring Boot 3.5 需显式开启,否则 @Observed 静默无效
生产环境 Actuator 最小化暴露#
# application-prod.yaml
management:
endpoints:
web:
exposure:
include: health, prometheus # 生产只暴露健康检查和 metrics 端点
endpoint:
health:
show-details: never # 不对外暴露详细健康信息
五、Prometheus 用户如何直接使用#
如果你的团队使用 Prometheus 而不是 OTel Collector,应用层代码通常可以保持不变,只需调整依赖和配置:
依赖替换:
// build.gradle.kts(Kotlin DSL)
// 移除(如果有):
// implementation("io.micrometer:micrometer-registry-otlp")
// 添加:
implementation("io.micrometer:micrometer-registry-prometheus")
// Spring Boot BOM 已管理版本,无需指定
配置:Prometheus 是拉取模式,不需要配置 push endpoint,只需确保 /actuator/prometheus 已暴露(见上节),然后在 Prometheus 的 prometheus.yml 中添加 scrape 配置:
# prometheus.yml
scrape_configs:
- job_name: 'hello-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['hello-service:18080']
- job_name: 'user-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['user-service:18081']
应用层代码:@Observed、ObservationRegistry、Observation API 的使用方式基本一致。这正是 Micrometer 抽象层的价值所在。
六、OTel Collector 侧的最佳实践#
如果使用 OTel Collector 作为中间层,可以在 Collector 层面统一处理一些横切关注点:
PII(个人信息)脱敏#
# otel-collector-config.yaml
processors:
attributes/sanitize:
actions:
- key: user.id
action: hash # 对用户 ID 做单向哈希,保留可关联性,去除可识别性
- key: user.email
action: delete # 直接删除邮箱等敏感字段
- key: user.phone
action: delete
这是一个重要的合规实践:即使开发者在代码中意外写入了 PII 字段,Collector 侧的
attributes/sanitize也能作为最后一道防线在数据出应用层后立即清理。
Batch 优化#
processors:
batch:
timeout: 5s # 最多等 5s 凑一批
send_batch_size: 1024 # 或达到 1024 条就发送
memory_limiter:
check_interval: 1s
limit_mib: 256 # 防止 Collector OOM
七、Metrics 命名规范#
Micrometer 使用 . 分隔的命名(http.server.requests),自动转换为各后端格式:
| Micrometer 名称 | Prometheus 格式 | OTel 格式 |
|---|---|---|
http.server.requests |
http_server_requests_seconds_count |
http.server.requests |
greeting.resolve |
greeting_resolve_seconds_count |
greeting.resolve |
jvm.memory.used |
jvm_memory_used_bytes |
jvm.memory.used |
命名建议:
- 使用
<服务域>.<操作>结构,如order.payment、greeting.resolve - 避免在名称中带单位(Micrometer 会自动附加),不要用
order.payment.duration.seconds - 使用名词而非动词:
greeting.resolve而非resolve.greeting - 保持
name(Metric 名)简短稳定,contextualName(Span 名)可更具描述性
Key 名遵循 OTel Semantic Conventions:
在定义 lowCardinalityKeyValues 时,优先使用 OTel Semantic Conventions 规范的 Key 名,而非自定义名称。这样可以直接兼容 Grafana 官方 OTel Dashboard,无需额外映射:
| 场景 | 推荐 Key(OTel 规范) | 避免自定义 Key |
|---|---|---|
| 数据库类型 | db.system = "postgresql" |
database_type |
| 消息系统 | messaging.system = "kafka" |
mq_type |
| HTTP 方法 | http.request.method = "GET" |
method |
| 缓存系统 | db.system = "redis" |
cache_type |
| 错误类型 | error.type = ex.getClass().getName() |
exception_class |
Micrometer 自动生成的基础设施指标(HTTP、JDBC、Redis)已遵循 OTel 规范;自定义业务指标的 Key 名同样对齐规范,可避免在 Grafana 中手动转换字段名。
总结#
| 场景 | 推荐方式 |
|---|---|
| 业务方法的整体耗时 + 计数 | @Observed(name=..., contextualName=...) |
| 方法内特定代码段的独立计时 | Observation.createNotStarted().start() |
| 请求级别的业务 ID(userId 等) | current.highCardinalityKeyValue(...) |
| 固定枚举型维度(status、region) | lowCardinalityKeyValues 或 @Observed 参数 |
| 未捕获异常的 Span 标记 | GlobalExceptionHandler 中 observation.error(ex) |
| HTTP / JDBC / Redis / Kafka 基础指标 | 依赖 + 配置,零代码 |
Spring Boot 的 Metrics 体系现在已经相当成熟——自动埋点覆盖了基础设施层,@Observed 覆盖了大部分业务场景,手动 Observation API 负责细粒度控制。团队真正需要主动设计的,主要还是高低基数字段的边界和命名规范。