Jenkins JFR 插件主要是用来解析JRockit Flight Record (不了解JRockit 飞行日志可以google一下),并且以SVG图形展示CPU,MEM等情况。想进一步了解代码的可以戳这里:
https://github.com/WalseWu/jenkins-jfr。
言归正传,回到hudson plugin,关于hudson的介绍就戳这里: http://wiki.eclipse.org/The_Hudson_Book 。简述下:hudson的插件机制主要基于Stapler和Jelly实现,Stapler主要用于将Hudson Classes绑定到URLs,举个简单的例子: 比如有个方法Hudson.getJob(String jobName),那么URL /job/foo/ 将会和 Hudson.getJob("foo")的返回值对象(应该是一个Project对象)绑定。Jelly可以理解为类似jsp和jstl;hudson通过jelly和stapler展示页面,和获取model对象。既然是插件机制,hudson提供了很多扩展点,详细请戳这里:http://wiki.hudson-ci.org/display/HUDSON/Extension+points。下面从扩展点入手,来介绍下Jenkins JFR 插件的开发:
1. Recorder
该扩展点(hudson.ExtensionPoint)是一种Publisher(hudson.tasks.Publisher),其类图我就不画了(其实是OEL系统上木有装画图工具)。看javadoc:
Recorder is a kind of Publisher that collects statistics from the build, and can mark builds as unstable/failure. This marking ensures that builds are marked accordingly before notifications are sent via Notifiers. Otherwise, if the build is marked failed after some notifications are sent, inconsistency ensues.
To register a custom Publisher from a plugin, put Extension on your descriptor.
意思表述的很明确,我用代码来解释吧。
1)扩展Recorder,我写一个JFRPublisher作为整个插件的入口,类的架子大概如下:
public class JFRPublisher extends Recorder
{
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publishe>
{
@Override
public String getDisplayName()
{
return Messages.Publisher_DisplayName();
}
@Override
public String getHelpFile()
{
return "/plugin/hudson-jfr/help.html";
}
public List<JFRReportParserDescriptor> getParserDescriptors()
{
return JFRReportParserDescriptor.all();
}
@Override
public boolean isApplicable(@SuppressWarnings("rawtypes") Class<? extends AbstractProject> jobType)
{
return true;
}
}
private List<JFRReportParser> parsers;
@DataBoundConstructor
public JFRPublisher(List<? extends JFRReportParser> parsers)
{
if (parsers == null) {
parsers = Collections.emptyList();
}
this.parsers = new ArrayList<JFRReportParser>(parsers);
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException,
IOException
{
// .......暂时省略具体代码
}
//省略具体代码
}
现在来解释前面的javadoc的描述,首先看最后一句注册一个custom publisher可以简单理解为代码中@Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher>,该类的作用是将JFRPubliusher注册为一个Publisher,于是在hudson配置页面会去查找hudson.plugins.jfr.JFRPublisher.config.jelly,用以显示页面,这里的的jelly是这样的:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry
field="parsers">
<f:hetero-list name="parsers" hasHeader="true"
descriptors="${descriptor.getParserDescriptors()}"
items="${instance.parsers}"
addCaption="${%Add a new report}"/>
</f:entry>
</j:jelly>
hetero-list,descriptors="${descriptor.getParserDescriptors()}"的值就是DescriptorImpl。getDisplayName()的返回值(这里是“Publish JFR result report”),items="${instance.parsers}"就是JFRPublisher的属性private List<JFRReportParser> parsers。先看下页面显示效果是这样的:
当选中了Publish JFR result repot后,就会弹出一个parses(来自private List<JFRReportParser> parsers)的下拉框提供选择(标题Add a new report来自上面jelly中addCaption设置),选择JFR Report就显示页面如下:
2)ExtensionPoint
该页面来自何处呢?显然来自JFRReportParser,因为上面下拉框中的每一项都是一个JFRReportParser对象。看代码:
public class JFRReportParser implements Describable<JFRReportParser>, ExtensionPoint
{
@Extension
public static class DescriptorImpl extends JFRReportParserDescriptor
{
@Override
public String getDisplayName()
{
return "JFRReport";
}
}
/**
* All registered implementations.
*/
public static ExtensionList<JFRReportParser> all()
{
return Hudson.getInstance().getExtensionList(JFRReportParser.class);
}
/**
* GLOB patterns that specify the jfr report.
*/
public final String glob;
public final String title;
public final int width;
public final int height;
/**
* JRockit Flight Recording events values which will be show on the graphs.
*/
public final String jfrEventSettingStr;
@DataBoundConstructor
public JFRReportParser(String glob, String jfrEventSettingStr, String title, int width, int height)
{
this.glob = glob == null || glob.length() == 0 ? getDefaultGlobPattern() : glob;
this.jfrEventSettingStr = jfrEventSettingStr == null || jfrEventSettingStr.length() == 0 ? getDefaultJFREventsPattern()
: jfrEventSettingStr.trim();
resetDisplayName();
this.title = title == null || title.length() == 0 ? "" : title;
this.width = width <= 0 ? DEFAULT_WIDTH : width;
this.height = height <= 0 ? DEFAULT_HEIGHT : height;
}
//........省略JFR event parse的逻辑,有兴趣可以 checkout git 源码:https://github.com/WalseWu/jenkins-jfr。
}
Describable<JFRReportParser>, ExtensionPoint,但是和上面的publisher也大同小异,每一项的配置都会对应一个类中的属性,通过@DataBoundConstructor从页面提交中获取设置到变量中,对应jelly如下:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Report files}" field="glob">
<f:textbox />
</f:entry>
<f:entry title="${%JFR Events}" field="jfrEventSettingStr">
<f:textbox />
</f:entry>
<f:entry title="${%Graph Title:}" help="/plugin/hudson-jfr/graph-help.html">
<table width="100%">
<tr width="100%">
<td width="488">
<f:textbox field="title" width="488"/>
</td>
<td width="10"></td>
<td style="vertical-align:middle">${%Width}:</td>
<td>
<f:textbox field="width" width="100"/>
</td>
<td width="10"></td>
<td style="vertical-align:middle">${%Height}:</td>
<td>
<f:textbox field="height" width="100"/>
</td>
</tr>
</table>
</f:entry>
</j:jelly>
3)BuildStep
其实recorder本身就是BuildStep的实现类,该接口主要定义了一些生命周期函数,如prebuild,perform. perform方法可以用来添加一下action以便一些report保存到build中,著名的junit plugin就是这么做的。我们也类似:
//JFRPublisher
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException,
IOException
{
PrintStream logger = listener.getLogger();
// add the report to the build object.
JFRBuildAction buildAction = new JFRBuildAction(build, logger, parsers);
build.addAction(buildAction);
for (JFRReportParser parser : parsers) {
String glob = parser.glob;
logger.println("JFR: Recording " + parser.getReportName() + " reports '" + glob + "'");
List<FilePath> files = locateJFRReports(build.getWorkspace(), glob, logger);
if (files.isEmpty()) {
// build.setResult(Result.FAILURE);
logger.println("JFR: no " + parser.getReportName() + " files matching '" + glob
+ "' have been found. Has the report generated?. Setting Build to " + build.getResult());
return true;
}
copyReportsToMaster(build, logger, files, parser.getDisplayName());
}
return true;
}
上面代码,每次build完,我创建了一个JFRBuildAction并添加到build中,这样buildAction就可以做他该做的事情(本插件当然是去解析展示JFR Report了);然后我遍历jenkins配置的parsers,根据配置去相应的jfr report 文件拷贝到每个build自己统一房jfr的地方。
4)StaplerProxy,Action
下面是JFRBuilgAction:
public class JFRBuildAction implements Action, StaplerProxy{
private final AbstractBuild<?, ?> build;
private final List<JFRReportParser> parsers;
private transient final PrintStream hudsonConsoleWriter;
private transient WeakReference<JFRReportMap> jfrReportMap;
private static final Logger logger = Logger.getLogger(JFRBuildAction.class.getName());
public JFRBuildAction(AbstractBuild<?, ?> pBuild, PrintStream logger, List<JFRReportParser> parsers)
{
build = pBuild;
hudsonConsoleWriter = logger;
this.parsers = parsers;
}
public JFRReportParser getParserByDisplayName(String displayName)
{
if (parsers != null) {
for (JFRReportParser parser : parsers) {
if (parser.getDisplayName().equals(displayName)) {
return parser;
}
}
}
return null;
}
public Object getTarget()
{
return getJfrReportMap();
}
public String getUrlName()
{
return "JFRReport";
}
private JFRReportMap getJfrReportMap()
{
JFRReportMap reportMap = null;
WeakReference<JFRReportMap> wr = jfrReportMap;
if (wr != null) {
reportMap = wr.get();
if (reportMap != null) {
return reportMap;
}
}
try {
reportMap = new JFRReportMap(this, new StreamTaskListener(System.err, Computer.currentComputer().getDefaultCharset()));
}
catch (IOException e) {
logger.log(Level.SEVERE, "Error creating new JFRReportMap()", e);
}
jfrReportMap = new WeakReference<JFRReportMap>(reportMap);
return reportMap;
}
//。。。省略部分代码
}
重要的几个点:
a) getDisplayName返回值(我们config中会返回JFR Report)会在每个build页面的左侧生成一个link
b) 点击这个link会跳转到 由方法getTarget()返回值对应的地方, 所以我们接下来要看JFRReportMap
5)an ModelObject JFRReportMap
上面点了link跳转到JFRReportMap到底是什么页面呢?先要看hudson.plugins.jfr.JFRReportMap.index.jelly
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout xmlns:jm="/hudson/plugins/jfr/tags" css="/plugin/jfr-plugin/css/style.css">
<st:include it="${it.build}" page="sidepanel.jelly" />
<l:main-panel>
<h1>JFR Report</h1>
<j:forEach var="jfrReport" items="${it.getJFRListOrdered()}">
<a href="./jfrEventTimeGraph?height=-1&width=-1&jfrReportPosition=${jfrReport.getReportFileName()}"
target="_blank" title="${%Click for larger image}">
<h2>${jfrReport.getReportFileName()}</h2>
</a>
<object data="./jfrEventTimeGraph?jfrReportPosition=${jfrReport.getReportFileName()}" width="${jfrReport.getWidth()}" height="${jfrReport.getHeight()}" />
</j:forEach>
</l:main-panel>
</l:layout>
</j:jelly>
./jfrEventTimeGraph?,后面是参数,这个url对应一个方法jfrReportMap。doJfrEventTimeGraph(StaplerRequest,StaplerResponse),这就是Staplar的作用。看代码:
//in JFRReportMap.java
public void doJfrEventTimeGraph(StaplerRequest request, StaplerResponse response) throws IOException
{
try {
String parameter = request.getParameter("jfrReportPosition");
JFreeChart chart = GraphHelper.createOverlappingJFREventChart(getJFRReport(parameter));
GraphHelper.exportChartAsSVG(chart, getJFRReport(parameter).getWidth(), getJFRReport(parameter).getHeight(), request,
response);
}
catch (Exception e) {
String newline = System.getProperty("line.separator");
//Set exception message back for the failed test.
StringWriter sb = new StringWriter(2048);
sb.append(e.getMessage());
sb.append(newline);
e.printStackTrace(new PrintWriter(sb, true));
logger.info(sb.toString());
}
}
public List<JFRReport> getJFRListOrdered()
{
List<JFRReport> listJFR = new ArrayList<JFRReport>(getJFRReportMap().values());
logger.info("Ordered JFR Reports:" + listJFR.size());
Collections.sort(listJFR);
return listJFR;
}
这样SVG图就展示了, 大图和小图的区别是url后面的参数不一样。
到这里jenkins相关的都介绍完了,还剩下具体report的解析:
//JFRReportMap.java 在构造时就解析jfr了
/**
* Parses the reports and build a {@link JFRReportMap}.
*
* @throws IOException
* If a report fails to parse.
*/
JFRReportMap(final JFRBuildAction buildAction, TaskListener listener) throws IOException
{
this.buildAction = buildAction;
parseReports(getBuild(), listener, null);
}
private void parseReports(AbstractBuild<?, ?> build, TaskListener listener, final String filename) throws IOException
{
File repo = new File(build.getRootDir(), JFRReportMap.getJFRReportDirRelativePath());
File[] dirs = repo.listFiles(new FileFilter() {
public boolean accept(File f)
{
return f.isDirectory();
}
});
// this may fail, if the build itself failed, we need to recover
// gracefully
if (dirs != null) {
for (File dir : dirs) {
JFRReportParser p = buildAction.getParserByDisplayName(dir.getName());
if (p != null) {
File[] listFiles = dir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name)
{
if (filename == null) {
return true;
}
if (name.equals(filename)) {
return true;
}
return false;
}
});
addAll(p.parse(build, Arrays.asList(listFiles), listener));
}
}
}
}
//下面是JFRReportParser。parse
public Collection<JFRReport> parse(AbstractBuild<?, ?> build, Collection<File> reports, TaskListener listener)
throws IOException
{
Set<FileGroup> groupReports = groupFiles(reports);
List<JFRReport> result = new LinkedList<JFRReport>();
for (FileGroup fg : groupReports) {
JFRReport r = new JFRReport(width, height);
r.setGlobalName(glob, fg, getTitle());
parseEventsReport(fg, r, getJfrEventSettings());
result.add(r);
}
Collections.sort(result);
return result;
}
具体jfr report的结构设计见下面类图,代码就不占篇幅了,有兴趣的可以去check out code: https://github.com/WalseWu/jenkins-jfr。