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}