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