// TODO: add startTimeSec - integer epoch seconds only - why startTime does not apply?
// TODO: buffer file writes to bigger chunks?
package kg.apc.jmeter.reporters;
import kg.apc.jmeter.JMeterPluginsUtils;
import org.apache.jmeter.engine.util.NoThreadClone;
import org.apache.jmeter.reporters.AbstractListenerElement;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.samplers.Remoteable;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleListener;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @see ResultCollector
*/
public class FlexibleFileWriter
extends AbstractListenerElement
implements SampleListener, Serializable,
TestStateListener, Remoteable, NoThreadClone {
public static final String AVAILABLE_FIELDS = "isSuccsessful "
+ "startTime endTime "
+ "sentBytes receivedBytes "
+ "responseTime latency "
+ "responseCode responseMessage "
+ "isFailed " // surrogates
+ "threadName sampleLabel "
+ "startTimeMillis endTimeMillis "
+ "responseTimeMicros latencyMicros "
+ "requestData responseData responseHeaders "
+ "threadsCount requestHeaders connectTime "
+ "grpThreads sampleCount errorCount "
+ "responseHeaderSize responseSize URL";
private static final Logger log = LoggingManager.getLoggerForClass();
private static final String OVERWRITE = "overwrite";
private static final String FILENAME = "filename";
private static final String COLUMNS = "columns";
private static final String HEADER = "header";
private static final String FOOTER = "footer";
private static final String VAR_PREFIX = "variable#";
private static final String WRITE_BUFFER_LEN_PROPERTY = "kg.apc.jmeter.reporters.FFWBufferSize";
private final int writeBufferSize = JMeterUtils.getPropDefault(WRITE_BUFFER_LEN_PROPERTY, 1024 * 10);
protected volatile FileChannel fileChannel;
private int[] compiledVars;
private int[] compiledFields;
private ByteBuffer[] compiledConsts;
private ArrayList<String> availableFieldNames = new ArrayList<>(Arrays.asList(AVAILABLE_FIELDS.trim().split(" ")));
private static final byte[] b1 = "1".getBytes();
private static final byte[] b0 = "0".getBytes();
public FlexibleFileWriter() {
super();
}
@Override
public void sampleStarted(SampleEvent e) {
}
@Override
public void sampleStopped(SampleEvent e) {
}
@Override
public void testStarted() {
compileColumns();
try {
openFile();
} catch (FileNotFoundException ex) {
log.error("Cannot open file " + getFilename(), ex);
} catch (IOException ex) {
log.error("Cannot write file header " + getFilename(), ex);
}
}
@Override
public void testStarted(String host) {
testStarted();
}
@Override
public void testEnded() {
closeFile();
}
@Override
public void testEnded(String host) {
testEnded();
}
public void setFilename(String name) {
setProperty(FILENAME, name);
}
public String getFilename() {
return getPropertyAsString(FILENAME);
}
public void setColumns(String cols) {
setProperty(COLUMNS, cols);
}
public String getColumns() {
return getPropertyAsString(COLUMNS);
}
public boolean isOverwrite() {
return getPropertyAsBoolean(OVERWRITE, false);
}
public void setOverwrite(boolean ov) {
setProperty(OVERWRITE, ov);
}
public void setFileHeader(String str) {
setProperty(HEADER, str);
}
public String getFileHeader() {
return getPropertyAsString(HEADER);
}
public void setFileFooter(String str) {
setProperty(FOOTER, str);
}
public String getFileFooter() {
return getPropertyAsString(FOOTER);
}
/**
* making this once to be efficient and avoid manipulating strings in switch
* operators
*/
private void compileColumns() {
log.debug("Compiling columns string: " + getColumns());
String[] chunks = JMeterPluginsUtils.replaceRNT(getColumns()).split("\\|");
log.debug("Chunks " + chunks.length);
compiledFields = new int[chunks.length];
compiledVars = new int[chunks.length];
compiledConsts = new ByteBuffer[chunks.length];
for (int n = 0; n < chunks.length; n++) {
int fieldID = availableFieldNames.indexOf(chunks[n]);
if (fieldID >= 0) {
//log.debug(chunks[n] + " field id: " + fieldID);
compiledFields[n] = fieldID;
} else {
compiledFields[n] = -1;
compiledVars[n] = -1;
if (chunks[n].contains(VAR_PREFIX)) {
log.debug(chunks[n] + " is sample variable");
String varN = chunks[n].substring(VAR_PREFIX.length());
try {
compiledVars[n] = Integer.parseInt(varN);
} catch (NumberFormatException e) {
log.error("Seems it is not variable spec: " + chunks[n]);
compiledConsts[n] = ByteBuffer.wrap(chunks[n].getBytes());
}
} else {
log.debug(chunks[n] + " is const");
if (chunks[n].length() == 0) {
//log.debug("Empty const, treated as |");
chunks[n] = "|";
}
compiledConsts[n] = ByteBuffer.wrap(chunks[n].getBytes());
}
}
}
}
protected void openFile() throws IOException {
String filename = getFilename();
FileOutputStream fos = new FileOutputStream(filename, !isOverwrite());
fileChannel = fos.getChannel();
String header = JMeterPluginsUtils.replaceRNT(getFileHeader());
if (!header.isEmpty()) {
syncWrite(ByteBuffer.wrap(header.getBytes()));
}
}
private synchronized void closeFile() {
if (fileChannel != null && fileChannel.isOpen()) {
try {
String footer = JMeterPluginsUtils.replaceRNT(getFileFooter());
if (!footer.isEmpty()) {
syncWrite(ByteBuffer.wrap(footer.getBytes()));
}
fileChannel.force(false);
fileChannel.close();
} catch (IOException ex) {
log.error("Failed to close file: " + getFilename(), ex);
}
}
}
@Override
public void sampleOccurred(SampleEvent evt) {
if (fileChannel == null || !fileChannel.isOpen()) {
if (log.isWarnEnabled()) {
log.warn("File writer is closed! Maybe test has already been stopped");
}
return;
}
ByteBuffer buf = ByteBuffer.allocateDirect(writeBufferSize);
for (int n = 0; n < compiledConsts.length; n++) {
if (compiledConsts[n] != null) {
//noinspection SynchronizeOnNonFinalField
synchronized (compiledConsts) {
buf.put(compiledConsts[n].duplicate());
}
} else {
if (!appendSampleResultField(buf, evt.getResult(), compiledFields[n])) {
appendSampleVariable(buf, evt, compiledVars[n]);
}
}
}
buf.flip();
try {
syncWrite(buf);
} catch (IOException ex) {
log.error("Problems writing to file", ex);
}
}
private synchronized void syncWrite(ByteBuffer buf) throws IOException {
FileLock lock = fileChannel.lock();
try {
fileChannel.write(buf);
} finally {
lock.release();
}
}
/*
* we work with timestamps, so we assume number > 1000 to avoid tests
* to be faster
*/
private String getShiftDecimal(long number, int shift) {
StringBuilder builder = new StringBuilder();
builder.append(number);
int index = builder.length() - shift;
builder.insert(index, ".");
return builder.toString();
}
private void appendSampleVariable(ByteBuffer buf, SampleEvent evt, int varID) {
if (SampleEvent.getVarCount() < varID + 1) {
buf.put(("UNDEFINED_variable#" + varID).getBytes());
log.warn("variable#" + varID + " does not exist!");
} else {
if (evt.getVarValue(varID) != null) {
buf.put(evt.getVarValue(varID).getBytes());
}
}
}
/**
* @return boolean true if existing field found, false instead
*/
private boolean appendSampleResultField(ByteBuffer buf, SampleResult result, int fieldID) {
// IMPORTANT: keep this as fast as possible
switch (fieldID) {
case 0:
buf.put(result.isSuccessful() ? b1 : b0);
break;
case 1:
buf.put(String.valueOf(result.getStartTime()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 2:
buf.put(String.valueOf(result.getEndTime()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 3:
if (result.getSamplerData() != null) {
buf.put(String.valueOf(result.getSamplerData().length()).getBytes(JMeterPluginsUtils.CHARSET));
} else {
buf.put(b0);
}
break;
case 4:
if (result.getResponseData() != null) {
buf.put(String.valueOf(result.getResponseData().length).getBytes(JMeterPluginsUtils.CHARSET));
} else {
buf.put(b0);
}
break;
case 5:
buf.put(String.valueOf(result.getTime()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 6:
buf.put(String.valueOf(result.getLatency()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 7:
buf.put(result.getResponseCode().getBytes(JMeterPluginsUtils.CHARSET));
break;
case 8:
buf.put(result.getResponseMessage().getBytes(JMeterPluginsUtils.CHARSET));
break;
case 9:
buf.put(!result.isSuccessful() ? b1 : b0);
break;
case 10:
buf.put(result.getThreadName().getBytes(JMeterPluginsUtils.CHARSET));
break;
case 11:
buf.put(result.getSampleLabel().getBytes(JMeterPluginsUtils.CHARSET));
break;
case 12:
buf.put(getShiftDecimal(result.getStartTime(), 3).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 13:
buf.put(getShiftDecimal(result.getEndTime(), 3).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 14:
buf.put(String.valueOf(result.getTime() * 1000).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 15:
buf.put(String.valueOf(result.getLatency() * 1000).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 16:
if (result.getSamplerData() != null) {
buf.put(result.getSamplerData().getBytes(JMeterPluginsUtils.CHARSET));
} else {
buf.put(b0);
}
break;
case 17:
buf.put(result.getResponseData());
break;
case 18:
buf.put(result.getResponseHeaders().getBytes(JMeterPluginsUtils.CHARSET));
break;
case 19:
buf.put(String.valueOf(result.getAllThreads()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 20:
buf.put(String.valueOf(result.getRequestHeaders()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 21:
buf.put(String.valueOf(result.getConnectTime()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 22:
buf.put(String.valueOf(result.getGroupThreads()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 23:
buf.put(String.valueOf(result.getSampleCount()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 24:
buf.put(String.valueOf(result.getErrorCount()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 25:
buf.put(String.valueOf(result.getHeadersSize()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 26:
buf.put(String.valueOf(result.getBodySize()).getBytes(JMeterPluginsUtils.CHARSET));
break;
case 27:
buf.put(result.getUrlAsString().getBytes(JMeterPluginsUtils.CHARSET));
break;
default:
return false;
}
return true;
}
}