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 的标签。
解决思路:
- 使用
ObservationAPI 添加全局标签: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。主要分为以下几个步骤:
- 创建自定义的
MeterRegistry类: 继承AbstractMeterRegistry类,并实现其中的抽象方法。 - 实现
publish()方法: 将指标数据发送到目标监控系统。 - 实现
start()和stop()方法: 启动和停止 MeterRegistry,例如启动一个定时任务来定期发送指标数据。 - 配置
Clock: 为 MeterRegistry 配置一个Clock,用于获取当前时间。 - 注册自定义的
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 添加了 region 和 environment 标签。我们还创建了一个 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,构建完善的指标体系。