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}