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。
相关推荐
《Gradle Jenkins Plugin详解及其应用》 在软件开发过程中,自动化构建工具如Gradle和持续集成服务器如Jenkins是不可或缺的组成部分。它们协同工作,帮助开发者实现高效的代码构建、测试和部署。今天我们将深入探讨...
jenkins集成测试allure 插件
Jenkins 插件(metersphere):metersphere-jenkins-plugin-v1.19.2.hpi
jenkins的Deploy Plugin插件 jenkins的Deploy Plugin插件 jenkins的Deploy Plugin插件
"Jenkins常用插件大全jenkins-plugin-war.tar.gz"这个压缩包文件很可能包含了多个在生产环境中常用的Jenkins插件,使得用户可以快速配置出满足需求的Jenkins环境。 1. **Jenkins插件系统**:Jenkins的插件系统是其...
**Jenkins Android Plugin详解** Jenkins Android Plugin是针对Android开发的一款强大的持续集成工具插件,它使得在Jenkins环境中构建、测试和部署Android项目变得更加便捷和高效。此插件不仅支持自动化构建流程,...
《IDEA离线安装Jenkins Control Plugin及其使用详解》 Jenkins Control Plugin是一款强大的插件,它使得在IntelliJ IDEA(简称IDEA)中管理Jenkins任务变得极为便捷。标题中的"jenkins-control-plugin-0.10.2016-...
《Gitee-Jenkins-Plugin:打造高效持续集成与版本管理》 在现代软件开发流程中,持续集成(Continuous Integration, CI)与版本控制系统扮演着至关重要的角色。Gitee-Jenkins-Plugin作为一款专为Gitee设计的Jenkins...
aws-codebuild-jenkins-plugin, 一个插件,允许Jenkins用户使用 AWS CodeBuild运行一个构建 AWS CodeBuild插件Jenkins CodeBuild插件为Jenkins项目提供了一个构建步骤。 设置 Jenkins我们已经写了一个博客帖子,以...
标题"jenkins-plugin-lan-svn"指向的是Jenkins的一个插件,主要目的是实现本地局域网内Subversion仓库的集成。Subversion是一种集中式的版本控制系统,广泛用于软件开发团队协作中,它允许团队成员跟踪和控制代码...
Jenkins Email Extension Plugin是Jenkins自动化构建工具中的一个重要插件,它扩展了Jenkins的核心邮件通知功能,提供了更丰富的自定义选项和灵活性,使得开发者和持续集成团队能够更有效地进行沟通和协作。...
"jenkins-plugin"指的是 Jenkins 的插件系统,这些插件扩展了 Jenkins 的核心功能,使其能够更好地适应各种开发环境和项目需求。下面将详细讨论这些插件的功能及其在IT开发中的重要性。 1. **maven-plugin**: Maven...
这通常意味着先安装基础组件(如Jenkins Core更新或SVNKit),然后安装特定的插件(如Jenkins Subversion Plugin)。 在配置Jenkins与Subversion的集成时,以下是一些关键步骤: 1. **配置Subversion仓库URL**:在...
jenkins需要安装的plugins,安装方法:jenkins安装好后,直接将plugins.zip上传至jenkins工作目录,并在工作目录(和原有plugins目录同级目录)下解压即可,然后重启jenkins服务。
Jenkins SVN Publisher Plugin是Jenkins生态系统中的一个重要组件,主要用于自动化构建过程中的版本控制集成。它允许用户在Jenkins持续集成服务器完成构建后,自动将生成的成果物发布到Subversion(SVN)仓库中,...
phabricator-jenkins-plugin, Jenkins插件,集成 Phabricator,Harbormaster和 Uberalls phabricator-jenkins插件 这个插件提供了与Jenkins集成的Phabricator 。 它允许Jenkins通过 Harbormaster ( 或者如果...
copy the resulting ./target/credentials.hpi file to the $JENKINS_HOME/plugins directory. Don't forget to restart Jenkins afterwards. 2. or use the plugin management console ...