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