View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.layout;
18  
19  import java.io.UnsupportedEncodingException;
20  import java.nio.charset.Charset;
21  import java.nio.charset.StandardCharsets;
22  
23  import org.apache.logging.log4j.core.LogEvent;
24  import org.apache.logging.log4j.core.StringLayout;
25  import org.apache.logging.log4j.core.config.Configuration;
26  import org.apache.logging.log4j.core.config.LoggerConfig;
27  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
28  import org.apache.logging.log4j.core.config.plugins.PluginElement;
29  import org.apache.logging.log4j.core.impl.DefaultLogEventFactory;
30  import org.apache.logging.log4j.core.util.Constants;
31  import org.apache.logging.log4j.core.util.StringEncoder;
32  import org.apache.logging.log4j.util.PropertiesUtil;
33  import org.apache.logging.log4j.util.Strings;
34  
35  /**
36   * Abstract base class for Layouts that result in a String.
37   * <p>
38   * Since 2.4.1, this class has custom logic to convert ISO-8859-1 or US-ASCII Strings to byte[] arrays to improve
39   * performance: all characters are simply cast to bytes.
40   * </p>
41   */
42  /*
43   * Implementation note: prefer String.getBytes(String) to String.getBytes(Charset) for performance reasons. See
44   * https://issues.apache.org/jira/browse/LOG4J2-935 for details.
45   */
46  public abstract class AbstractStringLayout extends AbstractLayout<String> implements StringLayout {
47  
48      public abstract static class Builder<B extends Builder<B>> extends AbstractLayout.Builder<B> {
49          
50          @PluginBuilderAttribute(value = "charset")
51          private Charset charset;
52  
53          @PluginElement("footerSerializer")
54          private Serializer footerSerializer;
55  
56          @PluginElement("headerSerializer")
57          private Serializer headerSerializer;
58  
59          public Charset getCharset() {
60              return charset;
61          }
62  
63          public Serializer getFooterSerializer() {
64              return footerSerializer;
65          }
66  
67          public Serializer getHeaderSerializer() {
68              return headerSerializer;
69          }
70  
71          public B setCharset(Charset charset) {
72              this.charset = charset;
73              return asBuilder();
74          }
75  
76          public B setFooterSerializer(Serializer footerSerializer) {
77              this.footerSerializer = footerSerializer;
78              return asBuilder();
79          }
80  
81          public B setHeaderSerializer(Serializer headerSerializer) {
82              this.headerSerializer = headerSerializer;
83              return asBuilder();
84          }
85          
86      }
87      
88      public interface Serializer {
89          String toSerializable(final LogEvent event);
90      }
91  
92      /**
93       * Variation of {@link Serializer} that avoids allocating temporary objects.
94       * @since 2.6
95       */
96      public interface Serializer2 {
97          StringBuilder toSerializable(final LogEvent event, final StringBuilder builder);
98      }
99  
100     /**
101      * Default length for new StringBuilder instances: {@value} .
102      */
103     protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
104 
105     protected static final int MAX_STRING_BUILDER_SIZE = Math.max(DEFAULT_STRING_BUILDER_SIZE,
106             size("log4j.layoutStringBuilder.maxSize", 2 * 1024));
107 
108     private static final ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
109 
110     /**
111      * Returns a {@code StringBuilder} that this Layout implementation can use to write the formatted log event to.
112      *
113      * @return a {@code StringBuilder}
114      */
115     protected static StringBuilder getStringBuilder() {
116         StringBuilder result = threadLocal.get();
117         if (result == null) {
118             result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
119             threadLocal.set(result);
120         }
121         trimToMaxSize(result);
122         result.setLength(0);
123         return result;
124     }
125 
126     // LOG4J2-1151: If the built-in JDK 8 encoders are available we should use them.
127     private static boolean isPreJava8() {
128         final String version = System.getProperty("java.version");
129         final String[] parts = version.split("\\.");
130         try {
131             final int major = Integer.parseInt(parts[1]);
132             return major < 8;
133         } catch (final Exception ex) {
134             return true;
135         }
136     }
137 
138     private static int size(final String property, final int defaultValue) {
139         return PropertiesUtil.getProperties().getIntegerProperty(property, defaultValue);
140     }
141 
142     protected static void trimToMaxSize(final StringBuilder stringBuilder) {
143         if (stringBuilder.length() > MAX_STRING_BUILDER_SIZE) {
144             stringBuilder.setLength(MAX_STRING_BUILDER_SIZE);
145             stringBuilder.trimToSize();
146         }
147     }
148 
149     private Encoder<StringBuilder> textEncoder;
150     /**
151      * The charset for the formatted message.
152      */
153     // LOG4J2-1099: charset cannot be final due to serialization needs, so we serialize as charset name instead
154     private transient Charset charset;
155 
156     private final String charsetName;
157 
158     private final Serializer footerSerializer;
159 
160     private final Serializer headerSerializer;
161 
162     private final boolean useCustomEncoding;
163 
164     protected AbstractStringLayout(final Charset charset) {
165         this(charset, (byte[]) null, (byte[]) null);
166     }
167 
168     /**
169      * Builds a new layout.
170      * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
171      *      converted from strings to bytes.
172      * @param header the header bytes
173      * @param footer the footer bytes
174      */
175     protected AbstractStringLayout(final Charset aCharset, final byte[] header, final byte[] footer) {
176         super(null, header, footer);
177         this.headerSerializer = null;
178         this.footerSerializer = null;
179         this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
180         this.charsetName = this.charset.name();
181         useCustomEncoding = isPreJava8()
182                 && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
183         textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
184     }
185 
186     /**
187      * Builds a new layout.
188      * @param config the configuration
189      * @param aCharset the charset used to encode the header bytes, footer bytes and anything else that needs to be
190      *      converted from strings to bytes.
191      * @param headerSerializer the header bytes serializer
192      * @param footerSerializer the footer bytes serializer
193      */
194     protected AbstractStringLayout(final Configuration config, final Charset aCharset,
195             final Serializer headerSerializer, final Serializer footerSerializer) {
196         super(config, null, null);
197         this.headerSerializer = headerSerializer;
198         this.footerSerializer = footerSerializer;
199         this.charset = aCharset == null ? StandardCharsets.UTF_8 : aCharset;
200         this.charsetName = this.charset.name();
201         useCustomEncoding = isPreJava8()
202                 && (StandardCharsets.ISO_8859_1.equals(aCharset) || StandardCharsets.US_ASCII.equals(aCharset));
203         textEncoder = Constants.ENABLE_DIRECT_ENCODERS ? new StringBuilderEncoder(charset) : null;
204     }
205 
206     protected byte[] getBytes(final String s) {
207         if (useCustomEncoding) { // rely on branch prediction to eliminate this check if false
208             return StringEncoder.encodeSingleByteChars(s);
209         }
210         try { // LOG4J2-935: String.getBytes(String) gives better performance
211             return s.getBytes(charsetName);
212         } catch (final UnsupportedEncodingException e) {
213             return s.getBytes(charset);
214         }
215     }
216 
217     @Override
218     public Charset getCharset() {
219         return charset;
220     }
221 
222     /**
223      * @return The default content type for Strings.
224      */
225     @Override
226     public String getContentType() {
227         return "text/plain";
228     }
229 
230     /**
231      * Returns the footer, if one is available.
232      *
233      * @return A byte array containing the footer.
234      */
235     @Override
236     public byte[] getFooter() {
237         return serializeToBytes(footerSerializer, super.getFooter());
238     }
239 
240     public Serializer getFooterSerializer() {
241         return footerSerializer;
242     }
243 
244     /**
245      * Returns the header, if one is available.
246      *
247      * @return A byte array containing the header.
248      */
249     @Override
250     public byte[] getHeader() {
251         return serializeToBytes(headerSerializer, super.getHeader());
252     }
253 
254     public Serializer getHeaderSerializer() {
255         return headerSerializer;
256     }
257 
258     private DefaultLogEventFactory getLogEventFactory() {
259         return DefaultLogEventFactory.getInstance();
260     }
261 
262     /**
263      * Returns a {@code Encoder<StringBuilder>} that this Layout implementation can use for encoding log events.
264      *
265      * @return a {@code Encoder<StringBuilder>}
266      */
267     protected Encoder<StringBuilder> getStringBuilderEncoder() {
268         if (textEncoder == null) {
269             textEncoder = new StringBuilderEncoder(getCharset());
270         }
271         return textEncoder;
272     }
273 
274     protected byte[] serializeToBytes(final Serializer serializer, final byte[] defaultValue) {
275         final String serializable = serializeToString(serializer);
276         if (serializer == null) {
277             return defaultValue;
278         }
279         return StringEncoder.toBytes(serializable, getCharset());
280     }
281 
282     protected String serializeToString(final Serializer serializer) {
283         if (serializer == null) {
284             return null;
285         }
286         final LoggerConfig rootLogger = getConfiguration().getRootLogger();
287         // Using "" for the FQCN, does it matter?
288         final LogEvent logEvent = getLogEventFactory().createEvent(rootLogger.getName(), null, Strings.EMPTY,
289                 rootLogger.getLevel(), null, null, null);
290         return serializer.toSerializable(logEvent);
291     }
292 
293     /**
294      * Formats the Log Event as a byte array.
295      *
296      * @param event The Log Event.
297      * @return The formatted event as a byte array.
298      */
299     @Override
300     public byte[] toByteArray(final LogEvent event) {
301         return getBytes(toSerializable(event));
302     }
303 
304 }