    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     */
    017    package org.apache.logging.log4j.core.pattern;
    018
    019    import java.util.Arrays;
    020    import java.util.HashMap;
    021    import java.util.List;
    022    import java.util.Locale;
    023    import java.util.Map;
    024
    025    import org.apache.logging.log4j.Level;
    026    import org.apache.logging.log4j.core.LogEvent;
    027    import org.apache.logging.log4j.core.config.Configuration;
    028    import org.apache.logging.log4j.core.config.plugins.Plugin;
    029    import org.apache.logging.log4j.core.layout.PatternLayout;
    030    import org.apache.logging.log4j.util.Strings;
    031
    032    /**
    033     * Highlight pattern converter. Formats the result of a pattern using a color appropriate for the Level in the LogEvent.
    034     * <p>
    035     * For example:
    036     * </p>
    037     *
    038     * <pre>
    039     * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}
    040     * </pre>
    041     * <p>
    042     * You can define custom colors for each Level:
    043     * </p>
    044     *
    045     * <pre>
    046     * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=cyan,
    047     * TRACE=black}
    048     * </pre>
    049     * <p>
    050     * You can use a predefined style:
    051     * </p>
    052     *
    053     * <pre>
    054     * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}{STYLE=Log4j}
    055     * </pre>
    056     * <p>
    057     * The available predefined styles are:
    058     * </p>
    059     * <ul>
    060     * <li>{@code Default}</li>
    061     * <li>{@code Log4j} - The same as {@code Default}</li>
    062     * <li>{@code Logback}</li>
    063     * </ul>
    064     * <p>
    065     * You can use whitespace around the comma and equal sign. The names in values MUST come from the
    066     * {@linkplain AnsiEscape} enum, case is normalized to upper-case internally.
    067     * </p>
    068     */
    069    @Plugin(name = "highlight", category = PatternConverter.CATEGORY)
    070    @ConverterKeys({ "highlight" })
    071    public final class HighlightConverter extends LogEventPatternConverter implements AnsiConverter {
    072
    073        private static final Map<Level, String> DEFAULT_STYLES = new HashMap<Level, String>();
    074
    075        private static final Map<Level, String> LOGBACK_STYLES = new HashMap<Level, String>();
    076
    077        private static final String STYLE_KEY = "STYLE";
    078
    079        private static final String STYLE_KEY_DEFAULT = "DEFAULT";
    080
    081        private static final String STYLE_KEY_LOGBACK = "LOGBACK";
    082
    083        private static final Map<String, Map<Level, String>> STYLES = new HashMap<String, Map<Level, String>>();
    084
    085        static {
    086            // Default styles:
    087            DEFAULT_STYLES.put(Level.FATAL, AnsiEscape.createSequence("BRIGHT", "RED"));
    088            DEFAULT_STYLES.put(Level.ERROR, AnsiEscape.createSequence("BRIGHT", "RED"));
    089            DEFAULT_STYLES.put(Level.WARN, AnsiEscape.createSequence("YELLOW"));
    090            DEFAULT_STYLES.put(Level.INFO, AnsiEscape.createSequence("GREEN"));
    091            DEFAULT_STYLES.put(Level.DEBUG, AnsiEscape.createSequence("CYAN"));
    092            DEFAULT_STYLES.put(Level.TRACE, AnsiEscape.createSequence("BLACK"));
    093            // Logback styles:
    094            LOGBACK_STYLES.put(Level.FATAL, AnsiEscape.createSequence("BLINK", "BRIGHT", "RED"));
    095            LOGBACK_STYLES.put(Level.ERROR, AnsiEscape.createSequence("BRIGHT", "RED"));
    096            LOGBACK_STYLES.put(Level.WARN, AnsiEscape.createSequence("RED"));
    097            LOGBACK_STYLES.put(Level.INFO, AnsiEscape.createSequence("BLUE"));
    098            LOGBACK_STYLES.put(Level.DEBUG, AnsiEscape.createSequence((String[]) null));
    099            LOGBACK_STYLES.put(Level.TRACE, AnsiEscape.createSequence((String[]) null));
    100            // Style map:
    101            STYLES.put(STYLE_KEY_DEFAULT, DEFAULT_STYLES);
    102            STYLES.put(STYLE_KEY_LOGBACK, LOGBACK_STYLES);
    103        }
    104
    105        /**
    106         * Creates a level style map where values are ANSI escape sequences given configuration options in {@code option[1]}
    107         * .
    108         * <p>
    109         * The format of the option string in {@code option[1]} is:
    110         * </p>
    111         *
    112         * <pre>
    113         * Level1=Value, Level2=Value, ...
    114         * </pre>
    115         *
    116         * <p>
    117         * For example:
    118         * </p>
    119         *
    120         * <pre>
    121         * ERROR=red bold, WARN=yellow bold, INFO=green, ...
    122         * </pre>
    123         *
    124         * <p>
    125         * You can use whitespace around the comma and equal sign. The names in values MUST come from the
    126         * {@linkplain AnsiEscape} enum, case is normalized to upper-case internally.
    127         * </p>
    128         *
    129         * @param options
    130         *        The second slot can optionally contain the style map.
    131         * @return a new map
    132         */
    133        private static Map<Level, String> createLevelStyleMap(final String[] options) {
    134            if (options.length < 2) {
    135                return DEFAULT_STYLES;
    136            }
    137            // Feels like a hack. Should String[] options change to a Map<String,String>?
    138            final String string = options[1].replaceAll(PatternParser.NO_CONSOLE_NO_ANSI + "=(true|false)", Strings.EMPTY);
    139            //
    140            final Map<String, String> styles = AnsiEscape.createMap(string, new String[] {STYLE_KEY});
    141            final Map<Level, String> levelStyles = new HashMap<Level, String>(DEFAULT_STYLES);
    142            for (final Map.Entry<String, String> entry : styles.entrySet()) {
    143                final String key = entry.getKey().toUpperCase(Locale.ENGLISH);
    144                final String value = entry.getValue();
    145                if (STYLE_KEY.equalsIgnoreCase(key)) {
    146                    final Map<Level, String> enumMap = STYLES.get(value.toUpperCase(Locale.ENGLISH));
    147                    if (enumMap == null) {
    148                        LOGGER.error("Unknown level style: " + value + ". Use one of " +
    149                            Arrays.toString(STYLES.keySet().toArray()));
    150                    } else {
    151                        levelStyles.putAll(enumMap);
    152                    }
    153                } else {
    154                    final Level level = Level.toLevel(key);
    155                    if (level == null) {
    156                        LOGGER.error("Unknown level name: " + key + ". Use one of " +
    157                            Arrays.toString(DEFAULT_STYLES.keySet().toArray()));
    158                    } else {
    159                        levelStyles.put(level, value);
    160                    }
    161                }
    162            }
    163            return levelStyles;
    164        }
    165
    166        /**
    167         * Gets an instance of the class.
    168         *
    169         * @param config The current Configuration.
    170         * @param options pattern options, may be null. If first element is "short", only the first line of the
    171         *                throwable will be formatted.
    172         * @return instance of class.
    173         */
    174        public static HighlightConverter newInstance(final Configuration config, final String[] options) {
    175            if (options.length < 1) {
    176                LOGGER.error("Incorrect number of options on style. Expected at least 1, received " + options.length);
    177                return null;
    178            }
    179            if (options[0] == null) {
    180                LOGGER.error("No pattern supplied on style");
    181                return null;
    182            }
    183            final PatternParser parser = PatternLayout.createPatternParser(config);
    184            final List<PatternFormatter> formatters = parser.parse(options[0]);
    185            final boolean noConsoleNoAnsi = options.length > 1
    186                    && (PatternParser.NO_CONSOLE_NO_ANSI + "=true").equals(options[1]);
    187            final boolean hideAnsi = noConsoleNoAnsi && System.console() == null;
    188            return new HighlightConverter(formatters, createLevelStyleMap(options), hideAnsi);
    189        }
    190
    191        private final Map<Level, String> levelStyles;
    192
    193        private final List<PatternFormatter> patternFormatters;
    194
    195        private boolean noAnsi;
    196
    197        /**
    198         * Construct the converter.
    199         *
    200         * @param patternFormatters
    201         *            The PatternFormatters to generate the text to manipulate.
    202         * @param noAnsi
    203         *            If true, do not output ANSI escape codes.
    204         */
    205        private HighlightConverter(final List<PatternFormatter> patternFormatters, final Map<Level, String> levelStyles, final boolean noAnsi) {
    206            super("style", "style");
    207            this.patternFormatters = patternFormatters;
    208            this.levelStyles = levelStyles;
    209            this.noAnsi = noAnsi;
    210        }
    211
    212        /**
    213         * {@inheritDoc}
    214         */
    215        @Override
    216        public void format(final LogEvent event, final StringBuilder toAppendTo) {
    217            final StringBuilder buf = new StringBuilder();
    218            for (final PatternFormatter formatter : patternFormatters) {
    219                formatter.format(event, buf);
    220            }
    221
    222            if (buf.length() > 0) {
    223                if (noAnsi) {
    224                    toAppendTo.append(buf.toString());
    225                } else {
    226                    toAppendTo.append(levelStyles.get(event.getLevel())).append(buf.toString()).
    227                        append(AnsiEscape.getDefaultStyle());
    228                }
    229            }
    230        }
    231
    232        @Override
    233        public boolean handlesThrowable() {
    234            for (final PatternFormatter formatter : patternFormatters) {
    235                if (formatter .handlesThrowable()) {
    236                    return true;
    237                }
    238            }
    239            return false;
    240        }
    241    }