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.varia;
18  
19  import java.io.BufferedReader;
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.io.InputStreamReader;
23  import java.io.Reader;
24  import java.net.MalformedURLException;
25  import java.net.URL;
26  import java.text.SimpleDateFormat;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Hashtable;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.TreeMap;
36  
37  import org.apache.log4j.Level;
38  import org.apache.log4j.Logger;
39  import org.apache.log4j.helpers.Constants;
40  import org.apache.log4j.plugins.Receiver;
41  import org.apache.log4j.rule.ExpressionRule;
42  import org.apache.log4j.rule.Rule;
43  import org.apache.log4j.spi.LoggingEvent;
44  import org.apache.log4j.spi.ThrowableInformation;
45  import org.apache.log4j.spi.LocationInfo;
46  import org.apache.oro.text.perl.Perl5Util;
47  import org.apache.oro.text.regex.MalformedPatternException;
48  import org.apache.oro.text.regex.MatchResult;
49  import org.apache.oro.text.regex.Pattern;
50  import org.apache.oro.text.regex.Perl5Compiler;
51  import org.apache.oro.text.regex.Perl5Matcher;
52  
53  /***
54   * LogFilePatternReceiver can parse and tail log files, converting entries into
55   * LoggingEvents.  If the file doesn't exist when the receiver is initialized, the
56   * receiver will look for the file once every 10 seconds.
57   * <p>
58   * This receiver relies on ORO Perl5 features to perform the parsing of text in the 
59   * log file, however the only regular expression field explicitly supported is 
60   * a glob-style wildcard used to ignore fields in the log file if needed.  All other
61   * fields are parsed by using the supplied keywords.
62   * <p>
63   * <b>Features:</b><br>
64   * - specify the URL of the log file to be processed<br>
65   * - specify the timestamp format in the file (if one exists)<br>
66   * - specify the pattern (logFormat) used in the log file using keywords, a wildcard character (*) and fixed text<br>
67   * - 'tail' the file (allows the contents of the file to be continually read and new events processed)<br>
68   * - supports the parsing of multi-line messages and exceptions
69   * - 'hostname' property set to URL host (or 'file' if not available)
70   * - 'application' property set to URL path (or value of fileURL if not available) 
71   *<p>
72   * <b>Keywords:</b><br>
73   * TIMESTAMP<br>
74   * LOGGER<br>
75   * LEVEL<br>
76   * THREAD<br>
77   * CLASS<br>
78   * FILE<br>
79   * LINE<br>
80   * METHOD<br>
81   * RELATIVETIME<br>
82   * MESSAGE<br>
83   * NDC<br>
84   * PROP(key)<br>
85   * <p>
86   * Use a * to ignore portions of the log format that should be ignored
87   * <p>
88   * Example:<br>
89   * If your file's patternlayout is this:<br>
90   * <b>%d %-5p [%t] %C{2} (%F:%L) - %m%n</b>
91   *<p>
92   * specify this as the log format:<br>
93   * <b>TIMESTAMP LEVEL [THREAD] CLASS (FILE:LINE) - MESSAGE</b>
94   *<p>
95   * To define a PROPERTY field, use PROP(key)
96   * <p>
97   * Example:<br> 
98   * If you used the RELATIVETIME pattern layout character in the file, 
99   * you can use PROP(RELATIVETIME) in the logFormat definition to assign 
100  * the RELATIVETIME field as a property on the event.
101  * <p>
102  * If your file's patternlayout is this:<br>
103  * <b>%r [%t] %-5p %c %x - %m%n</b>
104  *<p>
105  * specify this as the log format:<br>
106  * <b>PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE</b>
107  * <p>
108  * Note the * - it can be used to ignore a single word or sequence of words in the log file
109  * (in order for the wildcard to ignore a sequence of words, the text being ignored must be
110  *  followed by some delimiter, like '-' or '[') - ndc is being ignored in this example.
111  * <p>
112  * Assign a filterExpression in order to only process events which match a filter.
113  * If a filterExpression is not assigned, all events are processed.
114  *<p>
115  * <b>Limitations:</b><br>
116  * - no support for the single-line version of throwable supported by patternlayout<br>
117  *   (this version of throwable will be included as the last line of the message)<br>
118  * - the relativetime patternLayout character must be set as a property: PROP(RELATIVETIME)<br>
119  * - messages should appear as the last field of the logFormat because the variability in message content<br>
120  * - exceptions are converted if the exception stack trace (other than the first line of the exception)<br>
121  *   is stored in the log file with a tab followed by the word 'at' as the first characters in the line<br>
122  * - tailing may fail if the file rolls over. 
123  *<p>
124  * <b>Example receiver configuration settings</b> (add these as params, specifying a LogFilePatternReceiver 'plugin'):<br>
125  * param: "timestampFormat" value="yyyy-MM-d HH:mm:ss,SSS"<br>
126  * param: "logFormat" value="RELATIVETIME [THREAD] LEVEL LOGGER * - MESSAGE"<br>
127  * param: "fileURL" value="file:///c:/events.log"<br>
128  * param: "tailing" value="true"
129  *<p>
130  * This configuration will be able to process these sample events:<br>
131  * 710    [       Thread-0] DEBUG                   first.logger first - <test>   <test2>something here</test2>   <test3 blah=something/>   <test4>       <test5>something else</test5>   </test4></test><br>
132  * 880    [       Thread-2] DEBUG                   first.logger third - <test>   <test2>something here</test2>   <test3 blah=something/>   <test4>       <test5>something else</test5>   </test4></test><br>
133  * 880    [       Thread-0] INFO                    first.logger first - infomsg-0<br>
134  * java.lang.Exception: someexception-first<br>
135  *     at Generator2.run(Generator2.java:102)<br>
136  *
137  *@author Scott Deboy
138  */
139 public class LogFilePatternReceiver extends Receiver {
140   private final Set keywords = new HashSet();
141 
142   private static final String PROP_START = "PROP(";
143   private static final String PROP_END = ")";
144 
145   private static final String LOGGER = "LOGGER";
146   private static final String MESSAGE = "MESSAGE";
147   private static final String TIMESTAMP = "TIMESTAMP";
148   private static final String NDC = "NDC";
149   private static final String LEVEL = "LEVEL";
150   private static final String THREAD = "THREAD";
151   private static final String CLASS = "CLASS";
152   private static final String FILE = "FILE";
153   private static final String LINE = "LINE";
154   private static final String METHOD = "METHOD";
155   
156   private static final String DEFAULT_HOST = "file";
157   
158   //all lines other than first line of exception begin with tab followed by 'at' followed by text
159   private static final String EXCEPTION_PATTERN = "\tat.*";
160   private static final String REGEXP_DEFAULT_WILDCARD = ".+?";
161   private static final String REGEXP_GREEDY_WILDCARD = ".+";
162   private static final String PATTERN_WILDCARD = "*";
163   private static final String DEFAULT_GROUP = "(" + REGEXP_DEFAULT_WILDCARD + ")";
164   private static final String GREEDY_GROUP = "(" + REGEXP_GREEDY_WILDCARD + ")";
165   private static final String MULTIPLE_SPACES_REGEXP = "[ ]+";
166   
167   private final String newLine = System.getProperty("line.separator");
168 
169   private final String[] emptyException = new String[] { "" };
170 
171   private SimpleDateFormat dateFormat;
172   private String timestampFormat = "yyyy-MM-d HH:mm:ss,SSS";
173   private String logFormat;
174   private String fileURL;
175   private String host;
176   private String path;
177   private boolean tailing;
178   private String filterExpression;
179 
180   private Perl5Util util = null;
181   private Perl5Compiler exceptionCompiler = null;
182   private Perl5Matcher exceptionMatcher = null;
183   private static final String VALID_DATEFORMAT_CHAR_PATTERN = "[GyMwWDdFEaHkKhmsSzZ]";
184 
185   private Rule expressionRule;
186 
187   private Map currentMap;
188   private List additionalLines;
189   private List matchingKeywords;
190 
191   private String regexp;
192   private Reader reader;
193   private Pattern regexpPattern;
194   private String timestampPatternText;
195 
196 private boolean useCurrentThread;
197 
198   public LogFilePatternReceiver() {
199     keywords.add(TIMESTAMP);
200     keywords.add(LOGGER);
201     keywords.add(LEVEL);
202     keywords.add(THREAD);
203     keywords.add(CLASS);
204     keywords.add(FILE);
205     keywords.add(LINE);
206     keywords.add(METHOD);
207     keywords.add(MESSAGE);
208     keywords.add(NDC);
209   }
210 
211   /***
212    * Accessor
213    * 
214    * @return file URL
215    */
216   public String getFileURL() {
217     return fileURL;
218   }
219 
220   /***
221    * Mutator
222    * 
223    * @param fileURL
224    */
225   public void setFileURL(String fileURL) {
226     this.fileURL = fileURL;
227   }
228 
229   /***
230    * Accessor
231    * 
232    * @return filter expression
233    */
234   public String getFilterExpression() {
235     return filterExpression;
236   }
237 
238   /***
239    * Mutator
240    * 
241    * @param filterExpression
242    */
243   public void setFilterExpression(String filterExpression) {
244     this.filterExpression = filterExpression;
245   }
246 
247   /***
248    * Accessor
249    * 
250    * @return tailing
251    */
252   public boolean isTailing() {
253     return tailing;
254   }
255 
256   /***
257    * Mutator
258    * 
259    * @param tailing
260    */
261   public void setTailing(boolean tailing) {
262     this.tailing = tailing;
263   }
264 
265   /***
266    * When true, this property uses the current Thread to perform the import,
267    * otherwise when false (the default), a new Thread is created and started to manage
268    * the import.
269    * @return
270    */ 
271  public final boolean isUseCurrentThread() {
272      return useCurrentThread;
273  }
274 
275  /***
276   * Sets whether the current Thread or a new Thread is created to perform the import,
277   * the default being false (new Thread created).
278   * 
279   * @param useCurrentThread
280   */
281  public final void setUseCurrentThread(boolean useCurrentThread) {
282      this.useCurrentThread = useCurrentThread;
283  }
284 
285   /***
286    * Accessor
287    * 
288    * @return log format
289    */
290   public String getLogFormat() {
291     return logFormat;
292   }
293 
294   /***
295    * Mutator
296    * 
297    * @param logFormat
298    *          the format
299    */
300   public void setLogFormat(String logFormat) {
301     this.logFormat = logFormat;
302   }
303 
304   /***
305    * Mutator
306    * 
307    * @param timestampFormat
308    */
309   public void setTimestampFormat(String timestampFormat) {
310     this.timestampFormat = timestampFormat;
311   }
312 
313   /***
314    * Accessor
315    * 
316    * @return timestamp format
317    */
318   public String getTimestampFormat() {
319     return timestampFormat;
320   }
321 
322   /***
323    * Walk the additionalLines list, looking for the EXCEPTION_PATTERN.
324    * <p>
325    * Return the index of the first matched line minus 1 
326    * (the match is the 2nd line of an exception)
327    * <p>
328    * Assumptions: <br>
329    * - the additionalLines list may contain both message and exception lines<br>
330    * - message lines are added to the additionalLines list and then
331    * exception lines (all message lines occur in the list prior to all 
332    * exception lines)
333    * 
334    * @return -1 if no exception line exists, line number otherwise
335    */
336   private int getExceptionLine() {
337     try {
338       Pattern exceptionPattern = exceptionCompiler.compile(EXCEPTION_PATTERN);
339       for (int i = 0; i < additionalLines.size(); i++) {
340         if (exceptionMatcher.matches((String) additionalLines.get(i), exceptionPattern)) {
341           return i - 1;
342         }
343       }
344     } catch (MalformedPatternException mpe) {
345       getLogger().warn("Bad pattern: " + EXCEPTION_PATTERN);
346     }
347     return -1;
348   }
349 
350   /***
351    * Combine all message lines occuring in the additionalLines list, adding
352    * a newline character between each line
353    * <p>
354    * the event will already have a message - combine this message
355    * with the message lines in the additionalLines list 
356    * (all entries prior to the exceptionLine index)
357    * 
358    * @param firstMessageLine primary message line
359    * @param exceptionLine index of first exception line
360    * @return message
361    */
362   private String buildMessage(String firstMessageLine, int exceptionLine) {
363     if (additionalLines.size() == 0 || exceptionLine == 0) {
364       return firstMessageLine;
365     }
366     StringBuffer message = new StringBuffer();
367     if (firstMessageLine != null) {
368       message.append(firstMessageLine);
369     }
370       
371     int linesToProcess = (exceptionLine == -1?additionalLines.size(): exceptionLine);
372 
373     for (int i = 0; i < linesToProcess; i++) {
374       message.append(newLine);
375       message.append(additionalLines.get(i));
376     }
377     return message.toString();
378   }
379 
380   /***
381    * Combine all exception lines occuring in the additionalLines list into a 
382    * String array
383    * <p>
384    * (all entries equal to or greater than the exceptionLine index)
385    * 
386    * @param exceptionLine index of first exception line
387    * @return exception
388    */
389   private String[] buildException(int exceptionLine) {
390     if (exceptionLine == -1) {
391       return emptyException;
392     }
393     String[] exception = new String[additionalLines.size() - exceptionLine];
394     for (int i = 0; i < additionalLines.size() - exceptionLine; i++) {
395       exception[i] = (String) additionalLines.get(i + exceptionLine);
396     }
397     return exception;
398   }
399 
400   /***
401    * Construct a logging event from currentMap and additionalLines 
402    * (additionalLines contains multiple message lines and any exception lines)
403    * <p>
404    * CurrentMap and additionalLines are cleared in the process
405    * 
406    * @return event
407    */
408   private LoggingEvent buildEvent() {
409     if (currentMap.size() == 0) {
410       if (additionalLines.size() > 0) {
411         for (Iterator iter = additionalLines.iterator();iter.hasNext();) {
412           getLogger().info("found non-matching line: " + iter.next());
413         }
414       }
415       additionalLines.clear();
416       return null;
417     }
418     //the current map contains fields - build an event
419     int exceptionLine = getExceptionLine();
420     String[] exception = buildException(exceptionLine);
421 
422     //messages are listed before exceptions in additionallines
423     if (additionalLines.size() > 0 && exceptionLine != 0) {
424       currentMap.put(MESSAGE, buildMessage((String) currentMap.get(MESSAGE),
425           exceptionLine));
426     }
427     LoggingEvent event = convertToEvent(currentMap, exception);
428     currentMap.clear();
429     additionalLines.clear();
430     return event;
431   }
432 
433   /***
434    * Read, parse and optionally tail the log file, converting entries into logging events.
435    * 
436    * A runtimeException is thrown if the logFormat pattern is malformed 
437    * according to ORO's Perl5Compiler.
438    * 
439    * @param unbufferedReader
440    * @throws IOException
441    */
442   protected void process(BufferedReader bufferedReader) throws IOException {
443 
444         Perl5Matcher eventMatcher = new Perl5Matcher();
445         String line;
446         while ((line = bufferedReader.readLine()) != null) {
447             if (eventMatcher.matches(line, regexpPattern)) {
448                 //build an event from the previous match (held in current map)
449                 LoggingEvent event = buildEvent();
450                 if (event != null) {
451                     if (passesExpression(event)) {
452                         doPost(event);
453                     }
454                 }
455                 currentMap.putAll(processEvent(eventMatcher.getMatch()));
456             } else {
457                 //getLogger().debug("line doesn't match pattern - must be ")
458                 //may be an exception or additional message lines
459                 additionalLines.add(line);
460             }
461         }
462 
463         //process last event if one exists
464         LoggingEvent event = buildEvent();
465         if (event != null) {
466             if (passesExpression(event)) {
467                 doPost(event);
468             }
469             getLogger().debug("no further lines to process in " + fileURL);
470         }
471     }
472 
473     /***
474      * create the regular expression pattern using the input regular expression
475      */
476     protected void createPattern() {
477         Perl5Compiler compiler = new Perl5Compiler();
478         try {
479             regexpPattern = compiler.compile(regexp);
480         } catch (MalformedPatternException mpe) {
481             throw new RuntimeException("Bad pattern: " + regexp);
482         }
483     }
484 
485   /***
486    * Helper method that supports the evaluation of the expression
487    * 
488    * @param event
489    * @return true if expression isn't set, or the result of the evaluation otherwise 
490    */
491   private boolean passesExpression(LoggingEvent event) {
492     if (event != null) {
493       if (expressionRule != null) {
494         return (expressionRule.evaluate(event));
495       }
496     }
497     return true;
498   }
499 
500   /***
501    * Convert the ORO match into a map.
502    * <p>
503    * Relies on the fact that the matchingKeywords list is in the same
504    * order as the groups in the regular expression
505    *  
506    * @param result
507    * @return map
508    */
509   private Map processEvent(MatchResult result) {
510     Map map = new HashMap();
511     //group zero is the entire match - process all other groups
512     for (int i = 1; i < result.groups(); i++) {
513       map.put(matchingKeywords.get(i - 1), result.group(i));
514     }
515     return map;
516   }
517   
518   /***
519    * Helper method that will convert timestamp format to a pattern
520    * 
521    * 
522    * @return string
523    */
524   private String convertTimestamp() {
525     return util.substitute("s/("+VALID_DATEFORMAT_CHAR_PATTERN+")+/////w+/g", timestampFormat);
526   }
527   
528   protected void setHost(String host) {
529 	  this.host = host;
530   }
531   
532   protected void setPath(String path) {
533 	  this.path = path;
534   }
535 
536   /***
537    * Build the regular expression needed to parse log entries
538    *  
539    */
540   protected void initialize() {
541 	if (host == null && path == null) {
542 		try {
543 			URL url = new URL(fileURL);
544 			host = url.getHost();
545 			path = url.getPath();
546 		} catch (MalformedURLException e1) {
547 			// TODO Auto-generated catch block
548 			e1.printStackTrace();
549 		}
550 	}
551 	if (host == null || host.trim().equals("")) {
552 		host = DEFAULT_HOST; 
553 	}
554 	if (path == null || path.trim().equals("")) {
555 		path = fileURL;
556 	}
557 	
558     util = new Perl5Util();
559     exceptionCompiler = new Perl5Compiler();
560     exceptionMatcher = new Perl5Matcher();
561 
562     currentMap = new HashMap();
563     additionalLines = new ArrayList();
564     matchingKeywords = new ArrayList();
565     
566     if (timestampFormat != null) {
567       dateFormat = new SimpleDateFormat(timestampFormat);
568       timestampPatternText = convertTimestamp();
569     }
570 
571     try {
572       if (filterExpression != null) {
573         expressionRule = ExpressionRule.getRule(filterExpression);
574       }
575     } catch (Exception e) {
576       getLogger().warn("Invalid filter expression: " + filterExpression, e);
577     }
578 
579     Map keywordMap = new TreeMap();
580 
581     String newPattern = logFormat;
582 
583     /*
584      * examine pattern, adding properties to an index-based map where the key is the 
585      * numeric offset from the start of the pattern so that order can be preserved
586      * 
587      * Replaces PROP(X) definitions in the pattern with the short version X, so 
588      * that the name can be used as the event property later 
589      */
590     int index = 0;
591     int currentPosition = 0;
592     String current = newPattern;
593     while (index > -1) {
594       index = current.indexOf(PROP_START);
595       currentPosition = currentPosition + index;
596       if (index > -1) {
597         String currentProp = current.substring(current.indexOf(PROP_START));
598         String prop = currentProp.substring(0,
599             currentProp.indexOf(PROP_END) + 1);
600         current = current.substring(current.indexOf(currentProp) + 1);
601         String shortProp = prop.substring(PROP_START.length(),
602             prop.length() - 1);
603         keywordMap.put(new Integer(currentPosition), shortProp);
604         newPattern = replace(prop, shortProp, newPattern);
605       }
606     }
607 
608     newPattern = replaceMetaChars(newPattern);
609 
610     //compress one or more spaces in the pattern into the [ ]+ regexp
611     //(supports padding of level in log files)
612     newPattern = util.substitute("s/" + MULTIPLE_SPACES_REGEXP +"/" + MULTIPLE_SPACES_REGEXP + "/g", newPattern);
613     newPattern = replace(PATTERN_WILDCARD, REGEXP_DEFAULT_WILDCARD, newPattern);
614 
615     /*
616      * we're using a treemap, so the index will be used as the key to ensure
617      * keywords are ordered correctly
618      * 
619      * examine pattern, adding keywords to an index-based map patterns can
620      * contain only one of these per entry...properties are the only 'keyword'
621      * that can occur multiple times in an entry
622      */
623     Iterator iter = keywords.iterator();
624     while (iter.hasNext()) {
625       String keyword = (String) iter.next();
626       int index2 = newPattern.indexOf(keyword);
627       if (index2 > -1) {
628         keywordMap.put(new Integer(index2), keyword);
629       }
630     }
631 
632     //keywordMap should be ordered by index..add all values to a list
633     matchingKeywords.addAll(keywordMap.values());
634 
635     /*
636      * iterate over the keywords found in the pattern and replace with regexp
637      * group
638      */
639     String currentPattern = newPattern;
640     for (int i = 0;i<matchingKeywords.size();i++) {
641       String keyword = (String) matchingKeywords.get(i);
642       //make the final keyword greedy
643       if (i == (matchingKeywords.size() - 1)) {
644         currentPattern = replace(keyword, GREEDY_GROUP, currentPattern);
645       } else if (TIMESTAMP.equals(keyword)) {
646         currentPattern = replace(keyword, "(" + timestampPatternText + ")", currentPattern);
647       } else {
648         currentPattern = replace(keyword, DEFAULT_GROUP, currentPattern);
649       }
650     }
651 
652     regexp = currentPattern;
653     getLogger().debug("regexp is " + regexp);
654   }
655 
656   /***
657    * Helper method that will globally replace a section of text
658    * 
659    * @param pattern
660    * @param replacement
661    * @param input 
662    * 
663    * @return string
664    */
665   private String replace(String pattern, String replacement, String input) {
666     return util.substitute("s/" + Perl5Compiler.quotemeta(pattern) + "/"
667         + Perl5Compiler.quotemeta(replacement) + "/g", input);
668   }
669 
670   /***
671    * Some perl5 characters may occur in the log file format.  
672    * Escape these characters to prevent parsing errors.
673    * 
674    * @param input
675    * @return string
676    */
677   private String replaceMetaChars(String input) {
678     input = replace("(", "//(", input);
679     input = replace(")", "//)", input);
680     input = replace("[", "//[", input);
681     input = replace("]", "//]", input);
682     input = replace("{", "//{", input);
683     input = replace("}", "//}", input);
684     input = replace("#", "//#", input);
685     input = replace("/", "///", input);
686     return input;
687   }
688 
689   /***
690    * Convert a keyword-to-values map to a LoggingEvent
691    * 
692    * @param fieldMap
693    * @param exception
694    * 
695    * @return logging event
696    */
697   private LoggingEvent convertToEvent(Map fieldMap, String[] exception) {
698     if (fieldMap == null) {
699       return null;
700     }
701 
702     //a logger must exist at a minimum for the event to be processed
703     if (!fieldMap.containsKey(LOGGER)) {
704       fieldMap.put(LOGGER, "Unknown");
705     }
706     if (exception == null) {
707       exception = emptyException;
708     }
709 
710     Logger logger = null;
711     long timeStamp = 0L;
712     String level = null;
713     String threadName = null;
714     Object message = null;
715     String ndc = null;
716     String className = null;
717     String methodName = null;
718     String eventFileName = null;
719     String lineNumber = null;
720     Hashtable properties = new Hashtable();
721 
722     logger = Logger.getLogger((String) fieldMap.remove(LOGGER));
723 
724     if ((dateFormat != null) && fieldMap.containsKey(TIMESTAMP)) {
725       try {
726         timeStamp = dateFormat.parse((String) fieldMap.remove(TIMESTAMP))
727             .getTime();
728       } catch (Exception e) {
729         e.printStackTrace();
730       }
731     }
732     //use current time if timestamp not parseable
733     if (timeStamp == 0L) {
734       timeStamp = System.currentTimeMillis();
735     }
736 
737     level = (String) fieldMap.remove(LEVEL);
738     Level levelImpl = (level == null ? Level.DEBUG : Level.toLevel(level.trim()));
739 
740     threadName = (String) fieldMap.remove(THREAD);
741 
742     message = fieldMap.remove(MESSAGE);
743     if (message == null) {
744       message = "";
745     }
746 
747     ndc = (String) fieldMap.remove(NDC);
748 
749     className = (String) fieldMap.remove(CLASS);
750 
751     methodName = (String) fieldMap.remove(METHOD);
752 
753     eventFileName = (String) fieldMap.remove(FILE);
754 
755     lineNumber = (String) fieldMap.remove(LINE);
756 
757     properties.put(Constants.HOSTNAME_KEY, host);
758     properties.put(Constants.APPLICATION_KEY, path);
759     properties.put(Constants.RECEIVER_NAME_KEY, getName());
760 
761     //all remaining entries in fieldmap are properties
762     properties.putAll(fieldMap);
763 
764     LocationInfo info = null;
765 
766     if ((eventFileName != null) || (className != null) || (methodName != null)
767         || (lineNumber != null)) {
768       info = new LocationInfo(eventFileName, className, methodName, lineNumber);
769     } else {
770       info = LocationInfo.NA_LOCATION_INFO;
771     }
772 
773     LoggingEvent event = new LoggingEvent(null,
774             logger, timeStamp, levelImpl, message,
775             threadName,
776             new ThrowableInformation(exception),
777             ndc,
778             info,
779             properties);
780 
781     return event;
782   }
783 
784   public static void main(String[] args) {
785     /*
786     LogFilePatternReceiver test = new LogFilePatternReceiver();
787     test.setLogFormat("TIMESTAMP LEVEL [THREAD] LOGGER (FILE:LINE) - MESSAGE");
788     test.setTailing(true);
789     test.setFileURL("file:///C:/log/test.log");
790     test.initialize();
791     try {
792       test.process(new InputStreamReader(new URL(test.getFileURL())
793           .openStream()));
794     } catch (IOException ioe) {
795       ioe.printStackTrace();
796     }
797     */
798   }
799 
800   /***
801    * Close the reader. 
802    */
803   public void shutdown() {
804     try {
805       if (reader != null) {
806         reader.close();
807         reader = null;
808       }
809     } catch (IOException ioe) {
810       ioe.printStackTrace();
811     }
812   }
813 
814   /***
815    * Read and process the log file.
816    */
817   public void activateOptions() {
818 	Runnable runnable = new Runnable() {
819 	             public void run() {
820                 initialize();
821                 while (reader == null) {
822                     getLogger().info("attempting to load file: " + getFileURL());
823                     try {
824                         reader = new InputStreamReader(new URL(getFileURL()).openStream());
825                     } catch (FileNotFoundException fnfe) {
826                         getLogger().info("file not available - will try again in 10 seconds");
827                         synchronized (this) {
828                             try {
829                                 wait(10000);
830                             } catch (InterruptedException ie) {}
831                         }
832                     } catch (IOException ioe) {
833                         getLogger().warn("unable to load file", ioe);
834                         return;
835                     }
836                 }
837                 try {
838                     BufferedReader bufferedReader = new BufferedReader(reader);
839                     createPattern();
840                     do {
841                         getLogger().debug("tailing file: " + tailing);
842                         process(bufferedReader);
843                         try {
844                             synchronized (this) {
845                                 wait(2000);
846                             }
847                         } catch (InterruptedException ie) {
848                         }
849                     } while (tailing);
850 
851                 } catch (IOException ioe) {
852                     //io exception - probably shut down
853                     getLogger().info("stream closed");
854                 }
855                 getLogger().debug("processing " + fileURL + " complete");
856                 shutdown();
857             }
858         };
859         if(useCurrentThread) {
860             runnable.run();
861         }else {
862             new Thread(runnable, "LogFilePatternReceiver-"+getName()).start();
863         }
864     }
865 }