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