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("<", "<");//$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}