以Timer为例,梳理spring-boot-actuator全流程

程禄
2023-12-01

Timer的注册

以下是一段使用Timer的代码。

@Autowired
private MeterRegistry registry; //引用注册中心

Timer.Sample sample = Timer.start(registry); // 开始计时
//注册与计时
sample.stop(
    Timer.builder("business.request.code")
         .publishPercentiles(0.5, 0.95, 0.99)
         .description("http请求状态统计")
         .sla(Duration.ofMillis(800), Duration.ofMillis(1000),
             Duration.ofMillis(1200), Duration.ofMillis(1500),
 		     Duration.ofMillis(3000))
         .tags("methodName", methodName, "code",code,"message",e.getMessage())
         .register(registry));

这段代码里,Timer设置了各种参数后,调用register方法从注册中心中创建或获取timer实例。

通过断点,可以看到MeterRegistry registry 的实现类是PrometheusMeterRegistry,而Timer的实现类是PrometheusTimer

        Timer. Register里做了什么?

    /**
     * Add the timer to a single registry, or return an existing timer in that registry. The returned
     * timer will be unique for each registry, but each registry is guaranteed to only create one timer
     * for the same combination of name and tags.
     *
     * @param registry A registry to add the timer to, if it doesn't already exist.
     * @return A new or existing timer.
     */
    public Timer register(MeterRegistry registry) {
    // the base unit for a timer will be determined by the monitoring system implementation
        return registry.timer(
            new Meter.Id(name, tags, null, description, Type.TIMER), 
            distributionConfigBuilder.build(),
            pauseDetector == null ? registry.config().pauseDetector() : pauseDetector);
     }

继续往下看,registry.timer又做了什么?

/**
* Only used by {@link Timer#builder(String)}.
*
* @param id                          The identifier for this timer.
* @param distributionStatisticConfig Configuration that governs how distribution statistics are computed.
* @return A new or existing timer.
*/
Timer timer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig,
                 PauseDetector pauseDetectorOverride) {
  return registerMeterIfNecessary(
           Timer.class,
           id,
           distributionStatisticConfig,
           (id2, filteredConfig) -> {
             Meter.Id withUnit = id2.withBaseUnit(getBaseTimeUnitStr());
             return newTimer(withUnit,     
                            filteredConfig.merge(defaultHistogramConfig()), 
                            pauseDetectorOverride);
                    },
           NoopTimer::new);
}

注意这里有个newTimer,newTimer方法创建了Timer。

// 这是io.micrometer.prometheus.PrometheusMeterRegistry#newTimer的源码
protected Timer newTimer(Id id, DistributionStatisticConfig distributionStatisticConfig, PauseDetector pauseDetector) {
        MicrometerCollector collector = this.collectorByName(id);
        PrometheusTimer timer = new PrometheusTimer(id, this.clock, distributionStatisticConfig, pauseDetector);
        List<String> tagValues = tagValues(id);
        collector.add((conventionName, tagKeys) -> {
            Builder<Sample> samples = Stream.builder();
            ValueAtPercentile[] percentileValues = timer.takeSnapshot().percentileValues();
            CountAtBucket[] histogramCounts = timer.histogramCounts();
            double count = (double)timer.count();
            int var14;
            if (percentileValues.length > 0) {
                List<String> quantileKeys = new LinkedList(tagKeys);
                quantileKeys.add("quantile");
                ValueAtPercentile[] var12 = percentileValues;
                int var13 = percentileValues.length;

                for(var14 = 0; var14 < var13; ++var14) {
                    ValueAtPercentile v = var12[var14];
                    List<String> quantileValues = new LinkedList(tagValues);
                    quantileValues.add(Collector.doubleToGoString(v.percentile()));
                    samples.add(new Sample(conventionName, quantileKeys, quantileValues, v.value(TimeUnit.SECONDS)));
                }
            }

            Type type = distributionStatisticConfig.isPublishingHistogram() ? Type.HISTOGRAM : Type.SUMMARY;
            if (histogramCounts.length > 0) {
                type = Type.HISTOGRAM;
                List<String> histogramKeys = new LinkedList(tagKeys);
                histogramKeys.add("le");
                CountAtBucket[] var20 = histogramCounts;
                var14 = histogramCounts.length;

                for(int var22 = 0; var22 < var14; ++var22) {
                    CountAtBucket c = var20[var22];
                    List<String> histogramValues = new LinkedList(tagValues);
                    histogramValues.add(Collector.doubleToGoString(c.bucket(TimeUnit.SECONDS)));
                    samples.add(new Sample(conventionName + "_bucket", histogramKeys, histogramValues, c.count()));
                }

                List<String> histogramValuesx = new LinkedList(tagValues);
                histogramValuesx.add("+Inf");
                samples.add(new Sample(conventionName + "_bucket", histogramKeys, histogramValuesx, count));
            }

            samples.add(new Sample(conventionName + "_count", tagKeys, tagValues, count));
            samples.add(new Sample(conventionName + "_sum", tagKeys, tagValues, timer.totalTime(TimeUnit.SECONDS)));
            return Stream.of(new Family(type, conventionName, samples.build()), new Family(Type.GAUGE, conventionName + "_max", Stream.of(new Sample(conventionName + "_max", tagKeys, tagValues, timer.max(this.getBaseTimeUnit())))));
        });
        return timer;
    }

