Jenkins JFR Plugin
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。 }
虽然扩展点不是Recoder,而是直接扩展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>
解读下,主要是遍历jfrReportMap.getJFRListOrdered(),每个JFRReport会生成一个<a href=... />和一个<object data="..." />,什么意思呢?其实url是./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。
上一篇: 笔记:Sql数据统计
下一篇: 转载 程序员技术练级攻略
推荐阅读
-
Windows环境配置jenkins打包Android项目和vue项目
-
解决Jenkins集成docker插件问题的一些方法
-
InnoDB Memcached Plugin源码实现调研
-
MySQL Audit Plugin now available in Percona Server 5.5 and 5_MySQL
-
centos 7系统下安装Jenkins的步骤详解
-
Docker中完成Jenkins的安装
-
Jenkins 安装配置
-
Jenkins + Svn + Ant持续集成(增量包处理)
-
spring+mybatis利用interceptor(plugin)兑现数据库读写分离
-
spring+mybatis利用interceptor(plugin)兑现数据库读写分离