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}