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