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