Spring Boot如何将Prometheus指标暴露到默认/actuator/prometheus端点
问题说明
在Spring Boot应用中基于Micrometer搭配Prometheus registry暴露自定义Gauge指标时,参考非Spring环境的Micrometer示例手动配置PrometheusRegistry后,应用额外启动了监听8081端口的独立HTTP服务器暴露Prometheus指标,目标是取消该独立服务,将所有指标统一暴露到Spring Boot默认的/actuator/prometheus路径下。
原有错误实现代码
PrometheusRegistry.java
package com.xyz.abc.prometheus; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.time.Duration; import com.sun.net.httpserver.HttpServer; import io.micrometer.core.lang.Nullable; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; public class PrometheusRegistry { public static PrometheusMeterRegistry prometheus() { PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(new PrometheusConfig() { @Override public Duration step() { return Duration.ofSeconds(10); } @Override @Nullable public String get(String k) { return null; } }); try { HttpServer server = HttpServer.create(new InetSocketAddress(8081), 0); server.createContext("/sample-data/prometheus", httpExchange -> { String response = prometheusRegistry.scrape(); httpExchange.sendResponseHeaders(200, response.length()); OutputStream os = httpExchange.getResponseBody(); os.write(response.getBytes()); os.close(); }); new Thread(server::start).run(); } catch (IOException e) { throw new RuntimeException(e); } return prometheusRegistry; } }
MicrometerConfig.java
package com.xyz.abc.prometheus; import io.micrometer.core.instrument.MeterRegistry; public class MicrometerConfig { public static MeterRegistry carMonitoringSystem() { return PrometheusRegistry.prometheus(); } }
接口内注册指标的错误写法
@SuppressWarnings({ "unchecked", "rawtypes" }) @RequestMapping(value = "/sampleApi", method= RequestMethod.GET) @ResponseBody // @Timed生成的指标可正常出现在/actuator/prometheus路径 @Timed(value = "car.healthcheck", description = "Time taken to return healthcheck") public ResponseEntity healthCheck(){ MeterRegistry registry = MicrometerConfig.carMonitoringSystem(); AtomicLong n = new AtomicLong(); // 以下Gauge指标仅出现在8081端口的/sample-data/prometheus路径,不会进入默认Actuator端点 registry.gauge("car.gauge.one", Tags.of("k", "v"), n); registry.gauge("car.gauge.two", Tags.of("k", "v1"), n, n2 -> n2.get() - 1); registry.gauge("car.help.gauge", 89); // 以下Gauge.builder定义的指标在所有端点都不显示 Gauge.builder("car.gauge.test", cpu) .description("car.device.cpu") .tags("customer", "demo") .register(registry); return new ResponseEntity("Car is working fine.", HttpStatus.OK); }
问题根因
- 手动实例化了独立的
PrometheusMeterRegistry,还自行启动了8081端口的HTTP服务。你参考的是无Spring容器的纯Java示例,在Spring Boot环境下,只要引入micrometer-registry-prometheus依赖,框架会自动创建并管理全局唯一的PrometheusMeterRegistry实例,/actuator/prometheus端点只会抓取这个Spring管理的实例中的指标。你自己new出来的Registry和Spring管理的Registry完全隔离,所以往自建Registry里注册的指标不会出现在默认端点。@Timed注解的指标能正常展示,就是因为它自动注册到了Spring管理的Registry中。 - 指标注册时机错误,且被观测对象是方法局部变量。你把Gauge注册逻辑写在了接口请求方法里,每次请求都会重复执行注册逻辑;同时作为Gauge观测值载体的
AtomicLong n是方法内局部变量,请求结束后就会被GC回收。Micrometer对Gauge的被观测对象默认只持弱引用,一旦对象被回收,对应的Gauge指标会直接消失。 - Gauge用法错误:直接传入固定常量
89注册Gauge是无效写法,Gauge的设计目的是观测动态变化的值,固定字面量不会生成可被采集的有效指标。
修复方案
- 直接删除你自行编写的
PrometheusRegistry.java和MicrometerConfig.java两个类,不需要手动创建Registry实例、不需要手动启动HTTP服务暴露端点,Spring Boot自动配置会完成这部分工作。 - 确保项目中引入了正确的依赖,以Maven为例:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
- 编写专门的指标注册类,在Bean初始化阶段一次性完成所有Gauge注册,被观测的对象必须是持有强引用的类成员变量,避免被GC回收:
package com.xyz.abc.prometheus; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.concurrent.atomic.AtomicLong; @Component public class CarMetrics { private final MeterRegistry meterRegistry; // 指标值载体定义为类成员变量,持有强引用 private final AtomicLong gaugeValueHolder = new AtomicLong(0); // 被观测的CPU业务对象同样定义为成员变量 private final CpuStats cpu = new CpuStats(); // 直接注入Spring自动配置的全局MeterRegistry public CarMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } // Bean初始化完成后执行一次指标注册,不要重复注册 @PostConstruct public void initMetrics() { Gauge.builder("car.gauge.one", gaugeValueHolder, AtomicLong::get) .tags("k", "v") .register(meterRegistry); Gauge.builder("car.gauge.two", gaugeValueHolder, val -> val.get() - 1) .tags("k", "v1") .register(meterRegistry); Gauge.builder("car.gauge.test", cpu, CpuStats::getUsage) .description("car.device.cpu") .tags("customer", "demo") .register(meterRegistry); } // 对外提供更新指标值的方法,供业务逻辑调用 public void updateGaugeValue(long value) { this.gaugeValueHolder.set(value); } // CPU指标统计内部类示例 public static class CpuStats { public double getUsage() { // 替换为实际的CPU使用率采集逻辑 return Runtime.getRuntime().availableProcessors(); } } }
- 修改接口逻辑,接口中仅做指标值更新,不要执行指标注册操作:
@SuppressWarnings({ "unchecked", "rawtypes" }) @RequestMapping(value = "/sampleApi", method= RequestMethod.GET) @ResponseBody @Timed(value = "car.healthcheck", description = "Time taken to return healthcheck") public ResponseEntity healthCheck(){ // 注入CarMetrics Bean,更新指标值即可 carMetrics.updateGaugeValue(ThreadLocalRandom.current().nextLong(100)); return new ResponseEntity("Car is working fine.", HttpStatus.OK); }
- 在
application.yml中开启Prometheus端点暴露:
management: endpoints: web: exposure: include: prometheus,health endpoint: prometheus: enabled: true
配置完成后重启应用,8081端口的独立HTTP服务不会再启动,所有@Timed指标、自定义Gauge指标都会统一在/actuator/prometheus路径下对外暴露。
内容的提问来源于stack exchange,提问作者Harshit Gupta
相关产品推荐
相关产品推荐

