文章目录
- [1. 演示说明](#1. 演示说明)
- [2. 环境搭建](#2. 环境搭建)
-
- [2.1 搭建 Spring Boot 工程](#2.1 搭建 Spring Boot 工程)
-
- [2.2.1 引入依赖](#2.2.1 引入依赖)
- [2.1.2 应用配置](#2.1.2 应用配置)
- [2.2 Docker Compose 部署 Prometheus + Grafana](#2.2 Docker Compose 部署 Prometheus + Grafana)
- [3. 代码实现](#3. 代码实现)
-
- [3.1 上下文(Context)](#3.1 上下文(Context))
- [3.2 观测约定(Convention)](#3.2 观测约定(Convention))
- [3.3 观测文档化(Documentation)](#3.3 观测文档化(Documentation))
- [3.4 指标处理器(Handler)](#3.4 指标处理器(Handler))
- [3.5 装配(Config)](#3.5 装配(Config))
- [3.6 业务服务(Service)](#3.6 业务服务(Service))
- [3.7 控制器(Controller)](#3.7 控制器(Controller))
- [4. 运行与验证](#4. 运行与验证)
-
- [4.1 启动应用](#4.1 启动应用)
- [4.2 触发业务](#4.2 触发业务)
- [4.3 查看指标](#4.3 查看指标)
- [4.4 Grafana 展示](#4.4 Grafana 展示)
- [5. 总结](#5. 总结)
1. 演示说明
Micrometer Observation 把「采集一次业务操作的时间、标签、结果」这件事抽象成一个统一模型,业务代码只需要声明"这里是一次可观测操作"并填充业务上下文,剩下的指标生成、标签组装、异常记录全部交给框架完成。
本案例参考 Spring AI 的 ChatModelObservationDocumentation、ChatModelObservationContext、ChatModelObservationConvention 等类,把「订单 → 支付」完整链路做成三个互相嵌套的观测点。
三个观测点:
| 观测名称 | 指标名 | 含义 |
|---|---|---|
order.create |
order_create_seconds |
创建订单一次操作的耗时/次数 |
payment.create |
payment_create_seconds |
支付一次操作的耗时/次数 |
checkout.create |
checkout_create_seconds |
结算编排(下单+支付整体),作为父观测 |
三者关系如下:
text
checkout.create(父观测,openScope)
├── order.create (子观测)
└── payment.create (子观测)
演示能力:
- 自动指标 :框架根据低基数标签自动生成
Timer,如order_create_seconds_count、order_create_seconds_max。 - 自定义业务指标 :通过
ObservationHandler在观测停止(onStop)时写入business_order_amount、business_payment_amount等金额分布(summary)。 - 异常观测 :支付传入
FAIL渠道会抛异常,observe()自动调用observation.error(ex),指标上会多出error="IllegalStateException"标签,便于统计失败率。 - 可观测后端对接 :指标经
/actuator/prometheus暴露,由Prometheus采集、Grafana展示。
2. 环境搭建
2.1 搭建 Spring Boot 工程
本模块是独立 Maven 工程,父 POM 直接用 spring-boot-starter-parent,版本 4.1.0 (Boot 4),JDK 17。
2.2.1 引入依赖
要点说明:
micrometer-observation是actuator的传递依赖,无需单独声明;ObservationRegistry、ObservationHandler等类型都来自它。micrometer-registry-prometheus负责把指标输出成Prometheus文本格式,由/actuator/prometheus提供。
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- 观测 API、ObservationRegistry、Actuator 端点都来自这里 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- MVC 入口(Boot 4 模块化后的 starter) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- Prometheus 指标暴露格式(micrometer-registry-prometheus) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
2.1.2 应用配置
src/main/resources/application.yaml 核心片段:
yaml
server:
port: 8080
spring:
application:
name: spring-micrometer-service # 指标里的应用名
management:
endpoints:
web:
exposure:
include: metrics,prometheus # 暴露指标端点
metrics:
enable:
jvm: false # 关闭 JVM 指标,聚焦业务指标
http.server.requests: true
all: true
只需暴露 metrics 与 prometheus 两个端点即可,/actuator/prometheus 就是 Prometheus 的抓取地址。
2.2 Docker Compose 部署 Prometheus + Grafana
应用运行在本机 8080 端口。通过 Docker Compose 一键拉起 Prometheus(采集) 与 Grafana(展示) ,容器内通过 host.docker.internal 访问宿主机上的应用。
docker-compose.yaml:
yaml
services:
prometheus:
image: prom/prometheus:latest
container_name: ob-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
grafana:
image: grafana/grafana:latest
container_name: ob-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheus
prometheus.yml:
yaml
scrape_configs:
- job_name: spring-micrometer-service
metrics_path: /actuator/prometheus
scrape_interval: 5s
static_configs:
- targets: ["host.docker.internal:8080"]
启动:
bash
docker compose up -d
Prometheus:http://localhost:9090Grafana:http://localhost:3000(admin/admin,数据源填http://prometheus:9090)
3. 代码实现
包结构总览(参考 Spring AI 的分层方式):
text
ob/
├── metadata/ # 属性名常量、操作类型、操作元数据
├── context/ # 观测上下文(承载业务数据与结果)
├── documentation/ # 观测文档化枚举(定义低/高基数 KeyName)
├── convention/ # 观测约定接口 + 默认实现(决定指标名、标签)
├── handler/ # 指标处理器(自定义业务指标)
└── config/ # 把 handler 注册成 Bean
service/ # 业务服务:发起观测
controller/ # REST 入口
调用链(一次完整的观测生命周期):
text
业务代码
└─ Documentation.XXX.observation(custom, default, contextSupplier, registry)
└─ .observe(() -> 业务逻辑)
├─ 打开 scope
├─ 触发 getLowCardinalityKeyValues() / getHighCardinalityKeyValues()
├─ 正常结束 → onStop(Timer 计数、自定义指标落库)
└─ 异常结束 → onError → onStop(Timer 带 error 标签)
3.1 上下文(Context)
作用 :一个可携带业务数据的容器,是观测的状态对象 。它继承 Observation.Context,用 Builder 构建;输入参数(订单号、用户 ID)一旦创建不可变,业务结果(订单 ID、金额、状态)在观测执行过程中写入。
定义属性名常量与操作元数据:
java
// BusinessObservationAttributes:集中管理所有 key 名字符串
public final class BusinessObservationAttributes {
public static final String BUSINESS_OPERATION_TYPE = "business.operation.type";
public static final String BUSINESS_PROVIDER = "business.provider";
public static final String ORDER_TYPE = "order.type";
public static final String ORDER_NO = "order.no";
public static final String USER_ID = "user.id";
public static final String ORDER_ID = "order.id";
public static final String ORDER_AMOUNT = "order.amount";
public static final String ORDER_STATUS = "order.status";
public static final String PAY_CHANNEL = "pay.channel";
public static final String PAY_NO = "pay.no";
public static final String PAY_AMOUNT = "pay.amount";
public static final String PAY_STATUS = "pay.status";
public static final String TOTAL_AMOUNT = "checkout.total.amount";
private BusinessObservationAttributes() {}
}
// BusinessOperationType:操作类型枚举,防止魔法字符串
public enum BusinessOperationType {
ORDER("order"), PAYMENT("payment"), CHECKOUT("checkout");
private final String value;
public String value() { return this.value; }
// 构造器略
}
// BusinessOperationMetadata:操作元数据 record,带非空校验
public record BusinessOperationMetadata(String operationType, String provider) {
public BusinessOperationMetadata {
Assert.hasText(operationType, "operationType cannot be null or empty");
Assert.hasText(provider, "provider cannot be null or empty");
}
// builder() 略
}
定义观测上下文,以订单为例(支付、结算结构一致,此处省略 getter/setter):
java
public class OrderObservationContext extends Observation.Context {
// ------ 输入参数(final,创建后不可变)------
private final BusinessOperationMetadata operationMetadata;
private final String orderNo;
private final Long userId;
private final String orderType;
// ------ 业务结果(观测执行过程中写入)------
private String orderId;
private BigDecimal amount;
private String status;
private OrderObservationContext(/* ... */) { /* ... */ }
public static Builder builder() { return new Builder(); }
// getter/setter 略,关键 Builder:
public static final class Builder {
private BusinessOperationMetadata operationMetadata;
private String orderNo;
private Long userId;
private String orderType;
public Builder operationMetadata(BusinessOperationMetadata v) { this.operationMetadata = v; return this; }
public Builder orderNo(String v) { this.orderNo = v; return this; }
public Builder userId(Long v) { this.userId = v; return this; }
public Builder orderType(String v) { this.orderType = v; return this; }
public OrderObservationContext build() {
return new OrderObservationContext(operationMetadata, orderNo, userId, orderType);
}
}
}
三个 Context 的设计一致:
OrderObservationContext(订单号/用户ID/订单类型)、PaymentObservationContext(订单ID/支付渠道)、CheckoutObservationContext(订单号/用户ID/订单类型)。区别只在于"输入参数"和"业务结果"字段不同。
3.2 观测约定(Convention)
作用 :决定这个观测叫什么名字、取哪些低基数标签、取哪些高基数属性。
约定接口 (空接口,标记用途,用于 ObjectProvider 注入自定义实现):
java
public interface OrderObservationConvention extends ObservationConvention<OrderObservationContext> {
}
默认实现指标名、上下文名称、标签的组装逻辑都在这里:
java
public class DefaultOrderObservationConvention implements OrderObservationConvention {
public static final String DEFAULT_NAME = "order.create";
@Override
public String getName() {
return DEFAULT_NAME;
}
@Override
public String getContextualName(OrderObservationContext context) {
return "order " + context.getOrderType(); // 上下文名称,供日志/trace 阅读
}
@Override
public KeyValues getLowCardinalityKeyValues(OrderObservationContext context) {
return KeyValues.of(
KeyValue.of("business.operation.type", context.getOperationMetadata().operationType()),
KeyValue.of("business.provider", context.getOperationMetadata().provider()),
KeyValue.of("order.type", context.getOrderType()));
}
@Override
public KeyValues getHighCardinalityKeyValues(OrderObservationContext context) {
KeyValues kv = KeyValues.of(
KeyValue.of("order.no", context.getOrderNo()),
KeyValue.of("user.id", String.valueOf(context.getUserId())));
if (context.getOrderId() != null) {
kv = kv.and(KeyValue.of("order.id", context.getOrderId()));
}
if (context.getAmount() != null) {
kv = kv.and(KeyValue.of("order.amount", context.getAmount().toPlainString()));
}
if (context.getStatus() != null) {
kv = kv.and(KeyValue.of("order.status", context.getStatus()));
}
return kv;
}
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof OrderObservationContext;
}
}
注意两个细节:
supportsContext必须显式public,否则会被编译器判为"缩小了接口方法访问权限"。- 高基数标签在业务执行完才有值(订单
ID、金额),因此用if (context.getXxx() != null)渐进式追加。 - 一个
Key同时出现在低/高基数里是允许的:低基数用于指标标签,高基数用于span属性,各司其职。
3.3 观测文档化(Documentation)
作用 :以枚举形式把"观测点 "整体声明出来,包括它的名字(getDefaultConvention)、它有哪些低基数 Key、哪些高基数 Key。这样一处声明,代码各处引用,避免魔法字符串散落。
java
public enum OrderObservationDocumentation implements ObservationDocumentation {
ORDER_CREATE { // 一个枚举常量 = 一个观测点
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultOrderObservationConvention.class;
}
@Override
public KeyName[] getLowCardinalityKeyNames() { return LowCardinalityKeyNames.values(); }
@Override
public KeyName[] getHighCardinalityKeyNames() { return HighCardinalityKeyNames.values(); }
};
// 低基数 Key(作为指标标签)
public enum LowCardinalityKeyNames implements KeyName {
BUSINESS_OPERATION_TYPE { public String asString() { return "business.operation.type"; } },
BUSINESS_PROVIDER { public String asString() { return "business.provider"; } },
ORDER_TYPE { public String asString() { return "order.type"; } }
}
// 高基数 Key(作为 span 属性)
public enum HighCardinalityKeyNames implements KeyName {
ORDER_NO { public String asString() { return "order.no"; } },
USER_ID { public String asString() { return "user.id"; } },
ORDER_ID { public String asString() { return "order.id"; } },
ORDER_AMOUNT { public String asString() { return "order.amount"; } },
ORDER_STATUS { public String asString() { return "order.status"; } }
}
}
这里把
Key名写在asString()里是为了展示结构;工程中它们指向BusinessObservationAttributes常量,保证"常量唯一来源"。
3.4 指标处理器(Handler)
作用 :观测生命周期钩子。框架默认的 DefaultMeterObservationHandler 已自动生成 Timer 指标,这里再自定义一个 handler,在观测 停止 时把业务金额写入 DistributionSummary,得到 business_order_amount_* 这类业务指标。
java
public class OrderMeterObservationHandler implements ObservationHandler<OrderObservationContext> {
private final MeterRegistry meterRegistry;
public OrderMeterObservationHandler(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public void onStop(OrderObservationContext context) {
if (context.getAmount() != null) {
this.meterRegistry.summary("business.order.amount", "order.type", context.getOrderType())
.record(context.getAmount().doubleValue());
}
}
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof OrderObservationContext;
}
}
支付侧 PaymentMeterObservationHandler 结构完全一致,指标名换为 business.payment.amount、标签换为 pay.channel。
3.5 装配(Config)
作用 :把自定义 handler 声明成 Spring Bean。Boot 4 的 ObservationAutoConfiguration 会自动收集容器里的 ObservationHandler Bean,挂到全局 ObservationRegistry 上,无需手动注册。
java
@Configuration
public class BusinessObservationConfiguration {
@Bean
@ConditionalOnMissingBean
OrderMeterObservationHandler orderMeterObservationHandler(MeterRegistry meterRegistry) {
return new OrderMeterObservationHandler(meterRegistry);
}
@Bean
@ConditionalOnMissingBean
PaymentMeterObservationHandler paymentMeterObservationHandler(MeterRegistry meterRegistry) {
return new PaymentMeterObservationHandler(meterRegistry);
}
}
3.6 业务服务(Service)
作用 :真正的观测发起点。业务代码只做三件事:建 Context → 取 Observation → 包住业务逻辑。
订单服务(OrderService):
java
@Service
public class OrderService {
private final ObservationRegistry observationRegistry;
private final ObjectProvider<OrderObservationConvention> observationConvention; // 允许用户覆盖约定
// 构造器注入略
public OrderResult createOrder(String orderNo, Long userId, String orderType) {
OrderObservationContext context = OrderObservationContext.builder()
.operationMetadata(BusinessOperationMetadata.builder()
.operationType(BusinessOperationType.ORDER.value())
.provider("order-service")
.build())
.orderNo(orderNo)
.userId(userId)
.orderType(orderType)
.build();
return OrderObservationDocumentation.ORDER_CREATE
.observation(
this.observationConvention.getIfAvailable(), // 自定义约定(可为 null)
new DefaultOrderObservationConvention(), // 默认约定
() -> context,
this.observationRegistry)
.observe(() -> {
// ------ 业务逻辑 ------
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
BigDecimal amount = new BigDecimal("99.90");
context.setOrderId(orderId);
context.setAmount(amount);
context.setStatus("CREATED");
return new OrderResult(orderId, orderNo, amount, "CREATED");
});
}
public record OrderResult(String orderId, String orderNo, BigDecimal amount, String status) {}
}
这段代码有三个关键点:
observation(...)四参重载:customConvention / defaultConvention / contextSupplier / registry。getIfAvailable()取用户自定义约定,没有就用默认的;getDefaultConvention()由文档化枚举提供,所以四参重载可用。observe(Supplier)包裹业务:正常返回时自动stop()记录指标;抛异常时自动error(ex)再stop(),Timer 上自动出现error=异常类名标签。Context贯穿:业务逻辑往Context里写结果,约定在停止时读取这些结果生成标签。
支付服务(PaymentService)演示异常观测:
java
@Service
public class PaymentService {
private final ObservationRegistry observationRegistry;
private final ObjectProvider<PaymentObservationConvention> observationConvention;
public PaymentResult pay(String orderId, String payChannel) {
PaymentObservationContext context = PaymentObservationContext.builder()
.operationMetadata(BusinessOperationMetadata.builder()
.operationType(BusinessOperationType.PAYMENT.value())
.provider("payment-gateway")
.build())
.orderId(orderId)
.payChannel(payChannel)
.build();
return PaymentObservationDocumentation.PAYMENT_CREATE
.observation(this.observationConvention.getIfAvailable(),
new DefaultPaymentObservationConvention(), () -> context, this.observationRegistry)
.observe(() -> {
if ("FAIL".equalsIgnoreCase(payChannel)) { // 故意失败:演示 error 观测
throw new IllegalStateException("模拟支付失败: channel=" + payChannel);
}
String payNo = "PAY-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
context.setPayNo(payNo);
context.setAmount(new BigDecimal("99.90"));
context.setStatus("PAID");
return new PaymentResult(payNo, orderId, new BigDecimal("99.90"), "PAID");
});
}
public record PaymentResult(String payNo, String orderId, BigDecimal amount, String status) {}
}
结算服务(CheckoutService)演示嵌套观测:
java
@Service
public class CheckoutService {
private final ObservationRegistry observationRegistry;
private final ObjectProvider<CheckoutObservationConvention> observationConvention;
private final OrderService orderService;
private final PaymentService paymentService;
public CheckoutResult checkout(String orderNo, Long userId, String orderType) {
CheckoutObservationContext context = CheckoutObservationContext.builder()
.operationMetadata(BusinessOperationMetadata.builder()
.operationType(BusinessOperationType.CHECKOUT.value())
.provider("checkout-service")
.build())
.orderNo(orderNo)
.userId(userId)
.orderType(orderType)
.build();
return CheckoutObservationDocumentation.CHECKOUT
.observation(this.observationConvention.getIfAvailable(),
new DefaultCheckoutObservationConvention(), () -> context, this.observationRegistry)
.observe(() -> {
// 在 checkout 的 openScope 内再发起两个观测:
// 它们自动成为 checkout 的子节点(parent-child 嵌套)
OrderService.OrderResult order = this.orderService.createOrder(orderNo, userId, orderType);
PaymentService.PaymentResult payment = this.paymentService.pay(order.orderId(), "WECHAT");
context.setOrderId(order.orderId());
context.setPayNo(payment.payNo());
context.setTotalAmount(payment.amount());
context.setStatus("SUCCESS");
return new CheckoutResult(order.orderId(), payment.payNo(), payment.amount(), "SUCCESS");
});
}
public record CheckoutResult(String orderId, String payNo, BigDecimal amount, String status) {}
}
observe()在回调运行期间持有打开的scope,期间新建的观测会被ObservationRegistry自动关联为当前观测的子节点------这就是嵌套的由来,业务代码零额外成本。
3.7 控制器(Controller)
作用 :REST 入口,把观测能力暴露成可调用的 HTTP 接口。
java
@RestController
@RequestMapping("/api")
public class OrderController {
private final OrderService orderService;
private final PaymentService paymentService;
private final CheckoutService checkoutService;
public OrderController(OrderService orderService, PaymentService paymentService, CheckoutService checkoutService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.checkoutService = checkoutService;
}
@PostMapping("/order/create")
public OrderService.OrderResult createOrder(@RequestParam String orderNo, @RequestParam Long userId,
@RequestParam(defaultValue = "NORMAL") String orderType) {
return this.orderService.createOrder(orderNo, userId, orderType);
}
@PostMapping("/order/pay")
public PaymentService.PaymentResult pay(@RequestParam String orderId,
@RequestParam(defaultValue = "WECHAT") String payChannel) {
return this.paymentService.pay(orderId, payChannel);
}
@PostMapping("/checkout")
public CheckoutService.CheckoutResult checkout(@RequestParam String orderNo, @RequestParam Long userId,
@RequestParam(defaultValue = "NORMAL") String orderType) {
return this.checkoutService.checkout(orderNo, userId, orderType);
}
}
4. 运行与验证
4.1 启动应用
bash
cd micrometer-boot-demo
mvn spring-boot:run
4.2 触发业务
bash
# 下单
curl -X POST "http://localhost:8080/api/order/create?orderNo=NO-001&userId=1001&orderType=NORMAL"
# 支付成功
curl -X POST "http://localhost:8080/api/order/pay?orderId=ORD-TEST001&payChannel=WECHAT"
# 支付失败(演示 error 观测)
curl -X POST "http://localhost:8080/api/order/pay?orderId=ORD-TEST002&payChannel=FAIL"
# 结算(下单+支付嵌套)
curl -X POST "http://localhost:8080/api/checkout?orderNo=NO-002&userId=1002&orderType=VIP"
4.3 查看指标
bash
curl "http://localhost:8080/actuator/prometheus"
自动生成的 Timer 指标(低基数标签 + error 标签):
text
order_create_seconds_count{business_operation_type="order",business_provider="order-service",order_type="NORMAL",error="none"} 1
payment_create_seconds_count{business_operation_type="payment",business_provider="payment-gateway",pay_channel="FAIL",error="IllegalStateException"} 1
payment_create_seconds_count{business_operation_type="payment",business_provider="payment-gateway",pay_channel="WECHAT",error="none"} 1
checkout_create_seconds_count{business_operation_type="checkout",business_provider="checkout-service",order_type="VIP",error="none"} 1
自定义业务指标(来自 handler 的 onStop):
text
business_order_amount_count{order_type="NORMAL"} 1
business_order_amount_sum{order_type="NORMAL"} 99.9
business_payment_amount_count{pay_channel="WECHAT"} 1
business_payment_amount_sum{pay_channel="WECHAT"} 99.9
4.4 Grafana 展示
在 Grafana 中添加 Prometheus 数据源(http://prometheus:9090),新建 Dashboard 后即可用 PromQL 查询,例如:
sql
# 支付失败率
rate(payment_create_seconds_count{error!="none"}[5m])
/ rate(payment_create_seconds_count[5m])
# 订单支付总金额(每分钟)
sum(rate(business_payment_amount_sum[5m]))
5. 总结
本案例把观测五件套完整复刻到业务代码里:
| 层 | 类 | 职责 |
|---|---|---|
| metadata | BusinessObservationAttributes / BusinessOperationType / BusinessOperationMetadata |
属性名常量与操作元数据 |
| context | *ObservationContext |
承载业务输入与结果 |
| documentation | *ObservationDocumentation |
声明观测点、低/高基数 Key |
| convention | *ObservationConvention + Default* |
决定指标名与标签 |
| handler | *MeterObservationHandler |
停止时写业务指标 |
| config | BusinessObservationConfiguration |
注册 handler Bean |
业务代码的改动被压缩到最小,只需 三行固定套路:
java
Context ctx = XxxContext.builder()...build();
return XxxDocumentation.XXX
.observation(customConvention, defaultConvention, () -> ctx, registry)
.observe(() -> { /* 原业务逻辑 */ });
指标、错误统计、链路嵌套、Prometheus/Grafana 对接全部由框架与约定完成,业务与可观测性彻底解耦。