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 java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.io.PrintWriter; 023import java.io.StringWriter; 024import java.math.BigDecimal; 025import java.nio.charset.StandardCharsets; 026import java.util.Collections; 027import java.util.Map; 028import java.util.zip.DeflaterOutputStream; 029import java.util.zip.GZIPOutputStream; 030 031import org.apache.logging.log4j.Level; 032import org.apache.logging.log4j.core.Layout; 033import org.apache.logging.log4j.core.LogEvent; 034import org.apache.logging.log4j.core.config.Node; 035import org.apache.logging.log4j.core.config.plugins.Plugin; 036import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 037import org.apache.logging.log4j.core.config.plugins.PluginElement; 038import org.apache.logging.log4j.core.config.plugins.PluginFactory; 039import org.apache.logging.log4j.core.net.Severity; 040import org.apache.logging.log4j.core.util.KeyValuePair; 041import org.apache.logging.log4j.status.StatusLogger; 042import org.apache.logging.log4j.util.Strings; 043 044import com.fasterxml.jackson.core.io.JsonStringEncoder; 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://graylog2.org/gelf">GELF home page</a> 069 * @see <a href="http://graylog2.org/resources/gelf/specification">GELF 070 * specification</a> 071 */ 072@Plugin(name = "GelfLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) 073public final class GelfLayout extends AbstractStringLayout { 074 075 public static enum CompressionType { 076 077 GZIP { 078 @Override 079 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 080 return new GZIPOutputStream(os); 081 } 082 }, 083 ZLIB { 084 @Override 085 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 086 return new DeflaterOutputStream(os); 087 } 088 }, 089 OFF { 090 @Override 091 public DeflaterOutputStream createDeflaterOutputStream(final OutputStream os) throws IOException { 092 return null; 093 } 094 }; 095 096 public abstract DeflaterOutputStream createDeflaterOutputStream(OutputStream os) throws IOException; 097 098 } 099 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 //@formatter:off 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 // @formatter:on 118 return new GelfLayout(host, additionalFields, compressionType, compressionThreshold); 119 } 120 121 /** 122 * http://en.wikipedia.org/wiki/Syslog#Severity_levels 123 */ 124 static int formatLevel(final Level level) { 125 return Severity.getSeverity(level).getCode(); 126 } 127 128 static String formatThrowable(final Throwable throwable) { 129 // stack traces are big enough to provide a reasonably large initial capacity here 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}