spring boot 源码解析51-MetricExporters详解

前言

我们在上篇文章中讲解了Exporter的实现,Exporter最终是需要定时器来调度的,那么本文就来分析一下其实现.同时,定时器是需要配置的,因此抽象出TriggerProperties.其类图如下:

《spring boot 源码解析51-MetricExporters详解》

解析

TriggerProperties

TriggerProperties–> 定时器配置的抽象基类.其声明了如下字段:

// 2次export之间的间隔时间
private Long delayMillis;

// 是否该metric 可以导出
private boolean enabled = true;

// 是否关闭任何可优化的但是未导出的指标值
private Boolean sendLatest;

/** * List of patterns for metric names to include. */
private String[] includes;

/** * List of patterns for metric names to exclude. Applied after the includes. */
private String[] excludes;

SpecificTriggerProperties

SpecificTriggerProperties–> 继承自TriggerProperties.其声明了如下字段:

// 指定满足该名字(或者正则)的bean 的id
private String[] names;

MetricExportProperties

MetricExportProperties 继承自TriggerProperties.

  1. 声明了如下字段:

    // key--> MetricWriter bean的id,value --> SpecificTriggerProperties
    private Map<String, SpecificTriggerProperties> triggers = new LinkedHashMap<String, SpecificTriggerProperties>();
    
    private Aggregate aggregate = new Aggregate();
    
    private Redis redis = new Redis();
    
    private Statsd statsd = new Statsd();

    其中:

    1. Aggregate–> Aggregate的配置类,声明了如下字段:

      // repository的全局前缀.应该在jvm中是全局唯一的,但是大多数这个是很有用的,如果其形如"a.b",a-->逻辑 b-->物理进程.
      private String prefix = "";
      
      // 指明该aggregator 该对repository 中的什么类型的key 进行操作
      private String keyPattern = "";
    2. Redis–> redis的配置类,声明了如下字段:

      // redis repository 的前缀,应该全局唯一
      private String prefix = "spring.metrics";
      
      // redis repository export 的key,全局唯一
      private String key = "keys.spring.metrics";
    3. Statsd –> Statsd的配置类,声明了如下字段:

      // statsd的主机名
      private String host;
      
      // statsd的端口号
      private int port = 8125;
      
      // 向statsd暴露metrics时添加的前缀s
      private String prefix;
  2. 由于该类中的setUpDefaults被@PostConstruct注解,因此该方法会在MetricExportProperties初始化完毕之后调用.代码如下:

    @PostConstruct
    public void setUpDefaults() {
        TriggerProperties defaults = this;
        // 1. 遍历triggers,
        for (Entry<String, SpecificTriggerProperties> entry : this.triggers.entrySet()) {
            String key = entry.getKey();
            SpecificTriggerProperties value = entry.getValue();
            // 1.1 如果SpecificTriggerProperties 没有指定名字,则将其赋值为MetricWriter bean的id
            if (value.getNames() == null || value.getNames().length == 0) {
                value.setNames(new String[] { key });
            }
        }
        if (defaults.isSendLatest() == null) {
            defaults.setSendLatest(true);
        }
        if (defaults.getDelayMillis() == null) {
            defaults.setDelayMillis(5000);
        }
        for (TriggerProperties value : this.triggers.values()) {
            if (value.isSendLatest() == null) {
                value.setSendLatest(defaults.isSendLatest());
            }
            if (value.getDelayMillis() == null) {
                value.setDelayMillis(defaults.getDelayMillis());
            }
        }
    }
  3. 由于该类声明了@ConfigurationProperties(prefix = “spring.metrics.export”)注解,因此可以通过如下进行配置:

    spring.metrics.export.aggregate.key-pattern= # Pattern that tells the aggregator what to do with the keys from the source repository.
    spring.metrics.export.aggregate.prefix= # Prefix for global repository if active.
    spring.metrics.export.delay-millis=5000 # Delay in milliseconds between export ticks. Metrics are exported to external sources on a schedule with this delay.
    spring.metrics.export.enabled=true # Flag to enable metric export (assuming a MetricWriter is available).
    spring.metrics.export.excludes= # List of patterns for metric names to exclude. Applied after the includes.
    spring.metrics.export.includes= # List of patterns for metric names to include.
    spring.metrics.export.redis.key=keys.spring.metrics # Key for redis repository export (if active).
    spring.metrics.export.redis.prefix=spring.metrics # Prefix for redis repository if active.
    spring.metrics.export.send-latest= # Flag to switch off any available optimizations based on not exporting unchanged metric values.
    spring.metrics.export.statsd.host= # Host of a statsd server to receive exported metrics.
    spring.metrics.export.statsd.port=8125 # Port of a statsd server to receive exported metrics.
    spring.metrics.export.statsd.prefix= # Prefix for statsd exported metrics.
    spring.metrics.export.triggers.*= # Specific trigger properties per MetricWriter bean name.

