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.layout;
    18
    19  import java.io.ByteArrayOutputStream;
    20  import java.io.IOException;
    21  import java.io.OutputStream;
    22  import java.io.PrintWriter;
    23  import java.io.StringWriter;
    24  import java.math.BigDecimal;
    25  import java.util.Collections;
    26  import java.util.Map;
    27  import java.util.zip.DeflaterOutputStream;
    28  import java.util.zip.GZIPOutputStream;
    29
    30  import org.apache.logging.log4j.Level;
    31  import org.apache.logging.log4j.core.Layout;
    32  import org.apache.logging.log4j.core.LogEvent;
    33  import org.apache.logging.log4j.core.config.Node;
    34  import org.apache.logging.log4j.core.config.plugins.Plugin;
    35  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    36  import org.apache.logging.log4j.core.config.plugins.PluginElement;
    37  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    38  import org.apache.logging.log4j.core.net.Severity;
    39  import org.apache.logging.log4j.core.util.Constants;
    40  import org.apache.logging.log4j.core.util.KeyValuePair;
    41  import org.apache.logging.log4j.status.StatusLogger;
    42
    43  import com.fasterxml.jackson.core.io.JsonStringEncoder;
    44
    45  /**
    46   * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
    47   * <p>
    48   * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if log event data is larger than 1024 bytes
    49   * (the {@code compressionThreshold}). This layout does not implement chunking.
    50   * </p>
    51   * <p>
    52   * Configure as follows to send to a Graylog2 server:
    53   * </p>
    54   *
    55   * <pre>
    56   * &lt;Appenders&gt;
    57   *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
    58   *            &lt;GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"&gt;
    59   *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
    60   *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
    61   *            &lt;/GelfLayout&gt;
    62   *        &lt;/Socket&gt;
    63   * &lt;/Appenders&gt;
    64   * </pre>
    65   *
    66   * @see <a href="http://graylog2.org/gelf">GELF home page</a>
    67   * @see <a href="http://graylog2.org/resources/gelf/specification">GELF specification</a>
    68   */
    69  @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
    70  public final class GelfLayout extends AbstractStringLayout {
    71
    72      public static enum CompressionType {
    73
    74          GZIP {
    75              @Override
    76              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
    77                  return new GZIPOutputStream(os);
    78              }
    79          },
    80          ZLIB {
    81              @Override
    82              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
    83                  return new DeflaterOutputStream(os);
    84              }
    85          },
    86          OFF {
    87              @Override
    88              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
    89                  return null;
    90              }
    91          };
    92
    93          public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
    94
    95      }
    96
    97      private static final char C = ',';
    98      private static final int COMPRESSION_THRESHOLD = 1024;
    99      private static final char Q = '\"';
    100     private static final String QC = "\",";
    101     private static final String QU = "\"_";
    102     private static final long serialVersionUID = 1L;
    103     private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
    104
    105     @PluginFactory
    106     public static GelfLayout createLayout(
    107             //@formatter:off
    108             @PluginAttribute("host") final String host,
    109             @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
    110             @PluginAttribute(value = "compressionType",
    111                 defaultString = "GZIP") final CompressionType compressionType,
    112             @PluginAttribute(value = "compressionThreshold",
    113                 defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
    114             // @formatter:on
    115         return new GelfLayout(host, additionalFields, compressionType, compressionThreshold);
    116     }
    117
    118     /**
    119      * http://en.wikipedia.org/wiki/Syslog#Severity_levels
    120      */
    121     static int formatLevel(final Level level) {
    122         return Severity.getSeverity(level).getCode();
    123     }
    124
    125     static String formatThrowable(final Throwable throwable) {
    126         // stack traces are big enough to provide a reasonably large initial capacity here
    127         final StringWriter sw = new StringWriter(2048);
    128         final PrintWriter pw = new PrintWriter(sw);
    129         throwable.printStackTrace(pw);
    130         pw.flush();
    131         return sw.toString();
    132     }
    133
    134     static String formatTimestamp(final long timeMillis) {
    135         return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
    136     }
    137
    138     private final KeyValuePair[] additionalFields;
    139
    140     private final int compressionThreshold;
    141
    142     private final CompressionType compressionType;
    143
    144     private final String host;
    145
    146     public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
    147             final int compressionThreshold) {
    148         super(Constants.UTF_8);
    149         this.host = host;
    150         this.additionalFields = additionalFields;
    151         this.compressionType = compressionType;
    152         this.compressionThreshold = compressionThreshold;
    153     }
    154
    155     private byte[] compress(final byte[] bytes) {
    156         try {
    157             final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
    158             final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos);
    159             if (stream == null) {
    160                 return bytes;
    161             }
    162             stream.write(bytes);
    163             stream.finish();
    164             stream.close();
    165             return baos.toByteArray();
    166         } catch (final IOException e) {
    167             StatusLogger.getLogger().error(e);
    168             return bytes;
    169         }
    170     }
    171
    172     @Override
    173     public Map<String, String> getContentFormat() {
    174         return Collections.emptyMap();
    175     }
    176
    177     @Override
    178     public String getContentType() {
    179         return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
    180     }
    181
    182     @Override
    183     public byte[] toByteArray(final LogEvent event) {
    184         final byte[] bytes = getBytes(toSerializable(event));
    185         return bytes.length > compressionThreshold ? compress(bytes) : bytes;
    186     }
    187
    188     @Override
    189     public String toSerializable(final LogEvent event) {
    190         final StringBuilder builder = new StringBuilder(256);
    191         final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance();
    192         builder.append('{');
    193         builder.append("\"version\":\"1.1\",");
    194         builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(host)).append(QC);
    195         builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
    196         builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
    197         if (event.getThreadName() != null) {
    198             builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC);
    199         }
    200         if (event.getLoggerName() != null) {
    201             builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC);
    202         }
    203
    204         for (final KeyValuePair additionalField : additionalFields) {
    205             builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"")
    206                     .append(jsonEncoder.quoteAsString(additionalField.getValue())).append(QC);
    207         }
    208         for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
    209             builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"")
    210                     .append(jsonEncoder.quoteAsString(entry.getValue())).append(QC);
    211         }
    212         if (event.getThrown() != null) {
    213             builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown())))
    214                     .append(QC);
    215         }
    216
    217         builder.append("\"short_message\":\"")
    218                 .append(jsonEncoder.quoteAsString(event.getMessage().getFormattedMessage())).append(Q);
    219         builder.append('}');
    220         return builder.toString();
    221     }
    222 }