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}