最近在玩 jmeter,看到结果的报告中有一列数据是 timestamp,可读性太差了,
就想着能不能自己加一列比较直观的时间数据到报告中,于是开始了源码折腾之旅
这是修改前
timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1614223478272,5396,访问百度,200,OK,线程组 1-1,text,true,,2497,118,2,2,https://www.baidu.com/,5392,0,3678
1614223478608,5358,访问百度,200,OK,线程组 1-2,text,true,,2497,118,1,1,https://www.baidu.com/,5358,0,5032
这是修改后
timeStamp,readableTime,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1614223478272,2021-2-25-11-24-38-72,5396,访问百度,200,OK,线程组 1-1,text,true,,2497,118,2,2,https://www.baidu.com/,5392,0,3678
1614223478608,2021-2-25-11-24-38-608,5358,访问百度,200,OK,线程组 1-2,text,true,,2497,118,1,1,https://www.baidu.com/,5358,0,5032
增加可读时间列: 年 - 月-日 - 小时 - 分钟 - 秒-毫秒
首先要能本地调试跑起来,找到程序的入口类,在 src 目录中 launcher 模块:
org.apache.jmeter.NewDriver
修改该类中的启动目录的位置
//JMETER_INSTALLATION_DIRECTORY=tmpDir;
JMETER_INSTALLATION_DIRECTORY = "jmeter(本地jmeter源码目录的绝对路径)";
尝试启动 NewDriver
# 带GUI
org.apache.jmeter.NewDriver
# 不带GUI
org.apache.jmeter.NewDriver -n -t /Users/chencarl/Downloads/workspace/java/github/jmeter/jmx/HTTP请求-baidu.jmx -l baiduResult.jtl
从 jmeter.log 中查看相关日志信息;
找到了下面的代码:
public class HTTPHC4Impl extends HTTPHCAbstractImpl {
@Override
protected HTTPSampleResult sample(URL url, String method,
boolean areFollowingRedirect, int frameDepth) {
if (log.isDebugEnabled()) {
log.debug("Start : sample {} method {} followingRedirect {} depth {}",
url, method, areFollowingRedirect, frameDepth);
}
JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables();
HTTPSampleResult res = createSampleResult(url, method);
CloseableHttpClient httpClient = null;
HttpRequestBase httpRequest = null;
HttpContext localContext = new BasicHttpContext();
HttpClientContext clientContext = HttpClientContext.adapt(localContext);
clientContext.setAttribute(CONTEXT_ATTRIBUTE_AUTH_MANAGER, getAuthManager());
HttpClientKey key = createHttpClientKey(url);
MutableTriple<CloseableHttpClient, AuthState, PoolingHttpClientConnectionManager> triple;
try {
triple = setupClient(key, jMeterVariables, clientContext);
httpClient = triple.getLeft();
URI uri = url.toURI();
httpRequest = createHttpRequest(uri, method, areFollowingRedirect);
setupRequest(url, httpRequest, res); // can throw IOException
} catch (Exception e) {
res.sampleStart();
res.sampleEnd();
errorResult(e, res);
return res;
}
setupClientContextBeforeSample(jMeterVariables, localContext);
res.sampleStart();
final CacheManager cacheManager = getCacheManager();
if (cacheManager != null && HTTPConstants.GET.equalsIgnoreCase(method) && cacheManager.inCache(url, httpRequest.getAllHeaders())) {
return updateSampleResultForResourceInCache(res);
}
CloseableHttpResponse httpResponse = null;
try {
currentRequest = httpRequest;
handleMethod(method, res, httpRequest, localContext);
// store the SampleResult in LocalContext to compute connect time
localContext.setAttribute(CONTEXT_ATTRIBUTE_SAMPLER_RESULT, res);
// perform the sample
httpResponse =
executeRequest(httpClient, httpRequest, localContext, url);
saveProxyAuth(triple, localContext);
if (log.isDebugEnabled()) {
log.debug("Headers in request before:{}", Arrays.asList(httpRequest.getAllHeaders()));
}
// Needs to be done after execute to pick up all the headers
final HttpRequest request = (HttpRequest) localContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
if (log.isDebugEnabled()) {
log.debug("Headers in request after:{}, in localContext#request:{}",
Arrays.asList(httpRequest.getAllHeaders()),
Arrays.asList(request.getAllHeaders()));
}
extractClientContextAfterSample(jMeterVariables, localContext);
// We've finished with the request, so we can add the LocalAddress to it for display
if (localAddress != null) {
request.addHeader(HEADER_LOCAL_ADDRESS, localAddress.toString());
}
res.setRequestHeaders(getAllHeadersExceptCookie(request));
Header contentType = httpResponse.getLastHeader(HTTPConstants.HEADER_CONTENT_TYPE);
if (contentType != null){
String ct = contentType.getValue();
res.setContentType(ct);
res.setEncodingAndType(ct);
}
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
res.setResponseData(readResponse(res, entity.getContent(), entity.getContentLength()));
}
res.sampleEnd(); // Done with the sampling proper.
currentRequest = null;
嗯,看重点有 2 行:
// 在执行之前,设置时间戳,timeStamp,由于我们是增加可读时间,所以就在sampleStart方法里面增加数据了
res.sampleStart();
// 在执行之后,设置其他数据
res.sampleEnd();
增加 setReadableTime 方法调用
public void sampleStart() {
if (startTime == 0) {
long currentTime = currentTimeInMillis();
setStartTime(currentTime);
setReadableTime(currentTime);
} else {
log.error("sampleStart called twice", new Throwable(INVALID_CALL_SEQUENCE_MSG));
}
}
public void setReadableTime(long currentTime) {
LocalDateTime localDateTime = Instant.ofEpochMilli(currentTime).atZone(ZoneId.systemDefault()).toLocalDateTime();
StringBuilder sb = new StringBuilder();
sb.append(localDateTime.getYear()).
append("-").append(localDateTime.getMonthValue())
.append("-").append(localDateTime.getDayOfMonth())
.append("-").append(localDateTime.getHour())
.append("-").append(localDateTime.getMinute())
.append("-").append(localDateTime.getSecond())
.append("-").append(localDateTime.getNano()/1000000);
this.readableTime = sb.toString();
}
第一张图是说在执行 jmeter 界面中可以设置结果的位置,
第二张图是说在结果中要显示哪些列,其中 Save ReadableTime 是新增加的选项,
而这个字符串是写在 messages.properties 文件中的,
因为开始我以为是用 this.getClass().getClassLoader().getResourceAsStream 方法读取的,
找了半天也没有找到,实际它是在下面的代码中进行读取的,读取的结果存放在 ResourceBundle 中
public class JMeterUtils implements UnitTestManager {
public static void setLocale(Locale loc) {
log.info("Setting Locale to {}", loc);
/*
* See bug 29920. getBundle() defaults to the property file for the
* default Locale before it defaults to the base property file, so we
* need to change the default Locale to ensure the base property file is
* found.
*/
Locale def = null;
boolean isDefault = false; // Are we the default language?
if (loc.getLanguage().equals(ENGLISH_LANGUAGE)) {
isDefault = true;
def = Locale.getDefault();
// Don't change locale from en_GB to en
if (!def.getLanguage().equals(ENGLISH_LANGUAGE)) {
Locale.setDefault(Locale.ENGLISH);
} else {
def = null; // no need to reset Locale
}
}
if ("ignoreResources".equals(loc.toString())){ // $NON-NLS-1$
log.warn("Resource bundles will be ignored");
ignoreResources = true;
// Keep existing settings
} else {
ignoreResources = false;
ResourceBundle resBund = ResourceBundle.getBundle("org.apache.jmeter.resources.messages", loc); // $NON-NLS-1$
resources = resBund;
locale = loc;
final Locale resBundLocale = resBund.getLocale();
if (!isDefault && !resBundLocale.equals(loc)) {
// Check if we at least found the correct language:
if (resBundLocale.getLanguage().equals(loc.getLanguage())) {
log.info("Could not find resources for '{}', using '{}'", loc, resBundLocale);
} else {
log.error("Could not find resources for '{}'", loc);
}
}
}
保存结果的动作在 CSVSaveService 中,分两部分,
public static String printableFieldNamesToString(
SampleSaveConfiguration saveConfig) {
StringBuilder text = new StringBuilder();
String delim = saveConfig.getDelimiter();
appendFields(saveConfig.saveTimestamp(), text, delim, TIME_STAMP);
appendFields(saveConfig.saveReadableTime(), text, delim, READABLE_TIME);
public static String resultToDelimitedString(SampleEvent event,
SampleResult sample,
SampleSaveConfiguration saveConfig,
final String delimiter) {
StringQuoter text = new StringQuoter(delimiter.charAt(0));
if (saveConfig.saveTimestamp()) {
if (saveConfig.printMilliseconds()) {
text.append(sample.getTimeStamp());
} else if (saveConfig.threadSafeLenientFormatter() != null) {
String stamp = saveConfig.threadSafeLenientFormatter().format(
new Date(sample.getTimeStamp()));
text.append(stamp);
}
}
if (saveConfig.saveReadableTime()) {
text.append(sample.getReadableTime());
}