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