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 org.apache.logging.log4j.Level;
20 import org.apache.logging.log4j.core.Layout;
21 import org.apache.logging.log4j.core.LogEvent;
22 import org.apache.logging.log4j.core.config.Node;
23 import org.apache.logging.log4j.core.config.plugins.Plugin;
24 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
25 import org.apache.logging.log4j.core.config.plugins.PluginElement;
26 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
27 import org.apache.logging.log4j.core.net.Severity;
28 import org.apache.logging.log4j.core.util.JsonUtils;
29 import org.apache.logging.log4j.core.util.KeyValuePair;
30 import org.apache.logging.log4j.message.Message;
31 import org.apache.logging.log4j.status.StatusLogger;
32 import org.apache.logging.log4j.util.StringBuilderFormattable;
33 import org.apache.logging.log4j.util.Strings;
34
35 import java.io.ByteArrayOutputStream;
36 import java.io.IOException;
37 import java.io.OutputStream;
38 import java.io.PrintWriter;
39 import java.io.StringWriter;
40 import java.nio.charset.StandardCharsets;
41 import java.util.Collections;
42 import java.util.Map;
43 import java.util.zip.DeflaterOutputStream;
44 import java.util.zip.GZIPOutputStream;
45
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 @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
71 public final class GelfLayout extends AbstractStringLayout {
72
73 public enum CompressionType {
74
75 GZIP {
76 @Override
77 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
78 return new GZIPOutputStream(os);
79 }
80 },
81 ZLIB {
82 @Override
83 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
84 return new DeflaterOutputStream(os);
85 }
86 },
87 OFF {
88 @Override
89 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
90 return null;
91 }
92 };
93
94 public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
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
103 private final KeyValuePair[] additionalFields;
104 private final int compressionThreshold;
105 private final CompressionType compressionType;
106 private final String host;
107 private final boolean includeStacktrace;
108
109 public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
110 final int compressionThreshold, final boolean includeStacktrace) {
111 super(StandardCharsets.UTF_8);
112 this.host = host;
113 this.additionalFields = additionalFields;
114 this.compressionType = compressionType;
115 this.compressionThreshold = compressionThreshold;
116 this.includeStacktrace = includeStacktrace;
117 }
118
119 @PluginFactory
120 public static GelfLayout createLayout(
121
122 @PluginAttribute("host") final String host,
123 @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
124 @PluginAttribute(value = "compressionType",
125 defaultString = "GZIP") final CompressionType compressionType,
126 @PluginAttribute(value = "compressionThreshold",
127 defaultInt = COMPRESSION_THRESHOLD) final int compressionThreshold,
128 @PluginAttribute(value = "includeStacktrace",
129 defaultBoolean = true) final boolean includeStacktrace) {
130
131 return new GelfLayout(host, additionalFields, compressionType, compressionThreshold, includeStacktrace);
132 }
133
134 @Override
135 public Map<String, String> getContentFormat() {
136 return Collections.emptyMap();
137 }
138
139 @Override
140 public String getContentType() {
141 return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
142 }
143
144 @Override
145 public byte[] toByteArray(final LogEvent event) {
146 final StringBuilder text = toText(event, getStringBuilder(), false);
147 final byte[] bytes = getBytes(text.toString());
148 return compressionType != CompressionType.OFF && bytes.length > compressionThreshold ? compress(bytes) : bytes;
149 }
150
151 @Override
152 public void encode(final LogEvent event, final ByteBufferDestination destination) {
153 if (compressionType != CompressionType.OFF) {
154 super.encode(event, destination);
155 return;
156 }
157 final StringBuilder text = toText(event, getStringBuilder(), true);
158 final Encoder<StringBuilder> helper = getStringBuilderEncoder();
159 helper.encode(text, destination);
160 }
161
162 private byte[] compress(final byte[] bytes) {
163 try {
164 final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
165 try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
166 if (stream == null) {
167 return bytes;
168 }
169 stream.write(bytes);
170 stream.finish();
171 }
172 return baos.toByteArray();
173 } catch (final IOException e) {
174 StatusLogger.getLogger().error(e);
175 return bytes;
176 }
177 }
178
179 @Override
180 public String toSerializable(final LogEvent event) {
181 final StringBuilder text = toText(event, getStringBuilder(), false);
182 return text.toString();
183 }
184
185 private StringBuilder toText(final LogEvent event, final StringBuilder builder, final boolean gcFree) {
186 builder.append('{');
187 builder.append("\"version\":\"1.1\",");
188 builder.append("\"host\":\"");
189 JsonUtils.quoteAsString(toNullSafeString(host), builder);
190 builder.append(QC);
191 builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
192 builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
193 if (event.getThreadName() != null) {
194 builder.append("\"_thread\":\"");
195 JsonUtils.quoteAsString(event.getThreadName(), builder);
196 builder.append(QC);
197 }
198 if (event.getLoggerName() != null) {
199 builder.append("\"_logger\":\"");
200 JsonUtils.quoteAsString(event.getLoggerName(), builder);
201 builder.append(QC);
202 }
203
204 for (final KeyValuePair additionalField : additionalFields) {
205 builder.append(QU);
206 JsonUtils.quoteAsString(additionalField.getKey(), builder);
207 builder.append("\":\"");
208 JsonUtils.quoteAsString(toNullSafeString(additionalField.getValue()), builder);
209 builder.append(QC);
210 }
211 for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
212 builder.append(QU);
213 JsonUtils.quoteAsString(entry.getKey(), builder);
214 builder.append("\":\"");
215 JsonUtils.quoteAsString(toNullSafeString(entry.getValue()), builder);
216 builder.append(QC);
217 }
218 if (event.getThrown() != null) {
219 builder.append("\"full_message\":\"");
220 if (includeStacktrace) {
221 JsonUtils.quoteAsString(formatThrowable(event.getThrown()), builder);
222 } else {
223 JsonUtils.quoteAsString(event.getThrown().toString(), builder);
224 }
225 builder.append(QC);
226 }
227
228 builder.append("\"short_message\":\"");
229 final Message message = event.getMessage();
230 if (message instanceof CharSequence) {
231 JsonUtils.quoteAsString(((CharSequence)message), builder);
232 } else if (gcFree && message instanceof StringBuilderFormattable) {
233 final StringBuilder messageBuffer = getMessageStringBuilder();
234 ((StringBuilderFormattable)message).formatTo(messageBuffer);
235 JsonUtils.quoteAsString(messageBuffer, builder);
236 } else {
237 JsonUtils.quoteAsString(toNullSafeString(message.getFormattedMessage()), builder);
238 }
239 builder.append(Q);
240 builder.append('}');
241 return builder;
242 }
243
244 private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
245
246 private static StringBuilder getMessageStringBuilder() {
247 StringBuilder result = messageStringBuilder.get();
248 if (result == null) {
249 result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
250 messageStringBuilder.set(result);
251 }
252 result.setLength(0);
253 return result;
254 }
255
256 private CharSequence toNullSafeString(final CharSequence s) {
257 return s == null ? Strings.EMPTY : s;
258 }
259
260
261
262
263 static CharSequence formatTimestamp(final long timeMillis) {
264 if (timeMillis < 1000) {
265 return "0";
266 }
267 final StringBuilder builder = getTimestampStringBuilder();
268 builder.append(timeMillis);
269 builder.insert(builder.length() - 3, '.');
270 return builder;
271 }
272
273 private static final ThreadLocal<StringBuilder> timestampStringBuilder = new ThreadLocal<>();
274
275 private static StringBuilder getTimestampStringBuilder() {
276 StringBuilder result = timestampStringBuilder.get();
277 if (result == null) {
278 result = new StringBuilder(20);
279 timestampStringBuilder.set(result);
280 }
281 result.setLength(0);
282 return result;
283 }
284
285
286
287
288 private int formatLevel(final Level level) {
289 return Severity.getSeverity(level).getCode();
290 }
291
292
293
294
295 static CharSequence formatThrowable(final Throwable throwable) {
296
297 final StringWriter sw = new StringWriter(2048);
298 final PrintWriter pw = new PrintWriter(sw);
299 throwable.printStackTrace(pw);
300 pw.flush();
301 return sw.getBuffer();
302 }
303 }