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 - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test><br>
138 * 880 [ Thread-2] DEBUG first.logger third - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test><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 }