JAVA Micrometer 指标不全面?自定义 meter registry 的正确方式

Micrometer 指标不全面?自定义 Meter Registry 的正确方式

大家好!今天我们来聊聊 Micrometer 指标不全面的问题,以及如何通过自定义 Meter Registry 来解决这个问题。Micrometer 作为一个 vendor-neutral 的指标收集门面,极大地简化了应用指标的集成和暴露。但有时候,我们发现默认的配置或者已有的 Meter Registry 并不能满足所有的需求,比如:

  • 缺少特定维度的标签: 某些业务场景需要根据特定的业务属性进行指标聚合,而默认的标签可能无法提供这些信息。
  • 指标单位不一致: 不同系统或组件可能使用不同的单位来表示同一个指标,需要进行统一转换。
  • 自定义指标类型: Micrometer 提供的 Counter、Gauge、Timer 等基本类型可能无法完全表达某些复杂的指标逻辑,需要自定义指标类型。
  • 对接特殊的监控系统: 默认的 Meter Registry 可能不支持某些私有的或者特殊的监控系统。
  • 定制化指标过滤: 选择性地上报某些指标,减少不必要的资源消耗。

当遇到这些问题时,就需要我们深入了解 Micrometer 的工作原理,并学会如何自定义 Meter Registry,来实现更灵活、更精准的指标监控。

1. Micrometer 的核心概念回顾

首先,我们快速回顾一下 Micrometer 的几个核心概念,这对于理解自定义 Meter Registry 至关重要。

  • Meter: Micrometer 中指标的抽象。包括 Counter(计数器)、Gauge(瞬时值)、Timer(计时器)、DistributionSummary(分布统计)和 LongTaskTimer(长任务计时器)等。
  • MeterRegistry: Meter 的注册中心,负责创建、存储和管理 Meter,并将指标数据暴露给监控系统。每个 MeterRegistry 对应一个特定的监控系统,比如 PrometheusMeterRegistry、StatsdMeterRegistry 等。
  • MeterFilter: 用于修改或过滤 MeterRegistry 中注册的 Meter。可以添加、修改标签,或者完全阻止某些 Meter 的注册。
  • Tag: 指标的维度,用于对指标进行分组和过滤。例如,host=server1, application=my-app
  • Naming Convention: 命名规范,定义了 Meter 和 Tag 的命名规则,确保指标在不同的监控系统中能够被正确识别。

2. 常见的问题和解决思路

我们先来看几个实际场景,然后分析如何通过自定义 Meter Registry 来解决这些问题。

场景 1:缺少特定维度的标签

假设我们有一个电商系统,需要统计每个商品的平均订单金额。默认的 Meter Registry 可能只提供了 http.status, http.method 等通用的 HTTP 请求指标,并没有商品 ID 的标签。

解决思路:

  • 使用 Observation API 添加全局标签: Observation 提供了更高级的拦截器功能,可以方便地添加全局的、请求相关的标签。
  • 自定义 MeterFilter: 创建一个自定义的 MeterFilter,拦截所有相关的 Meter,并添加商品 ID 的标签。

场景 2:指标单位不一致

假设我们的系统同时使用了 Kafka 和 RabbitMQ,它们分别使用不同的单位来表示消息延迟(例如 Kafka 使用毫秒,RabbitMQ 使用微秒)。我们需要将它们统一到毫秒单位。

解决思路:

  • 自定义 MeterFilter: 创建一个自定义的 MeterFilter,拦截 Kafka 的消息延迟指标,将其单位转换为毫秒。

场景 3:对接特殊的监控系统

假设我们需要将指标数据发送到一个私有的监控系统,该系统不支持 Micrometer 默认的 Meter Registry。

解决思路:

  • 自定义 MeterRegistry: 创建一个自定义的 MeterRegistry,实现 Micrometer 的 MeterRegistry 接口,并将指标数据发送到私有的监控系统。

3. 自定义 Meter Registry 的具体步骤

