当前位置: 首页 > news >正文

Micrometer | 手册 - [直方图配置]

目录
  • §1 依赖 & 基本配置
  • §2 完全官方注解的配置说明
    • §2.1 基本信息
    • §2.2 基本用例
      • 用法
      • 说明
  • §3 直方图增强 @Histogram
    • §3.1 基本信息
    • §3.2 使用方式
      • 依赖
      • 配置
      • 使用
      • 代码

§1 依赖 & 基本配置

本文档所需依赖与基本配置,请直接参考 Micrometer | 手册 - [官方 API] §1 环境准备 部分

§2 完全官方注解的配置说明

§2.1 基本信息

Micrometer 中,TimerDistributionSummary 都可以开启直方图
但 Micrometer 提供了 Timer 的注解 @Timed,却没有提供后者的,所以通过注解其实只能开启 Timer 的直方图

为啥没有 @Summaried
大概率是因为很难通过注解声明如何获取 amount 值,并不是没有完成对其的支持,而是一开始就在设计层面否决了

  • 可以有 @Timed 是因为此场景已经固定了方法执行时长即 amount,而 DistributionSummary 的 amount 是无法确认固定意义的
  • 或者换句话说,DistributionSummary 的 amount 很可能是一个纯粹的业务值,很有可能需要在业务处理过程中才能确认具体值
  • 因此,如果把它做成注解,最多只能从出入参、通过 SpEl 表达式把某个属性作为 amount,明显不能覆盖所有场景,同时比较容易埋坑
  • 也不要设想通过 lamda 获取,基于 lamda 的灵活性,它可以完成取值,但是无法在注解上声明,所以这个方式也走不通

在全注解的场景下(Api 场景没有问题,不在讨论范围),为了描述简单,我们把正确的使用直方图拆分为两个阶段

  • 开启直方图
  • 配置直方图

下面是一个通过 API 声明 Timer 并开启直方图的完整样例,开启、配置的区别已在配置里标注
以注解的形式开启直方图,需要通过注解完整的代替下面全部功能,后面的内容就是说下例中各处如何用注解等代替

Timer ht = Timer.builder("ht").tags("for","static")// 开启客户端百分位,但是不需要、不提倡//.publishPercentiles(0.5, 0.95)// 开启预设桶百分位直方图.publishPercentileHistogram()// 开启自定义桶(slo)百分位直方图// 或对预设桶百分位直方图配置增设的自定义桶.serviceLevelObjectives(Duration.ofMillis(200),Duration.ofMillis(500))// 配置直方图桶最小值.minimumExpectedValue(Duration.ofMillis(100))// 配置直方图桶最大值.maximumExpectedValue(Duration.ofMillis(600)).register(registry);

§2.2 基本用例

用法

注解声明
可以代替 publishPercentileHistogram()

@Timed(value = "ft",histogram = true)
public int ft(String tag, CountCondition condition){return 22;
}

per-meter 参数配置
per-meter 参数可以代替 minimumExpectedValue()/maximumExpectedValue()/serviceLevelObjectives(), 注意:

  • 所有通测参数都应该携带 meter-name,形如 management.metrics.distribution.slo.<meter-name>=xx
  • 下述参数的值支持 long 与 Duration 表达式,极力推荐 Duration 表达式,因其可读性很高

long 的写法,1s 需要写成 1000000000,是不是显得很浪

management.metrics.distribution.minimum-expected-value.ft=100ms
management.metrics.distribution.maximum-expected-value.ft=1s
management.metrics.distribution.slo.ft=200ms,300ms

说明

@Timed 只能帮我们开启直方图,其声明如下
更准确的说,@Timed 只能开启基于预设桶的百分位直方图,并且无法在同一个位置进行配置

public @interface Timed {String value() default "";String[] extraTags() default {};boolean longTask() default false;double[] percentiles() default {};boolean histogram() default false;String description() default "";
}

可见仅下面两个属性与直方图有关

  • histogram() 可以开启直方图
  • percentiles() 用于配置客户端百分位,但在以 Prometheus 为数据源的场景下,不应该开启客户端百分位
    不难看出,注解只能帮我们开启直方图,且其直方图桶的上下限是默认的 1ms ~ 30s,这个范围对于绝大多数请求来说都过于宽松了

其他能力需要通过其他方式进行,通常使用 per-meter 参数,如上文【用法】示例
当然,也有其他方式,比如通过 MeterFilter,如下例,但对比 per-meter 参数形式,并没有提供额外的优势

@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() throws UnknownHostException {return (registry) -> registry.config().meterFilter(new MeterFilter() {@Overridepublic DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {if(id.getName().equals("ft")) {return DistributionStatisticConfig.builder().minimumExpectedValue(Duration.ofMillis(100).toNanos()).maximumExpectedValue(Duration.ofMillis(200).toNanos()).build().merge(config);}return config;}});
}

§3 直方图增强 @Histogram

§3.1 基本信息

