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 上下文:

1
2
traceId
spanId

关联。

以后如果加入 Loki:

1
2
3
4
5
6
7
                Grafana
│
┌──────────────┼──────────────┐
│ │ │
Prometheus Tempo Loki
│ │ │
Metrics Trace Logs

形成完整三大支柱。

Spring Boot 的 Observability 本身就是围绕:

1
2
3
Logging
Metrics
Traces

设计的,而 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

也就是说:

1
Micrometer

更接近应用侧 API 和抽象。

而:

1
OpenTelemetry

负责 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>

<!-- Actuator + Micrometer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


<!-- Prometheus Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>


<!-- Micrometer Tracing -> OpenTelemetry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>


<!-- OTLP Trace Export -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>


<!-- @Observed / @Timed / @Counted / @NewSpan -->
<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
# ==============================

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


# ==============================
# Observation
# ==============================

observations:
annotations:
enabled: true


# ==============================
# Tracing
# ==============================

tracing:

enabled: true

sampling:
probability: 1.0

propagation:
type: w3c


# ==============================
# OpenTelemetry
# ==============================

opentelemetry:

resource-attributes:

service.name: ${spring.application.name}

service.version: ${APP_VERSION:dev}

deployment.environment.name: ${APP_ENV:local}


# ==============================
# OTLP
# Spring Boot 3.x
# ==============================

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

这里把:

1
2
业务端口
8080

和:

1
2
Actuator
18081

分开。

生产环境强烈推荐这样做。

外部:

1
2
3
Client
↓
8080

监控网络:

1
2
3
Prometheus
↓
18081

不要把所有 Actuator Endpoint 暴露到公网。


6. Trace Sampling

这里:

1
2
3
4
management:
tracing:
sampling:
probability: 1.0

表示:

1
100% Trace

Spring Boot 默认 Trace sampling 是:

1
0.1

也就是约 10%。

本地开发建议:

1
1.0

生产环境通常:

1
2
3
0.01
0.05
0.1

根据流量决定。

Spring Boot 默认只采样约 10% 的请求,目的就是避免 Trace backend 被大量 span 淹没。

例如生产:

1
2
3
4
5
6
7
management:

tracing:

sampling:

probability: 0.05

表示:

1
5%

7. OTLP 是什么

OTLP:

1
OpenTelemetry Protocol

负责传输:

1
2
3
Trace
Metrics
Logs

支持:

1
2
3
4
5
OTLP/gRPC
4317

OTLP/HTTP
4318

本文使用:

1
HTTP

所以:

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
Spring Boot 3.5

所以使用:

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
Timer

接口耗时
结算耗时
远程调用耗时
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

前面的:

1
2
3
Counter
Timer
Gauge

主要解决 Metrics。

但加入 OpenTelemetry 后,更推荐核心业务操作使用:

1
Observation

因为一次 Observation 可以同时产生:

1
2
3
Metric
+
Trace

例如:

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() {

// business
}
}

这一段非常关键。


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
JOINT
COMPANY
PURCHASE

只有有限几个值。

但是:

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

假设:

1
100 万 bill

那么可能产生:

1
100 万条 Time Series

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
Observation

会被:

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:

1
2
3
Client
↓
Controller

会自动创建 Server Span。

例如:

1
GET /api/orders

Trace:

1
GET /api/orders

同时 HTTP Metrics:

1
2
3
http_server_requests_seconds_count
http_server_requests_seconds_sum
http_server_requests_seconds_bucket

所以普通 Controller 不需要自己写:

1
Observation.start(...)

22. 服务之间 Trace 传播

例如:

1
2
3
4
5
finance-service
↓
goods-service
↓
inventory-service

需要把:

1
traceparent

传下去。

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

不要自己:

1
RestClient.create();

同样 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
WebFlux
Mono
Flux

建议:

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

或者:

1
ThreadPoolTaskExecutor

需要注意 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()

自己创建任务,也要意识到:

1
Trace Context

本质仍然需要传播。

不要假设:

1
Virtual Thread

天然会帮你复制 Micrometer Observation ThreadLocal。

如果你封装自己的 Executor,建议统一做:

1
ContextSnapshot

或交给 Spring 管理执行器,而不是到处:

1
Thread.startVirtualThread(...)

否则可能出现:

1
2
3
主线程有 traceId

虚拟线程里面 traceId 消失

26. Log 自动关联 Trace

使用 Micrometer Tracing 后,Spring Boot 会默认将:

1
2
traceId
spanId

