View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.core.pattern;
    18
    19  import java.util.Arrays;
    20  import java.util.HashMap;
    21  import java.util.List;
    22  import java.util.Locale;
    23  import java.util.Map;
    24
    25  import org.apache.logging.log4j.Level;
    26  import org.apache.logging.log4j.core.LogEvent;
    27  import org.apache.logging.log4j.core.config.Configuration;
    28  import org.apache.logging.log4j.core.config.plugins.Plugin;
    29  import org.apache.logging.log4j.core.layout.PatternLayout;
    30  import org.apache.logging.log4j.util.Strings;
    31
    32  /**
    33   * Highlight pattern converter. Formats the result of a pattern using a color appropriate for the Level in the LogEvent.
    34   * <p>
    35   * For example:
    36   * </p>
    37   *
    38   * <pre>
    39   * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}
    40   * </pre>
    41   * <p>
    42   * You can define custom colors for each Level:
    43   * </p>
    44   *
    45   * <pre>
    46   * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=cyan,
    47   * TRACE=black}
    48   * </pre>
    49   * <p>
    50   * You can use a predefined style:
    51   * </p>
    52   *
    53   * <pre>
    54   * %highlight{%d{ ISO8601 } [%t] %-5level: %msg%n%throwable}{STYLE=Log4j}
    55   * </pre>
    56   * <p>
    57   * The available predefined styles are:
    58   * </p>
    59   * <ul>
    60   * <li>{@code Default}</li>
    61   * <li>{@code Log4j} - The same as {@code Default}</li>
    62   * <li>{@code Logback}</li>
    63   * </ul>
    64   * <p>
    65   * You can use whitespace around the comma and equal sign. The names in values MUST come from the
    66   * {@linkplain AnsiEscape} enum, case is normalized to upper-case internally.
    67   * </p>
    68   */
    69  @Plugin(name = "highlight", category = PatternConverter.CATEGORY)
    70  @ConverterKeys({ "highlight" })
    71  public final class HighlightConverter extends LogEventPatternConverter implements AnsiConverter {
    72
    73      private static final Map<Level, String> DEFAULT_STYLES = new HashMap<Level, String>();
    74
    75      private static final Map<Level, String> LOGBACK_STYLES = new HashMap<Level, String>();
    76
    77      private static final String STYLE_KEY = "STYLE";
    78
    79      private static final String STYLE_KEY_DEFAULT = "DEFAULT";
    80
    81      private static final String STYLE_KEY_LOGBACK = "LOGBACK";
    82
    83      private static final Map<String, Map<Level, String>> STYLES = new HashMap<String, Map<Level, String>>();
    84
    85      static {
    86          // Default styles:
    87          DEFAULT_STYLES.put(Level.FATAL, AnsiEscape.createSequence("BRIGHT", "RED"));
    88          DEFAULT_STYLES.put(Level.ERROR, AnsiEscape.createSequence("BRIGHT", "RED"));
    89          DEFAULT_STYLES.put(Level.WARN, AnsiEscape.createSequence("YELLOW"));
    90          DEFAULT_STYLES.put(Level.INFO, AnsiEscape.createSequence("GREEN"));
    91          DEFAULT_STYLES.put(Level.DEBUG, AnsiEscape.createSequence("CYAN"));
    92          DEFAULT_STYLES.put(Level.TRACE, AnsiEscape.createSequence("BLACK"));
    93          // Logback styles:
    94          LOGBACK_STYLES.put(Level.FATAL, AnsiEscape.createSequence("BLINK", "BRIGHT", "RED"));
    95          LOGBACK_STYLES.put(Level.ERROR, AnsiEscape.createSequence("BRIGHT", "RED"));
    96          LOGBACK_STYLES.put(Level.WARN, AnsiEscape.createSequence("RED"));
    97          LOGBACK_STYLES.put(Level.INFO, AnsiEscape.createSequence("BLUE"));
    98          LOGBACK_STYLES.put(Level.DEBUG, AnsiEscape.createSequence((String[]) null));
    99          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 }