Java OpenTelemetry 监控实战:Spring Boot 指标接入完整教程

为 Java/Spring Boot 应用接入 OpenTelemetry 指标:依赖引入、Counter/Gauge/Histogram 实现、用属性为指标添加上下文维度、外部 API 调用监控,以及导出到观测云实现看板与告警。附 Filter 与 Service 完整代码。

最佳实践
Java OpenTelemetry 监控实战:Spring Boot 指标接入完整教程封面

Java 的 OpenTelemetry 指标接入有两种路线:零代码的 Java Agent(自动埋点主流框架)与显式的 SDK 编程(完全掌控业务指标)——生产实践通常是两者结合:Agent 兜底框架层,手写代码覆盖核心业务指标。

核心要点速览

  • Spring Boot 场景用 Filter/拦截器统一埋点 HTTP 指标;
  • Counter 记总量、Gauge 记水位、Histogram 记延迟分布;
  • Attributes 为指标加维度(端点/方法/状态码),但要防高基数;
  • 导出后核对指标名称、单位和标签,再配置图表与检测规则。

引入依赖

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

或者更简单——直接用 Java Agent(-javaagent:opentelemetry-javaagent.jar),通过环境变量配置 OTLP 端点,具体库及其版本是否产出指标应查支持表,不能把链路自动埋点等同于所有框架指标。本文以下按 Agent 已初始化 SDK 的方式使用 API;仅加入依赖不会自动创建 MeterProvider Bean。依赖版本应通过兼容的 BOM 或版本属性对齐。

为 Agent 已初始化的 provider 注册 Spring Bean(无 Agent/SDK 时全局 API 可能是 no-op):

@Configuration
public class TelemetryConfiguration {
    @Bean
    public io.opentelemetry.api.metrics.MeterProvider meterProvider() {
        return io.opentelemetry.api.GlobalOpenTelemetry.getMeterProvider();
    }
}

@Configuration、@Bean 来自 Spring 的 org.springframework.context.annotation。以下各片段需导入对应 OTel API、Spring 与 Servlet 类型。

封装指标 Service

@Service
public class MetricsService {
    private final LongCounter requestCounter;
    private final LongUpDownCounter activeRequests;
    private final DoubleHistogram requestDuration;

    public MetricsService(MeterProvider meterProvider) {
        Meter meter = meterProvider.get("shop-service");
        this.requestCounter = meter.counterBuilder("shop.http.requests")
            .setDescription("Total HTTP requests").setUnit("{request}").build();
        this.activeRequests = meter.upDownCounterBuilder("shop.http.active_requests")
            .setDescription("Active HTTP requests").build();
        this.requestDuration = meter.histogramBuilder("shop.http.request.duration")
            .setDescription("Request latency").setUnit("s").build();
    }

    public void incrementRequestCounter(Attributes attrs) { requestCounter.add(1, attrs); }
    public void incrementActiveRequests(Attributes attrs) { activeRequests.add(1, attrs); }
    public void decrementActiveRequests(Attributes attrs) { activeRequests.add(-1, attrs); }
    public void recordDuration(double seconds, Attributes attrs) { requestDuration.record(seconds, attrs); }
}

用 Filter 统一埋点并附加属性

@Component
public class MetricsFilter extends OncePerRequestFilter {
    private final MetricsService metricsService;

    public MetricsFilter(MetricsService metricsService) {
        this.metricsService = metricsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        Attributes attrs = Attributes.builder()
            .put("method", request.getMethod())
            .build();
        metricsService.incrementRequestCounter(attrs);
        metricsService.incrementActiveRequests(attrs);
        long start = System.nanoTime();
        try {
            chain.doFilter(request, response);
        } finally {
            metricsService.decrementActiveRequests(attrs);
            metricsService.recordDuration(
                (System.nanoTime() - start) / 1e9,
                attrs.toBuilder().put("status", response.getStatus()).build());
        }
    }
}

基数警告:request.getRequestURI() 若含路径参数(/user/12345)会让每个用户一个标签组合——务必改用路由模板(Spring 的 HandlerMapping 最佳匹配模式),按实际路由数和标签组合控制基数;本例移除原始 URI 标签。路由模板通常需在路由匹配后读取;此 Filter 仅测同步调用耗时,不覆盖完整异步请求生命周期。

监控外部 API 调用

对下游依赖单独建指标,出问题时分得清"是我慢还是它慢":

public Product fetchProduct(String id) {
    long start = System.nanoTime();
    try {
        Product p = restTemplate.getForObject("/products/" + id, Product.class);
        productApiCalls.add(1, Attributes.builder()
            .put("api", "product-service").put("result", "success").build());
        return p;
    } catch (Exception e) {
        productApiCalls.add(1, Attributes.builder()
            .put("api", "product-service").put("result", "failure").build());
        throw e;
    } finally {
        productApiDuration.record((System.nanoTime() - start) / 1e9,
            Attributes.of(AttributeKey.stringKey("api"), "product-service"));
    }
}

Java Agent 与手写指标的分工

层 推荐手段
HTTP/JDBC/Redis/MQ 按 Agent 支持表分别核对链路与指标覆盖
JVM 运行时(GC/堆/线程) Agent 自带
业务指标(订单量、转化率) 手写 SDK 代码

Agent 的 OTLP 端点用环境变量配置:例如已配置的 Collector HTTP 接收端使用 OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf 与 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318;DataKit 需按其实际监听和路由设置,不默认是 4318。

验证指标接收与查询

使用 DataKit 接收指标时,先启用 OpenTelemetry 采集器,并按实际协议、端口和路由配置 exporter [1]。发送少量测试请求,检查请求计数、耗时单位与路由标签;再按服务建立查询,对外部 API 失败次数或活动请求数设置检测窗口和阈值 [2]。指标与链路分别上报,不能从指标聚合结果推断每次请求的链路都被保留。

常见问题(FAQ)

Q:有了 Micrometer 还需要 OTel 指标吗? Micrometer 是指标门面,可通过支持的 OTLP registry/export 路径导出,不必为接入 OTel 重写现有指标。

Q:Agent 和 SDK 同时用会重复上报吗? 框架层指标由 Agent 产出、业务指标由手写代码产出,名称空间不同不冲突;注意别让手写代码重复统计 Agent 已覆盖的 HTTP 指标。

Q:Histogram 桶边界怎么定? 按 SLO 定:如接口要求 P99 < 1s,桶边界围绕 0.1/0.3/0.5/1/2s 分布,让关键区间分辨率足够。

Q:多实例部署指标怎么区分? service.instance.id 是否自动设置依实现和资源检测配置而定,需检查并必要时显式设置,观测云中按实例标签过滤或聚合即可。

系列阅读

[1]: 观测云文档《OpenTelemetry》https://docs.guance.com/integrations/opentelemetry/
[2]: 观测云文档《监控》https://docs.guance.com/monitoring/monitor/monitor-rule/

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台