@Timed 结合辅助配置确实可以完整的开启直方图,但存在硬伤,即配置的割裂

  • 我们需要再方法上打 @Timed(histogram = true),以开启直方图
  • 然后再配置文件中通过 per-meter 参数配置其直方图桶的最大、最小值以及 slo
    但是,我们更希望在方法上一次性的完成对应直方图的配置,因此对原生注解的直方图能力进行增强

为此,提供 @Histogram 注解

  • 需要与 @Timed 一同标注在方法上来配置直方图
  • 需要开启对应的处理器才能生效
  • 推荐精确的指定处理器的基础扫描包,以防止处理器无效的扫描了所有 bean

§3.2 使用方式

依赖

<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-registry-prometheus</artifactId><version>1.7.4</version>
</dependency>
<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-core</artifactId><version>1.12.13</version>
</dependency>

配置

开启 @Histogram 处理器

@Bean
@ConditionalOnMissingBean(HistogramConfigBeanPostProcessor.class)
public HistogramConfigBeanPostProcessor histogramConfigBeanPostProcessor() {return new HistogramConfigBeanPostProcessor();
}

配置处理器扫描位置

  • 基础扫描包可以配置多个,用 , 分割即可
  • 处理器只会处理配置的包及其子包下,打了 @Histogram 注解的方法
  • 极力推荐以尽量精准的范围指定此配置,即用尽量少并且尽量小的基本扫描包完整的覆盖所有需要处理的方法

通常情况下,定为每个应用的 service/manager 或同等级包即可

  • 缺省此配置时,会默认以启动类所在包作为基础扫描包
    • 如果启动类在默认位置上,此默认可以以稍大的范围覆盖完全,可以但不推荐
    • 但是如果启动类不在默认位置,很可能缩减默认包到一个极小的范围,且不产生实际效果(就是啥都扫不上)
#histogram.scan.locations=com.juzifenqi.dashboard.source.boot.service,com.juzifenqi.dashboard.source.boot.service2
histogram.scan.locations=com.juzifenqi.dashboard.source.boot.service

使用

基本用法如下例

  • 通过 @Histogram 可以配置当前直方图的 min/max/slo
  • min/max 可以缺省,缺省时其值默认为 10ms - 500 ms
  • min/max/slo 的值,可以支持 long 数字串 与 Duration 表达式(推荐)
@Timed(value = "ft",histogram = true)
@Histogram(min = "10ms",max="50ms",slo = {"20ms","40ms"})
public int ft(String tag, CountCondition condition){return 22;
}

如上配置后,可在 /actuator/prometheus 端点看到如下直方图桶

