blob: a0fc0d849bf6f7f1b2fa0e56e199aee14a5e97fd [file] [log] [blame]
/*
* Copyright 1999,2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.log4j.chainsaw.vfs;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileSystemManager;
import org.apache.commons.vfs.FileSystemOptions;
import org.apache.commons.vfs.RandomAccessContent;
import org.apache.commons.vfs.VFS;
import org.apache.commons.vfs.provider.URLFileName;
import org.apache.commons.vfs.provider.sftp.SftpFileSystemConfigBuilder;
import org.apache.commons.vfs.util.RandomAccessMode;
import org.apache.log4j.chainsaw.receivers.VisualReceiver;
import org.apache.log4j.varia.LogFilePatternReceiver;
/**
* A VFS-enabled version of org.apache.log4j.varia.LogFilePatternReceiver.
*
* VFSLogFilePatternReceiver can parse and tail log files, converting entries into
* LoggingEvents. If the file doesn't exist when the receiver is initialized, the
* receiver will look for the file once every 10 seconds.
* <p>
* See the Chainsaw page (http://logging.apache.org/log4j/docs/chainsaw.html) for information
* on how to set up Chainsaw with VFS.
* <p>
* See http://jakarta.apache.org/commons/vfs/filesystems.html for a list of VFS-supported
* file systems and the URIs needed to access the file systems.
* <p>
* Because some VFS file systems allow you to provide username/password, this receiver
* provides an optional GUI dialog for entering the username/password fields instead
* of requiring you to hard code usernames and passwords into the URI.
* <p>
* If the 'promptForUserInfo' param is set to true (default is false),
* the receiver will wait for a call to 'setContainer', and then display
* a username/password dialog.
* <p>
* If you are using this receiver without a GUI, don't set promptForUserInfo
* to true - it will block indefinitely waiting for a visual component.
* <p>
* If the 'promptForUserInfo' param is set to true, the fileURL should -leave out-
* the username/password portion of the VFS-supported URI. Examples:
* <p>
* An sftp URI that would be used with promptForUserInfo=true:
* sftp://192.168.1.100:22/home/thisuser/logfile.txt
* <p>
* An sftp URI that would be used with promptForUserInfo=false:
* sftp://username:password@192.168.1.100:22/home/thisuser/logfile.txt
* <p>
* This receiver relies on java.util.regex features to perform the parsing of text in the
* log file, however the only regular expression field explicitly supported is
* a glob-style wildcard used to ignore fields in the log file if needed. All other
* fields are parsed by using the supplied keywords.
* <p>
* <b>Features:</b><br>
* - specify the URL of the log file to be processed<br>
* - specify the timestamp format in the file (if one exists, using patterns from {@link java.text.SimpleDateFormat})<br>
* - specify the pattern (logFormat) used in the log file using keywords, a wildcard character (*) and fixed text<br>
* - 'tail' the file (allows the contents of the file to be continually read and new events processed)<br>
* - supports the parsing of multi-line messages and exceptions
* - to access
*<p>
* <b>Keywords:</b><br>
* TIMESTAMP<br>
* LOGGER<br>
* LEVEL<br>
* THREAD<br>
* CLASS<br>
* FILE<br>
* LINE<br>
* METHOD<br>
* RELATIVETIME<br>
* MESSAGE<br>
* NDC<br>
* PROP(key)<br>
* <p>
* Use a * to ignore portions of the log format that should be ignored
* <p>
* Example:<br>
* If your file's patternlayout is this:<br>
* <b>%d %-5p [%t] %C{2} (%F:%L) - %m%n</b>
*<p>
* specify this as the log format:<br>
* <b>TIMESTAMP LEVEL [THREAD] CLASS (FILE:LINE) - MESSAGE</b>
*<p>
* To define a PROPERTY field, use PROP(key)
* <p>
* Example:<br>
* If you used the RELATIVETIME pattern layout character in the file,
* you can use PROP(RELATIVETIME) in the logFormat definition to assign
* the RELATIVETIME field as a property on the event.
* <p>
* If your file's patternlayout is this:<br>
* <b>%r [%t] %-5p %c %x - %m%n</b>
*<p>
* specify this as the log format:<br>
* <b>PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE</b>
* <p>
* Note the * - it can be used to ignore a single word or sequence of words in the log file
* (in order for the wildcard to ignore a sequence of words, the text being ignored must be
* followed by some delimiter, like '-' or '[') - ndc is being ignored in this example.
* <p>
* Assign a filterExpression in order to only process events which match a filter.
* If a filterExpression is not assigned, all events are processed.
*<p>
* <b>Limitations:</b><br>
* - no support for the single-line version of throwable supported by patternlayout<br>
* (this version of throwable will be included as the last line of the message)<br>
* - the relativetime patternLayout character must be set as a property: PROP(RELATIVETIME)<br>
* - messages should appear as the last field of the logFormat because the variability in message content<br>
* - exceptions are converted if the exception stack trace (other than the first line of the exception)<br>
* is stored in the log file with a tab followed by the word 'at' as the first characters in the line<br>
* - tailing may fail if the file rolls over.
*<p>
* <b>Example receiver configuration settings</b> (add these as params, specifying a LogFilePatternReceiver 'plugin'):<br>
* param: "timestampFormat" value="yyyy-MM-d HH:mm:ss,SSS"<br>
* param: "logFormat" value="RELATIVETIME [THREAD] LEVEL LOGGER * - MESSAGE"<br>
* param: "fileURL" value="file:///c:/events.log"<br>
* param: "tailing" value="true"
* param: "promptForUserInfo" value="false"
*<p>
* This configuration will be able to process these sample events:<br>
* 710 [ Thread-0] DEBUG first.logger first - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test><br>
* 880 [ Thread-2] DEBUG first.logger third - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test><br>
* 880 [ Thread-0] INFO first.logger first - infomsg-0<br>
* java.lang.Exception: someexception-first<br>
* at Generator2.run(Generator2.java:102)<br>
*
*@author Scott Deboy
*/
public class VFSLogFilePatternReceiver extends LogFilePatternReceiver implements VisualReceiver {
private boolean promptForUserInfo = false;
private Container container;
private Object waitForContainerLock = new Object();
private boolean autoReconnect;
private VFSReader vfsReader;
public VFSLogFilePatternReceiver() {
super();
}
public void shutdown() {
getLogger().info("shutdown VFSLogFilePatternReceiver");
active = false;
container = null;
if (vfsReader != null) {
vfsReader.terminate();
vfsReader = null;
}
}
/**
* If set to true, will cause the receiver to block indefinitely until 'setContainer' has been called,
* at which point a username/password dialog will appear.
*
* @param promptForUserInfo
*/
public void setPromptForUserInfo(boolean promptForUserInfo) {
this.promptForUserInfo = promptForUserInfo;
}
public boolean isPromptForUserInfo() {
return promptForUserInfo;
}
/**
* Accessor
* @return
*/
public boolean isAutoReconnect() {
return autoReconnect;
}
/**
* Mutator
* @param autoReconnect
*/
public void setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
}
/**
* Implementation of VisualReceiver interface - allows this receiver to provide
* a username/password dialog.
*/
public void setContainer(Container container) {
if (promptForUserInfo) {
synchronized(waitForContainerLock) {
this.container=container;
waitForContainerLock.notify();
}
}
}
/**
* Read and process the log file.
*/
public void activateOptions() {
//we don't want to call super.activateOptions, but we do want active to be set to true
active = true;
//on receiver restart, only prompt for credentials if we don't already have them
if (promptForUserInfo && getFileURL().indexOf("@") == -1) {
/*
if promptforuserinfo is true, wait for a reference to the container
(via the VisualReceiver callback).
We need to display a login dialog on top of the container, so we must then
wait until the container has been added to a frame
*/
//get a reference to the container
new Thread(new Runnable() {
public void run() {
synchronized(waitForContainerLock) {
while (container == null) {
try {
waitForContainerLock.wait(1000);
getLogger().debug("waiting for setContainer call");
} catch (InterruptedException ie){}
}
}
Frame containerFrame1;
if (container instanceof Frame) {
containerFrame1 = (Frame)container;
} else {
synchronized(waitForContainerLock) {
//loop until the container has a frame
while ((containerFrame1 = (Frame)SwingUtilities.getAncestorOfClass(Frame.class, container)) == null) {
try {
waitForContainerLock.wait(1000);
getLogger().debug("waiting for container's frame to be available");
} catch (InterruptedException ie) {}
}
}
}
final Frame containerFrame = containerFrame1;
//create the dialog
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Frame owner = null;
if (container != null) {
owner = (Frame)SwingUtilities.getAncestorOfClass(Frame.class, containerFrame);
}
final UserNamePasswordDialog f = new UserNamePasswordDialog(owner);
f.pack();
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
f.setLocation(d.width /2, d.height/2);
f.setVisible(true);
if (null == f.getUserName() || null == f.getPassword()) {
getLogger().info("Username and password not both provided, not using credentials");
} else {
String oldURL = getFileURL();
int index = oldURL.indexOf("://");
String firstPart = oldURL.substring(0, index);
String lastPart = oldURL.substring(index + "://".length());
setFileURL(firstPart + "://" + f.getUserName()+ ":" + new String(f.getPassword()) + "@" + lastPart);
setHost(oldURL.substring(0, index + "://".length()));
setPath(oldURL.substring(index + "://".length()));
}
vfsReader = new VFSReader();
new Thread(vfsReader).start();
}
});
}}).start();
} else {
//starts with protocol:/ but not protocol://
String oldURL = getFileURL();
if (oldURL != null && oldURL.indexOf(":/") > -1 && oldURL.indexOf("://") == -1) {
int index = oldURL.indexOf(":/");
String lastPart = oldURL.substring(index + ":/".length());
int passEndIndex = lastPart.indexOf("@");
if (passEndIndex > -1) { //we have a username/password
setHost(oldURL.substring(0, index + ":/".length()));
setPath(lastPart.substring(passEndIndex + 1));
}
vfsReader = new VFSReader();
new Thread(vfsReader).start();
} else if (oldURL != null && oldURL.indexOf("://") > -1) {
//starts with protocol://
int index = oldURL.indexOf("://");
String lastPart = oldURL.substring(index + "://".length());
int passEndIndex = lastPart.indexOf("@");
if (passEndIndex > -1) { //we have a username/password
setHost(oldURL.substring(0, index + "://".length()));
setPath(lastPart.substring(passEndIndex + 1));
}
vfsReader = new VFSReader();
new Thread(vfsReader).start();
} else {
getLogger().info("null URL - unable to parse file");
}
}
}
private class VFSReader implements Runnable {
private boolean terminated = false;
private Reader reader;
private FileObject fileObject;
public void run() {
//thread should end when we're no longer active
while (reader == null && !terminated) {
int atIndex = getFileURL().indexOf("@");
int protocolIndex = getFileURL().indexOf("://");
String loggableFileURL = atIndex > -1? getFileURL().substring(0, protocolIndex + "://".length()) + "username:password" + getFileURL().substring(atIndex) : getFileURL();
getLogger().info("attempting to load file: " + loggableFileURL);
try {
FileSystemManager fileSystemManager = VFS.getManager();
FileSystemOptions opts = new FileSystemOptions();
//if jsch not in classpath, can get NoClassDefFoundError here
try {
SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
} catch (NoClassDefFoundError ncdfe) {
getLogger().warn("JSch not on classpath!", ncdfe);
}
synchronized(fileSystemManager) {
fileObject = fileSystemManager.resolveFile(getFileURL(), opts);
if (fileObject.exists()) {
reader = new InputStreamReader(fileObject.getContent().getInputStream() , "UTF-8");
//now that we have a reader, remove additional portions of the file url (sftp passwords, etc.)
//check to see if the name is a URLFileName..if so, set file name to not include username/pass
if (fileObject.getName() instanceof URLFileName) {
URLFileName urlFileName = (URLFileName) fileObject.getName();
setHost(urlFileName.getHostName());
setPath(urlFileName.getPath());
}
} else {
getLogger().info(loggableFileURL + " not available - will re-attempt to load after waiting " + MISSING_FILE_RETRY_MILLIS + " millis");
}
}
} catch (FileSystemException fse) {
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);
} catch (UnsupportedEncodingException e) {
getLogger().info("UTF-8 not available", e);
}
if (reader == null) {
synchronized (this) {
try {
wait(MISSING_FILE_RETRY_MILLIS);
} catch (InterruptedException ie) {}
}
}
}
if (terminated) {
//shut down while waiting for a file
return;
}
initialize();
getLogger().debug(getPath() + " exists");
do {
long lastFilePointer = 0;
long lastFileSize = 0;
createPattern();
try {
do {
FileSystemManager fileSystemManager = VFS.getManager();
FileSystemOptions opts = new FileSystemOptions();
//if jsch not in classpath, can get NoClassDefFoundError here
try {
SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
} catch (NoClassDefFoundError ncdfe) {
getLogger().warn("JSch not on classpath!", ncdfe);
}
//fileobject was created above, release it and construct a new one
synchronized(fileSystemManager) {
if (fileObject != null) {
fileObject.getFileSystem().getFileSystemManager().closeFileSystem(fileObject.getFileSystem());
fileObject.close();
fileObject = null;
}
fileObject = fileSystemManager.resolveFile(getFileURL(), opts);
}
//file may not exist..
boolean fileLarger = false;
if (fileObject != null && fileObject.exists()) {
try {
//available in vfs as of 30 Mar 2006 - will load but not tail if not available
fileObject.refresh();
} catch (Error err) {
getLogger().info(getPath() + " - unable to refresh fileobject", err);
}
//could have been truncated or appended to (don't do anything if same size)
if (fileObject.getContent().getSize() < lastFileSize) {
reader = new InputStreamReader(fileObject.getContent().getInputStream(), "UTF-8");
getLogger().debug(getPath() + " was truncated");
lastFileSize = 0; //seek to beginning of file
lastFilePointer = 0;
} else if (fileObject.getContent().getSize() > lastFileSize) {
fileLarger = true;
RandomAccessContent rac = fileObject.getContent().getRandomAccessContent(RandomAccessMode.READ);
rac.seek(lastFilePointer);
reader = new InputStreamReader(rac.getInputStream(), "UTF-8");
BufferedReader bufferedReader = new BufferedReader(reader);
process(bufferedReader);
lastFilePointer = rac.getFilePointer();
lastFileSize = fileObject.getContent().getSize();
rac.close();
}
try {
if (reader != null) {
reader.close();
reader = null;
}
} catch (IOException ioe) {
getLogger().debug(getPath() + " - unable to close reader", ioe);
}
} else {
getLogger().info(getPath() + " - not available - will re-attempt to load after waiting " + getWaitMillis() + " millis");
}
try {
synchronized (this) {
wait(getWaitMillis());
}
} catch (InterruptedException ie) {}
if (isTailing() && fileLarger && !terminated) {
getLogger().debug(getPath() + " - tailing file - file size: " + lastFileSize);
}
} while (isTailing() && !terminated);
} catch (IOException ioe) {
getLogger().info(getPath() + " - exception processing file", ioe);
try {
if (fileObject != null) {
fileObject.close();
}
} catch (FileSystemException e) {
getLogger().info(getPath() + " - exception processing file", e);
}
try {
synchronized(this) {
wait(getWaitMillis());
}
} catch (InterruptedException ie) {}
}
} while (isAutoReconnect() && !terminated);
getLogger().debug(getPath() + " - processing complete");
}
public void terminate()
{
terminated = true;
}
}
public class UserNamePasswordDialog extends JDialog {
private String userName;
private char[] password;
private UserNamePasswordDialog(Frame containerFrame) {
super(containerFrame, "Login", true);
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gc = new GridBagConstraints();
gc.fill=GridBagConstraints.NONE;
gc.anchor=GridBagConstraints.NORTH;
gc.gridx=0;
gc.gridy=0;
gc.gridwidth=3;
gc.insets=new Insets(7, 7, 7, 7);
panel.add(new JLabel("URI: " + getFileURL()), gc);
gc.gridx=0;
gc.gridy=1;
gc.gridwidth=1;
gc.insets=new Insets(2, 2, 2, 2);
panel.add(new JLabel("Username"), gc);
gc.gridx=1;
gc.gridy=1;
gc.gridwidth=2;
gc.weightx=1.0;
gc.fill=GridBagConstraints.HORIZONTAL;
final JTextField userNameTextField = new JTextField(15);
panel.add(userNameTextField, gc);
gc.gridx=0;
gc.gridy=2;
gc.gridwidth=1;
gc.fill=GridBagConstraints.NONE;
panel.add(new JLabel("Password"), gc);
gc.gridx=1;
gc.gridy=2;
gc.gridwidth=2;
gc.fill=GridBagConstraints.HORIZONTAL;
final JPasswordField passwordTextField = new JPasswordField(15);
panel.add(passwordTextField, gc);
gc.gridy=3;
gc.anchor=GridBagConstraints.SOUTH;
gc.fill=GridBagConstraints.NONE;
JButton submitButton = new JButton(" Submit ");
panel.add(submitButton, gc);
getContentPane().add(panel);
submitButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent evt) {
userName = userNameTextField.getText();
password = passwordTextField.getPassword();
getContentPane().setVisible(false);
UserNamePasswordDialog.this.dispose();
}
});
}
public String getUserName() {
return userName;
}
public char[] getPassword() {
return password;
}
}
}