如何从已执行的Observation中获取起止时间?
核心需求
你在完全控制Observation实例的场景下,执行任务后需要获取它的开始/结束时间,尤其要把结束时间传给后续Kafka消息发送逻辑,用于消费者的半幂等校验(收到早于上次的消息则忽略)。目前Observation已向DataDog上报计时指标,希望复用该结束时间,而非自行生成。
关键疑问解答
1. 为什么Observation的Timer不公开?
Spring Observability的设计里,Timer属于Observation的内部实现细节,并非对外暴露的公开API。框架团队刻意隐藏它,是为了避免绑定具体的计时实现(比如底层用的是Micrometer的Timer,未来可能替换),保证Observation API的稳定性——要是公开Timer,后续API结构一变更,你的代码直接就崩了。
2. 为什么不推荐用反射获取Timer?
反射绕开了框架的封装,会严重破坏代码兼容性:一旦框架内部Timer的类结构、字段名变了,你的代码直接报错。而且反射本身有性能开销,也不符合面向接口编程的原则。
3. 要不要复用Observation的结束时间?
不建议强行复用,原因有两点:
- Observation的计时是为监控指标设计的,它的时间戳精度、存储逻辑未必完全匹配业务需求(比如业务要毫秒级时间戳,而监控Timer可能只保留统计用的时长数据);
- 没有合法的官方API能获取这个时间,强行用非正规手段拿会带来极高的维护风险。
最优实现方案
既然业务需要可靠的任务结束时间,直接在业务逻辑里自行记录是最稳妥的方式,不依赖Observation的内部实现,代码更健壮。这里给三种实用的实现方式:
方式一:手动记录时间戳(最简单)
Observation observation = Observation.createNotStarted("something", observationRegistry); long startTime = System.currentTimeMillis(); // 要更高精度就用Instant.now() try { observation.observe(() -> doSomething(observation)); } finally { long endTime = System.currentTimeMillis(); // 把endTime传入Kafka消息发送逻辑 sendKafkaMessageWithEndTime(endTime); }
如果需要纳秒级精度,换成Instant.now()即可,业务场景下两者都能满足半幂等校验的需求。
方式二:用自定义Observation上下文存储时间(便于扩展)
如果想把时间和Observation绑定管理,可以自定义Observation.Context来存储开始/结束时间:
// 自定义上下文类 class TaskObservationContext extends Observation.Context { private Instant startTime; private Instant endTime; public void setStartTime(Instant startTime) { this.startTime = startTime; } public void setEndTime(Instant endTime) { this.endTime = endTime; } public Instant getEndTime() { return endTime; } } // 使用示例 TaskObservationContext context = new TaskObservationContext(); context.setStartTime(Instant.now()); Observation observation = Observation.createNotStarted("something", context, observationRegistry); try { observation.observe(() -> doSomething(observation)); } finally { context.setEndTime(Instant.now()); sendKafkaMessageWithEndTime(context.getEndTime().toEpochMilli()); }
这种方式能把时间和Observation的上下文关联起来,后续要扩展其他观测字段时也方便统一管理。
方式三:利用Observation生命周期回调(时间同步性最高)
如果要求业务记录的结束时间和Observation上报给DataDog的时间完全同步,可以用自定义ObservationListener捕获结束时机:
class TaskEndTimeListener implements ObservationListener<TaskObservationContext> { private Instant endTime; @Override public void onStop(Observation.Context context) { if (context instanceof TaskObservationContext taskContext) { taskContext.setEndTime(Instant.now()); this.endTime = taskContext.getEndTime(); } } public Instant getEndTime() { return endTime; } } // 使用示例 TaskObservationContext context = new TaskObservationContext(); context.setStartTime(Instant.now()); TaskEndTimeListener endTimeListener = new TaskEndTimeListener(); Observation observation = Observation.createNotStarted("something", context, observationRegistry) .listener(endTimeListener); observation.observe(() -> doSomething(observation)); // 获取同步的结束时间 Instant endTime = endTimeListener.getEndTime(); sendKafkaMessageWithEndTime(endTime.toEpochMilli());
这种方式借助Observation的生命周期回调记录时间,和Observation的执行时机完全同步,同时不依赖内部Timer实现,完全符合框架设计规范。
内容的提问来源于stack exchange,提问作者vab2048