放入 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。

这意味着以后查日志:

1
traceId

就能直接找到对应 Tempo Trace。


27. OpenTelemetry Collector

不要推荐:

1
2
3
Spring Boot
↓
Tempo

直接连接。

更推荐:

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:

1
HTTP P99 突然升高

只能告诉你:

1
服务慢了

Exemplar 可以把某一个:

1
Histogram Bucket

直接关联:

1
trace_id

于是 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
# ==============================

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"



# ==============================
# OpenTelemetry Collector
# ==============================

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
# ==============================

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
# ==============================

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
latest

生产环境一定固定:

1
2
3
4
Prometheus version
Grafana version
Tempo version
OTel Collector version

避免组件升级造成兼容问题。


35. 启动

1
docker compose up -d

查看:

1
docker compose ps

应该看到:

1
2
3
4
prometheus
otel-collector
tempo
grafana

36. 服务地址

1
2
3
Spring Boot

http://localhost:8080

Actuator:

1
http://localhost:18081

Prometheus:

1
http://localhost:9090

Grafana:

1
http://localhost:3000

Tempo:

1
http://localhost:3200

OTLP:

1
2
3
4317 gRPC

4318 HTTP

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:

1
2
Status
→ Targets

应该:

1
2
3
finance-service

UP

或者 PromQL:

1
up

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:

1
2
3
Explore
↓
Tempo

然后:

1
Search

应该看到:

1
finance-service

的 Trace。


39. TraceQL

Tempo 支持:

1
TraceQL

例如:

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

还可以根据具体:

1
bill.id

查 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 进程:

1
process_cpu_usage * 100

系统:

1
system_cpu_usage * 100

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 会显示:

1
2
3
JOINT

COMPANY

不同曲线。


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
Trace to Metrics

可以根据:

1
2
3
service.name
namespace
cluster

映射成 Prometheus label。

这时形成:

1
2
3
Metrics
⇅
Trace

双向跳转。


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 对:

1
2
3
Metrics
Logs
Traces

之间的关联都有原生支持。


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

不要:

1
registry.counter(...)

散落几十个 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)
);

这样:

1
业务逻辑

和:

1
可观测逻辑

基本分开。


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
所有 Timer
全部 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

很快就变成:

1
20,000+

Time Series。

因此只给:

1
2
3
4
5
6
7
http.server.requests

核心 RPC

核心业务

核心 MQ consumer

开启。


57. Histogram 优于客户端 Percentile

不推荐主要依赖:

1
percentile=0.95

原因是:

1
2
3
4
5
instance A P95

instance B P95

instance C P95

不能简单:

1
AVG()

得到集群 P95。

Histogram:

1
bucket

可以:

1
sum by (le) (...)

然后:

1
histogram_quantile()

得到真正的:

1
Cluster P95

所以 Prometheus 环境优先:

1
Histogram

58. Trace Sampling 和 Metrics 不一样

必须理解:

1
Metrics

应该接近:

1
100%

因为 Prometheus Metrics 是聚合数据。

而:

1
Trace

可以:

1
2
3
1%
5%
10%

因为 Trace 数据量巨大。

不要因为:

1
Tracing 5%

就担心:

1
HTTP QPS

只剩 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%

这比:

1
应用随机 5%

更加智能。

因为应用端 Head Sampling 在请求开始时并不知道:

1
2
3
最终会不会 ERROR

最终是不是慢请求

而 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

不要:

1
完整 URL + QueryString

作为 label。

不要:

1
exception.getMessage()

作为 label。

应该:

1
error_type=timeout

而具体:

1
Read timed out calling xxx

进入:

1
2
Trace
Log

64. Actuator 安全

生产建议:

1
2
3
4
5
8080
业务网络

18081
管理网络

然后:

1
2
3
Security Group
Firewall
K8s NetworkPolicy

限制:

1
18081

只能让:

1
2
Prometheus
运维网络

访问。

同时生产不要:

1
include: "*"

推荐:

1
2
3
4
5
include:

- health

- prometheus

需要调试时再开启:

1
2
metrics
info

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:

1
Grafana

Logging:

1
2
3
4
5
SLF4J
+
Logback
+
traceId/spanId

以后增加:

1
Loki

即可。

最终形成:

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 体系。


https://allendericdalexander.github.io/2026/09/15/java/spring/Spring-Micrometer-OpenTelemetry-Prometheus-Tempo-Grafana/
作者
AtLuoFu
发布于
2026年9月15日
许可协议