001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.microservice.resources;
014
015import static org.apache.juneau.internal.CollectionUtils.*;
016
017import java.text.*;
018import java.util.*;
019import java.util.concurrent.*;
020import java.util.concurrent.atomic.*;
021import java.util.logging.*;
022import java.util.logging.Formatter;
023import java.util.regex.*;
024
025import org.apache.juneau.common.internal.*;
026
027/**
028 * Log entry formatter.
029 *
030 * <p>
031 * Uses three simple parameter for configuring log entry formats:
032 * <ul class='spaced-list'>
033 *    <li>
034 *       <c>dateFormat</c> - A {@link SimpleDateFormat} string describing the format for dates.
035 *    <li>
036 *       <c>format</c> - A string with <c>{...}</c> replacement variables representing predefined fields.
037 *    <li>
038 *       <c>useStackTraceHashes</c> - A setting that causes duplicate stack traces to be replaced with 8-character
039 *        hash strings.
040 * </ul>
041 *
042 * <p>
043 * This class converts the format strings into a regular expression that can be used to parse the resulting log file.
044 */
045public class LogEntryFormatter extends Formatter {
046
047   private ConcurrentHashMap<String,AtomicInteger> hashes;
048   private DateFormat df;
049   private String format;
050   private Pattern rePattern;
051   private Map<String,Integer> fieldIndexes;
052
053   /**
054    * Create a new formatter.
055    *
056    * @param format
057    *    The log entry format.  e.g. <js>"[{date} {level}] {msg}%n"</js>
058    *    The string can contain any of the following variables:
059    *    <ol>
060    *       <li><js>"{date}"</js> - The date, formatted per <js>"Logging/dateFormat"</js>.
061    *       <li><js>"{class}"</js> - The class name.
062    *       <li><js>"{method}"</js> - The method name.
063    *       <li><js>"{logger}"</js> - The logger name.
064    *       <li><js>"{level}"</js> - The log level name.
065    *       <li><js>"{msg}"</js> - The log message.
066    *       <li><js>"{threadid}"</js> - The thread ID.
067    *       <li><js>"{exception}"</js> - The localized exception message.
068    *    </ol>
069    * @param dateFormat
070    *    The {@link SimpleDateFormat} format to use for dates.  e.g. <js>"yyyy.MM.dd hh:mm:ss"</js>.
071    * @param useStackTraceHashes
072    *    If <jk>true</jk>, only print unique stack traces once and then refer to them by a simple 8 character hash
073    *    identifier.
074    */
075   public LogEntryFormatter(String format, String dateFormat, boolean useStackTraceHashes) {
076      this.df = new SimpleDateFormat(dateFormat);
077      if (useStackTraceHashes)
078         hashes = new ConcurrentHashMap<>();
079
080      fieldIndexes = new HashMap<>();
081
082      format = format
083         .replaceAll("\\{date\\}", "%1\\$s")
084         .replaceAll("\\{class\\}", "%2\\$s")
085         .replaceAll("\\{method\\}", "%3\\$s")
086         .replaceAll("\\{logger\\}", "%4\\$s")
087         .replaceAll("\\{level\\}", "%5\\$s")
088         .replaceAll("\\{msg\\}", "%6\\$s")
089         .replaceAll("\\{threadid\\}", "%7\\$s")
090         .replaceAll("\\{exception\\}", "%8\\$s");
091
092      this.format = format;
093
094      // Construct a regular expression to match this log entry.
095      int index = 1;
096      StringBuilder re = new StringBuilder();
097      int S1 = 1; // Looking for %
098      int S2 = 2; // Found %, looking for number.
099      int S3 = 3; // Found number, looking for $.
100      int S4 = 4; // Found $, looking for s.
101      int state = 1;
102      int i1 = 0;
103      for (int i = 0; i < format.length(); i++) {
104         char c = format.charAt(i);
105         if (state == S1) {
106            if (c == '%')
107               state = S2;
108            else {
109               if (! (Character.isLetterOrDigit(c) || Character.isWhitespace(c)))
110                  re.append('\\');
111               re.append(c);
112            }
113         } else if (state == S2) {
114            if (Character.isDigit(c)) {
115               i1 = i;
116               state = S3;
117            } else {
118               re.append("\\%").append(c);
119               state = S1;
120            }
121         } else if (state == S3) {
122            if (c == '$') {
123               state = S4;
124            } else {
125               re.append("\\%").append(format.substring(i1, i));
126               state = S1;
127            }
128         } else if (state == S4) {
129            if (c == 's') {
130               int group = Integer.parseInt(format.substring(i1, i-1));
131               switch (group) {
132                  case 1:
133                     fieldIndexes.put("date", index++);
134                     re.append("(" + dateFormat.replaceAll("[mHhsSdMy]", "\\\\d").replaceAll("\\.", "\\\\.") + ")");
135                     break;
136                  case 2:
137                     fieldIndexes.put("class", index++);
138                     re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
139                     break;
140                  case 3:
141                     fieldIndexes.put("method", index++);
142                     re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
143                     break;
144                  case 4:
145                     fieldIndexes.put("logger", index++);
146                     re.append("([\\w\\d\\.\\_]+)");
147                     break;
148                  case 5:
149                     fieldIndexes.put("level", index++);
150                     re.append("(SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST)");
151                     break;
152                  case 6:
153                     fieldIndexes.put("msg", index++);
154                     re.append("(.*)");
155                     break;
156                  case 7:
157                     fieldIndexes.put("threadid", index++);
158                     re.append("(\\\\d+)");
159                     break;
160                  case 8:
161                     fieldIndexes.put("exception", index++);
162                     re.append("(.*)");
163                     break;
164                  default: // Fall through.
165               }
166            } else {
167               re.append("\\%").append(format.substring(i1, i));
168            }
169            state = S1;
170         }
171      }
172
173      // The log parser
174      String sre = re.toString();
175      if (sre.endsWith("\\%n"))
176         sre = sre.substring(0, sre.length()-3);
177
178      // Replace instances of %n.
179      sre = sre.replaceAll("\\\\%n", "\\\\n");
180
181      rePattern = Pattern.compile(sre);
182      fieldIndexes = mapFrom(fieldIndexes);
183   }
184
185   /**
186    * Returns the regular expression pattern used for matching log entries.
187    *
188    * @return The regular expression pattern used for matching log entries.
189    */
190   public Pattern getLogEntryPattern() {
191      return rePattern;
192   }
193
194   /**
195    * Returns the {@link DateFormat} used for matching dates.
196    *
197    * @return The {@link DateFormat} used for matching dates.
198    */
199   public DateFormat getDateFormat() {
200      return df;
201   }
202
203   /**
204    * Given a matcher that has matched the pattern specified by {@link #getLogEntryPattern()}, returns the field value
205    * from the match.
206    *
207    * @param fieldName
208    *    The field name.
209    *    Possible values are:
210    *    <ul>
211    *       <li><js>"date"</js>
212    *       <li><js>"class"</js>
213    *       <li><js>"method"</js>
214    *       <li><js>"logger"</js>
215    *       <li><js>"level"</js>
216    *       <li><js>"msg"</js>
217    *       <li><js>"threadid"</js>
218    *       <li><js>"exception"</js>
219    *    </ul>
220    * @param m The matcher.
221    * @return The field value, or <jk>null</jk> if the specified field does not exist.
222    */
223   public String getField(String fieldName, Matcher m) {
224      Integer i = fieldIndexes.get(fieldName);
225      return (i == null ? null : m.group(i));
226   }
227
228   @Override /* Formatter */
229   public String format(LogRecord r) {
230      String msg = formatMessage(r);
231      Throwable t = r.getThrown();
232      String hash = null;
233      int c = 0;
234      if (hashes != null && t != null) {
235         hash = hashCode(t);
236         hashes.putIfAbsent(hash, new AtomicInteger(0));
237         c = hashes.get(hash).incrementAndGet();
238         if (c == 1) {
239            msg = '[' + hash + '.' + c + "] " + msg;
240         } else {
241            msg = '[' + hash + '.' + c + "] " + msg + ", " + t.getLocalizedMessage();
242            t = null;
243         }
244      }
245      String s = String.format(format,
246         df.format(new Date(r.getMillis())),
247         r.getSourceClassName(),
248         r.getSourceMethodName(),
249         r.getLoggerName(),
250         r.getLevel(),
251         msg,
252         r.getThreadID(),
253         r.getThrown() == null ? "" : r.getThrown().getMessage());
254      if (t != null)
255         s += String.format("%n%s", ThrowableUtils.getStackTrace(r.getThrown()));
256      return s;
257   }
258
259   private static String hashCode(Throwable t) {
260      int i = 0;
261      while (t != null) {
262         for (StackTraceElement e : t.getStackTrace())
263            i ^= e.hashCode();
264         t = t.getCause();
265      }
266      return Integer.toHexString(i);
267   }
268}