接下来,我们详细讲解如何自定义 Meter Registry。主要分为以下几个步骤:

  1. 创建自定义的 MeterRegistry 类: 继承 AbstractMeterRegistry 类,并实现其中的抽象方法。
  2. 实现 publish() 方法: 将指标数据发送到目标监控系统。
  3. 实现 start()stop() 方法: 启动和停止 MeterRegistry,例如启动一个定时任务来定期发送指标数据。
  4. 配置 Clock: 为 MeterRegistry 配置一个 Clock,用于获取当前时间。
  5. 注册自定义的 MeterRegistry: 将自定义的 MeterRegistry 注册到 Spring Boot 应用中。

下面是一个简单的例子,演示如何创建一个将指标数据打印到控制台的自定义 Meter Registry。

import io.micrometer.core.clock.Clock;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.distribution.pause.PauseDetector;
import io.micrometer.core.instrument.step.StepMeterRegistry;
import io.micrometer.core.instrument.util.HierarchicalNameMapper;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class ConsoleMeterRegistry extends StepMeterRegistry {

    public ConsoleMeterRegistry() {
        this(ConsoleConfig.DEFAULT, Clock.SYSTEM);
    }

    public ConsoleMeterRegistry(ConsoleConfig config, Clock clock) {
        super(config, clock);
        start();
    }

    @Override
    protected void publish() {
        this.getMeters().forEach(meter -> {
            System.out.println("Name: " + meter.getId().getName());
            meter.measure().forEach(measurement -> {
                System.out.println("  " + measurement.getStatistic() + ": " + measurement.getValue());
            });
        });
    }

    @Override
    protected TimeUnit getBaseTimeUnit() {
        return TimeUnit.MILLISECONDS;
    }

    public static class ConsoleConfig implements StepMeterRegistry.StepRegistryConfig {

        private static final ConsoleConfig DEFAULT = k -> null;

        @Override
        public String prefix() {
            return "console";
        }

        @Override
        public String get(String key) {
            return null;
        }

        @Override
        public Duration step() {
            return Duration.ofSeconds(10);
        }
    }

    // Example usage in a Spring Boot application:
    // @Bean
    // public MeterRegistryCustomizer<ConsoleMeterRegistry> metricsCommonTags() {
    //     return registry -> registry.config().commonTags("region", "us-east-1");
    // }
}

这个例子中,ConsoleMeterRegistry 继承了 StepMeterRegistry,这是一个方便的基类,用于实现周期性地发送指标数据的 Meter Registry。我们只需要实现 publish() 方法,将指标数据打印到控制台即可。ConsoleConfig 定义了配置信息,例如指标发送的周期。

配置 ConsoleConfig

import io.micrometer.core.instrument.step.StepMeterRegistry;

import java.time.Duration;

public interface ConsoleConfig extends StepMeterRegistry.StepRegistryConfig {

    @Override
    default String prefix() {
        return "console";
    }

    @Override
    default String get(String key) {
        return null;
    }

    @Override
    default Duration step() {
        return Duration.ofSeconds(10);
    }
}

在 Spring Boot 中注册 ConsoleMeterRegistry

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MicrometerConfig {

    @Bean
    public ConsoleMeterRegistry consoleMeterRegistry() {
        return new ConsoleMeterRegistry();
    }
}

这样,Spring Boot 会自动创建 ConsoleMeterRegistry 的实例,并将其注册到 Micrometer 中。

4. 使用 MeterFilter 进行定制化

MeterFilter 允许我们在 Meter 注册到 MeterRegistry 之前对其进行修改或过滤。这对于添加标签、修改指标名称、或者阻止某些指标的注册非常有用。

添加全局标签:

import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MeterFilterConfig {

    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config().commonTags("region", "us-east-1", "environment", "production");
    }

    @Bean
    public MeterFilter customMeterFilter() {
        return new MeterFilter() {
            @Override
            public MeterFilterReply accept(Meter.Id id) {
                // Only accept meters with names starting with "http.server.requests"
                if (id.getName().startsWith("http.server.requests")) {
                    return MeterFilterReply.ACCEPT;
                }
                return MeterFilterReply.NEUTRAL;  // Let other filters decide
            }

            @Override
            public Meter.Id map(Meter.Id id) {
                // Add a tag to meters with names starting with "http.server.requests"
                if (id.getName().startsWith("http.server.requests")) {
                    return id.withTag(Tag.of("customTag", "customValue"));
                }
                return id;
            }
        };
    }
}

