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.math.BigDecimal;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Collections;
27 import java.util.Map;
28 import java.util.zip.DeflaterOutputStream;
29 import java.util.zip.GZIPOutputStream;
30
31 import org.apache.logging.log4j.Level;
32 import org.apache.logging.log4j.core.Layout;
33 import org.apache.logging.log4j.core.LogEvent;
34 import org.apache.logging.log4j.core.config.Node;
35 import org.apache.logging.log4j.core.config.plugins.Plugin;
36 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
37 import org.apache.logging.log4j.core.config.plugins.PluginElement;
38 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
39 import org.apache.logging.log4j.core.net.Severity;
40 import org.apache.logging.log4j.core.util.KeyValuePair;
41 import org.apache.logging.log4j.status.StatusLogger;
42 import org.apache.logging.log4j.util.Strings;
43
44 import com.fasterxml.jackson.core.io.JsonStringEncoder;
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
71
72 @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
73 public final class GelfLayout extends AbstractStringLayout {
74
75 public static enum CompressionType {
76
77 GZIP {
78 @Override
79 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
80 return new GZIPOutputStream(os);
81 }
82 },
83 ZLIB {
84 @Override
85 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
86 return new DeflaterOutputStream(os);
87 }
88 },
89 OFF {
90 @Override
91 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException {
92 return null;
93 }
94 };
95
96 public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException;
97
98 }
99
100 private static final char C = ',';
101 private static final int COMPRESSION_THRESHOLD = 1024;
102 private static final char Q = '\"';
103 private static final String QC = "\",";
104 private static final String QU = "\"_";
105 private static final long serialVersionUID = 1L;
106 private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000);
107
108 @PluginFactory
109 public static GelfLayout createLayout(
110
111 @PluginAttribute("host") final String host,
112 @PluginElement("AdditionalField") final KeyValuePair[] additionalFields,
113 @PluginAttribute(value = "compressionType",
114 defaultString = "GZIP") final CompressionType compressionType,
115 @PluginAttribute(value = "compressionThreshold",
116 defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) {
117
118 return new GelfLayout(host, additionalFields, compressionType, compressionThreshold);
119 }
120
121
122
123
124 static int formatLevel(final Level level) {
125 return Severity.getSeverity(level).getCode();
126 }
127
128 static String formatThrowable(final Throwable throwable) {
129
130 final StringWriter sw = new StringWriter(2048);
131 final PrintWriter pw = new PrintWriter(sw);
132 throwable.printStackTrace(pw);
133 pw.flush();
134 return sw.toString();
135 }
136
137 static String formatTimestamp(final long timeMillis) {
138 return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString();
139 }
140
141 private final KeyValuePair[] additionalFields;
142
143 private final int compressionThreshold;
144
145 private final CompressionType compressionType;
146
147 private final String host;
148
149 public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType,
150 final int compressionThreshold) {
151 super(StandardCharsets.UTF_8);
152 this.host = host;
153 this.additionalFields = additionalFields;
154 this.compressionType = compressionType;
155 this.compressionThreshold = compressionThreshold;
156 }
157
158 private byte[] compress(final byte[] bytes) {
159 try {
160 final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8);
161 try (final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos)) {
162 if (stream == null) {
163 return bytes;
164 }
165 stream.write(bytes);
166 stream.finish();
167 }
168 return baos.toByteArray();
169 } catch (final IOException e) {
170 StatusLogger.getLogger().error(e);
171 return bytes;
172 }
173 }
174
175 @Override
176 public Map<String, String> getContentFormat() {
177 return Collections.emptyMap();
178 }
179
180 @Override
181 public String getContentType() {
182 return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset();
183 }
184
185 @Override
186 public byte[] toByteArray(final LogEvent event) {
187 final byte[] bytes = getBytes(toSerializable(event));
188 return bytes.length > compressionThreshold ? compress(bytes) : bytes;
189 }
190
191 @Override
192 public String toSerializable(final LogEvent event) {
193 final StringBuilder builder = new StringBuilder(256);
194 final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance();
195 builder.append('{');
196 builder.append("\"version\":\"1.1\",");
197 builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(host))).append(QC);
198 builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C);
199 builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C);
200 if (event.getThreadName() != null) {
201 builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC);
202 }
203 if (event.getLoggerName() != null) {
204 builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC);
205 }
206
207 for (final KeyValuePair additionalField : additionalFields) {
208 builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"")
209 .append(jsonEncoder.quoteAsString(toNullSafeString(additionalField.getValue()))).append(QC);
210 }
211 for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) {
212 builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"")
213 .append(jsonEncoder.quoteAsString(toNullSafeString(entry.getValue()))).append(QC);
214 }
215 if (event.getThrown() != null) {
216 builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown())))
217 .append(QC);
218 }
219
220 builder.append("\"short_message\":\"").append(jsonEncoder.quoteAsString(toNullSafeString(event.getMessage().getFormattedMessage())))
221 .append(Q);
222 builder.append('}');
223 return builder.toString();
224 }
225
226 private String toNullSafeString(final String s) {
227 return s == null ? Strings.EMPTY : s;
228 }
229 }