001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.microservice.resources;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022
023import java.io.*;
024import java.nio.charset.*;
025import java.text.*;
026import java.util.*;
027import java.util.regex.*;
028
029/**
030 * Utility class for reading log files.
031 *
032 * <p>
033 * Provides the capability of returning splices of log files based on dates and filtering based on thread and logger
034 * names.
035 */
036public class LogParser implements Iterable<LogParser.Entry>, Iterator<LogParser.Entry>, Closeable {
037   /**
038    * Represents a single line from the log file.
039    */
040   @SuppressWarnings("javadoc")
041   public class Entry {
042      public Date date;
043      public String severity, logger;
044      protected String line, text;
045      protected String thread;
046      protected List<String> additionalText;
047      protected boolean isRecord;
048
049      Entry(String line) throws IOException {
050         try {
051            this.line = line;
052            Matcher m = formatter.getLogEntryPattern().matcher(line);
053            if (m.matches()) {
054               isRecord = true;
055               String s = formatter.getField("date", m);
056               if (nn(s))
057                  date = formatter.getDateFormat().parse(s);
058               thread = formatter.getField("thread", m);
059               severity = formatter.getField("level", m);
060               logger = formatter.getField("logger", m);
061               text = formatter.getField("msg", m);
062               if (nn(logger) && logger.indexOf('.') > -1)
063                  logger = logger.substring(logger.lastIndexOf('.') + 1);
064            }
065         } catch (ParseException e) {
066            throw ioex(e);
067         }
068      }
069
070      public Writer appendHtml(Writer w) throws IOException {
071         w.append(toHtml(line)).append("<br>");
072         if (nn(additionalText))
073            for (var t : additionalText)
074               w.append(toHtml(t)).append("<br>");
075         return w;
076      }
077
078      public String getText() {
079         if (additionalText == null)
080            return text;
081         int i = text.length();
082         for (var s : additionalText)
083            i += s.length() + 1;
084         var sb = new StringBuilder(i);
085         sb.append(text);
086         for (var s : additionalText)
087            sb.append('\n').append(s);
088         return sb.toString();
089      }
090
091      public String getThread() { return thread; }
092
093      protected Writer append(Writer w) throws IOException {
094         w.append(line).append('\n');
095         if (nn(additionalText))
096            for (var t : additionalText)
097               w.append(t).append('\n');
098         return w;
099      }
100
101      void addText(String t) {
102         if (additionalText == null)
103            additionalText = new LinkedList<>();
104         additionalText.add(t);
105      }
106
107      boolean matches() {
108         if (! isRecord)
109            return false;
110         if (nn(start) && date.before(start))
111            return false;
112         if (nn(end) && date.after(end))
113            return false;
114         if (nn(threadFilter) && ! threadFilter.equals(thread))
115            return false;
116         if (nn(loggerFilter) && ! loggerFilter.contains(logger))
117            return false;
118         if (nn(severityFilter) && ! severityFilter.contains(severity))
119            return false;
120         return true;
121      }
122   }
123
124   static String toHtml(String s) {
125      if (s.indexOf('<') != -1)
126         return s.replaceAll("<", "&lt;");//$NON-NLS-2$
127      return s;
128   }
129
130   private BufferedReader br;
131   LogEntryFormatter formatter;
132   Date start, end;
133   Set<String> loggerFilter, severityFilter;
134
135   String threadFilter;
136
137   private Entry next;
138
139   /**
140    * Constructor.
141    *
142    * @param formatter The log entry formatter.
143    * @param f The log file.
144    * @param start Don't return rows before this date.  If <jk>null</jk>, start from the beginning of the file.
145    * @param end Don't return rows after this date.  If <jk>null</jk>, go to the end of the file.
146    * @param thread Only return log entries with this thread name.
147    * @param loggers Only return log entries produced by these loggers (simple class names).
148    * @param severity Only return log entries with the specified severity.
149    * @throws IOException Thrown by underlying stream.
150    */
151   public LogParser(LogEntryFormatter formatter, File f, Date start, Date end, String thread, String[] loggers, String[] severity) throws IOException {
152      br = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
153      this.formatter = formatter;
154      this.start = start;
155      this.end = end;
156      this.threadFilter = thread;
157      if (nn(loggers))
158         this.loggerFilter = new LinkedHashSet<>(l(loggers));
159      if (nn(severity))
160         this.severityFilter = new LinkedHashSet<>(l(severity));
161
162      // Find the first line.
163      String line;
164      while (next == null && nn(line = br.readLine())) {
165         var e = new Entry(line);
166         if (e.matches())
167            next = e;
168      }
169   }
170
171   @Override /* Overridden from Closeable */
172   public void close() throws IOException {
173      br.close();
174   }
175
176   @Override /* Overridden from Iterator */
177   public boolean hasNext() {
178      return nn(next);
179   }
180
181   @Override /* Overridden from Iterable */
182   public Iterator<Entry> iterator() {
183      return this;
184   }
185
186   @SuppressWarnings("null")
187   @Override /* Overridden from Iterator */
188   public Entry next() {
189      Entry current = next;
190      Entry prev = next;
191      try {
192         next = null;
193         var line = (String)null;
194         while (next == null && nn(line = br.readLine())) {
195            var e = new Entry(line);
196            if (e.isRecord) {
197               if (e.matches())
198                  next = e;
199               prev = null;
200            } else {
201               if (nn(prev))
202                  prev.addText(e.line);
203            }
204         }
205      } catch (IOException e) {
206         throw new UncheckedIOException(e);
207      }
208      return current;
209   }
210
211   @Override /* Overridden from Iterator */
212   public void remove() {
213      throw new NoSuchMethodError();
214   }
215
216   /**
217    * Serializes the contents of the parsed log file to the specified writer and then closes the underlying reader.
218    *
219    * @param w The writer to write the log file to.
220    * @throws IOException Thrown by underlying stream.
221    */
222   @SuppressWarnings("resource")
223   public void writeTo(Writer w) throws IOException {
224      try {
225         if (! hasNext())
226            w.append("[EMPTY]");
227         else
228            for (var le : this)
229               le.append(w);
230      } finally {
231         close();
232      }
233   }
234}