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 }