MetricExporters

MetricExporters–> 实现了SchedulingConfigurer, Closeable接口.

  1. 字段,构造器如下:

    private MetricReader reader;
    
    private Map<String, GaugeWriter> writers = new HashMap<String, GaugeWriter>();
    
    // 配置文件
    private final MetricExportProperties properties;
    
    // key--> Exporter bean 的id, value --> Exporter
    private final Map<String, Exporter> exporters = new HashMap<String, Exporter>();
    
    // MetricCopyExporter 所对应的 Exporter bean 的id(closeables中持有的是writers的key)
    private final Set<String> closeables = new HashSet<String>();
    
    public MetricExporters(MetricExportProperties properties) {
        this.properties = properties;
    }
  2. 方法实现如下:

    1. configureTasks–> SchedulingConfigurer 中声明的接口方法.代码如下:

      public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
          // 1. 遍历exporters
          for (Entry<String, Exporter> entry : this.exporters.entrySet()) {
              String name = entry.getKey();
              Exporter exporter = entry.getValue();
              // 1.1 获得属于该Exporter的TriggerProperties
              TriggerProperties trigger = this.properties.findTrigger(name);
              if (trigger != null) {
                  // 1.2 如果存在TriggerProperties,则实例化ExportRunner
                  ExportRunner runner = new ExportRunner(exporter);
                  // 实例化IntervalTask,其内部会执行ExportRunner的run方法.在实例化后延迟DelayMillis后执行,之后每隔DelayMillis后执行
                  IntervalTask task = new IntervalTask(runner, trigger.getDelayMillis(),
                          trigger.getDelayMillis());
                  // 1.3 添加到ScheduledTaskRegistrar中
                  taskRegistrar.addFixedDelayTask(task);
              }
          }
          // 2. 遍历writers
          for (Entry<String, GaugeWriter> entry : this.writers.entrySet()) {
              String name = entry.getKey();
              GaugeWriter writer = entry.getValue();
              // 2.1 获得属于该Exporter的TriggerProperties
              TriggerProperties trigger = this.properties.findTrigger(name);
              if (trigger != null) {
                  // 2.2 根据GaugeWriter 创建MetricCopyExporter
                  MetricCopyExporter exporter = getExporter(writer, trigger);
                  // 2.3 添加到exporters,closeables 中
                  this.exporters.put(name, exporter);
                  this.closeables.add(name);
                  // 2.4 实例化ExportRunner,IntervalTask,添加到ScheduledTaskRegistrar中 
                  ExportRunner runner = new ExportRunner(exporter);
                  IntervalTask task = new IntervalTask(runner, trigger.getDelayMillis(),
                          trigger.getDelayMillis());
                  taskRegistrar.addFixedDelayTask(task);
              }
          }
      }
      1. 遍历exporters

        1. 获得属于该Exporter的TriggerProperties
        2. 如果存在TriggerProperties,则实例化ExportRunner
        3. 实例化IntervalTask,其内部会执行ExportRunner的run方法.在实例化后延迟DelayMillis后执行,之后每隔DelayMillis后执行
        4. 添加到ScheduledTaskRegistrar中
      2. 遍历writers

        1. 获得属于该Exporter的TriggerProperties
        2. 根据GaugeWriter 创建MetricCopyExporter.代码如下:

          private MetricCopyExporter getExporter(GaugeWriter writer,
                  TriggerProperties trigger) {
              MetricCopyExporter exporter = new MetricCopyExporter(this.reader, writer);
              exporter.setIncludes(trigger.getIncludes());
              exporter.setExcludes(trigger.getExcludes());
              exporter.setSendLatest(trigger.isSendLatest());
              return exporter;
          }
        3. 添加到exporters,closeables 中
        4. 实例化ExportRunner,IntervalTask,添加到ScheduledTaskRegistrar中

      ExportRunner实现了Runnable接口,代码如下:

      private static class ExportRunner implements Runnable {
      
          private final Exporter exporter;
      
          ExportRunner(Exporter exporter) {
              this.exporter = exporter;
          }
      
          @Override
          public void run() {
              this.exporter.export();
          }
      
      }
    2. close,代码如下:

      public void close() throws IOException {
          for (String name : this.closeables) {
              Exporter exporter = this.exporters.get(name);
              if (exporter instanceof Closeable) {
                  ((Closeable) exporter).close();
              }
          }
      }
      1. 遍历其持有的closeables(closeables中持有的是writers的key)
      2. 依次获得对应的Exporter,如果其实现了Closeable接口,则调用其close方法(此时实际获得的是MetricCopyExporter,其实现了Closeable)