# HELP ft_seconds  
# TYPE ft_seconds histogram
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.01",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.011184809",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.01258291",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.013981011",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.015379112",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.016777216",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.02",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.022369621",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.027962026",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.033554431",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.039146836",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.04",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.044739241",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="0.05",} 1.0
ft_seconds_bucket{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",le="+Inf",} 1.0
ft_seconds_count{a="2",app="juzi-dashboard-source",application="juzi-dashboard-source",exception="none",t="tt",} 1.0

代码

//@Component
public class HistogramConfigBeanPostProcessor implements BeanPostProcessor {private static final Logger logger = LoggerFactory.getLogger(HistogramConfigBeanPostProcessor.class);public static final String SCAN_LOCATION_PROPERTY_KEY = "histogram.scan.locations";@Resourceprivate MetricsProperties metricsProperties;@Resourceprivate Environment env;// scanBasePackagesprivate String[] locations;private boolean skip = false;/**<pre>* 获取 locations,这是基础扫描包,scanBasePackages,* 如果忽略 locations 的限制,会对所有 Spring Bean 走 postProcess 逻辑,这是是无法接受的* 此实现宁可在此情况发生后禁用当前 postProcess 功能并通过日志告警,以促使研发同学修复* 也不会对做所有 bean 去套用它以保证生效** - 优先按配置获取 locations(极力推荐)* - 配置获取不到则找到项目启动类,将主类的包作为 locations*      - 如果启动类不在默认位置,则很可能导致不能正确扫描 @Histogram*      - 比如实际需要被扫描的包是 a.b.c.service, 默认的主类扫描 a.b.c, 而非默认的可能扫描 a.b.c.boot* - 极端情况下,可能完全无法确定 locations,此时会输出告警日志并无效化此 postProcess* </pre>*/@PostConstructpublic void init(){locations = scans();if(ArrayUtils.isNotEmpty(locations)) return;locations = mainPackage();if(ArrayUtils.isEmpty(locations)) {logger.warn("histogram: count found scan locations, skip for all beans");this.skip = true;}}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if(this.skip) return bean;Class<?> c = AopUtils.getTargetClass(bean);/* ******************************** 判断当前 bean 是否超出扫描范围,如果超出了,则跳过* 这是为了防止真的对所有 bean 都检查一遍是不是需要配置直方图* 对于所有 bean 而言,这个概率太低了******************************* */if (outOfLocations(c)) return bean;for (Method m : c.getDeclaredMethods()) {if (m.isAnnotationPresent(Histogram.class)) {processHistogram(m);}}return bean;}private void processHistogram(Method m) {logger.info("histogram: <- {}#{}",m.getDeclaringClass().getSimpleName(),m.getName());Timed t = m.getAnnotation(Timed.class);Histogram h = m.getAnnotation(Histogram.class);if (t == null || h == null) {logger.warn("No @Timed or @Histogram annotation found, may multi @Timed / no @Timed on method");return;}String meter = t.value();metricsProperties.getDistribution().getMinimumExpectedValue().put(meter, h.min());metricsProperties.getDistribution().getMaximumExpectedValue().put(meter, h.max());if(ArrayUtils.isNotEmpty(h.slo())){ServiceLevelObjectiveBoundary[] slos = Arrays.stream(h.slo()).map(ServiceLevelObjectiveBoundary::valueOf).toArray(ServiceLevelObjectiveBoundary[]::new);metricsProperties.getDistribution().getSlo().put(meter, slos);}}/* ******************************** 是否超出扫描范围******************************* */private boolean outOfLocations(Class<?> clazz) {String pkg = ClassUtils.getPackageName(clazz);for(String location : locations){if(StringUtils.equals(pkg, location)) return false;if(StringUtils.startsWith(pkg, location+".")) return false;}return true;}/* ******************************** 是否超出扫描范围******************************* */private boolean outOfLocations(String pkg) {for(String location : locations){if(StringUtils.equals(pkg, location)) return false;if(StringUtils.startsWith(pkg, location+".")) return false;}return true;}/* ******************************** 获取主类* 这是 SpringApplication.deduceMainApplicationClass 里的代码* 安全可靠******************************* */public static Class<?> mainClass() {try {StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();for (StackTraceElement stackTraceElement : stackTrace) {if ("main".equals(stackTraceElement.getMethodName())) {return Class.forName(stackTraceElement.getClassName());}}}catch (ClassNotFoundException ex) {}return null;}/* ******************************** 按主类获取 locations******************************* */private String[] mainPackage() {Class<?> mainClass = mainClass();if(null==mainClass) {logger.warn("histogram: main class not found");return null;}return new String[]{mainClass.getPackage().getName()};}/* ******************************** 按配置获取 locations******************************* */private String[] scans() {String scans = env.getProperty(SCAN_LOCATION_PROPERTY_KEY);if(StringUtils.isNotEmpty(scans))return StringUtils.split(scans, ",");return null;}
}
http://www.jsqmd.com/news/840237/

相关文章:

  • 2026年4月广州搬家公司排名前五:综合实力排行榜(资质+规模+口碑) - GrowthUME
  • 构建可进化智能体系统:从架构蓝图到工程实践
  • RK3588 LGA核心板:车规级嵌入式硬件开发新范式解析
  • Dify聊天应用嵌入式集成实战:从iframe通信到安全部署
  • 2026综合能力强小程序开发服务商推荐:优质小程序制作公司选型+评测指南 - 新闻快传
  • 长期使用Taotoken聚合API对开发效率提升的间接观察
  • 医疗器械超薄异形锂电池定制:起订量逻辑、一站式能力与选型参考 - 新闻快传
  • 终极指南:3分钟学会用VR-Reversal免费转换3D视频到2D格式
  • 2026年4月资质齐全的工字钢经销商批量采购,镀锌角钢/Q235 圆钢/焊管/冷拔H型钢,工字钢源头厂家价格多少 - 品牌推荐师
  • AzurLaneAutoScript:碧蓝航线玩家的终极自动刷图解决方案
  • 为OpenClaw配置Taotoken作为模型供应商,快速启动AI智能体工作流
  • 刚性防水套管大型公司市场格局生变, 如何抓住新机遇? - 新闻快传
  • 2027主管护师考试押题哪家准?3家机构押题率深度对比! - 医考机构品牌测评专家
  • 本地化代码搜索引擎部署指南:从原理到实践
  • 国内主流动力滚筒输送机厂家实测排行权威盘点 - 奔跑123
  • Win11Debloat深度解析:Windows系统优化与隐私保护技术实现
  • 南昌航空大学三次航空器配载与货运管理系统作业总结
  • MIMIC-IT数据集:构建多模态上下文,驱动下一代AI助手能力跃迁
  • 5步构建智能建筑通信系统:BACnet4J纯Java协议栈的架构师指南
  • LeetCode 不相邻最大和题解
  • 对于数据库等待事件 read by other session 的一次处理
  • 告别电脑噪音烦恼:Fan Control免费风扇控制软件完全指南
  • Grasscutter命令生成器终极指南:如何5分钟上手原神私服管理
  • 如何快速制作专业演示文稿?终极免费开源在线PPT工具PPTist完整指南
  • 5步轻松上手:Grasscutter命令生成器实用指南
  • 贵州游西南旅行社获多方官方认可,彰显贵州旅行社新标杆实力 - 新闻快传
  • 为什么你的ElevenLabs语音一接入电话就失真?揭秘采样率/编解码器/AGC三重冲突机制(含FFmpeg强制重采样脚本)
  • Cadence IC618与仿真工具套件一站式部署指南
  • Cursor AI插件深度解析:从自动化脚本到智能编程工作流
  • 2026十家优质小程序开发公司测评蓄能,解锁定制小程序设计制作新范式 - 新闻快传