001/*
002 * This is free and unencumbered software released into the public domain.
003 *
004 * Please see https://github.com/binkley/binkley/blob/master/LICENSE.md.
005 */
006
007package hm.binkley.util.logging;
008
009import ch.qos.logback.classic.PatternLayout;
010import ch.qos.logback.classic.pattern.ClassicConverter;
011import ch.qos.logback.classic.spi.ILoggingEvent;
012import ch.qos.logback.core.boolex.EvaluationException;
013import ch.qos.logback.core.boolex.EventEvaluator;
014import ch.qos.logback.core.status.ErrorStatus;
015
016import javax.annotation.Nonnull;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Map;
020
021import static ch.qos.logback.core.CoreConstants.EVALUATOR_MAP;
022import static java.util.Collections.emptyMap;
023
024/**
025 * {@code MarkedConverter} provides alternate conversions based on conditions.  Enable with:
026 * <pre>
027 * &lt;conversionRule
028 *     conversionWord="match"
029 *     converterClass="hm.binkley.util.logging.MatchConverter"/&gt;</pre> Use as:
030 * <pre>
031 * &lt;pattern&gt;%match{cond1,patt1,...,fallback}&lt;/pattern&gt;</pre> Example:
032 * <pre>
033 * &lt;evaluator name="WITH_MARKER"&gt;
034 *     &lt;expression&gt;null != marker &amp;mp;&amp;mp; "ALERT".equals(marker.getName())&lt;/expression&gt;
035 * &lt;/evaluator&gt;
036 * &lt;pattern&gt;%match(WITH_MARKER,%marker/%level,%level)&lt;/pattern&gt;</pre> will log
037 * "ALERT/ERROR" when marker is "ALERT" and level is "ERROR", otherwise just "ERROR".
038 *
039 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
040 * @todo Fix error reporting - logback swallows
041 */
042public final class MatchConverter
043        extends ClassicConverter {
044    private static final int MAX_ERROR_COUNT = 4;
045    private Map<String, String> conditions;
046    private String unmatched;
047    private Map<String, EventEvaluator<ILoggingEvent>> evaluators;
048    private int errors;
049
050    @SuppressWarnings("unchecked")
051    @Override
052    public void start() {
053        final List<String> options = getOptionList();
054        if (null == options || 2 > options.size()) {
055            addError("Missing options for %match - " + (null == options ? "missing options"
056                    : options));
057            conditions = emptyMap();
058            unmatched = "";
059            return;
060        }
061
062        conditions = new LinkedHashMap<>();
063        for (int i = 0; i < options.size() - 1; i += 2)
064            conditions.put(options.get(i), options.get(i + 1));
065        unmatched = 0 == options.size() % 2 ? "" : options.get(options.size() - 1);
066
067        evaluators = (Map<String, EventEvaluator<ILoggingEvent>>) getContext()
068                .getObject(EVALUATOR_MAP);
069
070        super.start();
071    }
072
073    @Nonnull
074    @Override
075    public String convert(@Nonnull final ILoggingEvent event) {
076        for (final Map.Entry<String, String> entry : conditions.entrySet())
077            if (evaluate(entry.getKey(), event))
078                return relayout(entry.getValue(), event);
079        return relayout(unmatched, event);
080    }
081
082    private boolean evaluate(final String name, final ILoggingEvent event) {
083        final EventEvaluator<ILoggingEvent> evaluator = evaluators.get(name);
084        try {
085            return null != evaluator && evaluator.evaluate(event);
086        } catch (final EvaluationException e) {
087            errors++;
088            if (MAX_ERROR_COUNT > errors) {
089                addError("Exception thrown for evaluator named [" + evaluator.getName() + "]", e);
090            } else if (MAX_ERROR_COUNT == errors) {
091                final ErrorStatus errorStatus = new ErrorStatus(
092                        "Exception thrown for evaluator named [" + evaluator.getName() + "].", this,
093                        e);
094                errorStatus.add(new ErrorStatus(
095                        "This was the last warning about this evaluator's errors."
096                                + "We don't want the StatusManager to get flooded.", this));
097                addStatus(errorStatus);
098            }
099            return false;
100        }
101    }
102
103    private String relayout(final String pattern, final ILoggingEvent event) {
104        final PatternLayout layout = new PatternLayout();
105        layout.setContext(getContext());
106        layout.setPattern(pattern);
107        layout.start();
108        return layout.doLayout(event);
109    }
110}