在这个例子中,我们创建了一个 MeterRegistryCustomizer,它使用 commonTags() 方法为所有 Meter 添加了 regionenvironment 标签。我们还创建了一个 MeterFilter,它只接受名称以 "http.server.requests" 开头的 Meter,并为这些 Meter 添加了 customTag 标签。

阻止某些指标的注册:

import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MeterFilterConfig {

    @Bean
    public MeterFilter denyMeterFilter() {
        return MeterFilter.deny(id -> id.getName().startsWith("jvm.memory"));
    }
}

这个例子中,我们使用 MeterFilter.deny() 方法阻止了所有名称以 "jvm.memory" 开头的 Meter 的注册。

5. 使用 Observation API 增强指标

Micrometer 的 Observation API 提供了一种更高级的方式来收集指标,它可以自动收集请求的开始时间、结束时间、状态码等信息,并可以方便地添加自定义的标签。

创建 ObservationHandler

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomObservationHandler implements ObservationHandler<Observation.Context> {

    @Override
    public void onStart(Observation.Context context) {
        // Logic to execute when an observation starts
        System.out.println("Observation started with name: " + context.getName());
    }

    @Override
    public void onError(Observation.Context context) {
        // Logic to execute when an observation ends with an error
        System.err.println("Observation failed with name: " + context.getName() + ", error: " + context.getError());
    }

    @Override
    public void onEvent(Observation.Event event, Observation.Context context) {
        // Logic to execute when an event occurs during an observation
        System.out.println("Event occurred during observation with name: " + context.getName() + ", event: " + event.getName());
    }

    @Override
    public void onScopeOpened(Observation.Context context) {
        // Logic to execute when an observation scope is opened
        System.out.println("Scope opened for observation with name: " + context.getName());
    }

    @Override
    public void onScopeClosed(Observation.Context context) {
        // Logic to execute when an observation scope is closed
        System.out.println("Scope closed for observation with name: " + context.getName());
    }

    @Override
    public void onStop(Observation.Context context) {
        // Logic to execute when an observation stops
        System.out.println("Observation stopped with name: " + context.getName());
    }

    @Override
    public boolean supportsContext(Observation.Context context) {
        return true; // Supports all contexts
    }
}

使用 Observation 包装代码:

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Autowired
    private ObservationRegistry observationRegistry;

    public String doSomething(String input) {
        return Observation.createNotStarted("my.operation", observationRegistry)
                .contextualName("Doing something important")
                .lowCardinalityKeyValues("input", input)
                .observe(() -> {
                    // Your business logic here
                    return "Result: " + input.toUpperCase();
                });
    }
}

在这个例子中,我们使用 Observation.createNotStarted() 方法创建了一个 Observation,并使用 observe() 方法包装了我们的业务逻辑。Observation 会自动收集请求的开始时间、结束时间等信息,并可以添加自定义的标签。

6. 最佳实践和注意事项

  • 命名规范: 遵循 Micrometer 的命名规范,确保指标名称和标签名称具有一致性和可读性。
  • 标签数量: 避免使用过多的标签,过多的标签会导致指标数据的爆炸式增长,影响监控系统的性能。
  • 指标类型: 选择合适的指标类型,例如使用 Counter 统计事件发生的次数,使用 Gauge 统计瞬时值,使用 Timer 统计请求的耗时。
  • 性能影响: 自定义 Meter Registry 可能会对应用的性能产生影响,需要进行充分的测试和优化。
  • 可维护性: 编写清晰、简洁的代码,并添加必要的注释,提高代码的可维护性。
  • 安全性: 避免在指标数据中暴露敏感信息。

7. 示例:自定义 Meter Registry 对接 InfluxDB

这是一个更完整的示例,演示如何创建一个自定义的 Meter Registry,将指标数据发送到 InfluxDB。

添加 InfluxDB 依赖:

<dependency>
    <groupId>com.influxdb</groupId>
    <artifactId>influxdb-client-java</artifactId>
    <version>6.9.0</version>
</dependency>

创建 InfluxDBConfig 接口:

import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.step.StepMeterRegistry;

import java.time.Duration;

public interface InfluxDBConfig extends StepMeterRegistry.StepRegistryConfig {

    @Override
    default String prefix() {
        return "influxdb";
    }

