Micrometer 系列【55】统一观测:ObservationConvention | 观测约定
文章目录
- 1. 概述
- 1.1 基础定义
- 1.2 核心特征
- 1.3 工作原理
- (1)创建阶段
- (2)启动观测
- (3)执行业务逻辑
- (4)观测结束
- 2. 源码分析
- 2.1 KeyValuesConvention:标记接口
- 2.2 ObservationConvention:基础契约接口
- 2.3 ChatModelObservationConvention:上层领域扩展接口
- 3. 完整示例
- 3.1 自定义上下文容器
- 3.2 自定义观测 Convention 规范
- 3.3 自定艺观测处理器
- 3.4 业务服务类 & 测试入口
- 4. 两种开发模式对比
- 4.1 方式一:业务代码直接添加标签
- 4.2 方式二:ObservationConvention 标准模式
- 4.3 选型建议
1. 概述
Micrometer Observation体系提供统一可观测抽象,实现Metrics、Tracing数据同源采集。ObservationConvention是标签标准化核心契约,用来统一从业务上下文提取高低基数标签、定义观测名称。
早期开发方式直接在Observation.Context硬编码KeyValue,标签逻辑散落在业务代码;生产级框架(Spring AI、Spring Cloud)统一采用Convention模式,实现业务数据与标签抽取逻辑解耦。
本文基于原生API,讲解Convention定义、运行机制、源码细节以及完整可运行示例。
1.1 基础定义
一套标准化契约接口,负责从自定义Observation.Context中提取观测元信息。
核心方法:
getLowCardinalityKeyValues:提取低基数标签,同时用于监控指标Tag、链路追踪Span;getHighCardinalityKeyValues:提取高基数标签,仅允许放入链路 Span,禁止作为指标标签,防止时序爆炸;getName():定义观测静态名称(指标名称);getContextualName():动态上下文名称,用于链路界面展示;supportsContext():判断当前规范实现是否匹配目标上下文类型。
1.2 核心特征
- 职责分离:
Context承载原始业务数据;Convention专注标签组装、命名规则;业务埋点不再关心标签规则。 - 强制区分高低基数:接口天然拆分两套标签方法,规范开发行为,规避高基数标签滥用风险。
- 分层扩展范式:顶层标记接口 → 泛型基础接口 → 领域专用子接口 → 默认通用实现 → 厂商/业务自定义实现,支持局部覆写。
- 无侵入扩展:新增业务维度、第三方适配时,仅新增
Convention实现,无需改动埋点业务代码。 - 统一消费入口:所有
ObservationHandler(指标处理器、追踪处理器)统一读取Convention产出的KeyValues,逻辑收敛。
1.3 工作原理
完整生命周期跟随Observation创建、启动、执行、停止流程。
(1)创建阶段
业务构建自定义Observation.Context,填充原始业务字段,不手动添加任何KeyValue;创建Observation实例并绑定对应ObservationConvention。
此阶段仅完成对象绑定,不会触发标签提取。
(2)启动观测
调用observation.start():
- 框架内部检测绑定的
Convention; - 主动调用
getLowCardinalityKeyValues()、getHighCardinalityKeyValues()提取标签; - 调用
getName()、getContextualName()确定观测名称; - 将
ContextView+Convention产出标签传递给所有ObservationHandler#onStart。
onStart阶段业务尚未执行,无法获取响应结果、异常信息。
(3)执行业务逻辑
observe()模板方法执行内部业务代码;业务可继续修改可变Context中的业务字段,但不建议动态追加标签,标签规则统一收敛在Convention。
(4)观测结束
业务执行完成触发observation.stop():
- 再次通过
Convention刷新高低基数标签; MetricsHandler使用低基数标签构建监控指标;TracingHandler将高低基数标签写入Span;- 观测生命周期结束,
Convention实例可复用,Context上下文不可复用。
2. 源码分析
2.1 KeyValuesConvention:标记接口
标记型顶层接口,无任何方法。作用:语义归类,统一标识所有标签规范类,方便框架内部类型识别,区分普通业务类与观测规范实现。
publicinterfaceKeyValuesConvention{}2.2 ObservationConvention:基础契约接口
- 使用泛型约束适配的上下文类型;
- 标签方法提供默认空实现,按需覆写;
supportsContext用于匹配上下文类型;getName静态指标名,getContextualName链路展示动态名称。
publicinterfaceObservationConvention<TextendsObservation.Context>extendsKeyValuesConvention{ObservationConvention<Observation.Context>EMPTY=context->false;defaultKeyValuesgetLowCardinalityKeyValues(Tcontext){returnKeyValues.empty();}defaultKeyValuesgetHighCardinalityKeyValues(Tcontext){returnKeyValues.empty();}booleansupportsContext(Observation.Contextcontext);@NullabledefaultStringgetName(){returnnull;}@NullabledefaultStringgetContextualName(Tcontext){returnnull;}}2.3 ChatModelObservationConvention:上层领域扩展接口
- 泛型锁定专属上下文
ChatModelObservationContext; - 默认实现类型匹配方法,子类无需重复编写类型判断;
- 作为领域规范接口,统一约束该领域下全部规范实现。
参考Spring AI设计范式,基于基础接口做领域锁定:
publicinterfaceChatModelObservationConventionextendsObservationConvention<ChatModelObservationContext>{@OverridedefaultbooleansupportsContext(Observation.Contextcontext){returncontextinstanceofChatModelObservationContext;}}3. 完整示例
场景:
- 纯原生
Java,无Spring; - 根观测:创建订单;
- 子观测:发起支付;
- 采用标准
Convention模式,Context不再手动添加KeyValue。
3.1 自定义上下文容器
订单观测上下文容器,仅存放原始业务字段,标签提取逻辑交给Convention:
publicclassOrderObservationContextextendsObservation.Context{privateStringorderNo;privateLonguserId;privateStringorderType;publicStringgetOrderNo(){returnorderNo;}publicvoidsetOrderNo(StringorderNo){this.orderNo=orderNo;}publicLonggetUserId(){returnuserId;}publicvoidsetUserId(LonguserId){this.userId=userId;}publicStringgetOrderType(){returnorderType;}publicvoidsetOrderType(StringorderType){this.orderType=orderType;}}支付观测上下文容器:
importio.micrometer.observation.Observation;/** * */publicclassPaymentObservationContextextendsObservation.Context{privateStringorderNo;privateStringpayChannel;publicStringgetOrderNo(){returnorderNo;}publicvoidsetOrderNo(StringorderNo){this.orderNo=orderNo;}publicStringgetPayChannel(){returnpayChannel;}publicvoidsetPayChannel(StringpayChannel){this.payChannel=payChannel;}}3.2 自定义观测 Convention 规范
订单观测规范接口:
publicinterfaceOrderObservationConventionextendsObservationConvention<OrderObservationContext>{@OverridedefaultbooleansupportsContext(Observation.Contextcontext){returncontextinstanceofOrderObservationContext;}}订单观测规范默认实现,统一提取高低基数标签:
publicclassDefaultOrderObservationConventionimplementsOrderObservationConvention{publicstaticfinalStringOBSERVATION_NAME="order.create";@OverridepublicStringgetName(){returnOBSERVATION_NAME;}@OverridepublicStringgetContextualName(OrderObservationContextcontext){return"order "+context.getOrderType();}@OverridepublicKeyValuesgetLowCardinalityKeyValues(OrderObservationContextcontext){returnKeyValues.of(KeyValue.of("order.type",context.getOrderType()));}@OverridepublicKeyValuesgetHighCardinalityKeyValues(OrderObservationContextcontext){returnKeyValues.of(KeyValue.of("order.no",context.getOrderNo()),KeyValue.of("user.id",String.valueOf(context.getUserId())));}}支付观测规范接口:
publicinterfacePaymentObservationConventionextendsObservationConvention<PaymentObservationContext>{@OverridedefaultbooleansupportsContext(Observation.Contextcontext){returncontextinstanceofPaymentObservationContext;}}支付观测规范默认实现,统一提取高低基数标签:
publicclassDefaultPaymentObservationConventionimplementsPaymentObservationConvention{publicstaticfinalStringOBSERVATION_NAME="payment.create";@OverridepublicStringgetName(){returnOBSERVATION_NAME;}@OverridepublicStringgetContextualName(PaymentObservationContextcontext){return"payment "+context.getPayChannel();}@OverridepublicKeyValuesgetLowCardinalityKeyValues(PaymentObservationContextcontext){returnKeyValues.of(KeyValue.of("pay.channel",context.getPayChannel()));}@OverridepublicKeyValuesgetHighCardinalityKeyValues(PaymentObservationContextcontext){returnKeyValues.of(KeyValue.of("pay.order.no",context.getOrderNo()));}}3.3 自定艺观测处理器
自定义观测处理器,读取Convention生成的标签进行打印
publicclassBizLogObservationHandlerimplementsObservationHandler<Observation.ContextView>{@OverridepublicvoidonStart(Observation.ContextViewcontextView){System.out.println("==== onStart ====");System.out.println("观测名称:"+contextView.getName());System.out.println("动态名称:"+contextView.getContextualName());System.out.println("低基数标签:"+contextView.getLowCardinalityKeyValues());System.out.println("高基数标签:"+contextView.getHighCardinalityKeyValues());System.out.println("================\n");}@OverridepublicvoidonStop(Observation.ContextViewcontextView){System.out.println("==== onStop ====");System.out.println("观测名称:"+contextView.getName());Throwableerror=contextView.getError();if(error!=null){System.out.println("异常信息:"+error.getMessage());}System.out.println("================\n");}@OverridepublicbooleansupportsContext(Observation.ContextViewcontext){returntrue;}}3.4 业务服务类 & 测试入口
同上一篇,不再赘述!!!
4. 两种开发模式对比
4.1 方式一:业务代码直接添加标签
业务代码直接调用context.addLowCardinalityKeyValue()
✅优点
- 上手简单,代码直观,快速实现
Demo、小型工具项目; - 无需新增
Convention接口与实现类,减少类数量; - 支持运行时动态追加标签,灵活性高。
❌缺点
- 标签逻辑散落在各个业务埋点处;标签名称、规则修改,需要改动全部埋点代码;
- 无法统一管控高低基数规范,容易出现开发随意新增高基数指标标签,引发
Prometheus时序爆炸; - 业务代码与可观测规则强耦合,埋点代码臃肿;
- 缺少统一扩展入口;同类上下文想要差异化标签,只能修改业务代码;
- 不符合
Spring AI、Spring Cloud等官方组件标准实现范式,无法对齐开源生态规范; ContextView只能读取已经存入的KeyValue;无法根据业务后置数据动态计算标签。
4.2 方式二:ObservationConvention 标准模式
✅优点
- 关注点分离:
Context只承载原始业务实体;标签抽取逻辑统一收敛到Convention;业务埋点干净纯粹; - 统一管控标签规范,集中管理Tag名称、高低基数划分;标准化评审、统一修改;
- 天然支持分层扩展:通用默认实现 + 业务/厂商自定义子类,局部覆写标签逻辑,开闭原则;
- 框架自动在
start()/stop()两个阶段重新执行标签提取;支持根据响应、异常等后置数据生成标签; - 对齐
Micrometer、Spring AI官方最佳实践,便于后续对接各类中间件观测规范; - 可统一设置观测静态名称、上下文展示名称,统一指标命名规范。
❌缺点
- 项目需要新增一组
Convention接口、实现类,前期代码量更多; - 学习成本更高,需要理解整套分层范式;
- 如果需要运行时动态标签,需要在
Context中预先存入临时字段供Convention读取。
4.3 选型建议
- Demo、小型脚本、一次性工具:直接使用
addLowCardinalityKeyValue,降低开发成本; - 中大型业务项目、SDK、中间件组件、框架封装:强制使用
ObservationConvention模式; - 长期维护、多团队协作、需要统一监控规范的工程:优先
Convention方案。
