Jmx_exporter是以代理的形式收集目标应用的jmx指标,这样做的好处在于无需对目标应用做任何的改动。
-javaagent:/path/to/JavaAgent.jar=[host:]:
下面开始进入源码分析
入口位于:
io.prometheus.jmx.JavaAgent#agentmain
public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception {
premain(agentArgument, instrumentation);
}
public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception {
// Bind to all interfaces by default (this includes IPv6).
String host = "0.0.0.0";
// If we have IPv6 address in square brackets, extract it first and then
// remove it from arguments to prevent confusion from too many colons.
Integer indexOfClosingSquareBracket = agentArgument.indexOf("]:");
if (indexOfClosingSquareBracket >= 0) {
host = agentArgument.substring(0, indexOfClosingSquareBracket + 1);
agentArgument = agentArgument.substring(indexOfClosingSquareBracket + 2);
}
String[] args = agentArgument.split(":");
if (args.length < 2 || args.length > 3) {
System.err.println("Usage: -javaagent:/path/to/JavaAgent.jar=[host:]:");
System.exit(1);
}
int port;
String file;
InetSocketAddress socket;
if (args.length == 3) {
port = Integer.parseInt(args[1]);
socket = new InetSocketAddress(args[0], port);
file = args[2];
} else {
port = Integer.parseInt(args[0]);
socket = new InetSocketAddress(host, port);
file = args[1];
}
new BuildInfoCollector().register();
new JmxCollector(new File(file)).register();
DefaultExports.initialize();
server = new HTTPServer(socket, CollectorRegistry.defaultRegistry, true);
}
agentArgument 是通过命令行传入的参数,从中解析出IP地址host,端口port,配置文件file
收集的信息会添加到MetricFamilySamples 的列表中,首先看看MetricFamilySamples的结构
public static class MetricFamilySamples {
public final String name;
public final Collector.Type type;
public final String help;
public final List samples;
。。。。。。。。
这个类主要由以下几个属性:
再进一步看看样本Sample的结构
io.prometheus.client.Collector.MetricFamilySamples.Sample
public static class Sample {
public final String name;
public final List labelNames;
public final List labelValues;
public final double value;
public final Long timestampMs;
用于收集 jxm_exporter的版本信息,让我们看看这个收集器是如何收集编译版本信息的
io.prometheus.jmx.BuildInfoCollector#collect
public List collect() {
List mfs = new ArrayList();
GaugeMetricFamily artifactInfo = new GaugeMetricFamily(
"jmx_exporter_build_info",
"A metric with a constant '1' value labeled with the version of the JMX exporter.",
asList("version", "name"));
Package pkg = this.getClass().getPackage();
String version = pkg.getImplementationVersion();
String name = pkg.getImplementationTitle();
artifactInfo.addMetric(asList(
version != null ? version : "unknown",
name != null ? name : "unknown"
), 1L);
mfs.add(artifactInfo);
return mfs;
}
1、 获得包名和版本号:name,version
2、 将name,version封装成Sample,然后添加进MetricFamilySamples中的样本集合中
主要收集jmx_exporter.yaml文件中指定的对象的jmx指标。
public JmxCollector(File in) throws IOException, MalformedObjectNameException {
configFile = in;
config = loadConfig((Map)new Yaml().load(new FileReader(in)));
config.lastUpdate = configFile.lastModified();
}
首先通过构造函数把配置文件加载进来到config中,下面看如何收集信息
io.prometheus.jmx.JmxCollector#collect
public List collect() {
if (configFile != null) {
long mtime = configFile.lastModified();
if (mtime > config.lastUpdate) {
LOGGER.fine("Configuration file changed, reloading...");
reloadConfig();
}
}
Receiver receiver = new Receiver();
JmxScraper scraper = new JmxScraper(config.jmxUrl, config.username, config.password, config.ssl,
config.whitelistObjectNames, config.blacklistObjectNames, receiver, jmxMBeanPropertyCache);
long start = System.nanoTime();
double error = 0;
if ((config.startDelaySeconds > 0) &&
((start - createTimeNanoSecs) / 1000000000L < config.startDelaySeconds)) {
throw new IllegalStateException("JMXCollector waiting for startDelaySeconds");
}
try {
scraper.doScrape();
} catch (Exception e) {
error = 1;
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
LOGGER.severe("JMX scrape failed: " + sw.toString());
}
List mfsList = new ArrayList();
mfsList.addAll(receiver.metricFamilySamplesMap.values());
List samples = new ArrayList();
samples.add(new MetricFamilySamples.Sample(
"jmx_scrape_duration_seconds", new ArrayList(), new ArrayList(), (System.nanoTime() - start) / 1.0E9));
mfsList.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", samples));
samples = new ArrayList();
samples.add(new MetricFamilySamples.Sample(
"jmx_scrape_error", new ArrayList(), new ArrayList(), error));
mfsList.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", samples));
return mfsList;
}
public void doScrape() throws Exception {
MBeanServerConnection beanConn;
JMXConnector jmxc = null;
if (jmxUrl.isEmpty()) {
beanConn = ManagementFactory.getPlatformMBeanServer();
} else {
Map environment = new HashMap();
if (username != null && username.length() != 0 && password != null && password.length() != 0) {
String[] credent = new String[] {username, password};
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credent);
}
if (ssl) {
environment.put(Context.SECURITY_PROTOCOL, "ssl");
SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory();
environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
}
jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment);
beanConn = jmxc.getMBeanServerConnection();
}
try {
// Query MBean names, see #89 for reasons queryMBeans() is used instead of queryNames()
Set mBeanNames = new HashSet();
for (ObjectName name : whitelistObjectNames) {
for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
mBeanNames.add(instance.getObjectName());
}
}
for (ObjectName name : blacklistObjectNames) {
for (ObjectInstance instance : beanConn.queryMBeans(name, null)) {
mBeanNames.remove(instance.getObjectName());
}
}
// Now that we have *only* the whitelisted mBeans, remove any old ones from the cache:
jmxMBeanPropertyCache.onlyKeepMBeans(mBeanNames);
for (ObjectName objectName : mBeanNames) {
long start = System.nanoTime();
scrapeBean(beanConn, objectName);
logger.fine("TIME: " + (System.nanoTime() - start) + " ns for " + objectName.toString());
}
} finally {
if (jmxc != null) {
jmxc.close();
}
}
}
private void scrapeBean(MBeanServerConnection beanConn, ObjectName mbeanName) {
MBeanInfo info;
try {
info = beanConn.getMBeanInfo(mbeanName);
} catch (IOException e) {
logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
return;
} catch (JMException e) {
logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
return;
}
MBeanAttributeInfo[] attrInfos = info.getAttributes();
Map name2AttrInfo = new LinkedHashMap();
for (int idx = 0; idx < attrInfos.length; ++idx) {
MBeanAttributeInfo attr = attrInfos[idx];
if (!attr.isReadable()) {
logScrape(mbeanName, attr, "not readable");
continue;
}
name2AttrInfo.put(attr.getName(), attr);
}
final AttributeList attributes;
try {
attributes = beanConn.getAttributes(mbeanName, name2AttrInfo.keySet().toArray(new String[0]));
} catch (Exception e) {
logScrape(mbeanName, name2AttrInfo.keySet(), "Fail: " + e);
return;
}
for (Attribute attribute : attributes.asList()) {
MBeanAttributeInfo attr = name2AttrInfo.get(attribute.getName());
logScrape(mbeanName, attr, "process");
processBeanValue(
mbeanName.getDomain(),
jmxMBeanPropertyCache.getKeyPropertyList(mbeanName),
new LinkedList(),
attr.getName(),
attr.getType(),
attr.getDescription(),
attribute.getValue()
);
}
}
scrapeBean方法会变量对象的每个属性,将属性信息交给processBeanValue方法处理,processBeanValue是一个递归方法,如果属性不是基本类型,可进一步解析出下一层属性。属性信息最终是通过receiver.recordBean方法转化为Sample对象添加入MetricFamilySamples中。
public void recordBean(
String domain,
LinkedHashMap beanProperties,
LinkedList attrKeys,
String attrName,
String attrType,
String attrDescription,
Object beanValue) {
String beanName = domain + angleBrackets(beanProperties.toString()) + angleBrackets(attrKeys.toString());
// attrDescription tends not to be useful, so give the fully qualified name too.
String help = attrDescription + " (" + beanName + attrName + ")";
String attrNameSnakeCase = toSnakeAndLowerCase(attrName);
for (Rule rule : config.rules) {
Matcher matcher = null;
String matchName = beanName + (rule.attrNameSnakeCase ? attrNameSnakeCase : attrName);
if (rule.pattern != null) {
matcher = rule.pattern.matcher(matchName + ": " + beanValue);
if (!matcher.matches()) {
continue;
}
}
Number value;
if (rule.value != null && !rule.value.isEmpty()) {
String val = matcher.replaceAll(rule.value);
try {
beanValue = Double.valueOf(val);
} catch (NumberFormatException e) {
LOGGER.fine("Unable to parse configured value '" + val + "' to number for bean: " + beanName + attrName + ": " + beanValue);
return;
}
}
if (beanValue instanceof Number) {
value = ((Number)beanValue).doubleValue() * rule.valueFactor;
} else if (beanValue instanceof Boolean) {
value = (Boolean)beanValue ? 1 : 0;
} else {
LOGGER.fine("Ignoring unsupported bean: " + beanName + attrName + ": " + beanValue);
return;
}
// If there's no name provided, use default export format.
if (rule.name == null) {
defaultExport(domain, beanProperties, attrKeys, rule.attrNameSnakeCase ? attrNameSnakeCase : attrName, help, value, rule.type);
return;
}
// Matcher is set below here due to validation in the constructor.
String name = safeName(matcher.replaceAll(rule.name));
if (name.isEmpty()) {
return;
}
if (config.lowercaseOutputName) {
name = name.toLowerCase();
}
// Set the help.
if (rule.help != null) {
help = matcher.replaceAll(rule.help);
}
// Set the labels.
ArrayList labelNames = new ArrayList();
ArrayList labelValues = new ArrayList();
if (rule.labelNames != null) {
for (int i = 0; i < rule.labelNames.size(); i++) {
final String unsafeLabelName = rule.labelNames.get(i);
final String labelValReplacement = rule.labelValues.get(i);
try {
String labelName = safeName(matcher.replaceAll(unsafeLabelName));
String labelValue = matcher.replaceAll(labelValReplacement);
if (config.lowercaseOutputLabelNames) {
labelName = labelName.toLowerCase();
}
if (!labelName.isEmpty() && !labelValue.isEmpty()) {
labelNames.add(labelName);
labelValues.add(labelValue);
}
} catch (Exception e) {
throw new RuntimeException(
format("Matcher '%s' unable to use: '%s' value: '%s'", matcher, unsafeLabelName, labelValReplacement), e);
}
}
}
// Add to samples.
LOGGER.fine("add metric sample: " + name + " " + labelNames + " " + labelValues + " " + value.doubleValue());
addSample(new MetricFamilySamples.Sample(name, labelNames, labelValues, value.doubleValue()), rule.type, help);
return;
}
}
在处理属性信息的过程中会根据配置中rule定义的正则表达式去过滤不需要的信息。
注册一些系统级别的采集器,这些采集器用于采集一些通用指标
DefaultExports.initialize();
public static synchronized void initialize() {
if (!initialized) {
(new StandardExports()).register();
(new MemoryPoolsExports()).register();
(new BufferPoolsExports()).register();
(new GarbageCollectorExports()).register();
(new ThreadExports()).register();
(new ClassLoadingExports()).register();
(new VersionInfoExports()).register();
initialized = true;
}
}
截止目前,已经介绍了jmx_exporter中所有的收集器了,我们接着看这些收集器如何注册的,因此需要进入register方法中:
io.prometheus.client.Collector#register()
public T register() {
return this.register(CollectorRegistry.defaultRegistry);
}
public T register(CollectorRegistry registry) {
registry.register(this);
return this;
}
public void register(Collector m) {
List names = this.collectorNames(m);
Map var3 = this.collectorsToNames;
synchronized(this.collectorsToNames) {
Iterator var4 = names.iterator();
String name;
while(var4.hasNext()) {
name = (String)var4.next();
if (this.namesToCollectors.containsKey(name)) {
throw new IllegalArgumentException("Collector already registered that provides name: " + name);
}
}
var4 = names.iterator();
while(var4.hasNext()) {
name = (String)var4.next();
this.namesToCollectors.put(name, m);
}
this.collectorsToNames.put(m, names);
}
}
收集器的最终是注册到
server = new HTTPServer(socket, CollectorRegistry.defaultRegistry, true);
public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
this.server = HttpServer.create();
this.server.bind(addr, 3);
HttpHandler mHandler = new HTTPServer.HTTPMetricHandler(registry);
this.server.createContext("/", mHandler);
this.server.createContext("/metrics", mHandler);
this.executorService = Executors.newFixedThreadPool(5, HTTPServer.DaemonThreadFactory.defaultThreadFactory(daemon));
this.server.setExecutor(this.executorService);
HttpHandler mHandler = new HTTPServer.HTTPMetricHandler(registry);
}
这里主要注意两点:
1、 注册器registry被处理http请求的handler持有了HttpHandler mHandler = new HTTPServer.HTTPMetricHandler(registry);
2、 服务通过HttpHandler mHandler = new HTTPServer.HTTPMetricHandler(registry);启动对外提供服务。
服务启动之后,就可以通过http协议去查询指标信息了。而服务的请求则是由mHandler处理的,下面我们看看请求是如何获取指标信息的
public void handle(HttpExchange t) throws IOException {
String query = t.getRequestURI().getRawQuery();
ByteArrayOutputStream response = (ByteArrayOutputStream)this.response.get();
response.reset();
OutputStreamWriter osw = new OutputStreamWriter(response);
TextFormat.write004(osw, this.registry.filteredMetricFamilySamples(HTTPServer.parseQuery(query)));
osw.flush();
osw.close();
response.flush();
response.close();
t.getResponseHeaders().set("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
t.getResponseHeaders().set("Content-Length", String.valueOf(response.size()));
if (HTTPServer.shouldUseCompression(t)) {
t.getResponseHeaders().set("Content-Encoding", "gzip");
t.sendResponseHeaders(200, 0L);
GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
response.writeTo(os);
os.finish();
} else {
t.sendResponseHeaders(200, (long)response.size());
response.writeTo(t.getResponseBody());
}
t.close();
}
可以看到这里调用了收集器注册器的方法registry.filteredMetricFamilySamples(HTTPServer.parseQuery(query))
public Enumeration filteredMetricFamilySamples(Set includedNames) {
return new CollectorRegistry.MetricFamilySamplesEnumeration(includedNames);
}
MetricFamilySamplesEnumeration(Set includedNames) {
this.includedNames = includedNames;
this.collectorIter = this.includedCollectorIterator(includedNames);
this.findNextElement();
}
this.includedCollectorIterator(includedNames)根据查询条件获取收集器,然后进入下一步this.findNextElement()
private void findNextElement() {
this.next = null;
while(this.metricFamilySamples != null && this.metricFamilySamples.hasNext()) {
this.next = this.filter((MetricFamilySamples)this.metricFamilySamples.next());
if (this.next != null) {
return;
}
}
if (this.next == null) {
while(this.collectorIter.hasNext()) {
this.metricFamilySamples = ((Collector)this.collectorIter.next()).collect().iterator();
while(this.metricFamilySamples.hasNext()) {
this.next = this.filter((MetricFamilySamples)this.metricFamilySamples.next());
if (this.next != null) {
return;
}
}
}
}
}
1、 用collectorIter中的收集器去获取标量信息
2、 根据includedNames去找到请求需要的标量信息。
这样第三方就可以通过http的方式来获取指标信息了。
至此,jmx_exporter的源码分析完毕!