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 */ 017 package org.apache.logging.log4j.core.layout; 018 019 import java.io.ByteArrayOutputStream; 020 import java.io.IOException; 021 import java.io.OutputStream; 022 import java.io.PrintWriter; 023 import java.io.StringWriter; 024 import java.math.BigDecimal; 025 import java.util.Collections; 026 import java.util.Map; 027 import java.util.zip.DeflaterOutputStream; 028 import java.util.zip.GZIPOutputStream; 029 030 import org.apache.logging.log4j.Level; 031 import org.apache.logging.log4j.core.Layout; 032 import org.apache.logging.log4j.core.LogEvent; 033 import org.apache.logging.log4j.core.config.Node; 034 import org.apache.logging.log4j.core.config.plugins.Plugin; 035 import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 036 import org.apache.logging.log4j.core.config.plugins.PluginElement; 037 import org.apache.logging.log4j.core.config.plugins.PluginFactory; 038 import org.apache.logging.log4j.core.net.Severity; 039 import org.apache.logging.log4j.core.util.Constants; 040 import org.apache.logging.log4j.core.util.KeyValuePair; 041 import org.apache.logging.log4j.status.StatusLogger; 042 043 import com.fasterxml.jackson.core.io.JsonStringEncoder; 044 045 /** 046 * Lays out events in the Graylog Extended Log Format (GELF) 1.1. 047 * <p> 048 * This layout compresses JSON to GZIP or ZLIB (the {@code compressionType}) if log event data is larger than 1024 bytes 049 * (the {@code compressionThreshold}). This layout does not implement chunking. 050 * </p> 051 * <p> 052 * Configure as follows to send to a Graylog2 server: 053 * </p> 054 * 055 * <pre> 056 * <Appenders> 057 * <Socket name="Graylog" protocol="udp" host="graylog.domain.com" port="12201"> 058 * <GelfLayout host="someserver" compressionType="GZIP" compressionThreshold="1024"> 059 * <KeyValuePair key="additionalField1" value="additional value 1"/> 060 * <KeyValuePair key="additionalField2" value="additional value 2"/> 061 * </GelfLayout> 062 * </Socket> 063 * </Appenders> 064 * </pre> 065 * 066 * @see <a href="http://graylog2.org/gelf">GELF home page</a> 067 * @see <a href="http://graylog2.org/resources/gelf/specification">GELF specification</a> 068 */ 069 @Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) 070 public final class GelfLayout extends AbstractStringLayout { 071 072 public static enum CompressionType { 073 074 GZIP { 075 @Override 076 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 077 return new GZIPOutputStream(os); 078 } 079 }, 080 ZLIB { 081 @Override 082 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 083 return new DeflaterOutputStream(os); 084 } 085 }, 086 OFF { 087 @Override 088 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 089 return null; 090 } 091 }; 092 093 public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException; 094 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 private static final long serialVersionUID = 1L; 103 private static final BigDecimal TIME_DIVISOR = new BigDecimal(1000); 104 105 @PluginFactory 106 public static GelfLayout createLayout( 107 //@formatter:off 108 @PluginAttribute("host") final String host, 109 @PluginElement("AdditionalField") final KeyValuePair[] additionalFields, 110 @PluginAttribute(value = "compressionType", 111 defaultString = "GZIP") final CompressionType compressionType, 112 @PluginAttribute(value = "compressionThreshold", 113 defaultInt= COMPRESSION_THRESHOLD) final int compressionThreshold) { 114 // @formatter:on 115 return new GelfLayout(host, additionalFields, compressionType, compressionThreshold); 116 } 117 118 /** 119 * http://en.wikipedia.org/wiki/Syslog#Severity_levels 120 */ 121 static int formatLevel(final Level level) { 122 return Severity.getSeverity(level).getCode(); 123 } 124 125 static String formatThrowable(final Throwable throwable) { 126 // stack traces are big enough to provide a reasonably large initial capacity here 127 final StringWriter sw = new StringWriter(2048); 128 final PrintWriter pw = new PrintWriter(sw); 129 throwable.printStackTrace(pw); 130 pw.flush(); 131 return sw.toString(); 132 } 133 134 static String formatTimestamp(final long timeMillis) { 135 return new BigDecimal(timeMillis).divide(TIME_DIVISOR).toPlainString(); 136 } 137 138 private final KeyValuePair[] additionalFields; 139 140 private final int compressionThreshold; 141 142 private final CompressionType compressionType; 143 144 private final String host; 145 146 public GelfLayout(final String host, final KeyValuePair[] additionalFields, final CompressionType compressionType, 147 final int compressionThreshold) { 148 super(Constants.UTF_8); 149 this.host = host; 150 this.additionalFields = additionalFields; 151 this.compressionType = compressionType; 152 this.compressionThreshold = compressionThreshold; 153 } 154 155 private byte[] compress(final byte[] bytes) { 156 try { 157 final ByteArrayOutputStream baos = new ByteArrayOutputStream(compressionThreshold / 8); 158 final DeflaterOutputStream stream = compressionType.createDeflaterOutputStream(baos); 159 if (stream == null) { 160 return bytes; 161 } 162 stream.write(bytes); 163 stream.finish(); 164 stream.close(); 165 return baos.toByteArray(); 166 } catch (final IOException e) { 167 StatusLogger.getLogger().error(e); 168 return bytes; 169 } 170 } 171 172 @Override 173 public Map<String, String> getContentFormat() { 174 return Collections.emptyMap(); 175 } 176 177 @Override 178 public String getContentType() { 179 return JsonLayout.CONTENT_TYPE + "; charset=" + this.getCharset(); 180 } 181 182 @Override 183 public byte[] toByteArray(final LogEvent event) { 184 final byte[] bytes = getBytes(toSerializable(event)); 185 return bytes.length > compressionThreshold ? compress(bytes) : bytes; 186 } 187 188 @Override 189 public String toSerializable(final LogEvent event) { 190 final StringBuilder builder = new StringBuilder(256); 191 final JsonStringEncoder jsonEncoder = JsonStringEncoder.getInstance(); 192 builder.append('{'); 193 builder.append("\"version\":\"1.1\","); 194 builder.append("\"host\":\"").append(jsonEncoder.quoteAsString(host)).append(QC); 195 builder.append("\"timestamp\":").append(formatTimestamp(event.getTimeMillis())).append(C); 196 builder.append("\"level\":").append(formatLevel(event.getLevel())).append(C); 197 if (event.getThreadName() != null) { 198 builder.append("\"_thread\":\"").append(jsonEncoder.quoteAsString(event.getThreadName())).append(QC); 199 } 200 if (event.getLoggerName() != null) { 201 builder.append("\"_logger\":\"").append(jsonEncoder.quoteAsString(event.getLoggerName())).append(QC); 202 } 203 204 for (final KeyValuePair additionalField : additionalFields) { 205 builder.append(QU).append(jsonEncoder.quoteAsString(additionalField.getKey())).append("\":\"") 206 .append(jsonEncoder.quoteAsString(additionalField.getValue())).append(QC); 207 } 208 for (final Map.Entry<String, String> entry : event.getContextMap().entrySet()) { 209 builder.append(QU).append(jsonEncoder.quoteAsString(entry.getKey())).append("\":\"") 210 .append(jsonEncoder.quoteAsString(entry.getValue())).append(QC); 211 } 212 if (event.getThrown() != null) { 213 builder.append("\"full_message\":\"").append(jsonEncoder.quoteAsString(formatThrowable(event.getThrown()))) 214 .append(QC); 215 } 216 217 builder.append("\"short_message\":\"") 218 .append(jsonEncoder.quoteAsString(event.getMessage().getFormattedMessage())).append(Q); 219 builder.append('}'); 220 return builder.toString(); 221 } 222 }