欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Jenkins JFR Plugin

程序员文章站 2022-04-30 08:41:52
...

        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。先看下页面显示效果是这样的:


Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson       当选中了Publish JFR result repot后,就会弹出一个parses(来自
private List<JFRReportParser> parsers)的下拉框提供选择(标题Add a new report来自上面jelly中addCaption设置),选择JFR Report就显示页面如下:


Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson  
 

 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&amp;width=-1&amp;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

 
Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson  
 

  • Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson  
  • 大小: 19 KB
  • Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson  
  • 大小: 10.5 KB
  • Jenkins JFR Plugin
            
    
    博客分类: Jenkins_hudson  
  • 大小: 48.8 KB