blob: eb4869e3746188d6348305c6b3ad067782328927 [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.microservice.resources;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
import java.util.logging.Formatter;
import java.util.regex.*;
/**
* Log entry formatter.
*
* <p>
* Uses three simple parameter for configuring log entry formats:
* <ul class='spaced-list'>
* <li>
* <code>dateFormat</code> - A {@link SimpleDateFormat} string describing the format for dates.
* <li>
* <code>format</code> - A string with <code>{...}</code> replacement variables representing predefined fields.
* <li>
* <code>useStackTraceHashes</code> - A setting that causes duplicate stack traces to be replaced with 8-character
* hash strings.
* </ul>
*
* <p>
* This class converts the format strings into a regular expression that can be used to parse the resulting log file.
*/
public class LogEntryFormatter extends Formatter {
private ConcurrentHashMap<String,AtomicInteger> hashes;
private DateFormat df;
private String format;
private Pattern rePattern;
private Map<String,Integer> fieldIndexes;
/**
* Create a new formatter.
*
* @param format
* The log entry format. e.g. <js>"[{date} {level}] {msg}%n"</js>
* The string can contain any of the following variables:
* <ol>
* <li><js>"{date}"</js> - The date, formatted per <js>"Logging/dateFormat"</js>.
* <li><js>"{class}"</js> - The class name.
* <li><js>"{method}"</js> - The method name.
* <li><js>"{logger}"</js> - The logger name.
* <li><js>"{level}"</js> - The log level name.
* <li><js>"{msg}"</js> - The log message.
* <li><js>"{threadid}"</js> - The thread ID.
* <li><js>"{exception}"</js> - The localized exception message.
* </ol>
* @param dateFormat
* The {@link SimpleDateFormat} format to use for dates. e.g. <js>"yyyy.MM.dd hh:mm:ss"</js>.
* @param useStackTraceHashes
* If <jk>true</jk>, only print unique stack traces once and then refer to them by a simple 8 character hash
* identifier.
*/
public LogEntryFormatter(String format, String dateFormat, boolean useStackTraceHashes) {
this.df = new SimpleDateFormat(dateFormat);
if (useStackTraceHashes)
hashes = new ConcurrentHashMap<>();
fieldIndexes = new HashMap<>();
format = format
.replaceAll("\\{date\\}", "%1\\$s")
.replaceAll("\\{class\\}", "%2\\$s")
.replaceAll("\\{method\\}", "%3\\$s")
.replaceAll("\\{logger\\}", "%4\\$s")
.replaceAll("\\{level\\}", "%5\\$s")
.replaceAll("\\{msg\\}", "%6\\$s")
.replaceAll("\\{threadid\\}", "%7\\$s")
.replaceAll("\\{exception\\}", "%8\\$s");
this.format = format;
// Construct a regular expression to match this log entry.
int index = 1;
StringBuilder re = new StringBuilder();
int S1 = 1; // Looking for %
int S2 = 2; // Found %, looking for number.
int S3 = 3; // Found number, looking for $.
int S4 = 4; // Found $, looking for s.
int state = 1;
int i1 = 0;
for (int i = 0; i < format.length(); i++) {
char c = format.charAt(i);
if (state == S1) {
if (c == '%')
state = S2;
else {
if (! (Character.isLetterOrDigit(c) || Character.isWhitespace(c)))
re.append('\\');
re.append(c);
}
} else if (state == S2) {
if (Character.isDigit(c)) {
i1 = i;
state = S3;
} else {
re.append("\\%").append(c);
state = S1;
}
} else if (state == S3) {
if (c == '$') {
state = S4;
} else {
re.append("\\%").append(format.substring(i1, i));
state = S1;
}
} else if (state == S4) {
if (c == 's') {
int group = Integer.parseInt(format.substring(i1, i-1));
switch (group) {
case 1:
fieldIndexes.put("date", index++);
re.append("(" + dateFormat.replaceAll("[mHhsSdMy]", "\\\\d").replaceAll("\\.", "\\\\.") + ")");
break;
case 2:
fieldIndexes.put("class", index++);
re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
break;
case 3:
fieldIndexes.put("method", index++);
re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
break;
case 4:
fieldIndexes.put("logger", index++);
re.append("([\\w\\d\\.\\_]+)");
break;
case 5:
fieldIndexes.put("level", index++);
re.append("(SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST)");
break;
case 6:
fieldIndexes.put("msg", index++);
re.append("(.*)");
break;
case 7:
fieldIndexes.put("threadid", index++);
re.append("(\\\\d+)");
break;
case 8:
fieldIndexes.put("exception", index++);
re.append("(.*)");
break;
default: // Fall through.
}
} else {
re.append("\\%").append(format.substring(i1, i));
}
state = S1;
}
}
// The log parser
String sre = re.toString();
if (sre.endsWith("\\%n"))
sre = sre.substring(0, sre.length()-3);
// Replace instances of %n.
sre = sre.replaceAll("\\\\%n", "\\\\n");
rePattern = Pattern.compile(sre);
fieldIndexes = unmodifiableMap(fieldIndexes);
}
/**
* Returns the regular expression pattern used for matching log entries.
*
* @return The regular expression pattern used for matching log entries.
*/
public Pattern getLogEntryPattern() {
return rePattern;
}
/**
* Returns the {@link DateFormat} used for matching dates.
*
* @return The {@link DateFormat} used for matching dates.
*/
public DateFormat getDateFormat() {
return df;
}
/**
* Given a matcher that has matched the pattern specified by {@link #getLogEntryPattern()}, returns the field value
* from the match.
*
* @param fieldName
* The field name.
* Possible values are:
* <ul>
* <li><js>"date"</js>
* <li><js>"class"</js>
* <li><js>"method"</js>
* <li><js>"logger"</js>
* <li><js>"level"</js>
* <li><js>"msg"</js>
* <li><js>"threadid"</js>
* <li><js>"exception"</js>
* </ul>
* @param m The matcher.
* @return The field value, or <jk>null</jk> if the specified field does not exist.
*/
public String getField(String fieldName, Matcher m) {
Integer i = fieldIndexes.get(fieldName);
return (i == null ? null : m.group(i));
}
@Override /* Formatter */
public String format(LogRecord r) {
String msg = formatMessage(r);
Throwable t = r.getThrown();
String hash = null;
int c = 0;
if (hashes != null && t != null) {
hash = hashCode(t);
hashes.putIfAbsent(hash, new AtomicInteger(0));
c = hashes.get(hash).incrementAndGet();
if (c == 1) {
msg = '[' + hash + '.' + c + "] " + msg;
} else {
msg = '[' + hash + '.' + c + "] " + msg + ", " + t.getLocalizedMessage();
t = null;
}
}
String s = String.format(format,
df.format(new Date(r.getMillis())),
r.getSourceClassName(),
r.getSourceMethodName(),
r.getLoggerName(),
r.getLevel(),
msg,
r.getThreadID(),
r.getThrown() == null ? "" : r.getThrown().getMessage());
if (t != null)
s += String.format("%n%s", getStackTrace(r.getThrown()));
return s;
}
private static String hashCode(Throwable t) {
int i = 0;
while (t != null) {
for (StackTraceElement e : t.getStackTrace())
i ^= e.hashCode();
t = t.getCause();
}
return Integer.toHexString(i);
}
}