到这里,timer已经被添加到MeterRegistry中了。

特别提示注意这段代码,跟后面我们讲的内容有联系:

MicrometerCollector collector = collectorByName(id);

private MicrometerCollector collectorByName(Meter.Id id) {
    return collectorMap.compute(
                getConventionName(id), 
                (name, existingCollector) -> {
                    if (existingCollector == null) {
                        return new MicrometerCollector(
                                    id,
                                    config().namingConvention(),
                                    prometheusConfig
                                    ).register(registry);
                    }

                    List<String> tagKeys =      
                        getConventionTags(id).stream().map(Tag::getKey).collect(toList());
                    if (existingCollector.getTagKeys().equals(tagKeys)) {
                        return existingCollector;
                    }

                    throw new IllegalArgumentException("Prometheus requires that all meters with the same name have the same" +
                    " set of tag keys. There is already an existing meter containing tag keys [" +
                    existingCollector.getTagKeys().stream().collect(joining(", ")) + "]. The meter you are attempting to register" +
                    " has keys [" + tagKeys.stream().collect(joining(", ")) + "].");
                });
    }

看到register(registry)为止,我终于找到Timer是如何跟CollectorRegistry联系起来的了。

这个方法最终调用了CollectorRegistry里的register方法,将指标放入到collectorRegistry的Map<String, Collector> namesToCollectors和Map<Collector, List<String>> collectorsToNames中。

  public void register(Collector m) {
    List<String> names = collectorNames(m);
    synchronized (collectorsToNames) {
      for (String name : names) {
        if (namesToCollectors.containsKey(name)) {
          throw new IllegalArgumentException("Collector already registered that provides name: " + name);
        }
      }
      for (String name : names) {
        namesToCollectors.put(name, m);
      }
      collectorsToNames.put(m, names);
    }
  }

                    

2 Timer记录数据如何对外输出。

我使用了prometheus采集数据,所以从prometheus采集数据的入口讲起。地址大概是这样的:http://localhost:8080/actuator/prometheus

这个访问地址对应的类是org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint

源码如下:

/*
 * Copyright 2012-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.actuate.metrics.export.prometheus;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;

/**
 * {@link Endpoint} that outputs metrics in a format that can be scraped by the Prometheus
 * server.
 *
 * @author Jon Schneider
 * @since 2.0.0
 */
@WebEndpoint(id = "prometheus")
public class PrometheusScrapeEndpoint {

	private final CollectorRegistry collectorRegistry;

	public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) {
		this.collectorRegistry = collectorRegistry;
	}

	@ReadOperation(produces = TextFormat.CONTENT_TYPE_004)
	public String scrape() {
		try {
			Writer writer = new StringWriter();
			TextFormat.write004(writer, c);
			return writer.toString();
		}
		catch (IOException ex) {
			// This actually never happens since StringWriter::write() doesn't throw any
			// IOException
			throw new RuntimeException("Writing metrics failed", ex);
		}
	}

}

this.collectorRegistry.metricFamilySamples()这个方法,从collectorRegistry的namesToCollectors中获取指标数据。

至此,存取指标终于全流程通畅了。

 类似资料: