package hudson.plugins.performance.parsers; import hudson.Extension; import hudson.plugins.performance.data.HttpSample; import hudson.plugins.performance.descriptors.PerformanceReportParserDescriptor; import hudson.plugins.performance.reports.PerformanceReport; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVRecord; import org.kohsuke.stapler.DataBoundConstructor; import org.xml.sax.SAXException; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; public class JMeterCsvParser extends AbstractParser { public char delimiter; public int timestampIdx = -1; public int elapsedIdx = -1; public int responseCodeIdx = -1; public int successIdx = -1; public int urlIdx = -1; @DataBoundConstructor public JMeterCsvParser(String glob) { super(glob); } @Extension public static class DescriptorImpl extends PerformanceReportParserDescriptor { @Override public String getDisplayName() { return "JMeterCSV"; } } @Override public String getDefaultGlobPattern() { return "**/*.csv"; } @Override PerformanceReport parse(File reportFile) throws Exception { this.dateFormat = null; this.isNumberDateFormat = false; final PerformanceReport report = new PerformanceReport(); report.setReportFileName(reportFile.getName()); String[] header = null; final BufferedReader reader = new BufferedReader(new FileReader(reportFile)); try { String line = reader.readLine(); if (line != null) { header = readCSVHeader(line); } } finally { if (reader != null) { reader.close(); } } Reader fileReader = new FileReader(reportFile); try { parseCSV(fileReader, header, report); } finally { if (fileReader != null) { fileReader.close(); } } return report; } protected void parseCSV(Reader in, String[] header, PerformanceReport report) throws IOException { CSVFormat csvFormat = CSVFormat.newFormat(delimiter).withHeader(header).withQuote('"').withSkipHeaderRecord(); Iterable<CSVRecord> records = csvFormat.parse(in); for (CSVRecord record : records) { final HttpSample sample = getSample(record); try { report.addSample(sample); } catch (SAXException e) { throw new RuntimeException("Error parsing file '" + report.getReportFileName() + "': Unable to add sample for CSVRecord " + record, e); } } } protected String[] readCSVHeader(String line) throws Exception { this.delimiter = lookingForDelimiter(line); final String[] header = line.split(String.valueOf(delimiter)); for (int i = 0; i < header.length; i++) { String field = header[i]; if ("timestamp".equalsIgnoreCase(field)) { timestampIdx = i; } else if ("elapsed".equalsIgnoreCase(field)) { elapsedIdx = i; } else if ("responseCode".equalsIgnoreCase(field)) { responseCodeIdx = i; } else if ("success".equalsIgnoreCase(field)) { successIdx = i; } else if ("URL".equalsIgnoreCase(field) && urlIdx < 0) { urlIdx = i; } else if ("label".equalsIgnoreCase(field) && urlIdx < 0) { urlIdx = i; } } if (timestampIdx < 0 || elapsedIdx < 0 || responseCodeIdx < 0 || successIdx < 0 || urlIdx < 0) { throw new Exception("Missing required column"); } return header; } protected static char lookingForDelimiter(String line) throws Exception { for (char ch : line.toCharArray()) { if (!Character.isLetter(ch)) { return ch; } } throw new Exception("Cannot find delimiter in header " + line); } /** * Parses a single HttpSample instance from a single CSV Record. * * @param record csv record from report file (cannot be null). * @return An sample instance (never null). */ private HttpSample getSample(CSVRecord record) { final HttpSample sample = new HttpSample(); sample.setDate(parseTimestamp(record.get(timestampIdx))); sample.setDuration(Long.valueOf(record.get(elapsedIdx))); sample.setHttpCode(record.get(responseCodeIdx)); sample.setSuccessful(Boolean.valueOf(record.get(successIdx))); sample.setUri(record.get(urlIdx)); return sample; } private boolean isNumberDateFormat = false; private SimpleDateFormat dateFormat; protected final static String[] DATE_FORMATS = new String[]{ "yyyy/MM/dd HH:mm:ss.SSS", "yyyy-MM-dd HH:mm:ss.SSS" }; private Date parseTimestamp(String timestamp) { if (this.dateFormat == null) { initDateFormat(timestamp); } try { return isNumberDateFormat ? new Date(Long.valueOf(timestamp)) : dateFormat.parse(timestamp); } catch (ParseException e) { throw new RuntimeException("Cannot parse timestamp: " + timestamp + ". Please, use one of supported formats: " + Arrays.toString(DATE_FORMATS), e); } } private void initDateFormat(String timestamp) { Date result = null; for (String format : DATE_FORMATS) { try { dateFormat = new SimpleDateFormat(format); result = dateFormat.parse(timestamp); } catch (ParseException ex) { // ok dateFormat = null; } if (result != null) { break; } } if (result == null) { try { Long.valueOf(timestamp); isNumberDateFormat = true; } catch (NumberFormatException ex) { throw new RuntimeException("Cannot parse timestamp: " + timestamp + ". Please, use one of supported formats: " + Arrays.toString(DATE_FORMATS), ex); } } } }