001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.layout; 018 019import org.apache.logging.log4j.Level; 020import org.apache.logging.log4j.core.Layout; 021import org.apache.logging.log4j.core.LogEvent; 022import org.apache.logging.log4j.core.config.Node; 023import org.apache.logging.log4j.core.config.plugins.Plugin; 024import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 025import org.apache.logging.log4j.core.config.plugins.PluginElement; 026import org.apache.logging.log4j.core.config.plugins.PluginFactory; 027import org.apache.logging.log4j.core.net.Severity; 028import org.apache.logging.log4j.core.util.JsonUtils; 029import org.apache.logging.log4j.core.util.KeyValuePair; 030import org.apache.logging.log4j.message.Message; 031import org.apache.logging.log4j.status.StatusLogger; 032import org.apache.logging.log4j.util.StringBuilderFormattable; 033import org.apache.logging.log4j.util.Strings; 034 035import java.io.ByteArrayOutputStream; 036import java.io.IOException; 037import java.io.OutputStream; 038import java.io.PrintWriter; 039import java.io.StringWriter; 040import java.nio.charset.StandardCharsets; 041import java.util.Collections; 042import java.util.Map; 043import java.util.zip.DeflaterOutputStream; 044import java.util.zip.GZIPOutputStream; 045 046/** 047 * Lays out events in the Graylog Extended Log Format (GELF) 1.1. 048 * <p> 049 * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if 050 * log event data is larger than 1024 bytes (the {@code compressionThreshold}). 051 * This layout does not implement chunking. 052 * </p> 053 * <p> 054 * Configure as follows to send to a Graylog2 server: 055 * </p> 056 * 057 * <pre> 058 * <Appenders> 059 * <Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"> 060 * <GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"> 061 * <KeyValuePair key="additionalField1" value="additional value 1"/> 062 * <KeyValuePair key="additionalField2" value="additional value 2"/> 063 * </GelfLayout> 064 * </Socket> 065 * </Appenders> 066 * </pre> 067 * 068 * @see <a href="http://docs.graylog.org/en/latest/pages/gelf.html#gelf">GELF specification</a> 069 */ 070@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) 071public final class GelfLayout extends AbstractStringLayout { 072 073 public enum CompressionType { 074 075 GZIP { 076 @Override 077 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 078 return new GZIPOutputStream(os); 079 } 080 }, 081 ZLIB { 082 @Override 083 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 084 return new DeflaterOutputStream(os); 085 } 086 }, 087 OFF { 088 @Override 089 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 090 return null; 091 } 092 }; 093 094 public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException; 095 } 096 097 private static final char C = ','; 098 private static final int COMPRESSION_THRESHOLD = 1024; 099 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 //@formatter:off 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 // @formatter:on 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 * Non-private to make it accessible from unit test. 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 * http://en.wikipedia.org/wiki/Syslog#Severity_levels 287 */ 288 private int formatLevel(final Level level) { 289 return Severity.getSeverity(level).getCode(); 290 } 291 292 /** 293 * Non-private to make it accessible from unit test. 294 */ 295 static CharSequence formatThrowable(final Throwable throwable) { 296 // stack traces are big enough to provide a reasonably large initial capacity here 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}