MetricExporters 自动装配

MetricExporters的自动配置类为MetricExportAutoConfiguration.

  1. 该类声明了如下注解:

    @Configuration
    @EnableScheduling
    @ConditionalOnProperty(value = "spring.metrics.export.enabled", matchIfMissing = true)
    @EnableConfigurationProperties
    • @Configuration –> 配置类
    • @EnableScheduling –> 引入了SchedulingConfiguration,在该类中注册了ScheduledAnnotationBeanPostProcessor,开启了对@Scheduled注解的处理
    • @ConditionalOnProperty(value = “spring.metrics.export.enabled”, matchIfMissing = true) –> 当配置有spring.metrics.export.enabled=true时生效,如果没有配置的话,则默认生效
    • @EnableConfigurationProperties –> 引入了EnableConfigurationPropertiesImportSelector,最终执行其selectImports,由于此时我们没有配置value值,因此会返回ConfigurationPropertiesBindingPostProcessorRegistrar.该类会注册ConfigurationBeanFactoryMetaData(关于这个,我们之前已经讲过很多次了)
  2. MetricExportAutoConfiguration中有2个配置类:

    1. MetricExportPropertiesConfiguration,代码如下:

      @Configuration
      protected static class MetricExportPropertiesConfiguration {
      
          // 默认为application.随机值 如果配置了spring.application.name 则为spring.application.name.随机值
          @Value("${spring.application.name:application}.${random.value:0000}")
          private String prefix = "";
      
          private String aggregateKeyPattern = "k.d";
      
          @Bean(name = "spring.metrics.export-org.springframework.boot.actuate.metrics.export.MetricExportProperties")
          @ConditionalOnMissingBean
          public MetricExportProperties metricExportProperties() {
              // 1. 实例化MetricExportProperties
              MetricExportProperties export = new MetricExportProperties();
              // 2. 设置Redis的前缀
              export.getRedis().setPrefix("spring.metrics"
                      + (this.prefix.length() > 0 ? "." : "") + this.prefix);
              // 3. 设置Aggregate的前缀
              export.getAggregate().setPrefix(this.prefix);
              // 4. 设置Aggregate的KeyPattern为k.d
              export.getAggregate().setKeyPattern(this.aggregateKeyPattern);
              return export;
          }
      
      }

      当BeanFactory中不存在MetricExportProperties类型的bean时,会执行metricExportProperties方法,进行注册–>id为spring.metrics.export-org.springframework.boot.actuate.metrics.export.MetricExportProperties

    2. StatsdConfiguration,代码如下:

      @Configuration
      static class StatsdConfiguration {
      
          @Bean
          @ExportMetricWriter
          @ConditionalOnMissingBean
          @ConditionalOnProperty(prefix = "spring.metrics.export.statsd", name = "host")
          public StatsdMetricWriter statsdMetricWriter(MetricExportProperties properties) {
              MetricExportProperties.Statsd statsdProperties = properties.getStatsd();
              return new StatsdMetricWriter(statsdProperties.getPrefix(),
                      statsdProperties.getHost(), statsdProperties.getPort());
          }
      
      }

      当满足如下条件进行注册:

      1. @ConditionalOnMissingBean –> BeanFactory中不存在StatsdMetricWriter类型的bean时生效
      2. @ConditionalOnProperty(prefix = “spring.metrics.export.statsd”, name = “host”) –> 配置有spring.metrics.export.statsd.host 时生效

      个人觉得,此处的代码有问题,当我们配置有spring.metrics.export.statsd.host,确没有在项目中加入java-statsd-client的依赖,此处就会抛出java.lang.ClassNotFoundException: com.timgroup.statsd.StatsDClientErrorHandler

      解决方案:在该方法上加上@ConditionalOnClass(name=”com.timgroup.statsd.NonBlockingStatsDClient”)

      该bug在spring-boot 2.0.0.M5中fix.在2.0.0.M4 中不支持StatsdMetricWriter.

  3. 该类的字段,构造器如下:

    
    // 默认情况,没有自动装配
    private final MetricsEndpointMetricReader endpointReader;
    
    // 此处获得的是被@ExportMetricReader注解的MetricReader
    private final List<MetricReader> readers;
    
    // 此处获得的是被@ExportMetricWriter注解的GaugeWriter,key--> GaugeWriter的id,value --> GaugeWriter
    private final Map<String, GaugeWriter> writers;
    
    // key --> Exporter bean 的id,value --> Exporter
    private final Map<String, Exporter> exporters;
    
    public MetricExportAutoConfiguration(MetricExportProperties properties,
            ObjectProvider<MetricsEndpointMetricReader> endpointReader,
            @ExportMetricReader ObjectProvider<List<MetricReader>> readers,
            @ExportMetricWriter ObjectProvider<Map<String, GaugeWriter>> writers,
            ObjectProvider<Map<String, Exporter>> exporters) {
        this.endpointReader = endpointReader.getIfAvailable();
        this.readers = readers.getIfAvailable();
        this.writers = writers.getIfAvailable();
        this.exporters = exporters.getIfAvailable();
    }

    其字段的说明如下:

    • MetricsEndpointMetricReader endpointReader –> 默认情况,没有自动装配
    • List readers –> 此处获得的是被@ExportMetricReader注解的MetricReader

      1. 在jdk1.8及以上的环境中并且没有加入metrics-core依赖时,获得的是BufferMetricReader
      2. 在jdk1.8以前的环境中并且BaanFactory中不存在id为actuatorMetricRepository的bean时,LegacyMetricRepositoryConfiguration会生效.因此此时获得的是InMemoryMetricRepository,InMemoryMultiMetricRepository
    • Map writers–> 此处获得的是被@ExportMetricWriter注解的GaugeWriter,key–> GaugeWriter的id,value –> GaugeWriter:

      在加入spring-boot-starter-integration,并且注册了id为metricsChannel,类型为metricsChannel的bean之后,MetricsChannelAutoConfiguration配置类就会生效,因此会获得MessageChannelMetricWriter

    • exporters –> key –> Exporter bean 的id,value –> Exporter.默认情况下,此时获得的是空集合

  4. 在该类中,声明了1个@Bean方法,如下:

    @Bean
    @ConditionalOnMissingBean(name = "metricWritersMetricExporter")
    public SchedulingConfigurer metricWritersMetricExporter(
            MetricExportProperties properties) {
        Map<String, GaugeWriter> writers = new HashMap<String, GaugeWriter>();
        // 1. 如果endpointReader 没有配置并且readers 不为空,则实例化CompositeMetricReader
        MetricReader reader = this.endpointReader;
        if (reader == null && !CollectionUtils.isEmpty(this.readers)) {
            reader = new CompositeMetricReader(
                    this.readers.toArray(new MetricReader[this.readers.size()]));
        }
        // 2. 如果reader 等于null并且exporters 为空,则返回NoOpSchedulingConfigurer
        if (reader == null && CollectionUtils.isEmpty(this.exporters)) {
            return new NoOpSchedulingConfigurer();
        }
        // 3. 实例化MetricExporters
        MetricExporters exporters = new MetricExporters(properties);
        // 3.1 如果reader存在的话
        if (reader != null) {
            // 如果GaugeWriter不回空,则加入到writers中
            if (!CollectionUtils.isEmpty(this.writers)) {
                writers.putAll(this.writers);
            }
            // 3.2 赋值到MetricExporters中
            exporters.setReader(reader);
            exporters.setWriters(writers);
        }
        // 3.3 设置exporters
        exporters.setExporters(this.exporters == null
                ? Collections.<String, Exporter>emptyMap() : this.exporters);
        return exporters;
    }

    该方法的逻辑如下:

    1. 如果endpointReader 没有配置并且readers 不为空,则实例化CompositeMetricReader. 默认情况下会执行此步.

    2. 如果reader 等于null并且exporters 为空,则返回NoOpSchedulingConfigurer,此时会在加入metrics-core依赖时生效.默认不执行此步骤

    3. 实例化MetricExporters

      1. 如果reader存在的话
      2. 如果GaugeWriter不回空,则加入到writers中,此步骤默认不执行,需要加入spring-boot-starter-integration依赖
      3. 赋值到MetricExporters中
    4. 设置exporters. 默认exporters等于null,则此时传入的是空集合

    因此:

    1. 默认情况下,MetricExporters中不会配置定时任务
    2. 加入spring-boot-starter-integration,并且注册了id为metricsChannel之后,会注册1个定时任务,此时会将BufferMetricReader中的信息写到名为metricsChannel的Channel中.该步骤要想成功,需要如下几步:

      1. 在pom文件加入如下依赖:

        </dependency>
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
            <version>4.3.12.RELEASE</version>
        </dependency>
      2. 在配置类加入如下配置:

        @Bean
        @ExportMetricWriter
        public MessageChannel metricsChannel() {
            return new DirectChannel();
        }
        
        @Bean
        public IntegrationFlow test() {
            return IntegrationFlows.from("metricsChannel")
                    .handle(CharacterStreamWritingMessageHandler.stdout())
                    .get();
        }
      3. 在controller中加入如下代码:

        @Autowired
        private CounterService counterService;
        
        @RequestMapping("/test-counter")
        public String testCounter() {
        
            counterService.increment("test-counter.count");
        
            return "操作成功";
        }
      4. 此时访问http://127.0.0.1:8080/test-counter后,会发现控制台打印如下日志:

        Metric [name=gauge.response.test-counter, value=32.0, timestamp=Wed Jan 31 17:36:36 CST 2018]Metric [name=gauge.response.star-star.favicon.ico, value=14.0, timestamp=Wed Jan 31 17:36:36 CST 2018]
        Metric [name=counter.status.200.star-star.favicon.ico, value=1, timestamp=Wed Jan 31 17:36:36 CST 2018]Metric [name=counter.test-counter.count, value=1, timestamp=Wed Jan 31 17:36:36 CST 2018]
        Metric [name=counter.status.200.test-counter, value=1, timestamp=Wed Jan 31 17:36:36 CST 2018]

        关于这部分的内容,请看spring boot 源码解析42-MessageChannelMetricWriter详解

    原文作者:Spring Boot
    原文地址: https://blog.csdn.net/qq_26000415/article/details/79220354
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