1 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中获取指标数据。
至此,存取指标终于全流程通畅了。