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