1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package org.apache.jmeter.reporters; 20 21 import java.io.BufferedInputStream; 22 import java.io.BufferedOutputStream; 23 import java.io.BufferedReader; 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileNotFoundException; 27 import java.io.FileOutputStream; 28 import java.io.FileReader; 29 import java.io.IOException; 30 import java.io.OutputStreamWriter; 31 import java.io.PrintWriter; 32 import java.io.RandomAccessFile; 33 import java.io.Serializable; 34 import java.util.HashMap; 35 import java.util.Iterator; 36 import java.util.Map; 37 38 import org.apache.avalon.framework.configuration.DefaultConfigurationSerializer; 39 import org.apache.jmeter.engine.event.LoopIterationEvent; 40 import org.apache.jmeter.engine.util.NoThreadClone; 41 import org.apache.jmeter.gui.GuiPackage; 42 import org.apache.jmeter.samplers.Clearable; 43 import org.apache.jmeter.samplers.Remoteable; 44 import org.apache.jmeter.samplers.SampleEvent; 45 import org.apache.jmeter.samplers.SampleListener; 46 import org.apache.jmeter.samplers.SampleResult; 47 import org.apache.jmeter.samplers.SampleSaveConfiguration; 48 import org.apache.jmeter.save.CSVSaveService; 49 import org.apache.jmeter.save.OldSaveService; 50 import org.apache.jmeter.save.SaveService; 51 import org.apache.jmeter.testelement.TestElement; 52 import org.apache.jmeter.testelement.TestListener; 53 import org.apache.jmeter.testelement.property.BooleanProperty; 54 import org.apache.jmeter.testelement.property.ObjectProperty; 55 import org.apache.jmeter.visualizers.Visualizer; 56 import org.apache.jorphan.logging.LoggingManager; 57 import org.apache.jorphan.util.JMeterError; 58 import org.apache.jorphan.util.JOrphanUtils; 59 import org.apache.log.Logger; 60 61 /** 62 * This class handles all saving of samples. 63 * The class must be thread-safe because it is shared between threads (NoThreadClone). 64 */ 65 public class ResultCollector extends AbstractListenerElement implements SampleListener, Clearable, Serializable, 66 TestListener, Remoteable, NoThreadClone { 67 68 private static final Logger log = LoggingManager.getLoggerForClass(); 69 70 private static final long serialVersionUID = 233L; 71 72 // This string is used to identify local test runs, so must not be a valid host name 73 private static final String TEST_IS_LOCAL = "*local*"; // $NON-NLS-1$ 74 75 private static final String TESTRESULTS_START = "<testResults>"; // $NON-NLS-1$ 76 77 private static final String TESTRESULTS_START_V1_1_PREVER = "<testResults version=\""; // $NON-NLS-1$ 78 79 private static final String TESTRESULTS_START_V1_1_POSTVER="\">"; // $NON-NLS-1$ 80 81 private static final String TESTRESULTS_END = "</testResults>"; // $NON-NLS-1$ 82 83 private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; // $NON-NLS-1$ 84 85 private static final int MIN_XML_FILE_LEN = XML_HEADER.length() + TESTRESULTS_START.length() 86 + TESTRESULTS_END.length(); 87 88 public static final String FILENAME = "filename"; // $NON-NLS-1$ 89 90 private static final String SAVE_CONFIG = "saveConfig"; // $NON-NLS-1$ 91 92 private static final String ERROR_LOGGING = "ResultCollector.error_logging"; // $NON-NLS-1$ 93 94 private static final String SUCCESS_ONLY_LOGGING = "ResultCollector.success_only_logging"; // $NON-NLS-1$ 95 96 // Static variables 97 98 // Lock used to guard static mutable variables 99 private static final Object LOCK = new Object(); 100 101 //@GuardedBy("LOCK") 102 private static final Map files = new HashMap(); // key=filename, entry=FileEntry 103 104 /* 105 * Keep track of the file writer and the configuration, 106 * as the instance used to close them is not the same as the instance that creates 107 * them. This means one cannot use the saved PrintWriter or use getSaveConfig() 108 */ 109 private static class FileEntry{ 110 final PrintWriter pw; 111 final SampleSaveConfiguration config; 112 FileEntry(PrintWriter _pw, SampleSaveConfiguration _config){ 113 pw =_pw; 114 config = _config; 115 } 116 } 117 118 /** 119 * The instance count is used to keep track of whether any tests are currently running. 120 * It's not possible to use the constructor or threadStarted etc as tests may overlap 121 * e.g. a remote test may be started, 122 * and then a local test started whilst the remote test is still running. 123 */ 124 //@GuardedBy("LOCK") 125 private static int instanceCount; // Keep track of how many instances are active 126 127 // Instance variables 128 129 private transient volatile DefaultConfigurationSerializer serializer; 130 131 private transient volatile PrintWriter out; 132 133 private volatile boolean inTest = false; 134 135 private volatile boolean isStats = false; 136 137 /** 138 * No-arg constructor. 139 */ 140 public ResultCollector() { 141 setErrorLogging(false); 142 setSuccessOnlyLogging(false); 143 setProperty(new ObjectProperty(SAVE_CONFIG, new SampleSaveConfiguration())); 144 } 145 146 // Ensure that the sample save config is not shared between copied nodes 147 // N.B. clone only seems to be used for client-server tests 148 public Object clone(){ 149 ResultCollector clone = (ResultCollector) super.clone(); 150 clone.setSaveConfig((SampleSaveConfiguration)clone.getSaveConfig().clone()); 151 return clone; 152 } 153 154 private void setFilenameProperty(String f) { 155 setProperty(FILENAME, f); 156 } 157 158 public String getFilename() { 159 return getPropertyAsString(FILENAME); 160 } 161 162 public boolean isErrorLogging() { 163 return getPropertyAsBoolean(ERROR_LOGGING); 164 } 165 166 public void setErrorLogging(boolean errorLogging) { 167 setProperty(new BooleanProperty(ERROR_LOGGING, errorLogging)); 168 } 169 170 public void setSuccessOnlyLogging(boolean value) { 171 if (value) { 172 setProperty(new BooleanProperty(SUCCESS_ONLY_LOGGING, true)); 173 } else { 174 removeProperty(SUCCESS_ONLY_LOGGING); 175 } 176 } 177 178 public boolean isSuccessOnlyLogging() { 179 return getPropertyAsBoolean(SUCCESS_ONLY_LOGGING,false); 180 } 181 182 /** 183 * Decides whether or not to a sample is wanted based on:<br/> 184 * - errorOnly<br/> 185 * - successOnly<br/> 186 * - sample success<br/> 187 * Should only be called for single samples. 188 * 189 * @param success is sample successful 190 * @return whether to log/display the sample 191 */ 192 public boolean isSampleWanted(boolean success){ 193 boolean errorOnly = isErrorLogging(); 194 boolean successOnly = isSuccessOnlyLogging(); 195 return isSampleWanted(success, errorOnly, successOnly); 196 } 197 198 /** 199 * Decides whether or not to a sample is wanted based on: <br/> 200 * - errorOnly <br/> 201 * - successOnly <br/> 202 * - sample success <br/> 203 * This version is intended to be called by code that loops over many samples; 204 * it is cheaper than fetching the settings each time. 205 * @param success status of sample 206 * @param errorOnly if errors only wanted 207 * @param successOnly if success only wanted 208 * @return whether to log/display the sample 209 */ 210 public static boolean isSampleWanted(boolean success, boolean errorOnly, 211 boolean successOnly) { 212 return (!errorOnly && !successOnly) || 213 (success && successOnly) || 214 (!success && errorOnly); 215 // successOnly and errorOnly cannot both be set 216 } 217 /** 218 * Sets the filename attribute of the ResultCollector object. 219 * 220 * @param f 221 * the new filename value 222 */ 223 public void setFilename(String f) { 224 if (inTest) { 225 return; 226 } 227 setFilenameProperty(f); 228 } 229 230 public void testEnded(String host) { 231 synchronized(LOCK){ 232 instanceCount--; 233 if (instanceCount <= 0) { 234 finalizeFileOutput(); 235 inTest = false; 236 } 237 } 238 } 239 240 public synchronized void testStarted(String host) { 241 synchronized(LOCK){ 242 instanceCount++; 243 try { 244 initializeFileOutput(); 245 if (getVisualizer() != null) { 246 this.isStats = getVisualizer().isStats(); 247 } 248 } catch (Exception e) { 249 log.error("", e); 250 } 251 } 252 inTest = true; 253 } 254 255 public void testEnded() { 256 testEnded(TEST_IS_LOCAL); 257 } 258 259 public void testStarted() { 260 testStarted(TEST_IS_LOCAL); 261 } 262 263 /** 264 * Loads an existing sample data (JTL) file. 265 * This can be one of: 266 * - XStream format 267 * - Avalon format 268 * - CSV format 269 * 270 */ 271 public void loadExistingFile() { 272 final Visualizer visualizer = getVisualizer(); 273 if (visualizer == null) { 274 return; // No point reading the file if there's no visualiser 275 } 276 boolean parsedOK = false; 277 String filename = getFilename(); 278 File file = new File(filename); 279 if (file.exists()) { 280 BufferedReader dataReader = null; 281 BufferedInputStream bufferedInputStream = null; 282 try { 283 dataReader = new BufferedReader(new FileReader(file)); 284 // Get the first line, and see if it is XML 285 String line = dataReader.readLine(); 286 dataReader.close(); 287 dataReader = null; 288 if (line == null) { 289 log.warn(filename+" is empty"); 290 } else { 291 if (!line.startsWith("<?xml ")){// No, must be CSV //$NON-NLS-1$ 292 CSVSaveService.processSamples(filename, visualizer, this); 293 parsedOK = true; 294 } else { // We are processing XML 295 try { // Assume XStream 296 bufferedInputStream = new BufferedInputStream(new FileInputStream(file)); 297 SaveService.loadTestResults(bufferedInputStream, 298 new ResultCollectorHelper(this, visualizer)); 299 parsedOK = true; 300 } catch (Exception e) { 301 log.info("Failed to load "+filename+" using XStream, trying old XML format. Error was: "+e); 302 try { 303 OldSaveService.processSamples(filename, visualizer, this); 304 parsedOK = true; 305 } catch (Exception e1) { 306 log.warn("Error parsing Avalon XML. " + e1.getLocalizedMessage()); 307 } 308 } 309 } 310 } 311 } catch (IOException e) { 312 log.warn("Problem reading JTL file: "+file); 313 } catch (JMeterError e){ 314 log.warn("Problem reading JTL file: "+file); 315 } catch (RuntimeException e){ // e.g. NullPointerException 316 log.warn("Problem reading JTL file: "+file,e); 317 } catch (OutOfMemoryError e) { 318 log.warn("Problem reading JTL file: "+file,e); 319 } finally { 320 JOrphanUtils.closeQuietly(dataReader); 321 JOrphanUtils.closeQuietly(bufferedInputStream); 322 if (!parsedOK) { 323 GuiPackage.showErrorMessage( 324 "Error loading results file - see log file", 325 "Result file loader"); 326 } 327 } 328 } else { 329 GuiPackage.showErrorMessage( 330 "Error loading results file - could not open file", 331 "Result file loader"); 332 } 333 } 334 335 private static void writeFileStart(PrintWriter writer, SampleSaveConfiguration saveConfig) { 336 if (saveConfig.saveAsXml()) { 337 writer.print(XML_HEADER); 338 // Write the EOL separately so we generate LF line ends on Unix and Windows 339 writer.print("\n"); // $NON-NLS-1$ 340 String pi=saveConfig.getXmlPi(); 341 if (pi.length() > 0) { 342 writer.println(pi); 343 } 344 // Can't do it as a static initialisation, because SaveService 345 // is being constructed when this is called 346 writer.print(TESTRESULTS_START_V1_1_PREVER); 347 writer.print(SaveService.getVERSION()); 348 writer.print(TESTRESULTS_START_V1_1_POSTVER); 349 // Write the EOL separately so we generate LF line ends on Unix and Windows 350 writer.print("\n"); // $NON-NLS-1$ 351 } else if (saveConfig.saveFieldNames()) { 352 writer.println(CSVSaveService.printableFieldNamesToString(saveConfig)); 353 } 354 } 355 356 private static void writeFileEnd(PrintWriter pw, SampleSaveConfiguration saveConfig) { 357 if (saveConfig.saveAsXml()) { 358 pw.print("\n"); // $NON-NLS-1$ 359 pw.print(TESTRESULTS_END); 360 pw.print("\n");// Added in version 1.1 // $NON-NLS-1$ 361 } 362 } 363 364 private static PrintWriter getFileWriter(String filename, SampleSaveConfiguration saveConfig) 365 throws IOException { 366 if (filename == null || filename.length() == 0) { 367 return null; 368 } 369 FileEntry fe = (FileEntry) files.get(filename); 370 PrintWriter writer = null; 371 boolean trimmed = true; 372 373 if (fe == null) { 374 if (saveConfig.saveAsXml()) { 375 trimmed = trimLastLine(filename); 376 } else { 377 trimmed = new File(filename).exists(); 378 } 379 // Find the name of the directory containing the file 380 // and create it - if there is one 381 File pdir = new File(filename).getParentFile(); 382 if (pdir != null) { 383 pdir.mkdirs();// returns false if directory already exists, so need to check again 384 if (!pdir.exists()){ 385 log.warn("Error creating directories for "+pdir.toString()); 386 } 387 } 388 writer = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(filename, 389 trimmed)), SaveService.getFileEncoding("UTF-8")), true); // $NON-NLS-1$ 390 log.debug("Opened file: "+filename); 391 files.put(filename, new FileEntry(writer, saveConfig)); 392 } else { 393 writer = fe.pw; 394 } 395 if (!trimmed) { 396 writeFileStart(writer, saveConfig); 397 } 398 return writer; 399 } 400 401 // returns false if the file did not contain the terminator 402 private static boolean trimLastLine(String filename) { 403 RandomAccessFile raf = null; 404 try { 405 raf = new RandomAccessFile(filename, "rw"); // $NON-NLS-1$ 406 long len = raf.length(); 407 if (len < MIN_XML_FILE_LEN) { 408 return false; 409 } 410 raf.seek(len - TESTRESULTS_END.length() - 10);// TODO: may not work on all OSes? 411 String line; 412 long pos = raf.getFilePointer(); 413 int end = 0; 414 while ((line = raf.readLine()) != null)// reads to end of line OR end of file 415 { 416 end = line.indexOf(TESTRESULTS_END); 417 if (end >= 0) // found the string 418 { 419 break; 420 } 421 pos = raf.getFilePointer(); 422 } 423 if (line == null) { 424 log.warn("Unexpected EOF trying to find XML end marker in " + filename); 425 raf.close(); 426 return false; 427 } 428 raf.setLength(pos + end);// Truncate the file 429 raf.close(); 430 raf = null; 431 } catch (FileNotFoundException e) { 432 return false; 433 } catch (IOException e) { 434 log.warn("Error trying to find XML terminator " + e.toString()); 435 return false; 436 } finally { 437 try { 438 if (raf != null) { 439 raf.close(); 440 } 441 } catch (IOException e1) { 442 log.info("Could not close " + filename + " " + e1.getLocalizedMessage()); 443 } 444 } 445 return true; 446 } 447 448 public void sampleStarted(SampleEvent e) { 449 } 450 451 public void sampleStopped(SampleEvent e) { 452 } 453 454 /** 455 * When a test result is received, display it and save it. 456 * 457 * @param event 458 * the sample event that was received 459 */ 460 public void sampleOccurred(SampleEvent event) { 461 SampleResult result = event.getResult(); 462 463 if (isSampleWanted(result.isSuccessful())) { 464 sendToVisualizer(result); 465 if (out != null && !isResultMarked(result) && !this.isStats) { 466 SampleSaveConfiguration config = getSaveConfig(); 467 result.setSaveConfig(config); 468 try { 469 if (config.saveAsXml()) { 470 if (SaveService.isSaveTestLogFormat20()) { 471 if (serializer == null) { 472 serializer = new DefaultConfigurationSerializer(); 473 } 474 out.write(OldSaveService.getSerializedSampleResult(result, serializer, config)); 475 } else { // !LogFormat20 476 SaveService.saveSampleResult(event, out); 477 } 478 } else { // !saveAsXml 479 String savee = CSVSaveService.resultToDelimitedString(event); 480 out.println(savee); 481 } 482 } catch (Exception err) { 483 log.error("Error trying to record a sample", err); // should throw exception back to caller 484 } 485 } 486 } 487 } 488 489 protected final void sendToVisualizer(SampleResult r) { 490 if (getVisualizer() != null) { 491 getVisualizer().add(r); 492 } 493 } 494 495 /** 496 * recordStats is used to save statistics generated by visualizers 497 * 498 * @param e 499 * @throws Exception 500 */ 501 // Used by: MonitorHealthVisualizer.add(SampleResult res) 502 public void recordStats(TestElement e) throws Exception { 503 if (out != null) { 504 SaveService.saveTestElement(e, out); 505 } 506 } 507 508 /** 509 * Checks if the sample result is marked or not, and marks it 510 * @param res - the sample result to check 511 * @return <code>true</code> if the result was marked 512 */ 513 private boolean isResultMarked(SampleResult res) { 514 String filename = getFilename(); 515 return res.markFile(filename); 516 } 517 518 private void initializeFileOutput() throws IOException { 519 520 String filename = getFilename(); 521 if (filename != null) { 522 if (out == null) { 523 try { 524 out = getFileWriter(filename, getSaveConfig()); 525 } catch (FileNotFoundException e) { 526 out = null; 527 } 528 } 529 } 530 } 531 532 private void finalizeFileOutput() { 533 Iterator it = files.entrySet().iterator(); 534 while(it.hasNext()){ 535 Map.Entry me = (Map.Entry) it.next(); 536 log.debug("Closing: "+me.getKey()); 537 FileEntry fe = (FileEntry) me.getValue(); 538 writeFileEnd(fe.pw, fe.config); 539 fe.pw.close(); 540 if (fe.pw.checkError()){ 541 log.warn("Problem detected during use of "+me.getKey()); 542 } 543 } 544 files.clear(); 545 } 546 547 /* 548 * (non-Javadoc) 549 * 550 * @see TestListener#testIterationStart(LoopIterationEvent) 551 */ 552 public void testIterationStart(LoopIterationEvent event) { 553 } 554 555 /** 556 * @return Returns the saveConfig. 557 */ 558 public SampleSaveConfiguration getSaveConfig() { 559 try { 560 return (SampleSaveConfiguration) getProperty(SAVE_CONFIG).getObjectValue(); 561 } catch (ClassCastException e) { 562 setSaveConfig(new SampleSaveConfiguration()); 563 return getSaveConfig(); 564 } 565 } 566 567 /** 568 * @param saveConfig 569 * The saveConfig to set. 570 */ 571 public void setSaveConfig(SampleSaveConfiguration saveConfig) { 572 getProperty(SAVE_CONFIG).setObjectValue(saveConfig); 573 } 574 575 // This is required so that 576 // @see org.apache.jmeter.gui.tree.JMeterTreeModel.getNodesOfType() 577 // can find the Clearable nodes - the userObject has to implement the interface. 578 public void clearData() { 579 } 580 581 }