Jacocoagent 改造-服务端覆盖率数据上报
背景
代码覆盖率服务已经上线一段时间了,用户也反馈了不少问题,大家反馈比较集中的问题就是:
测试忘记在服务器重启前生成覆盖率报告了,导致某段时间内覆盖率数据丢失。
解决这个问题的思路比较简单,就是改造 javaagent,在 jvm 停止时,上报覆盖率数据到我们的代码覆盖率服务,等待生成报告时,将上报的数据和实时的覆盖率数据做合并即可。
实现方案
具体实现方案涉及到两部分:
- jacoco 源码改造
- 代码覆盖率服务(以下简称 cov 服务)改造
jacoco改造
jacoco 改造主要涉及到以下几个类:
- org.jacoco.agent.rt.internal.Agent
- org.jacoco.core.runtime.AgentOptions
在 jacocoagent 的 org.jacoco.agent.rt.internal.Agent 类中,官方已经添加了一个 shutdownHook,只需要在此方法中实现我们的上报逻辑即可。为了方便测试,我们还需要对
org.jacoco.core.runtime.AgentOptions 做改造,增加两个参数 debug 和 host。
- debug:是否启用 debug 模式,如果是 debug 模式,则不会上报覆盖率数据,否则根据 host 配置推送覆盖率数据到代码覆盖率服务器。此参数默认为 false,表示会推送数据,也可以将 debug=true,表示无需推送数据到覆盖率服务。
- host: 测试平台的域名,用来发送覆盖率数据,可以根据 host 的配置,将数据发送到不同的环境。
改造前,需要在 org.jacoco.agent.rt 的 pom.xml 中加入以下依赖:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.konghq</groupId> <artifactId>unirest-java-core</artifactId> <version>4.2.4</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
|
AgentOptions 实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
public static final String DEBUG = "debug";
public static final String HOST = "host";
private static final Collection<String> VALID_OPTIONS = Arrays.asList( DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER, INCLBOOTSTRAPCLASSES, INCLNOLOCATIONCLASSES, SESSIONID, DUMPONEXIT, OUTPUT, ADDRESS, PORT, CLASSDUMPDIR, JMX, HOST, DEBUG);
|
Agent 实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| public static synchronized Agent getInstance(final AgentOptions options) throws Exception { if (singleton == null) { final Agent agent = new Agent(options, IExceptionLogger.SYSTEM_ERR); agent.startup(); Runtime.getRuntime().addShutdownHook( new Thread("Jacocoagent Shutdown hook thread.") { @Override public void run() { AgentLogger.info("Jacocoagent Shutdown hook running."); if (!options.getDebug()) { AgentLogger.info("Not in debug mode, push RuntimeData to OTS server."); File execFile = new File("output.exec"); try { FileOutputStream fos = new FileOutputStream(execFile); ExecutionDataWriter writer = new ExecutionDataWriter(fos); RuntimeData runtimeData = singleton.getData(); runtimeData.collect(writer, writer, false); fos.flush(); AgentLogger.info("Generate exec file success, exec file path is : " + execFile.getAbsolutePath()); } catch (Exception e) { AgentLogger.severe("Failed to generate exec file: " + e.getMessage()); } try { AgentLogger.info("OTS server host is: " + options.getHost()); String reportApiPath = "/coverage/rpc/jacocoagent/report"; HttpResponse<String> response = Unirest.post(options.getHost() + reportApiPath) .field("ip", InetAddress.getLocalHost().getHostAddress()) .field("file", execFile) .asString(); if (response.getStatus() == 200) { AgentLogger.info("Push data to OTS server success, request url is : " + options.getHost() + reportApiPath + ", request body is :" + " {ip:" + InetAddress.getLocalHost().getHostAddress() + ", file:" + execFile.getAbsolutePath() + "}"); } else { AgentLogger.severe("Failed to push data to OTS server, request url is : " + options.getHost() + reportApiPath + ", request body is :" + " {ip:" + InetAddress.getLocalHost().getHostAddress() + ", file:" + execFile.getAbsolutePath() + "}" + ", response status is " + response.getStatus() + ", response body is : " + response.getBody()); } } catch (Exception e) { AgentLogger.severe("Failed to push data to OTS server, on exception: " + e.getMessage()); } finally { if (execFile.exists()) { execFile.delete(); } } } else { AgentLogger.info("In debug mode, skip pushing data to OTS server."); } agent.shutdown(); } }); singleton = agent; } return singleton; }
|
以下是日志工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package org.jacoco.agent.rt.internal;
import java.io.File; import java.util.logging.FileHandler; import java.util.logging.Logger;
public class AgentLogger { private static final Logger logger; private static final String defaultLogFileName = "javaagent.log";
private static final File LOG_LOCK_FILE = new File("javaagent.log.lck");
static { if (LOG_LOCK_FILE.exists()) { LOG_LOCK_FILE.delete(); } logger = Logger.getLogger("AgentLogger"); try { FileHandler fileHandler = new FileHandler(defaultLogFileName, true); fileHandler.setFormatter(new SimpleFormatter()); logger.addHandler(fileHandler); logger.setUseParentHandlers(false); } catch (Exception e){ logger.warning("An error occurred initializing th AgentLogger: " + e.getMessage()); } } public static void info(String msg){ logger.info(msg); } }
|
cov服务改造
cov 服务改造很简单,添加一个 post 接口用来接受上传的文件即可,接口定义如下:
1 2 3 4 5 6 7 8 9 10 11
| @RestController @RequestMapping("/rpc/jacocoagent") public class JacocoagentReportController{ @Autowired private AgentService agentServie;
@PostMapping("/report") public void report(@RequestParam("ip") String ip, @RequestParam("file") MultipartFile file){ agentService.report(ip, file); } }
|
至此,jacocoagent 就实现了在 jvm 停止时,自动上报数据的功能。编译出来的 jacocoagent.jar 位于 org.jacoco.agent/target/classes 目录下。