View Javadoc
1   /*
2    * Copyright 1999,2004 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  
17  package org.apache.log4j.chainsaw.vfs;
18  
19  import java.awt.Container;
20  import java.awt.Dimension;
21  import java.awt.Frame;
22  import java.awt.GridBagConstraints;
23  import java.awt.GridBagLayout;
24  import java.awt.Insets;
25  import java.awt.Toolkit;
26  import java.awt.event.ActionEvent;
27  import java.awt.event.ActionListener;
28  import java.io.BufferedReader;
29  import java.io.IOException;
30  import java.io.InputStreamReader;
31  import java.io.Reader;
32  import java.io.UnsupportedEncodingException;
33  
34  import javax.swing.JButton;
35  import javax.swing.JDialog;
36  import javax.swing.JLabel;
37  import javax.swing.JPanel;
38  import javax.swing.JPasswordField;
39  import javax.swing.JTextField;
40  import javax.swing.SwingUtilities;
41  
42  import org.apache.commons.vfs.FileObject;
43  import org.apache.commons.vfs.FileSystemException;
44  import org.apache.commons.vfs.FileSystemManager;
45  import org.apache.commons.vfs.FileSystemOptions;
46  import org.apache.commons.vfs.RandomAccessContent;
47  import org.apache.commons.vfs.VFS;
48  import org.apache.commons.vfs.provider.URLFileName;
49  import org.apache.commons.vfs.provider.sftp.SftpFileSystemConfigBuilder;
50  import org.apache.commons.vfs.util.RandomAccessMode;
51  import org.apache.log4j.chainsaw.receivers.VisualReceiver;
52  import org.apache.log4j.varia.LogFilePatternReceiver;
53  
54  /**
55   * A VFS-enabled version of org.apache.log4j.varia.LogFilePatternReceiver.
56   * 
57   * VFSLogFilePatternReceiver can parse and tail log files, converting entries into
58   * LoggingEvents.  If the file doesn't exist when the receiver is initialized, the
59   * receiver will look for the file once every 10 seconds.
60   * <p>
61   * See the Chainsaw page (http://logging.apache.org/log4j/docs/chainsaw.html) for information
62   * on how to set up Chainsaw with VFS.
63   * <p>
64   * See http://jakarta.apache.org/commons/vfs/filesystems.html for a list of VFS-supported
65   * file systems and the URIs needed to access the file systems.
66   * <p>
67   * Because some VFS file systems allow you to provide username/password, this receiver
68   * provides an optional GUI dialog for entering the username/password fields instead 
69   * of requiring you to hard code usernames and passwords into the URI.
70   * <p>
71   * If the 'promptForUserInfo' param is set to true (default is false), 
72   * the receiver will wait for a call to 'setContainer', and then display 
73   * a username/password dialog.
74   * <p>
75   * If you are using this receiver without a GUI, don't set promptForUserInfo 
76   * to true - it will block indefinitely waiting for a visual component.
77   * <p> 
78   * If the 'promptForUserInfo' param is set to true, the fileURL should -leave out- 
79   * the username/password portion of the VFS-supported URI.  Examples:
80   * <p>
81   * An sftp URI that would be used with promptForUserInfo=true:
82   * sftp://192.168.1.100:22/home/thisuser/logfile.txt
83   * <p>
84   * An sftp URI that would be used with promptForUserInfo=false:
85   * sftp://username:password@192.168.1.100:22/home/thisuser/logfile.txt
86   * <p>
87   * This receiver relies on java.util.regex features to perform the parsing of text in the 
88   * log file, however the only regular expression field explicitly supported is 
89   * a glob-style wildcard used to ignore fields in the log file if needed.  All other
90   * fields are parsed by using the supplied keywords.
91   * <p>
92   * <b>Features:</b><br>
93   * - specify the URL of the log file to be processed<br>
94   * - specify the timestamp format in the file (if one exists, using patterns from {@link java.text.SimpleDateFormat})<br>
95   * - specify the pattern (logFormat) used in the log file using keywords, a wildcard character (*) and fixed text<br>
96   * - 'tail' the file (allows the contents of the file to be continually read and new events processed)<br>
97   * - supports the parsing of multi-line messages and exceptions
98   * - to access 
99   *<p>
100  * <b>Keywords:</b><br>
101  * TIMESTAMP<br>
102  * LOGGER<br>
103  * LEVEL<br>
104  * THREAD<br>
105  * CLASS<br>
106  * FILE<br>
107  * LINE<br>
108  * METHOD<br>
109  * RELATIVETIME<br>
110  * MESSAGE<br>
111  * NDC<br>
112  * PROP(key)<br>
113  * <p>
114  * Use a * to ignore portions of the log format that should be ignored
115  * <p>
116  * Example:<br>
117  * If your file's patternlayout is this:<br>
118  * <b>%d %-5p [%t] %C{2} (%F:%L) - %m%n</b>
119  *<p>
120  * specify this as the log format:<br>
121  * <b>TIMESTAMP LEVEL [THREAD] CLASS (FILE:LINE) - MESSAGE</b>
122  *<p>
123  * To define a PROPERTY field, use PROP(key)
124  * <p>
125  * Example:<br> 
126  * If you used the RELATIVETIME pattern layout character in the file, 
127  * you can use PROP(RELATIVETIME) in the logFormat definition to assign 
128  * the RELATIVETIME field as a property on the event.
129  * <p>
130  * If your file's patternlayout is this:<br>
131  * <b>%r [%t] %-5p %c %x - %m%n</b>
132  *<p>
133  * specify this as the log format:<br>
134  * <b>PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE</b>
135  * <p>
136  * Note the * - it can be used to ignore a single word or sequence of words in the log file
137  * (in order for the wildcard to ignore a sequence of words, the text being ignored must be
138  *  followed by some delimiter, like '-' or '[') - ndc is being ignored in this example.
139  * <p>
140  * Assign a filterExpression in order to only process events which match a filter.
141  * If a filterExpression is not assigned, all events are processed.
142  *<p>
143  * <b>Limitations:</b><br>
144  * - no support for the single-line version of throwable supported by patternlayout<br>
145  *   (this version of throwable will be included as the last line of the message)<br>
146  * - the relativetime patternLayout character must be set as a property: PROP(RELATIVETIME)<br>
147  * - messages should appear as the last field of the logFormat because the variability in message content<br>
148  * - exceptions are converted if the exception stack trace (other than the first line of the exception)<br>
149  *   is stored in the log file with a tab followed by the word 'at' as the first characters in the line<br>
150  * - tailing may fail if the file rolls over. 
151  *<p>
152  * <b>Example receiver configuration settings</b> (add these as params, specifying a LogFilePatternReceiver 'plugin'):<br>
153  * param: "timestampFormat" value="yyyy-MM-d HH:mm:ss,SSS"<br>
154  * param: "logFormat" value="RELATIVETIME [THREAD] LEVEL LOGGER * - MESSAGE"<br>
155  * param: "fileURL" value="file:///c:/events.log"<br>
156  * param: "tailing" value="true"
157  * param: "promptForUserInfo" value="false"
158  *<p>
159  * This configuration will be able to process these sample events:<br>
160  * 710    [       Thread-0] DEBUG                   first.logger first - &lt;test&gt;   &lt;test2&gt;something here&lt;/test2&gt;   &lt;test3 blah=something/&gt;   &lt;test4&gt;       &lt;test5&gt;something else&lt;/test5&gt;   &lt;/test4&gt;&lt;/test&gt;<br>
161  * 880    [       Thread-2] DEBUG                   first.logger third - &lt;test&gt;   &lt;test2&gt;something here&lt;/test2&gt;   &lt;test3 blah=something/&gt;   &lt;test4&gt;       &lt;test5&gt;something else&lt;/test5&gt;   &lt;/test4&gt;&lt;/test&gt;<br>
162  * 880    [       Thread-0] INFO                    first.logger first - infomsg-0<br>
163  * java.lang.Exception: someexception-first<br>
164  *     at Generator2.run(Generator2.java:102)<br>
165  *
166  *@author Scott Deboy
167  */
168 public class VFSLogFilePatternReceiver extends LogFilePatternReceiver implements VisualReceiver {
169 
170   private boolean promptForUserInfo = false;
171   private Container container;
172   private Object waitForContainerLock = new Object();
173   private boolean autoReconnect;
174   private VFSReader vfsReader;
175 
176     public VFSLogFilePatternReceiver() {
177     super();
178   }
179 
180   public void shutdown() {
181     getLogger().info("shutdown VFSLogFilePatternReceiver");
182     active = false;
183 	container = null;
184     if (vfsReader != null) {
185       vfsReader.terminate();
186       vfsReader = null;
187     }
188   }
189   
190   /**
191    * If set to true, will cause the receiver to block indefinitely until 'setContainer' has been called, 
192    * at which point a username/password dialog will appear.
193    * 
194    * @param promptForUserInfo
195    */
196   public void setPromptForUserInfo(boolean promptForUserInfo) {
197 	  this.promptForUserInfo = promptForUserInfo;
198   }
199   
200   public boolean isPromptForUserInfo() {
201 	  return promptForUserInfo;
202   }
203 
204     /**
205      * Accessor
206      * @return
207      */
208     public boolean isAutoReconnect() {
209       return autoReconnect;
210     }
211 
212     /**
213      * Mutator
214      * @param autoReconnect
215      */
216     public void setAutoReconnect(boolean autoReconnect) {
217         this.autoReconnect = autoReconnect;
218     }
219 
220   /**
221    * Implementation of VisualReceiver interface - allows this receiver to provide
222    * a username/password dialog.
223    */
224   public void setContainer(Container container) {
225       if (promptForUserInfo) {
226     	  synchronized(waitForContainerLock) {
227     		  this.container=container;
228     		  waitForContainerLock.notify();
229     	  }
230       }
231   }
232 
233   /**
234    * Read and process the log file.
235    */
236   public void activateOptions() {
237       //we don't want to call super.activateOptions, but we do want active to be set to true
238       active = true;
239       //on receiver restart, only prompt for credentials if we don't already have them
240       if (promptForUserInfo && getFileURL().indexOf("@") == -1) {
241     	  /*
242     	  if promptforuserinfo is true, wait for a reference to the container
243     	  (via the VisualReceiver callback).
244 
245     	  We need to display a login dialog on top of the container, so we must then
246     	  wait until the container has been added to a frame
247     	  */
248 
249     	  //get a reference to the container
250     	  new Thread(new Runnable() {
251     		  public void run() {
252     	  synchronized(waitForContainerLock) {
253     		  while (container == null) {
254     			  try {
255     				  waitForContainerLock.wait(1000);
256     				  getLogger().debug("waiting for setContainer call");
257     			  } catch (InterruptedException ie){}
258     		  }
259     	  }
260 
261     	  Frame containerFrame1;
262           if (container instanceof Frame) {
263               containerFrame1 = (Frame)container;
264           } else {
265               synchronized(waitForContainerLock) {
266                   //loop until the container has a frame
267                   while ((containerFrame1 = (Frame)SwingUtilities.getAncestorOfClass(Frame.class, container)) == null) {
268                       try {
269                           waitForContainerLock.wait(1000);
270                           getLogger().debug("waiting for container's frame to be available");
271                       } catch (InterruptedException ie) {}
272                   }
273               }
274           }
275             final Frame containerFrame = containerFrame1;
276     	  	  //create the dialog
277     	  	  SwingUtilities.invokeLater(new Runnable() {
278     	  		public void run() {
279     	  			  Frame owner = null;
280     	  			  if (container != null) {
281     	  				  owner = (Frame)SwingUtilities.getAncestorOfClass(Frame.class, containerFrame);
282     	  			  }
283     	  			  final UserNamePasswordDialog f = new UserNamePasswordDialog(owner);
284     	  			  f.pack();
285     	  			  Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
286     	  			  f.setLocation(d.width /2, d.height/2);
287     	  			  f.setVisible(true);
288     	  				if (null == f.getUserName() || null == f.getPassword()) {
289     	  					getLogger().info("Username and password not both provided, not using credentials");
290     	  				} else {
291     	  				    String oldURL = getFileURL();
292     	  					int index = oldURL.indexOf("://");
293     	  					String firstPart = oldURL.substring(0, index);
294     	  					String lastPart = oldURL.substring(index + "://".length());
295     	  					setFileURL(firstPart + "://" + f.getUserName()+ ":" + new String(f.getPassword()) + "@" + lastPart);
296 
297     	  			        setHost(oldURL.substring(0, index + "://".length()));
298     	  		            setPath(oldURL.substring(index + "://".length()));
299     	  				}
300                         vfsReader = new VFSReader();
301     	  				new Thread(vfsReader).start();
302     	  			  }
303     	  		  });
304     		  }}).start();
305       } else {
306         //starts with protocol:/  but not protocol://
307         String oldURL = getFileURL();
308         if (oldURL != null && oldURL.indexOf(":/") > -1 && oldURL.indexOf("://") == -1) {
309           int index = oldURL.indexOf(":/");
310           String lastPart = oldURL.substring(index + ":/".length());
311           int passEndIndex = lastPart.indexOf("@");
312           if (passEndIndex > -1) { //we have a username/password
313               setHost(oldURL.substring(0, index + ":/".length()));
314               setPath(lastPart.substring(passEndIndex + 1));
315           }
316           vfsReader = new VFSReader();
317           new Thread(vfsReader).start();
318         } else if (oldURL != null && oldURL.indexOf("://") > -1) {
319         //starts with protocol://
320             int index = oldURL.indexOf("://");
321             String lastPart = oldURL.substring(index + "://".length());
322             int passEndIndex = lastPart.indexOf("@");
323             if (passEndIndex > -1) { //we have a username/password
324                 setHost(oldURL.substring(0, index + "://".length()));
325                 setPath(lastPart.substring(passEndIndex + 1));
326             }
327             vfsReader = new VFSReader();
328             new Thread(vfsReader).start();
329         } else {
330             getLogger().info("null URL - unable to parse file");
331         }
332       }
333    }
334 
335   private class VFSReader implements Runnable {
336       private boolean terminated = false;
337       private Reader reader;
338       private FileObject fileObject;
339 
340       public void run() {
341         	//thread should end when we're no longer active
342             while (reader == null && !terminated) {
343             	int atIndex = getFileURL().indexOf("@");
344             	int protocolIndex = getFileURL().indexOf("://");
345             	
346             	String loggableFileURL = atIndex > -1? getFileURL().substring(0, protocolIndex + "://".length()) + "username:password" + getFileURL().substring(atIndex) : getFileURL();
347                 getLogger().info("attempting to load file: " + loggableFileURL);
348                 try {
349                     FileSystemManager fileSystemManager = VFS.getManager();
350                     FileSystemOptions opts = new FileSystemOptions();
351                     //if jsch not in classpath, can get NoClassDefFoundError here
352                     try {
353                     	SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
354                     } catch (NoClassDefFoundError ncdfe) {
355                     	getLogger().warn("JSch not on classpath!", ncdfe);
356                     }
357 
358 					synchronized(fileSystemManager) {
359                         fileObject = fileSystemManager.resolveFile(getFileURL(), opts);
360                         if (fileObject.exists()) {
361                             reader = new InputStreamReader(fileObject.getContent().getInputStream() , "UTF-8");
362                             //now that we have a reader, remove additional portions of the file url (sftp passwords, etc.)
363                             //check to see if the name is a URLFileName..if so, set file name to not include username/pass
364                             if (fileObject.getName() instanceof URLFileName) {
365                                 URLFileName urlFileName = (URLFileName) fileObject.getName();
366                                 setHost(urlFileName.getHostName());
367                                 setPath(urlFileName.getPath());
368                             }
369                         } else {
370                             getLogger().info(loggableFileURL + " not available - will re-attempt to load after waiting " + MISSING_FILE_RETRY_MILLIS + " millis");
371                         }
372                     }
373                 } catch (FileSystemException fse) {
374                     getLogger().info(loggableFileURL + " not available - may be due to incorrect credentials, but will re-attempt to load after waiting " + MISSING_FILE_RETRY_MILLIS + " millis", fse);
375                 } catch (UnsupportedEncodingException e) {
376                     getLogger().info("UTF-8 not available", e);
377                 }
378                 if (reader == null) {
379                     synchronized (this) {
380                         try {
381                             wait(MISSING_FILE_RETRY_MILLIS);
382                         } catch (InterruptedException ie) {}
383                     }
384                 }
385             }
386             if (terminated) {
387                 //shut down while waiting for a file
388                 return;
389             }
390             initialize();
391             getLogger().debug(getPath() + " exists");
392 
393             do {
394                 long lastFilePointer = 0;
395                 long lastFileSize = 0;
396                 createPattern();
397                 try {
398                     do {
399                         FileSystemManager fileSystemManager = VFS.getManager();
400                         FileSystemOptions opts = new FileSystemOptions();
401                         //if jsch not in classpath, can get NoClassDefFoundError here
402                         try {
403                             SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
404                         } catch (NoClassDefFoundError ncdfe) {
405                             getLogger().warn("JSch not on classpath!", ncdfe);
406                         }
407 
408                         //fileobject was created above, release it and construct a new one
409 						synchronized(fileSystemManager) {
410 	                        if (fileObject != null) {
411 	                              fileObject.getFileSystem().getFileSystemManager().closeFileSystem(fileObject.getFileSystem());
412 	                              fileObject.close();
413 	                              fileObject = null;
414                             }
415                         
416                         	fileObject = fileSystemManager.resolveFile(getFileURL(), opts);
417                         }
418 
419                         //file may not exist..
420                         boolean fileLarger = false;
421                         if (fileObject != null && fileObject.exists()) {
422                             try {
423                                 //available in vfs as of 30 Mar 2006 - will load but not tail if not available
424                                 fileObject.refresh();
425                             } catch (Error err) {
426                                 getLogger().info(getPath() + " - unable to refresh fileobject", err);
427                             }
428                             //could have been truncated or appended to (don't do anything if same size)
429                             if (fileObject.getContent().getSize() < lastFileSize) {
430                                 reader = new InputStreamReader(fileObject.getContent().getInputStream(), "UTF-8");
431                                 getLogger().debug(getPath() + " was truncated");
432                                 lastFileSize = 0; //seek to beginning of file
433                                 lastFilePointer = 0;
434                             } else if (fileObject.getContent().getSize() > lastFileSize) {
435                                 fileLarger = true;
436                                 RandomAccessContent rac = fileObject.getContent().getRandomAccessContent(RandomAccessMode.READ);
437                                 rac.seek(lastFilePointer);
438                                 reader = new InputStreamReader(rac.getInputStream(), "UTF-8");
439                                 BufferedReader bufferedReader = new BufferedReader(reader);
440                                 process(bufferedReader);
441                                 lastFilePointer = rac.getFilePointer();
442                                 lastFileSize = fileObject.getContent().getSize();
443                                 rac.close();
444                             }
445                             try {
446                                 if (reader != null) {
447                                     reader.close();
448                                     reader = null;
449                                 }
450                             } catch (IOException ioe) {
451                                 getLogger().debug(getPath() + " - unable to close reader", ioe);
452                             }
453                         } else {
454                             getLogger().info(getPath() + " - not available - will re-attempt to load after waiting " + getWaitMillis() + " millis");
455                         }
456 
457                         try {
458                             synchronized (this) {
459                                 wait(getWaitMillis());
460                             }
461                         } catch (InterruptedException ie) {}
462                         if (isTailing() && fileLarger && !terminated) {
463                             getLogger().debug(getPath() + " - tailing file - file size: " + lastFileSize);
464                         }
465                     } while (isTailing() && !terminated);
466                 } catch (IOException ioe) {
467                     getLogger().info(getPath() + " - exception processing file", ioe);
468                     try {
469                         if (fileObject != null) {
470                             fileObject.close();
471                         }
472                     } catch (FileSystemException e) {
473                         getLogger().info(getPath() + " - exception processing file", e);
474                     }
475                     try {
476                         synchronized(this) {
477                             wait(getWaitMillis());
478                         }
479                     } catch (InterruptedException ie) {}
480                 }
481             } while (isAutoReconnect() && !terminated);
482             getLogger().debug(getPath() + " - processing complete");
483         }
484 
485       public void terminate()
486       {
487           terminated = true;
488       }
489   }
490   
491   public class UserNamePasswordDialog extends JDialog {
492 	  private String userName;
493 	  private char[] password;
494 	  private UserNamePasswordDialog(Frame containerFrame) {
495 		  super(containerFrame, "Login", true);
496 	      JPanel panel = new JPanel(new GridBagLayout());
497 	      GridBagConstraints gc = new GridBagConstraints();
498 	      gc.fill=GridBagConstraints.NONE;
499 
500 	      gc.anchor=GridBagConstraints.NORTH;
501 	      gc.gridx=0;
502 	      gc.gridy=0;
503 	      gc.gridwidth=3;
504 	      gc.insets=new Insets(7, 7, 7, 7);
505 	      panel.add(new JLabel("URI: " + getFileURL()), gc);
506 	      
507 	      gc.gridx=0;
508 	      gc.gridy=1;
509 	      gc.gridwidth=1;
510 	      gc.insets=new Insets(2, 2, 2, 2);
511 	      panel.add(new JLabel("Username"), gc);
512 
513 		  gc.gridx=1;
514 		  gc.gridy=1;
515 		  gc.gridwidth=2;
516 		  gc.weightx=1.0;
517 	      gc.fill=GridBagConstraints.HORIZONTAL;
518 
519 		  final JTextField userNameTextField = new JTextField(15);
520 		  panel.add(userNameTextField, gc);
521 		  
522 		  gc.gridx=0;
523 		  gc.gridy=2;
524 		  gc.gridwidth=1;
525 	      gc.fill=GridBagConstraints.NONE;
526 
527 		  panel.add(new JLabel("Password"), gc);
528 
529 		  gc.gridx=1;
530 		  gc.gridy=2;
531 		  gc.gridwidth=2;
532 	      gc.fill=GridBagConstraints.HORIZONTAL;
533 
534 		  final JPasswordField passwordTextField = new JPasswordField(15);
535 		  panel.add(passwordTextField, gc);
536 		  
537 		  gc.gridy=3;
538 		  gc.anchor=GridBagConstraints.SOUTH;
539 	      gc.fill=GridBagConstraints.NONE;
540 
541 		  JButton submitButton = new JButton(" Submit ");
542 		  panel.add(submitButton, gc);
543 
544 		  getContentPane().add(panel);
545 		  submitButton.addActionListener(new ActionListener(){
546 			  public void actionPerformed(ActionEvent evt) {
547 				  userName = userNameTextField.getText();
548 				  password = passwordTextField.getPassword();
549 				  getContentPane().setVisible(false);
550 				  UserNamePasswordDialog.this.dispose();
551 			  }
552 		  });
553 	  }
554 	 
555 	  public String getUserName() {
556 		  return userName;
557 	  }
558 	  
559 	  public char[] getPassword() {
560 		  return password;
561 	  }
562   }
563 }