Spring Boot 3.5 Micrometer + OpenTelemetry + Prometheus + Tempo + Grafana 完整指南
本文基于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| JDK 21 Spring Boot 3.5.x
Micrometer Micrometer Observation Micrometer Tracing
OpenTelemetry OTLP OpenTelemetry Collector
Prometheus Grafana Tempo Grafana
|
目标是建立完整的:
1
| Metrics + Trace + Log Correlation
|
可观测体系。
1. 最终架构
推荐架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Spring Boot │ ┌────────────┴────────────┐ │ │ Micrometer Observation │ │ Metrics Metrics │ + Trace │ │ │ Micrometer Tracing │ │ │ OpenTelemetry Bridge │ │ │ OTLP │ │ ▼ ▼ /actuator/prometheus OpenTelemetry Collector │ │ │ scrape │ OTLP ▼ ▼ Prometheus Tempo │ │ └────────────┬────────────┘ │ ▼ Grafana
|
日志则利用 Trace 上下文:
关联。
以后如果加入 Loki:
1 2 3 4 5 6 7
| Grafana │ ┌──────────────┼──────────────┐ │ │ │ Prometheus Tempo Loki │ │ │ Metrics Trace Logs
|
形成完整三大支柱。
Spring Boot 的 Observability 本身就是围绕:
设计的,而 Metrics 和 Traces 的核心抽象是 Micrometer Observation。
2. Micrometer 和 OpenTelemetry 到底是什么关系
这一点非常重要。
不要理解成:
1 2 3
| Micrometer vs OpenTelemetry
|
在 Spring Boot 体系下更准确的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 业务代码 │ ▼ Micrometer Observation │ ├──────────────► Micrometer Metrics │ │ │ ▼ │ Prometheus │ ▼ Micrometer Tracing │ ▼ OpenTelemetry │ ▼ OTLP
|
也就是说:
更接近应用侧 API 和抽象。
而:
负责 Trace 标准、上下文传播、SDK 和 OTLP 协议。
Spring Boot 3.5 官方明确说明,对于 OpenTelemetry 集成,推荐使用 Micrometer Observation 或 Micrometer Tracing API,而不是让业务代码直接大量依赖 OpenTelemetry API。
3. 为什么 Metrics 不直接全部走 OpenTelemetry
理论上可以:
1 2 3 4 5 6 7
| Spring Boot ↓ OTLP Metrics ↓ OpenTelemetry Collector ↓ Prometheus / Mimir
|
Spring Boot 也支持:
1
| micrometer-registry-otlp
|
但如果你的最终指标存储就是 Prometheus,我更推荐:
1 2 3 4 5 6 7
| Spring Boot ↓ Micrometer ↓ /actuator/prometheus ↓ Prometheus scrape
|
因为这是:
1
| Spring Boot + Prometheus
|
最成熟、最简单的一条链路。
Spring Boot 官方明确表示 Spring 体系 Metrics 的选择是 Micrometer;Prometheus 则采用 scrape 模式,通过 /actuator/prometheus 拉取指标。
所以本文使用:
1 2 3 4 5 6 7
| Metrics:
Micrometer ↓ Prometheus Registry ↓ Prometheus
|
Trace:
1 2 3 4 5 6 7 8 9 10 11
| Observation ↓ Micrometer Tracing ↓ OpenTelemetry ↓ OTLP ↓ Collector ↓ Tempo
|
这样没有重复采集。
4. Maven 依赖
Spring Boot 3.5 项目加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency>
<dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
</dependencies>
|
不要自己给这些依赖指定版本:
1 2
| micrometer-* opentelemetry-*
|
优先交给:
1
| spring-boot-dependencies
|
管理。
对于 Spring Boot 3.x,OpenTelemetry OTLP Tracing 的典型依赖就是:
1 2 3
| micrometer-tracing-bridge-otel + opentelemetry-exporter-otlp
|
5. application.yml
推荐基础配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| spring: application: name: finance-service
reactor: context-propagation: auto
server: port: 8080
management:
server: port: 18081
endpoints: web: exposure: include: - health - info - metrics - prometheus
endpoint: health: show-details: when-authorized
metrics: tags: application: ${spring.application.name} env: ${APP_ENV:local}
distribution: percentiles-histogram: http.server.requests: true finance.settlement: true
slo: http.server.requests: - 100ms - 300ms - 500ms - 1s - 3s
finance.settlement: - 100ms - 500ms - 1s - 3s - 5s
prometheus: metrics: export: enabled: true
observations: annotations: enabled: true
tracing:
enabled: true
sampling: probability: 1.0
propagation: type: w3c
opentelemetry:
resource-attributes:
service.name: ${spring.application.name}
service.version: ${APP_VERSION:dev}
deployment.environment.name: ${APP_ENV:local}
otlp:
tracing:
endpoint: ${OTEL_TRACES_ENDPOINT:http://localhost:4318/v1/traces}
timeout: 10s
compression: gzip
logging:
pattern:
correlation: "[${spring.application.name:},traceId=%X{traceId:-},spanId=%X{spanId:-}] "
include-application-name: false
|
这里把:
和:
分开。
生产环境强烈推荐这样做。
外部:
监控网络:
不要把所有 Actuator Endpoint 暴露到公网。
6. Trace Sampling
这里:
1 2 3 4
| management: tracing: sampling: probability: 1.0
|
表示:
Spring Boot 默认 Trace sampling 是:
也就是约 10%。
本地开发建议:
生产环境通常:
根据流量决定。
Spring Boot 默认只采样约 10% 的请求,目的就是避免 Trace backend 被大量 span 淹没。
例如生产:
1 2 3 4 5 6 7
| management:
tracing:
sampling:
probability: 0.05
|
表示:
7. OTLP 是什么
OTLP:
负责传输:
支持:
1 2 3 4 5
| OTLP/gRPC 4317
OTLP/HTTP 4318
|
本文使用:
所以:
1 2 3 4 5 6 7
| management:
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
|
OTLP 本身是 OpenTelemetry 定义的标准遥测数据传输协议,Trace、Metric 和 Log signal 均已稳定。
8. Spring Boot 3 和 Spring Boot 4 注意区别
本文针对:
所以使用:
1 2 3 4 5 6 7
| management:
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
|
不要看到 Spring Boot 4 文档后直接复制:
1 2 3 4 5 6
| management: opentelemetry: tracing: export: otlp: endpoint:
|
Spring Boot 3.5 配置元数据显示的是:
1
| management.otlp.tracing.*
|
以后迁移 Spring Boot 4 时,再处理这部分配置变化。
9. Spring Boot 自动提供的 Metrics
引入:
1
| spring-boot-starter-actuator
|
之后,Spring Boot 已经自动提供大量指标。
访问:
1
| curl http://localhost:18081/actuator/metrics
|
可以看到类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| application.ready.time application.started.time
disk.free disk.total
executor.active executor.completed executor.pool.core executor.pool.max executor.queued
http.server.requests
jvm.buffer.count jvm.buffer.memory.used
jvm.classes.loaded
jvm.gc.live.data.size jvm.gc.memory.allocated jvm.gc.memory.promoted
jvm.memory.committed jvm.memory.max jvm.memory.used
jvm.threads.daemon jvm.threads.live jvm.threads.peak
process.cpu.usage process.uptime
system.cpu.count system.cpu.usage system.load.average.1m
|
查看:
1
| curl http://localhost:18081/actuator/metrics/http.server.requests
|
Prometheus 格式:
1
| curl http://localhost:18081/actuator/prometheus
|
Spring Boot 官方 Prometheus 集成就是通过该 endpoint 暴露 scrape 数据。
10. Micrometer 四种最重要的 Meter
实际项目最常见:
| Meter |
用途 |
| Counter |
次数 |
| Timer |
次数 + 时间 |
| Gauge |
当前状态 |
| DistributionSummary |
数值分布 |
例如:
1 2 3 4 5
| Counter
支付成功次数 支付失败次数 重试次数
|
1 2 3 4 5 6
| Gauge
等待任务数 运行任务数 连接数 队列长度
|
1 2 3 4 5
| DistributionSummary
一个批次多少 SKU 消息大小 文件大小
|
11. Counter
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Component @RequiredArgsConstructor public class FinanceMetrics {
private final MeterRegistry registry;
public void settlementSuccess(String billType) {
registry.counter( "finance.settlement.completed", "bill_type", billType, "result", "success" ).increment(); }
public void settlementFailure( String billType, String errorType ) {
registry.counter( "finance.settlement.completed", "bill_type", billType, "result", "failure", "error_type", errorType ).increment(); } }
|
Prometheus:
1 2 3 4
| finance_settlement_completed_total{ bill_type="joint", result="success" }
|
Grafana 不应该直接看:
1
| finance_settlement_completed_total
|
通常看:
1 2 3
| rate( finance_settlement_completed_total[5m] )
|
或者:
1 2 3
| increase( finance_settlement_completed_total[5m] )
|
12. Timer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component @RequiredArgsConstructor public class SettlementMetrics {
private final MeterRegistry registry;
public void record( String billType, Runnable action ) {
Timer timer = Timer.builder( "finance.settlement.duration" ) .tag("bill_type", billType) .description("Settlement execution duration") .register(registry);
timer.record(action); } }
|
产生:
1 2 3 4 5
| finance_settlement_duration_seconds_count
finance_settlement_duration_seconds_sum
finance_settlement_duration_seconds_max
|
开启 histogram 后还会有:
1
| finance_settlement_duration_seconds_bucket
|
13. Gauge
例如统计正在处理的结算单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Component public class SettlementGauge {
private final AtomicInteger processing = new AtomicInteger();
public SettlementGauge( MeterRegistry registry ) {
Gauge.builder( "finance.settlement.inflight", processing, AtomicInteger::get ) .description( "Current settlement tasks" ) .register(registry); }
public void increment() { processing.incrementAndGet(); }
public void decrement() { processing.decrementAndGet(); } }
|
业务:
1 2 3 4 5 6 7 8 9 10
| settlementGauge.increment();
try {
executeSettlement();
} finally {
settlementGauge.decrement(); }
|
14. DistributionSummary
例如一个批次处理多少 SKU:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component public class SettlementBatchMetrics {
private final DistributionSummary summary;
public SettlementBatchMetrics( MeterRegistry registry ) {
this.summary = DistributionSummary.builder( "finance.settlement.batch.size" ) .baseUnit("items") .publishPercentileHistogram() .register(registry); }
public void record(int size) {
summary.record(size); } }
|
使用:
1
| metrics.record(itemList.size());
|
15. 最重要的:Observation
前面的:
主要解决 Metrics。
但加入 OpenTelemetry 后,更推荐核心业务操作使用:
因为一次 Observation 可以同时产生:
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| @Service @RequiredArgsConstructor public class SettlementService {
private final ObservationRegistry observationRegistry;
public void settlement( String billType, String billId ) {
Observation observation = Observation.createNotStarted( "finance.settlement", observationRegistry );
observation.lowCardinalityKeyValue( "bill.type", billType );
observation.highCardinalityKeyValue( "bill.id", billId );
observation.start();
try (Observation.Scope scope = observation.openScope()) {
doSettlement();
observation.lowCardinalityKeyValue( "result", "success" );
} catch (Exception e) {
observation.lowCardinalityKeyValue( "result", "failure" );
observation.error(e);
throw e;
} finally {
observation.stop(); } }
private void doSettlement() {
} }
|
这一段非常关键。
16. Low Cardinality 和 High Cardinality
Observation 有:
1
| lowCardinalityKeyValue()
|
和:
1
| highCardinalityKeyValue()
|
区别:
1 2 3 4 5 6
| Low Cardinality
进入: Metrics + Trace
|
1 2 3 4
| High Cardinality
只进入: Trace
|
Spring Boot 官方明确说明:
1 2 3 4 5
| low cardinality → metrics + traces
high cardinality → traces only
|
因此:
1 2 3 4
| observation.lowCardinalityKeyValue( "bill.type", billType );
|
正确。
因为:
只有有限几个值。
但是:
1 2 3 4
| observation.highCardinalityKeyValue( "bill.id", billId );
|
也正确。
因为:
1 2 3 4
| FT202609020001 FT202609020002 FT202609020003 ...
|
billId 数量巨大。
17. 为什么不能把 billId 做 Metric Tag
错误:
1 2 3 4 5
| registry.counter( "finance.settlement.completed", "bill_id", billId );
|
假设:
那么可能产生:
Prometheus Cardinality 会直接爆炸。
类似:
1 2 3 4 5 6 7
| billId orderId skuId userId traceId requestId exceptionMessage
|
都不应该做 Metrics tag。
正确的 Metrics tag:
1 2 3 4 5 6 7 8 9 10 11
| bill_type
result
error_type
channel
operation
status
|
而:
1 2 3
| billId requestId traceId
|
应该进入 Trace 或 Log。
18. 推荐的字段划分
例如结算业务:
1 2 3 4 5 6 7 8 9
| Metric tags
application env
bill_type channel result error_type
|
Trace attributes:
1 2 3 4 5 6 7 8 9
| bill.id
supplier.id
organization.id
request.id
batch.id
|
Log:
1 2 3 4 5 6 7 8 9 10 11
| billId
requestId
traceId
spanId
exception
具体参数
|
这就是:
1 2 3 4 5 6 7 8 9 10 11
| Metrics ↓ 发现问题
Trace ↓ 定位调用链
Logs ↓ 定位具体业务数据
|
19. @Observed
如果不想每次手动创建 Observation:
1 2 3 4 5 6 7
| @Observed( name = "finance.settlement", contextualName = "settlement" ) public void settlement() {
}
|
需要:
1 2 3 4 5 6 7
| management:
observations:
annotations:
enabled: true
|
以及:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
Spring Boot 3.5 支持:
1 2 3 4 5 6 7 8 9
| @Observed
@Timed
@Counted
@MeterTag
@NewSpan
|
但不要给:
1 2
| Spring MVC Controller Spring Data Repository
|
所有方法再无脑添加 @Observed。
很多 Spring 组件已经自动 instrument,重复添加会产生 duplicate observation。
20. Trace 自动生成
加入:
1
| micrometer-tracing-bridge-otel
|
后:
会被:
1
| TracingAwareMeterObservationHandler
|
桥接成 Span。
也就是说:
1 2 3 4
| Observation.createNotStarted( "finance.settlement", observationRegistry )
|
最终既产生:
1
| finance_settlement_seconds
|
也可能产生:
1 2
| Trace └── finance.settlement
|
Spring Boot 会自动注册 tracing-aware Observation handler,将 Observation 转换为 Span。
21. HTTP Trace 自动采集
Spring MVC:
会自动创建 Server Span。
例如:
Trace:
同时 HTTP Metrics:
1 2 3
| http_server_requests_seconds_count http_server_requests_seconds_sum http_server_requests_seconds_bucket
|
所以普通 Controller 不需要自己写:
22. 服务之间 Trace 传播
例如:
1 2 3 4 5
| finance-service ↓ goods-service ↓ inventory-service
|
需要把:
传下去。
Spring Boot 可以自动完成,但有一个很容易踩坑的地方。
推荐:
1 2 3 4 5 6 7 8 9
| @Bean public RestClient goodsRestClient( RestClient.Builder builder ) {
return builder .baseUrl("http://goods-service") .build(); }
|
不要自己:
同样 WebClient:
1 2 3 4 5 6 7
| @Bean WebClient webClient( WebClient.Builder builder ) {
return builder.build(); }
|
使用 Spring Boot 自动配置的:
1 2 3
| RestTemplateBuilder RestClient.Builder WebClient.Builder
|
才能保证 tracing interceptor/filter 被正确挂载。
Spring Boot 官方也专门强调:自己直接构建 HTTP client 可能导致 Trace context 无法自动传播。
23. WebFlux Context Propagation
你的项目如果存在:
建议:
1 2 3 4 5
| spring:
reactor:
context-propagation: auto
|
因为 Reactor 默认不能保证 ThreadLocal Observation context 在所有 operator 切换中自动恢复。
Spring Boot 官方对 Reactive 场景明确提供:
1
| spring.reactor.context-propagation=auto
|
用于自动上下文传播。
24. @Async / Executor
例如:
1 2 3
| @Async public void execute() { }
|
或者:
需要注意 context propagation。
推荐:
1 2 3 4 5 6 7 8 9 10
| @Configuration public class AsyncConfiguration {
@Bean ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
return new ContextPropagatingTaskDecorator(); } }
|
Spring Boot 官方也专门说明:
1 2
| @Async AsyncTaskExecutor
|
发生线程切换时,如果没有 TaskDecorator,Observation/Trace 上下文可能丢失。
25. JDK 21 虚拟线程
如果:
1
| Executors.newVirtualThreadPerTaskExecutor()
|
自己创建任务,也要意识到:
本质仍然需要传播。
不要假设:
天然会帮你复制 Micrometer Observation ThreadLocal。
如果你封装自己的 Executor,建议统一做:
或交给 Spring 管理执行器,而不是到处:
1
| Thread.startVirtualThread(...)
|
否则可能出现:
1 2 3
| 主线程有 traceId
虚拟线程里面 traceId 消失
|
26. Log 自动关联 Trace
使用 Micrometer Tracing 后,Spring Boot 会默认将:
放入 MDC。
所以:
1
| log.info("开始执行结算单 {}", billId);
|
日志可以变成:
1 2 3 4 5
| [finance-service, traceId=5297c4a5cfe47f93a61b7d31c11732e9, spanId=a2c41d990bec01a1]
开始执行结算单 FT202609020001
|
配置:
1 2 3 4 5 6 7 8
| logging:
pattern:
correlation: > [${spring.application.name:}, traceId=%X{traceId:-}, spanId=%X{spanId:-}]
|
Spring Boot 使用 Micrometer Tracing 时默认支持 Trace/Span correlation ID。
这意味着以后查日志:
就能直接找到对应 Tempo Trace。
27. OpenTelemetry Collector
不要推荐:
直接连接。
更推荐:
1 2 3 4 5
| Spring Boot ↓ OTel Collector ↓ Tempo
|
Collector 可以负责:
1 2 3 4 5 6 7 8 9 10 11
| Batch
Retry
Sampling
Filtering
Attribute Processing
Multiple Exporters
|
OpenTelemetry Collector 本身就是 vendor-neutral 的 receive/process/export 中间层。
28. otel-collector-config.yml
创建:
1 2 3 4 5 6
| monitoring/ ├── docker-compose.yml ├── prometheus.yml ├── tempo.yml ├── otel-collector.yml └── grafana/
|
otel-collector.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
batch:
timeout: 5s
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: - otlp
processors: - memory_limiter - batch
exporters: - otlp/tempo
|
官方推荐 OTLP 作为 Collector → Tempo 的协议;Collector pipeline 本身就是:
1 2 3 4 5
| receivers ↓ processors ↓ exporters
|
这里:
1 2 3 4 5
| Spring Boot ↓ HTTP 4318 Collector ↓ gRPC 4317 Tempo
|
29. Tempo
Tempo 是:
1
| Distributed Tracing Backend
|
职责类似:
1 2 3 4 5
| Prometheus → Metrics
Tempo → Traces
Loki → Logs
|
Grafana 原生支持 Tempo datasource,并支持 TraceQL、TraceID 查询以及与 Prometheus/Loki 关联。
30. tempo.yml
本地开发可以使用单机模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| stream_over_http_enabled: true
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
storage:
trace:
backend: local
wal:
path: /var/tempo/wal
local:
path: /var/tempo/blocks
usage_report:
reporting_enabled: false
|
Tempo 官方也提供了这种本地单机 filesystem 配置。Local backend 适合开发测试;生产环境推荐对象存储。
生产可以改成:
1 2 3 4 5 6 7
| S3
MinIO
GCS
Azure Blob
|
31. Prometheus
prometheus.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "finance-service"
metrics_path: "/actuator/prometheus"
static_configs:
- targets:
- "host.docker.internal:18081"
labels:
application: "finance-service"
|
如果 Spring Boot 也运行在 Docker:
1 2 3
| targets:
- "finance-service:18081"
|
32. Prometheus Exemplars
这是 Metrics 和 Trace 真正连起来的重要功能。
普通 Metric:
只能告诉你:
Exemplar 可以把某一个:
直接关联:
于是 Grafana:
1 2 3 4 5
| P99 曲线 ↓ 点击一个 exemplar ↓ Tempo Trace
|
Grafana 和 Tempo 官方都支持通过 Prometheus exemplar 从 Metrics 直接跳到 Trace。
Prometheus 当前仍需要:
1
| --enable-feature=exemplar-storage
|
开启 exemplar storage。
所以 Docker:
1 2 3 4 5 6 7
| command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--enable-feature=exemplar-storage"
|
Micrometer Tracing 存在时,Spring Boot 可以自动为 OTLP/Prometheus Metrics 提供 Trace exemplar context。
33. Grafana Datasource
目录:
1 2 3 4
| grafana/ └── provisioning └── datasources └── datasources.yml
|
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
jsonData:
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
- name: Tempo
uid: tempo
type: tempo
access: proxy
url: http://tempo:3200
editable: true
jsonData:
nodeGraph:
enabled: true
|
Grafana 可以通过 provisioning YAML 自动创建 datasource。
34. Docker Compose
完整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--enable-feature=exemplar-storage"
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
container_name: otel-collector
restart: unless-stopped
command:
- "--config=/etc/otelcol/config.yml"
volumes:
- ./otel-collector.yml:/etc/otelcol/config.yml:ro
ports:
- "4317:4317"
- "4318:4318"
depends_on:
- tempo
tempo:
image: grafana/tempo:latest
container_name: tempo
restart: unless-stopped
command:
- "-config.file=/etc/tempo.yml"
volumes:
- ./tempo.yml:/etc/tempo.yml:ro
- tempo-data:/var/tempo
ports:
- "3200:3200"
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
depends_on:
- prometheus
- tempo
volumes:
prometheus-data:
tempo-data:
grafana-data:
|
本地学习可以使用:
生产环境一定固定:
1 2 3 4
| Prometheus version Grafana version Tempo version OTel Collector version
|
避免组件升级造成兼容问题。
35. 启动
查看:
应该看到:
1 2 3 4
| prometheus otel-collector tempo grafana
|
36. 服务地址
1 2 3
| Spring Boot
http://localhost:8080
|
Actuator:
Prometheus:
Grafana:
Tempo:
OTLP:
37. 检查 Metrics
1 2
| curl \ http://localhost:18081/actuator/prometheus
|
应该看到:
1 2 3 4 5 6 7
| jvm_memory_used_bytes
process_cpu_usage
http_server_requests_seconds_count
http_server_requests_seconds_sum
|
Prometheus:
应该:
或者 PromQL:
38. 检查 Trace
发送请求:
1
| curl http://localhost:8080/api/test
|
流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Spring Boot
↓
OTLP
↓
localhost:4318
↓
OTel Collector
↓
Tempo
|
Grafana:
然后:
应该看到:
的 Trace。
39. TraceQL
Tempo 支持:
例如:
1
| { resource.service.name = "finance-service" }
|
查询服务 Trace。
错误请求:
1 2 3 4 5
| { resource.service.name = "finance-service" && status = error }
|
业务属性:
1 2 3
| { span.bill.type = "joint" }
|
如果 Observation 中:
1 2 3 4
| highCardinalityKeyValue( "bill.id", billId );
|
还可以根据具体:
查 Trace。
40. HTTP QPS
Grafana:
1 2 3 4 5 6 7
| sum( rate( http_server_requests_seconds_count[ $__rate_interval ] ) )
|
按实例:
1 2 3 4 5 6 7 8 9
| sum by (instance) (
rate(
http_server_requests_seconds_count[ $__rate_interval ] ) )
|
41. HTTP Error Rate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ( sum( rate( http_server_requests_seconds_count{ status=~"5.." }[ $__rate_interval ] ) ) / sum( rate( http_server_requests_seconds_count[ $__rate_interval ] ) ) ) * 100
|
42. HTTP 平均耗时
1 2 3 4 5 6 7 8 9 10 11
| sum( rate( http_server_requests_seconds_sum[5m] ) ) / sum( rate( http_server_requests_seconds_count[5m] ) )
|
43. HTTP P95
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| histogram_quantile(
0.95,
sum by (le) (
rate(
http_server_requests_seconds_bucket[ $__rate_interval ] ) ) )
|
P99:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| histogram_quantile(
0.99,
sum by (le) (
rate(
http_server_requests_seconds_bucket[ $__rate_interval ] ) ) )
|
44. JVM Heap
1 2 3 4 5 6 7 8 9 10 11 12
| sum( jvm_memory_used_bytes{ area="heap" } ) / sum( jvm_memory_max_bytes{ area="heap" } ) * 100
|
45. CPU
Java 进程:
系统:
46. 业务 QPS
1 2 3 4 5 6 7
| sum( rate( finance_settlement_completed_total[ $__rate_interval ] ) )
|
成功:
1 2 3 4 5 6 7 8 9
| sum( rate( finance_settlement_completed_total{ result="success" }[ $__rate_interval ] ) )
|
失败:
1 2 3 4 5 6 7 8 9
| sum( rate( finance_settlement_completed_total{ result="failure" }[ $__rate_interval ] ) )
|
47. 业务成功率
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| sum( rate( finance_settlement_completed_total{ result="success" }[5m] ) ) / sum( rate( finance_settlement_completed_total[5m] ) ) * 100
|
48. 业务 P95
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| histogram_quantile(
0.95,
sum by ( le, bill_type ) (
rate(
finance_settlement_seconds_bucket[ 5m ] ) ) )
|
这样 Grafana 会显示:
不同曲线。
49. 推荐 Grafana Dashboard
最终一个微服务 Dashboard 建议包含:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| Service UP
QPS
HTTP Error Rate
HTTP P50
HTTP P95
HTTP P99
HTTP Max
JVM Heap
JVM Non Heap
GC Pause
Allocation Rate
Thread Count
CPU
Load Average
DB Connection Active
DB Connection Idle
DB Connection Pending
Business QPS
Business Success Rate
Business Failure Rate
Business P95
Business P99
Current Processing
Trace Volume
Trace Error
Trace Duration
|
50. Metrics → Trace
最终我们真正想达到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Grafana
HTTP P99 │ │ exemplar ▼
trace_id │ ▼
Tempo │ ▼
完整 Trace
|
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| finance-service
GET /settlements
↓
SettlementService
↓
goods-service
↓
inventory-service
↓
PostgreSQL
|
Prometheus exemplar 的核心价值就是把聚合 Metrics 和某一个具体 Trace 联系起来。
51. Trace → Metrics
反过来也可以:
1 2 3 4 5
| Tempo Trace ↓ Metrics for this span ↓ Prometheus
|
Grafana Tempo 支持:
可以根据:
1 2 3
| service.name namespace cluster
|
映射成 Prometheus label。
这时形成:
双向跳转。
52. Trace → Log
如果以后加入 Loki:
1 2 3 4 5 6 7
| Tempo Trace ↓ traceId ↓ Loki ↓ 相关日志
|
然后:
1 2 3 4 5
| Loki Log ↓ traceId ↓ Tempo Trace
|
Grafana 对:
之间的关联都有原生支持。
53. 推荐监控代码结构
项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| src/main/java └── top.xxx.finance
├── observability
│ ├── MetricsConfiguration.java
│ ├── FinanceMetrics.java
│ ├── FinanceObservations.java
│ └── binder
│ ├── SettlementQueueMetrics.java
│ ├── ExecutorMetrics.java
│ └── RemoteCallMetrics.java
└── service
|
不要:
散落几十个 Service。
应该:
1 2 3
| financeMetrics.success(...);
financeMetrics.failure(...);
|
或者:
1
| financeObservations.settlement(...)
|
54. 推荐最终的 FinanceObservability
例如统一封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| @Component @RequiredArgsConstructor public class FinanceObservability {
private final MeterRegistry meterRegistry;
private final ObservationRegistry observationRegistry;
public void settlementCompleted( String billType, String result ) {
meterRegistry.counter( "finance.settlement.completed", "bill_type", billType, "result", result ).increment(); }
public <T> T observeSettlement( String billType, String billId, Supplier<T> supplier ) {
Observation observation = Observation.createNotStarted( "finance.settlement", observationRegistry );
observation.lowCardinalityKeyValue( "bill.type", billType );
observation.highCardinalityKeyValue( "bill.id", billId );
observation.start();
try (Observation.Scope scope = observation.openScope()) {
T result = supplier.get();
observation.lowCardinalityKeyValue( "result", "success" );
return result;
} catch (Exception e) {
observation.lowCardinalityKeyValue( "result", "failure" );
observation.error(e);
throw e;
} finally {
observation.stop(); } } }
|
业务:
1 2 3 4 5
| return financeObservability.observeSettlement( billType, billId, () -> doSettlement(billId) );
|
这样:
和:
基本分开。
55. MeterFilter 防止 Cardinality 爆炸
最好再增加保险:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class MetricsConfiguration {
@Bean MeterFilter errorTypeLimit() {
return MeterFilter.maximumAllowableTags( "finance", "error_type", 20, MeterFilter.deny() ); } }
|
防止有人以后写:
1 2 3 4 5
| error_type=SQLException: xxx
error_type=Timeout xxx
error_type=请求 xxx
|
导致大量新的 label。
56. Histogram 不要全开
错误:
因为每一个:
1 2 3
| tag combination × histogram bucket
|
都会产生 Time Series。
例如:
1 2 3 4 5 6 7
| 10 bill_type × 5 result × 20 bucket × 20 service
|
很快就变成:
Time Series。
因此只给:
1 2 3 4 5 6 7
| http.server.requests
核心 RPC
核心业务
核心 MQ consumer
|
开启。
57. Histogram 优于客户端 Percentile
不推荐主要依赖:
原因是:
1 2 3 4 5
| instance A P95
instance B P95
instance C P95
|
不能简单:
得到集群 P95。
Histogram:
可以:
然后:
得到真正的:
所以 Prometheus 环境优先:
58. Trace Sampling 和 Metrics 不一样
必须理解:
应该接近:
因为 Prometheus Metrics 是聚合数据。
而:
可以:
因为 Trace 数据量巨大。
不要因为:
就担心:
只剩 5%。
不会。
Metrics 和 Trace 是两套数据流。
59. Collector Sampling
随着系统扩大,可以:
1 2 3 4 5 6 7
| Spring Boot ↓ 100% Trace ↓ Collector ↓ Tail Sampling
|
例如规则:
1 2 3 4 5 6 7 8
| Error Trace 100%
Slow Trace 100%
Normal Trace 5%
|
这比:
更加智能。
因为应用端 Head Sampling 在请求开始时并不知道:
而 Collector Tail Sampling 可以在 Trace 完整结束后决定。
这也是 OTel Collector 在生产环境最大的价值之一。
60. OpenTelemetry Collector 还能干什么
以后可以做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| filter
attributes
resource
transform
tail_sampling
batch
memory_limiter
k8sattributes
|
例如 K8s:
1 2 3 4 5 6 7
| namespace
pod
node
container
|
自动加入 Trace。
Grafana 官方也建议通过 Collector 的 k8sattributes 等 processor 给 Trace 增加 Kubernetes metadata。
61. 推荐命名规范
Metrics:
1
| <domain>.<module>.<metric>
|
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| finance.settlement.completed
finance.settlement.duration
finance.settlement.inflight
finance.settlement.batch.size
finance.payment.completed
finance.payment.duration
finance.stock.query.duration
finance.stock.query.completed
|
Observation:
1 2 3 4 5
| finance.settlement
finance.payment
finance.stock.query
|
Span:
1 2 3 4 5 6 7
| settlement.calculate
settlement.allocate
payment.create
stock.query
|
62. 推荐 Attribute 规范
低基数:
1 2 3 4 5 6 7 8 9 10 11
| operation
result
status
bill.type
channel
error.type
|
高基数:
1 2 3 4 5 6 7 8 9
| bill.id
order.id
request.id
supplier.id
organization.id
|
Resource:
1 2 3 4 5 6 7
| service.name
service.version
deployment.environment.name
service.namespace
|
63. 不要这样设计
不要:
1 2 3 4 5 6 7
| bill_id user_id sku_id request_id trace_id
作为 Prometheus label
|
不要:
1 2 3
| SQL
作为 Prometheus label
|
不要:
作为 label。
不要:
作为 label。
应该:
而具体:
1
| Read timed out calling xxx
|
进入:
64. Actuator 安全
生产建议:
然后:
1 2 3
| Security Group Firewall K8s NetworkPolicy
|
限制:
只能让:
访问。
同时生产不要:
推荐:
1 2 3 4 5
| include:
- health
- prometheus
|
需要调试时再开启:
65. 本地完整调用流程
最终:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| Browser │ ▼ finance-service:8080 │ ├──────────────────────────────┐ │ │ │ Metric │ Trace │ │ ▼ ▼ Micrometer Observation │ │ ▼ ▼ Prometheus Registry Micrometer Tracing │ │ │ ▼ │ OpenTelemetry │ │ │ ▼ │ OTLP │ │ │ Collector:4318 │ │ │ ▼ │ Tempo:4317 │ │ ▼ ▼ Prometheus:9090 Tempo:3200 │ │ └──────────────┬───────────────┘ │ ▼ Grafana:3000
|
66. 最后的技术选型
对于 Spring Boot 3.5,我推荐最终保持:
1 2 3
| 应用观测 API
Micrometer Observation
|
Metrics:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Micrometer
↓
Prometheus Registry
↓
/actuator/prometheus
↓
Prometheus
|
Tracing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Micrometer Observation
↓
Micrometer Tracing
↓
OpenTelemetry
↓
OTLP
↓
OpenTelemetry Collector
↓
Tempo
|
Visualization:
Logging:
1 2 3 4 5
| SLF4J + Logback + traceId/spanId
|
以后增加:
即可。
最终形成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Grafana
┌──────────┼──────────┐ │ │ │ ▼ ▼ ▼
Prometheus Tempo Loki
Metrics Trace Logs
▲ ▲ ▲ │ │ │ └────── Spring Boot ──┘
│
Micrometer Observation
│
OpenTelemetry
|
这套架构的重点不是简单把:
1 2 3 4 5
| Micrometer Prometheus OpenTelemetry Grafana Tempo
|
全部装上。
真正重要的是形成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Metric ↓ 发现异常 ↓ Exemplar ↓ Trace ↓ 找到慢 Span ↓ traceId ↓ Log ↓ 具体业务数据
|
到这里才是一套完整的 Spring Boot Observability 体系。