    String uri();

    String token();

    String org();

    String bucket();

    @Override
    default String get(String key) {
        return null;
    }

    @Override
    default Duration step() {
        return Duration.ofSeconds(10);
    }

    default NamingConvention namingConvention() { return NamingConvention.snakeCase; }
}

创建 InfluxDBMeterRegistry 类:

import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.WriteApiBlocking;
import com.influxdb.client.domain.WritePrecision;
import io.micrometer.core.clock.Clock;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.Measurement;
import io.micrometer.core.instrument.util.HierarchicalNameMapper;
import io.micrometer.core.instrument.step.StepMeterRegistry;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class InfluxDBMeterRegistry extends StepMeterRegistry {

    private final InfluxDBConfig config;
    private final InfluxDBClient influxDBClient;
    private final WriteApiBlocking writeApi;

    public InfluxDBMeterRegistry(InfluxDBConfig config, Clock clock) {
        super(config, clock);
        this.config = config;
        this.influxDBClient = InfluxDBClientFactory.create(config.uri(), config.token().toCharArray(), config.org(), config.bucket());
        this.writeApi = influxDBClient.getWriteApiBlocking();
        start();
    }

    @Override
    protected void publish() {
        List<String> lines = new ArrayList<>();

        this.getMeters().forEach(meter -> {
            String name = config().namingConvention().name(meter.getId().getName(), meter.getId().getType(), meter.getId().getBaseUnit());
            meter.measure().forEach(measurement -> {
                String line = toLineProtocol(name, meter, measurement);
                if (line != null) {
                    lines.add(line);
                }
            });
        });

        try {
            writeApi.writeRecords(WritePrecision.NS, lines);
        } catch (Exception e) {
            System.err.println("Failed to write metrics to InfluxDB: " + e.getMessage());
        }
    }

    private String toLineProtocol(String name, Meter meter, Measurement measurement) {
        StringBuilder line = new StringBuilder();
        line.append(name);

        meter.getId().getTags().forEach(tag -> {
            line.append(",").append(tag.getKey()).append("=").append(tag.getValue());
        });

        line.append(" ").append(measurement.getStatistic().toString()).append("=").append(measurement.getValue());
        line.append(" ").append(clock.wallTime());

        return line.toString();
    }

    @Override
    protected TimeUnit getBaseTimeUnit() {
        return TimeUnit.MILLISECONDS;
    }

    @Override
    public void close() {
        super.close();
        influxDBClient.close();
    }
}

在 Spring Boot 中配置 InfluxDBMeterRegistry:

import io.micrometer.core.instrument.Clock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MicrometerConfig {

    @Value("${management.metrics.export.influxdb.uri}")
    private String uri;

    @Value("${management.metrics.export.influxdb.token}")
    private String token;

    @Value("${management.metrics.export.influxdb.org}")
    private String org;

    @Value("${management.metrics.export.influxdb.bucket}")
    private String bucket;

    @Bean
    public InfluxDBConfig influxDBConfig() {
        return new InfluxDBConfig() {
            @Override
            public String uri() {
                return uri;
            }

            @Override
            public String token() {
                return token;
            }

            @Override
            public String org() {
                return org;
            }

            @Override
            public String bucket() {
                return bucket;
            }
        };
    }

    @Bean
    public InfluxDBMeterRegistry influxDBMeterRegistry(InfluxDBConfig influxDBConfig, Clock clock) {
        return new InfluxDBMeterRegistry(influxDBConfig, clock);
    }
}

application.properties 中配置 InfluxDB 连接信息:

management.metrics.export.influxdb.uri=http://localhost:8086
management.metrics.export.influxdb.token=your_token
management.metrics.export.influxdb.org=your_org
management.metrics.export.influxdb.bucket=your_bucket

这个示例展示了如何创建一个自定义的 InfluxDBMeterRegistry,并将指标数据以 InfluxDB Line Protocol 的格式发送到 InfluxDB。

指标体系的完善之路

通过自定义 Meter Registry,我们可以弥补 Micrometer 默认配置的不足,实现更灵活、更精准的指标监控。 掌握自定义 Meter Registry 的方法,能够帮助我们更好地理解和使用 Micrometer,构建完善的指标体系。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注