Index: src/components/org/apache/jmeter/reporters/MysqlCollector.java =================================================================== --- src/components/org/apache/jmeter/reporters/MysqlCollector.java (revision 0) +++ src/components/org/apache/jmeter/reporters/MysqlCollector.java (revision 0) @@ -0,0 +1,309 @@ +package org.apache.jmeter.reporters; + +import java.io.Serializable; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Connection; + +import org.apache.jmeter.samplers.SampleEvent; +import org.apache.jmeter.samplers.SampleListener; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractTestElement; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; + +import java.sql.Statement; + +/** + * + * @author Brett Cave + * + * Save responseData to a MySQL database. + */ +public class MysqlCollector extends AbstractTestElement implements + Serializable, SampleListener { + + /** + * Variables. + */ + private static final long serialVersionUID = 3127617254347460877L; + + private static final Logger log = LoggingManager.getLoggerForClass(); + + public static final String MYSQLHOST = "Mysql.host"; + public static final String MYSQLPORT = "Mysql.port"; + public static final String MYSQLDB = "Mysql.db"; + public static final String MYSQLTABLE = "Mysql.table"; + public static final String MYSQLUSER = "Mysql.user"; + public static final String MYSQLPASS = "Mysql.pass"; + public static final String MYSQLCREATETABLE = "Mysql.createtable"; + public static final String ERRORS_ONLY = "Mysql.errorsonly"; + public static final String SUCCESS_ONLY = "Mysql.successonly"; + + /** + * Default constructor. Nothing special. + */ + //TODO: The database should be initialized here, instead of after each sample. + public MysqlCollector() { + super(); + } + + + public MysqlCollector(String name) { + this(); + setName(name); + } + + public void clear() { + super.clear(); + synchronized (this) { + // reset connection? + } + } + + /** + * Required: this passes result to the processor. + */ + public void sampleOccurred(SampleEvent e) { + processSample(e.getResult(), new Counter()); + } + + /** + * + * @param s SampleResult - The result of the sample + * @param c Counter - The number of samples + * + * Sends the sample to be written to the database and increments counter, with a recursive function for sub-result arrays. + */ + private void processSample(SampleResult s, Counter c) { + writeSampleToDb(s, c.num++); + SampleResult[] sr = s.getSubResults(); + for (int i = 0; i < sr.length; i++) { + processSample(sr[i], c); + } + } + + /** + * + * @param s SampleResult - the result of the sample that will be written to the database + * @param num - the sample number. + * + * Writes the sample result data to the configured database. + */ + //TODO: It is very expensive to set up and tear down mysql connections, should the connect / close go elsewherE? + private void writeSampleToDb(SampleResult s, int num) { + if (s.isSuccessful()) { + if (getErrorsOnly()) { + return; + } + } else { + if (getSuccessOnly()) { + return; + } + } + + //TODO: Add an enum of database types and map to sampleResult types. + /* + * SampleResult getters that have relevant data. + * + * getBytes(): int + * getContentType(): varchar + * getDataType(): varchar + * getEndTime(): long + * getErrorCount(): int + * getGroupThreads(): int + * getIdleTime(): long + * getLatency(): long + * getMediaType(): varchar + * getParent(): varchar. reports need to take this into consideration. + * getRequestHeaders(): varchar + * getResponseCode(): varchar + * getResponseDataAsString(): text/blob + * getResponseHeaders(): varchar + * getResponseMessage(): varchar + * getSampleCount(): int + * getSampleLabel(): varchar + * getSamplerData(): text? + * getStartTime(): log + * getThreadName(): varchar + * getTime(): long + * getTimeStamp(): long + * getURL(): varchar + * getUrlAsString(): varchar + * isSuccessful(): boolean. + */ + + // no optimization. at least a PK or index so performance doesn't totally suck. + String dbSchema = "byteCount int, contentType varchar(200), dataType varchar(200), mediaType varchar(200)," + + "timeStamp long, startTime long, endTime long, idleTime long, totalTime long, latency long," + + "errorCount int, groupThreads int, parent varchar(200), requestHeaders varchar(200), repsonseCode varchar(200)," + + "responseData text, responseHeaders text, responseMessage text," + + "sampleCount int, sampleLabel text, samplerData text," + + "threadName text, url text, success tinyint(1)"; + + Connection con = connectDatabase(); // This should throw an exception if fails. the try below should catch it. + + //TODO: investigate asynchronous database writes - this could be slow if database is on a slow network connection. + + // Is this good here? + try { + Statement stmt = con.createStatement(); + if (getMysqlCreateTable()) { + // InnoDB is probably the best bet, can do FK's for parents, etc. Add dropdown to GUI to select Engine. + stmt.execute("CREATE TABLE IF NOT EXISTS `" + getMysqlTable() + + "` ("+dbSchema+") ENGINE=InnoDB DEFAULT CHARSET utf8"); + } + + // This is where mapping would come in handy. This is long and tedious, could be done much better. + String insertData = getSqlString(s.getBytes()); + insertData += ",'" + getSqlString(s.getContentType()) + "','" + getSqlString(s.getDataType()); + insertData += "','" + getSqlString(s.getMediaType()); + insertData += "'," + getSqlString(s.getTimeStamp()) + "," + getSqlString(s.getStartTime()); + insertData += "," + getSqlString(s.getEndTime()) + "," + getSqlString(s.getIdleTime()); + insertData += "," + getSqlString(s.getTime()) + "," + getSqlString(s.getLatency()); + insertData += ", " + getSqlString(s.getErrorCount()); + insertData += "," + getSqlString(s.getGroupThreads()); + //insertData += ",'" + getSqlString(s.getParent().toString()); parent is null... + insertData += ",'"; + insertData += "','" + getSqlString(s.getRequestHeaders()); + insertData += "','" + getSqlString(s.getResponseCode()); + //insertData += "','" + getSqlString(s.getResponseDataAsString()); // HTML Needs to be escaped properly. is this necesary? + insertData += "','"; + insertData += "','" + getSqlString(s.getRequestHeaders()) + "','" + getSqlString(s.getResponseMessage()); + insertData += "'," + getSqlString(s.getSampleCount()) + ",'" + getSqlString(s.getSampleLabel()); + insertData += "','" + getSqlString(s.getSamplerData()) + "','" + getSqlString(s.getThreadName()); + insertData += "','" + getSqlString(s.getUrlAsString()) + "'," + getSqlString(s.isSuccessful()); + + + + String insertString = "INSERT INTO " + getMysqlTable() + " values (" + insertData + ")"; + log.error("INSERT STRING: " + insertString); + // Bad - no column ordering. depends on schema created in specific order above. + stmt.execute(insertString); + } + catch (SQLException e) { + log.error("Cannot save sampleresult to database: " + e.getMessage()); + } + closeDatabase(con); // shouldn't throw, if close fails, it may be because connection is no longer valid. + + } + + public void sampleStarted(SampleEvent e) { + // not used + } + + public void sampleStopped(SampleEvent e) { + // not used + } + + private String getMysqlHost() { + return getPropertyAsString(MYSQLHOST); + } + + private String getMysqlPort() { + return getPropertyAsString(MYSQLPORT); + } + + private String getMysqlDb() { + return getPropertyAsString(MYSQLDB); + } + + private String getMysqlTable() { + return getPropertyAsString(MYSQLTABLE); + } + + private boolean getMysqlCreateTable() { + return getPropertyAsBoolean(MYSQLCREATETABLE); + } + + private String getMysqlUser() { + return getPropertyAsString(MYSQLUSER); + } + + private String getMysqlPass() { + return getPropertyAsString(MYSQLPASS); + } + + private boolean getErrorsOnly() { + return getPropertyAsBoolean(ERRORS_ONLY); + } + + private boolean getSuccessOnly() { + return getPropertyAsBoolean(SUCCESS_ONLY); + } + + private String getSqlString(String unescaped) { + try { + return unescaped.replaceAll("'", "\\'"); + } + catch (Exception e) { + return ""; + } + } + + private String getSqlString(int val) { + try { + return String.valueOf(val); + } + catch (Exception e) { + return ""; + } + } + + private String getSqlString(long val) { + try { + return String.valueOf(val); + } + catch (Exception e) { + return ""; + } + } + + private String getSqlString(boolean bool) { + try { + return String.valueOf(bool); + } + catch (Exception e) { + return ""; + } + } + + private Connection connectDatabase() { + log.info("Connecting to database with URL 'jdbc:mysql://" + getMysqlHost() + ":" + getMysqlPort() + "/" + getMysqlDb() + + "' and user '" + getMysqlUser() + "'"); + Connection con = null; + try { + Class.forName("com.mysql.jdbc.Driver"); + } catch (ClassNotFoundException e) { + log.error("Driver not on classpath (" + e.getMessage()); + } + + String url = "jdbc:mysql://" + getMysqlHost() + ":" + getMysqlPort() + + "/" + getMysqlDb(); + try { + con = DriverManager.getConnection(url, getMysqlUser(), + getMysqlPass()); + return con; + } + catch (SQLException e) { + log.error("MysqlCollector ERROR: " + e.getMessage()); + return null; + } + } + + private void closeDatabase(Connection con) { + try { + //con.commit(); // "cant call commit if autocommit is true.". + //This can be set up from GUI with "Autocommit?" option to enable or disable in driver if necessary. + con.close(); + } + catch (SQLException e) { + log.error("Could not close connection: " + e.getMessage()); + } + } + + // How many samples have been received? + private static class Counter { + int num; + } +} Index: src/components/org/apache/jmeter/reporters/gui/MysqlCollectorGui.java =================================================================== --- src/components/org/apache/jmeter/reporters/gui/MysqlCollectorGui.java (revision 0) +++ src/components/org/apache/jmeter/reporters/gui/MysqlCollectorGui.java (revision 0) @@ -0,0 +1,217 @@ +package org.apache.jmeter.reporters.gui; + +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; + +import javax.swing.Box; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import org.apache.jmeter.reporters.MysqlCollector; +import org.apache.jmeter.samplers.Clearable; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jmeter.visualizers.gui.AbstractListenerGui; + + +/** + * GUI for mysql collector. + * + * @author brett + * + */ +public class MysqlCollectorGui extends AbstractListenerGui implements Clearable { + + /** + * + */ + private static final long serialVersionUID = 2931300846585391445L; + + private JTextField mysqlHost; + + private JTextField mysqlPort; + + private JTextField mysqlDb; + + private JTextField mysqlTable; + + private JCheckBox mysqlCreateTable; + + private JTextField mysqlUser; + + private JPasswordField mysqlPassword; + + private JCheckBox errorsOnly; + + private JCheckBox successOnly; + + + public MysqlCollectorGui() { + super(); + init(); + } + + /** + * @see org.apache.jmeter.gui.JMeterGUIComponent#getStaticLabel() + */ + public String getLabelResource() { + return "MySQL Collector"; + } + + /** + * @see org.apache.jmeter.gui.JMeterGUIComponent#configure(TestElement) + */ + public void configure(TestElement el) { + super.configure(el); + mysqlHost.setText(el.getPropertyAsString(MysqlCollector.MYSQLHOST)); + mysqlPort.setText(el.getPropertyAsString(MysqlCollector.MYSQLPORT)); + mysqlDb.setText(el.getPropertyAsString(MysqlCollector.MYSQLDB)); + mysqlTable.setText(el.getPropertyAsString(MysqlCollector.MYSQLTABLE)); + mysqlCreateTable.setSelected(el.getPropertyAsBoolean(MysqlCollector.MYSQLCREATETABLE)); + mysqlUser.setText(el.getPropertyAsString(MysqlCollector.MYSQLUSER)); + mysqlPassword.setText(el.getPropertyAsString(MysqlCollector.MYSQLPASS)); + errorsOnly.setSelected(el.getPropertyAsBoolean(MysqlCollector.ERRORS_ONLY)); + successOnly.setSelected(el.getPropertyAsBoolean(MysqlCollector.SUCCESS_ONLY));; + } + + /** + * @see org.apache.jmeter.gui.JMeterGUIComponent#createTestElement() + */ + public TestElement createTestElement() { + MysqlCollector mysqlSaver = new MysqlCollector(); + modifyTestElement(mysqlSaver); + return mysqlSaver; + } + + /** + * Modifies a given TestElement to mirror the data in the gui components. + * + * @see org.apache.jmeter.gui.JMeterGUIComponent#modifyTestElement(TestElement) + */ + public void modifyTestElement(TestElement te) { + super.configureTestElement(te); + te.setProperty(MysqlCollector.MYSQLHOST, mysqlHost.getText()); + te.setProperty(MysqlCollector.MYSQLPORT, mysqlPort.getText()); + te.setProperty(MysqlCollector.MYSQLDB, mysqlDb.getText()); + te.setProperty(MysqlCollector.MYSQLUSER, mysqlUser.getText()); + te.setProperty(MysqlCollector.MYSQLTABLE, mysqlTable.getText()); + te.setProperty(MysqlCollector.MYSQLCREATETABLE, mysqlCreateTable.isSelected()); + te.setProperty(MysqlCollector.MYSQLPASS, String.valueOf(mysqlPassword.getPassword())); + te.setProperty(MysqlCollector.ERRORS_ONLY, errorsOnly.isSelected()); + te.setProperty(MysqlCollector.SUCCESS_ONLY, successOnly.isSelected()); + } + + /** + * Implements JMeterGUIComponent.clearGui + */ + public void clearGui() { + super.clearGui(); + mysqlDb.setText(""); + mysqlHost.setText(""); + mysqlPort.setText("3306"); + mysqlTable.setText(""); + mysqlCreateTable.setSelected(true); + mysqlUser.setText(""); + mysqlPassword.setText(""); + errorsOnly.setSelected(false); + successOnly.setSelected(false); + } + + private void init() { + setLayout(new BorderLayout()); + setBorder(makeBorder()); + Box box = Box.createVerticalBox(); + box.add(makeTitlePanel()); + box.add(createServerDetailsPanel()); + + mysqlCreateTable = new JCheckBox("Create table?"); + box.add(mysqlCreateTable); + errorsOnly = new JCheckBox("Only log errors?"); + box.add(errorsOnly); + successOnly = new JCheckBox("Only log success?"); + box.add(successOnly); + add(box, BorderLayout.NORTH); + } + + private JPanel createServerDetailsPanel() + { + JLabel hostLabel, portLabel, dbLabel, tableLabel, userLabel, passwordLabel; + + mysqlHost = new JTextField(10); + hostLabel = new JLabel("Host:Port"); + hostLabel.setLabelFor(mysqlHost); + + mysqlPort = new JTextField(4); + mysqlPort.setText("3306"); + portLabel = new JLabel(":"); + portLabel.setLabelFor(mysqlPort); + + mysqlDb = new JTextField(10); + dbLabel = new JLabel("Database/Table"); + dbLabel.setLabelFor(mysqlDb); + + mysqlTable = new JTextField(10); + tableLabel = new JLabel("/"); + tableLabel.setLabelFor(mysqlTable); + + mysqlUser = new JTextField(10); + userLabel = new JLabel("User/Password"); + userLabel.setLabelFor(mysqlUser); + + mysqlPassword = new JPasswordField(10); + passwordLabel = new JLabel("/"); + passwordLabel.setLabelFor(mysqlPassword); + + JPanel serverDetailsPanel = new JPanel(new GridBagLayout()); + + GridBagConstraints c = new GridBagConstraints(); + int gridxinc = 5, gridyinc = 5; + c.ipady = 2; + c.ipadx = 2; + + c.gridx = 0; + c.gridy = 0; + c.anchor = GridBagConstraints.WEST; + + serverDetailsPanel.add(hostLabel, c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlHost,c); + c.gridx += gridxinc; + serverDetailsPanel.add(portLabel,c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlPort,c); + + c.gridy += gridyinc; // nl + c.gridx = 0; // cr + + serverDetailsPanel.add(dbLabel,c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlDb,c); + c.gridx += gridxinc; + serverDetailsPanel.add(tableLabel,c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlTable,c); + + c.gridy += gridyinc; + c.gridx = 0; + + serverDetailsPanel.add(userLabel,c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlUser,c); + c.gridx += gridxinc; + serverDetailsPanel.add(passwordLabel,c); + c.gridx += gridxinc; + serverDetailsPanel.add(mysqlPassword,c); + c.gridy += gridyinc; + + return serverDetailsPanel; + } + + // Needed to avoid Class cast error in Clear.java + public void clearData() { + } + +}