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.examples;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.nio.charset.*;
023import java.text.*;
024import java.util.*;
025import java.util.regex.*;
026
027import org.apache.juneau.microservice.logging.*;
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 */
036@SuppressWarnings({
037   "java:S3776", // Cognitive complexity acceptable for log line parsing state machine
038   "resource" // br is closed via this class's close() method
039})
040public class LogParser implements Iterable<LogParser.Entry>, Closeable {
041
042   /**
043    * Represents a single line from the log file.
044    */
045   @SuppressWarnings({
046      "java:S1104" // Public fields accessed externally (e.g., le.severity)
047   })
048   public class Entry {
049      /** Log entry timestamp. */
050      public Date date;
051      /** Log level (SEVERE, WARNING, INFO, etc.). */
052      public String severity;
053      /** Logger name (simple class name). */
054      public String logger;
055      protected String line;
056      protected String text;
057      protected String thread;
058      protected List<String> additionalText;
059      protected boolean isRecord;
060
061      /**
062       * Constructor.
063       *
064       * @param line The raw log line.
065       * @throws IOException If parsing fails.
066       */
067      Entry(String line) throws IOException {
068         try {
069            this.line = line;
070            Matcher m = formatter.getLogEntryPattern().matcher(line);
071            if (m.matches()) {
072               isRecord = true;
073               String s = formatter.getField("date", m);
074               if (nn(s))
075                  date = formatter.getDateFormat().parse(s);
076               thread = formatter.getField("thread", m);
077               severity = formatter.getField("level", m);
078               logger = formatter.getField("logger", m);
079               text = formatter.getField("msg", m);
080               if (nn(logger) && logger.indexOf('.') > -1)
081                  logger = logger.substring(logger.lastIndexOf('.') + 1);
082            }
083         } catch (ParseException e) {
084            throw ioex(e);
085         }
086      }
087
088      /**
089       * Appends HTML-escaped log entry to the writer.
090       *
091       * @param w The writer.
092       * @return The writer for chaining.
093       * @throws IOException If writing fails.
094       */
095      public Writer appendHtml(Writer w) throws IOException {
096         w.append(toHtml(line)).append("<br>");
097         if (nn(additionalText))
098            for (var t : additionalText)
099               w.append(toHtml(t)).append("<br>");
100         return w;
101      }
102
103      /**
104       * Returns the full log message text, including additional stacked lines.
105       *
106       * @return The message text.
107       */
108      public String getText() {
109         if (additionalText == null)
110            return text;
111         int i = text.length();
112         for (var s : additionalText)
113            i += s.length() + 1;
114         var sb = new StringBuilder(i);
115         sb.append(text);
116         for (var s : additionalText)
117            sb.append('\n').append(s);
118         return sb.toString();
119      }
120
121      /**
122       * Returns the thread name.
123       *
124       * @return The thread name.
125       */
126      public String getThread() { return thread; }
127
128      protected Writer append(Writer w) throws IOException {
129         w.append(line).append('\n');
130         if (nn(additionalText))
131            for (var t : additionalText)
132               w.append(t).append('\n');
133         return w;
134      }
135
136      void addText(String t) {
137         if (additionalText == null)
138            additionalText = ll();
139         additionalText.add(t);
140      }
141
142      boolean matches() {
143         return isRecord
144            && !(nn(start) && nn(date) && date.before(start))
145            && !(nn(end) && nn(date) && date.after(end))
146            && !(nn(threadFilter) && neq(threadFilter, thread))
147            && !(nn(loggerFilter) && ! loggerFilter.contains(logger))
148            && !(nn(severityFilter) && ! severityFilter.contains(severity));
149      }
150   }
151
152   static String toHtml(String s) {
153      if (s.indexOf('<') != -1)
154         return s.replace("<", "&lt;");//$NON-NLS-2$
155      return s;
156   }
157
158   private BufferedReader br;
159   LogEntryFormatter formatter;
160   Date start;
161   Date end;
162   Set<String> loggerFilter;
163   Set<String> severityFilter;
164
165   String threadFilter;
166
167   private List<Entry> entries;
168
169   /**
170    * Constructor.
171    *
172    * @param formatter The log entry formatter.
173    * @param f The log file.
174    * @param start Don't return rows before this date.  If <jk>null</jk>, start from the beginning of the file.
175    * @param end Don't return rows after this date.  If <jk>null</jk>, go to the end of the file.
176    * @param thread Only return log entries with this thread name.
177    * @param loggers Only return log entries produced by these loggers (simple class names).
178    * @param severity Only return log entries with the specified severity.
179    * @throws IOException Thrown by underlying stream.
180    */
181   public LogParser(LogEntryFormatter formatter, File f, Date start, Date end, String thread, String[] loggers, String[] severity) throws IOException {
182      this.formatter = formatter;
183      this.start = start;
184      this.end = end;
185      this.threadFilter = thread;
186      if (nn(loggers))
187         this.loggerFilter = st(loggers);
188      if (nn(severity))
189         this.severityFilter = st(severity);
190
191      // Buffer all matching entries to support multiple traversal.
192      // Close the reader in a finally so a parse failure mid-loop doesn't leak the file descriptor.
193      br = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
194      try {
195         List<Entry> allEntries = l();
196         Entry prev = null;
197         String line;
198         while (nn(line = br.readLine())) {
199            var e = new Entry(line);
200            if (e.isRecord) {
201               if (nn(prev))
202                  allEntries.add(prev);
203               prev = e.matches() ? e : null;
204            } else {
205               if (prev != null)
206                  prev.addText(e.line);
207            }
208         }
209         if (nn(prev))
210            allEntries.add(prev);
211         this.entries = allEntries;
212      } finally {
213         br.close();
214         br = null;
215      }
216   }
217
218   @Override /* Overridden from Closeable */
219   public void close() throws IOException {
220      if (nn(br)) {
221         br.close();
222         br = null;
223      }
224   }
225
226   /**
227    * Returns whether any entries are available.
228    *
229    * @return <jk>true</jk> if entries are available, <jk>false</jk> otherwise.
230    */
231   public boolean hasNext() {
232      return !entries.isEmpty();
233   }
234
235   @Override /* Overridden from Iterable */
236   public Iterator<Entry> iterator() {
237      return entries.iterator();
238   }
239
240   /**
241    * Serializes the contents of the parsed log file to the specified writer and then closes the underlying reader.
242    *
243    * @param w The writer to write the log file to.
244    * @throws IOException Thrown by underlying stream.
245    */
246   public void writeTo(Writer w) throws IOException {
247      try {
248         if (! hasNext())
249            w.append("[EMPTY]");
250         else
251            for (var le : this)
252               le.append(w);
253      } finally {
254         close();
255      }
256   }
257}