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.nio.charset.StandardCharsets;
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.JsonUtils;
40  import org.apache.logging.log4j.core.util.KeyValuePair;
41  import org.apache.logging.log4j.util.TriConsumer;
42  import org.apache.logging.log4j.message.Message;
43  import org.apache.logging.log4j.status.StatusLogger;
44  import org.apache.logging.log4j.util.StringBuilderFormattable;
45  import org.apache.logging.log4j.util.Strings;
46  
47  /**
48   * Lays out events in the Graylog Extended Log Format (GELF) 1.1.
49   * <p>
50   * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if
51   * log event data is larger than 1024 bytes (the {@code compressionThreshold}).
52   * This layout does not implement chunking.
53   * </p>
54   * <p>
55   * Configure as follows to send to a Graylog2 server:
56   * </p>
57   *
58   * <pre>
59   * &lt;Appenders&gt;
60   *        &lt;Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"&gt;
61   *            &lt;GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"&gt;
62   *                &lt;KeyValuePair key="additionalField1" value="additional value 1"/&gt;
63   *                &lt;KeyValuePair key="additionalField2" value="additional value 2"/&gt;
64   *            &lt;/GelfLayout&gt;
65   *        &lt;/Socket&gt;
66   * &lt;/Appenders&gt;
67   * </pre>
68   *
69   * @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a>
70   */
71  @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
72  public final class GelfLayout extends AbstractStringLayout {
73  
74      public enum CompressionType {
75  
76          GZIP {
77              @Override
78              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
79                  return new GZIPOutputStream(os);
80              }
81          },
82          ZLIB {
83              @Override
84              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
85                  return new DeflaterOutputStream(os);
86              }
87          },
88          OFF {
89              @Override
90              public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
91                  return null;
92              }
93          };
94  
95          public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
96      }
97  
98      private static final char C = ',';
99      private static final int COMPRESSION_THRESHOLD = 1024;
100     private static final char Q = '\"';
101     private static final String QC = "\",";
102     private static final String QU = "\"_";
103 
104     private final KeyValuePair[] additionalFields;
105     private final int compressionThreshold;
106     private final CompressionType compressionType;
107     private final String host;
108     private final boolean includeStacktrace;
109 
110     public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
111                       final int compressionThreshold, final boolean includeStacktrace) {
112         super(StandardCharsets.UTF_8);
113         this.host = host;
114         this.additionalFields = additionalFields;
115         this.compressionType = compressionType;
116         this.compressionThreshold = compressionThreshold;
117         this.includeStacktrace = includeStacktrace;
118     }
119 
120     @PluginFactory
121     public static GelfLayout createLayout(
122             //@formatter:off
123             @PluginAttribute("host") final String host,
124             @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
125             @PluginAttribute(value = "compressionType",
126                 defaultString = "GZIP") final CompressionType compressionType,
127             @PluginAttribute(value = "compressionThreshold",
128                 defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
129             @PluginAttribute(value = "includeStacktrace",
130                 defaultBoolean = true) final boolean includeStacktrace) {
131             // @formatter:on
132         return new GelfLayout(host, additionalFields, compressionType, compressionThreshold, includeStacktrace);
133     }
134 
135     @Override
136     public Map<String, String> getContentFormat() {
137         return Collections.emptyMap();
138     }
139 
140     @Override
141     public String getContentType() {
142         return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
143     }
144 
145     @Override
146     public byte[] toByteArray(final LogEvent event) {
147         final StringBuilder text = toText(event, getStringBuilder(), false);
148         final byte[] bytes = getBytes(text.toString());
149         return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
150     }
151 
152     @Override
153     public void encode(final LogEvent event, final ByteBufferDestination destination) {
154         if (compressionType != CompressionType.OFF) {
155             super.encode(event, destination);
156             return;
157         }
158         final StringBuilder text = toText(event, getStringBuilder(), true);
159         final Encoder<StringBuilder> helper = getStringBuilderEncoder();
160         helper.encode(text, destination);
161     }
162 
163     private byte[] compress(final byte[] bytes) {
164         try {
165             final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
166             try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
167                 if (stream == null) {
168                     return bytes;
169                 }
170                 stream.write(bytes);
171                 stream.finish();
172             }
173             return baos.toByteArray();
174         } catch (final IOException e) {
175             StatusLogger.getLogger().error(e);
176             return bytes;
177         }
178     }
179 
180     @Override
181     public String toSerializable(final LogEvent event) {
182         final StringBuilder text = toText(event, getStringBuilder(), false);
183         return text.toString();
184     }
185 
186     private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
187         builder.append('{');
188         builder.append("\"version\":\"1.1\",");
189         builder.append("\"host\":\"");
190         JsonUtils.quoteAsString(toNullSafeString(host), builder);
191         builder.append(QC);
192         builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
193         builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
194         if (event.getThreadName() != null) {
195             builder.append("\"_thread\":\"");
196             JsonUtils.quoteAsString(event.getThreadName(), builder);
197             builder.append(QC);
198         }
199         if (event.getLoggerName() != null) {
200             builder.append("\"_logger\":\"");
201             JsonUtils.quoteAsString(event.getLoggerName(), builder);
202             builder.append(QC);
203         }
204 
205         for (final KeyValuePair additionalField : additionalFields) {
206             builder.append(QU);
207             JsonUtils.quoteAsString(additionalField.getKey(), builder);
208             builder.append("\":\"");
209             JsonUtils.quoteAsString(toNullSafeString(additionalField.getValue()), builder);
210             builder.append(QC);
211         }
212         event.getContextData().forEach(WRITE_KEY_VALUES_INTO, builder);
213         if (event.getThrown() != null) {
214             builder.append("\"full_message\":\"");
215             if (includeStacktrace) {
216                 JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
217             } else {
218                 JsonUtils.quoteAsString(event.getThrown().toString(), builder);
219             }
220             builder.append(QC);
221         }
222 
223         builder.append("\"short_message\":\"");
224         final Message message = event.getMessage();
225         if (message instanceof CharSequence) {
226             JsonUtils.quoteAsString(((CharSequence)message), builder);
227         } else if (gcFree && message instanceof StringBuilderFormattable) {
228             final StringBuilder messageBuffer = getMessageStringBuilder();
229             try {
230                 ((StringBuilderFormattable) message).formatTo(messageBuffer);
231                 JsonUtils.quoteAsString(messageBuffer, builder);
232             } finally {
233                 trimToMaxSize(messageBuffer);
234             }
235         } else {
236             JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
237         }
238         builder.append(Q);
239         builder.append('}');
240         return builder;
241     }
242 
243     private static final TriConsumer<String, Object, StringBuilder> WRITE_KEY_VALUES_INTO = new TriConsumer<String, Object, StringBuilder>() {
244         @Override
245         public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
246             stringBuilder.append(QU);
247             JsonUtils.quoteAsString(key, stringBuilder);
248             stringBuilder.append("\":\"");
249             JsonUtils.quoteAsString(toNullSafeString(String.valueOf(value)), stringBuilder);
250             stringBuilder.append(QC);
251         }
252     };
253 
254     private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
255 
256     private static StringBuilder getMessageStringBuilder() {
257         StringBuilder result = messageStringBuilder.get();
258         if (result == null) {
259             result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
260             messageStringBuilder.set(result);
261         }
262         result.setLength(0);
263         return result;
264     }
265 
266     private static CharSequence toNullSafeString(final CharSequence s) {
267         return s == null ? Strings.EMPTY : s;
268     }
269 
270     /**
271      * Non-private to make it accessible from unit test.
272      */
273     static CharSequence formatTimestamp(final long timeMillis) {
274         if (timeMillis < 1000) {
275             return "0";
276         }
277         final StringBuilder builder = getTimestampStringBuilder();
278         builder.append(timeMillis);
279         builder.insert(builder.length() - 3, '.');
280         return builder;
281     }
282 
283     private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
284 
285     private static StringBuilder getTimestampStringBuilder() {
286         StringBuilder result = timestampStringBuilder.get();
287         if (result == null) {
288             result = new StringBuilder(20);
289             timestampStringBuilder.set(result);
290         }
291         result.setLength(0);
292         return result;
293     }
294 
295     /**
296      * http://en.wikipedia.org/wiki/Syslog#Severity_levels
297      */
298     private int formatLevel(final Level level) {
299         return Severity.getSeverity(level).getCode();
300     }
301 
302     /**
303      * Non-private to make it accessible from unit test.
304      */
305     static CharSequence formatThrowable(final Throwable throwable) {
306         // stack traces are big enough to provide a reasonably large initial capacity here
307         final StringWriter sw = new StringWriter(2048);
308         final PrintWriter pw = new PrintWriter(sw);
309         throwable.printStackTrace(pw);
310         pw.flush();
311         return sw.getBuffer();
312     }
313 }