1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
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
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
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
297
298 private int formatLevel(final Level level) {
299 return Severity.getSeverity(level).getCode();
300 }
301
302
303
304
305 static CharSequence formatThrowable(final Throwable throwable) {
306
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 }