001package com.identityworksllc.iiq.common.logging; 002 003import org.apache.logging.log4j.core.LogEvent; 004import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy; 005import org.apache.logging.log4j.core.config.plugins.Plugin; 006import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 007import org.apache.logging.log4j.core.config.plugins.PluginFactory; 008import org.apache.logging.log4j.core.impl.Log4jLogEvent; 009import org.apache.logging.log4j.message.Message; 010 011import java.util.Comparator; 012import java.util.LinkedList; 013import java.util.Queue; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016 017/** 018 * A log rewrite policy to extract bits out of an extremely long log message. 019 * In IIQ, this might be web services output, a very large configuration 020 * object, or other things. 021 * 022 * The goal is to mimic the 'grep' command with its -A and -B arguments. When 023 * a part of the string matches the regex or the given static substring, 024 * a certain number of characters before and after the matching segment will 025 * be included in the log message. The remaining message will be dropped. 026 * 027 * Messages that don't match the regex or substring won't be included in the 028 * log output at all. 029 * 030 * TODO finish this 031 */ 032@Plugin(name = "SlicingRewritePolicy", category = "Core", elementType = "rewritePolicy", printObject = true) 033public class SlicingRewritePolicy implements RewritePolicy { 034 /** 035 * The indexes of the slices of the message, which will be used to reconstruct 036 * the string at the end of {@link #rewrite(LogEvent)}. Subsequent slices that 037 * overlap will be merged into a single slice. 038 */ 039 private static class StringSlice { 040 /** 041 * A comparator for sorting string slices by start index 042 * @return The comparator 043 */ 044 public static Comparator<StringSlice> comparator() { 045 return Comparator.comparingInt(StringSlice::getStart); 046 } 047 048 /** 049 * The end of the string segment 050 */ 051 private final int end; 052 053 /** 054 * The length of the string being sliced. The 'end' is constrained 055 * to be less than this value. 056 */ 057 private final int maxLength; 058 059 /** 060 * The start of the string segment 061 */ 062 private final int start; 063 064 public StringSlice(int start, int end, int maxLength) { 065 if (start < 0) { 066 start = 0; 067 } else if (end >= maxLength) { 068 end = maxLength - 1; 069 } 070 071 this.start = start; 072 this.end = end; 073 this.maxLength = maxLength; 074 } 075 076 /** 077 * Gets the start of the string slice 078 * @return The start index of the string slice 079 */ 080 public int getStart() { 081 return start; 082 } 083 084 /** 085 * Returns a new StringSlice that is the union of the two slices. The lowest 086 * start and the highest end will be used. 087 * 088 * If the two slices do not overlap, a bunch of stuff between the end of the 089 * earlier slice and the start of the later slice will be included. 090 * 091 * @param other The other slice to merge with 092 * @return A new merged slice 093 */ 094 public StringSlice mergeWith(StringSlice other) { 095 return new StringSlice(Math.min(this.start, other.start), Math.max(this.end, other.end), maxLength); 096 } 097 098 /** 099 * Returns true if this slice overlaps the other given slice. 100 * @param other The other slice to check 101 * @return True if the two slices overlap and can be merged 102 */ 103 public boolean overlaps(StringSlice other) { 104 return (this.start <= other.end) && (this.end >= other.start); 105 } 106 } 107 108 public static class SlicingRewriteContextConfig { 109 private final int contextChars; 110 private final int endChars; 111 private final int startChars; 112 113 public SlicingRewriteContextConfig(int startChars, int endChars, int contextChars) { 114 this.startChars = startChars; 115 this.endChars = endChars; 116 this.contextChars = contextChars; 117 } 118 } 119 120 /** 121 * The log4j2 factory method for creating one of these log event processors 122 * @param regex The regex to use, if any 123 * @param substring The substring to use, if any 124 * @param startChars The number of characters to log from the start of the string 125 * @param endChars The number of characters to log from the end of the string 126 * @param contextChars The number of characters to print on either side of a regex or substring match 127 * @return An instance of this rewrite policy 128 */ 129 @PluginFactory 130 public static SlicingRewritePolicy createSlicingPolicy( 131 @PluginAttribute("regex") String regex, 132 @PluginAttribute("substring") String substring, 133 @PluginAttribute(value = "startChars", defaultInt = 50) int startChars, 134 @PluginAttribute(value = "endChars", defaultInt = 50) int endChars, 135 @PluginAttribute(value = "contextChars", defaultInt = 100) int contextChars) { 136 137 SlicingRewriteContextConfig contextConfig = new SlicingRewriteContextConfig(startChars, endChars, contextChars); 138 return new SlicingRewritePolicy(substring, regex, contextConfig); 139 } 140 141 private final SlicingRewriteContextConfig contextConfig; 142 private final Pattern regexPattern; 143 private final String substring; 144 145 public SlicingRewritePolicy(String substring, String regex, SlicingRewriteContextConfig contextConfig) { 146 this.substring = substring; 147 148 if (regex != null && regex.length() > 0) { 149 this.regexPattern = Pattern.compile(regex); 150 } else { 151 this.regexPattern = null; 152 } 153 154 this.contextConfig = contextConfig; 155 } 156 157 @Override 158 public LogEvent rewrite(LogEvent source) { 159 160 Message messageObject = source.getMessage(); 161 162 if (messageObject != null) { 163 164 String message = messageObject.getFormattedMessage(); 165 if (message != null) { 166 167 168 Log4jLogEvent.Builder eventBuilder = new Log4jLogEvent.Builder(source); 169 return eventBuilder.build(); 170 } 171 } 172 173 174 return source; 175 } 176 177 /** 178 * API method to extract slices from the given message string, based on the data 179 * provided in the class configuration. 180 * 181 * If the message does not match either the substring or regex, the resulting Queue 182 * will be empty and the message ought to be ignored for log purposes. 183 * 184 * @param message The message string 185 * @return A linked list of string slices 186 */ 187 public Queue<StringSlice> extractSlices(String message) { 188 int size = message.length(); 189 boolean include = false; 190 191 int firstIndex = -1; 192 Matcher matcher = null; 193 194 Queue<StringSlice> slices = new LinkedList<>(); 195 if (this.substring != null && this.substring.length() > 0) { 196 firstIndex = message.indexOf(this.substring); 197 include = (firstIndex >= 0); 198 } else if (this.regexPattern != null) { 199 matcher = this.regexPattern.matcher(message); 200 include = matcher.find(); 201 } 202 if (include) { 203 204 if (firstIndex >= 0) { 205 // Substring model 206 StringSlice previousSlice = new StringSlice(0, this.contextConfig.startChars, size); 207 208 int index = firstIndex; 209 do { 210 StringSlice nextSlice = new StringSlice(index - this.contextConfig.contextChars, index + this.contextConfig.contextChars, size); 211 if (nextSlice.overlaps(previousSlice)) { 212 previousSlice = previousSlice.mergeWith(nextSlice); 213 } else { 214 slices.add(previousSlice); 215 previousSlice = nextSlice; 216 } 217 218 index = message.indexOf(this.substring, index + 1); 219 } while(index >= 0); 220 221 StringSlice endSlice = new StringSlice(size - this.contextConfig.endChars, size, size); 222 223 if (previousSlice.overlaps(endSlice)) { 224 previousSlice = previousSlice.mergeWith(endSlice); 225 endSlice = null; 226 } 227 228 slices.add(previousSlice); 229 230 if (endSlice != null) { 231 slices.add(endSlice); 232 } 233 } else { 234 StringSlice previousSlice = new StringSlice(0, this.contextConfig.startChars, size); 235 236 matcher.reset(); 237 238 while(matcher.find()) { 239 int startIndex = matcher.start(); 240 int endIndex = matcher.end(); 241 StringSlice nextSlice = new StringSlice(startIndex - this.contextConfig.contextChars, endIndex + this.contextConfig.contextChars, size); 242 if (nextSlice.overlaps(previousSlice)) { 243 previousSlice = previousSlice.mergeWith(nextSlice); 244 } else { 245 slices.add(previousSlice); 246 previousSlice = nextSlice; 247 } 248 } 249 250 StringSlice endSlice = new StringSlice(size - this.contextConfig.endChars, size, size); 251 252 if (previousSlice.overlaps(endSlice)) { 253 previousSlice = previousSlice.mergeWith(endSlice); 254 endSlice = null; 255 } 256 257 slices.add(previousSlice); 258 259 if (endSlice != null) { 260 slices.add(endSlice); 261 } 262 } 263 264 // TODO append the slices 265 266 } 267 268 return slices; 269 } 270}