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.IOException;
20  import java.io.Writer;
21  import java.nio.charset.Charset;
22  import java.util.LinkedHashMap;
23  import java.util.Map;
24  
25  import org.apache.logging.log4j.core.LogEvent;
26  import org.apache.logging.log4j.core.config.Configuration;
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.MutableLogEvent;
30  import org.apache.logging.log4j.core.jackson.XmlConstants;
31  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
32  import org.apache.logging.log4j.core.util.KeyValuePair;
33  import org.apache.logging.log4j.core.util.StringBuilderWriter;
34  import org.apache.logging.log4j.util.Strings;
35  
36  import com.fasterxml.jackson.annotation.JsonAnyGetter;
37  import com.fasterxml.jackson.annotation.JsonRootName;
38  import com.fasterxml.jackson.annotation.JsonUnwrapped;
39  import com.fasterxml.jackson.core.JsonGenerationException;
40  import com.fasterxml.jackson.databind.JsonMappingException;
41  import com.fasterxml.jackson.databind.ObjectWriter;
42  import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
43  
44  abstract class AbstractJacksonLayout extends AbstractStringLayout {
45  
46      protected static final String DEFAULT_EOL = "\r\n";
47      protected static final String COMPACT_EOL = Strings.EMPTY;
48  
49      public static abstract class Builder<B extends Builder<B>> extends AbstractStringLayout.Builder<B> {
50  
51          @PluginBuilderAttribute
52          private boolean eventEol;
53  
54          @PluginBuilderAttribute
55          private boolean compact;
56  
57          @PluginBuilderAttribute
58          private boolean complete;
59  
60          @PluginBuilderAttribute
61          private boolean locationInfo;
62  
63          @PluginBuilderAttribute
64          private boolean properties;
65  
66          @PluginBuilderAttribute
67          private boolean includeStacktrace = true;
68  
69          @PluginBuilderAttribute
70          private boolean stacktraceAsString = false;
71  
72          @PluginBuilderAttribute
73          private boolean includeNullDelimiter = false;
74  
75          @PluginElement("AdditionalField")
76          private KeyValuePair[] additionalFields;
77  
78          protected String toStringOrNull(final byte[] header) {
79              return header == null ? null : new String(header, Charset.defaultCharset());
80          }
81  
82          public boolean getEventEol() {
83              return eventEol;
84          }
85  
86          public boolean isCompact() {
87              return compact;
88          }
89  
90          public boolean isComplete() {
91              return complete;
92          }
93  
94          public boolean isLocationInfo() {
95              return locationInfo;
96          }
97  
98          public boolean isProperties() {
99              return properties;
100         }
101 
102         /**
103          * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true".
104          * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true".
105          */
106         public boolean isIncludeStacktrace() {
107             return includeStacktrace;
108         }
109 
110         public boolean isStacktraceAsString() {
111             return stacktraceAsString;
112         }
113 
114         public boolean isIncludeNullDelimiter() { return includeNullDelimiter; }
115 
116         public KeyValuePair[] getAdditionalFields() {
117             return additionalFields;
118         }
119 
120         public B setEventEol(final boolean eventEol) {
121             this.eventEol = eventEol;
122             return asBuilder();
123         }
124 
125         public B setCompact(final boolean compact) {
126             this.compact = compact;
127             return asBuilder();
128         }
129 
130         public B setComplete(final boolean complete) {
131             this.complete = complete;
132             return asBuilder();
133         }
134 
135         public B setLocationInfo(final boolean locationInfo) {
136             this.locationInfo = locationInfo;
137             return asBuilder();
138         }
139 
140         public B setProperties(final boolean properties) {
141             this.properties = properties;
142             return asBuilder();
143         }
144 
145         /**
146          * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true".
147          * @param includeStacktrace If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true".
148          * @return this builder
149          */
150         public B setIncludeStacktrace(final boolean includeStacktrace) {
151             this.includeStacktrace = includeStacktrace;
152             return asBuilder();
153         }
154 
155         /**
156          * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false).
157          *
158          * @return this builder
159          */
160         public B setStacktraceAsString(final boolean stacktraceAsString) {
161             this.stacktraceAsString = stacktraceAsString;
162             return asBuilder();
163         }
164 
165         /**
166          * Whether to include NULL byte as delimiter after each event (optional, default to false).
167          *
168          * @return this builder
169          */
170         public B setIncludeNullDelimiter(final boolean includeNullDelimiter) {
171             this.includeNullDelimiter = includeNullDelimiter;
172             return asBuilder();
173         }
174 
175         /**
176          * Additional fields to set on each log event.
177          *
178          * @return this builder
179          */
180         public B setAdditionalFields(KeyValuePair[] additionalFields) {
181             this.additionalFields = additionalFields;
182             return asBuilder();
183         }
184     }
185 
186     protected final String eol;
187     protected final ObjectWriter objectWriter;
188     protected final boolean compact;
189     protected final boolean complete;
190     protected final boolean includeNullDelimiter;
191     protected final ResolvableKeyValuePair[] additionalFields;
192 
193     @Deprecated
194     protected AbstractJacksonLayout(final Configuration config, final ObjectWriter objectWriter, final Charset charset,
195             final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer,
196             final Serializer footerSerializer) {
197         this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false);
198     }
199 
200     @Deprecated
201     protected AbstractJacksonLayout(final Configuration config, final ObjectWriter objectWriter, final Charset charset,
202             final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer,
203             final Serializer footerSerializer, final boolean includeNullDelimiter) {
204         this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, includeNullDelimiter, null);
205     }
206 
207     protected AbstractJacksonLayout(final Configuration config, final ObjectWriter objectWriter, final Charset charset,
208             final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer,
209             final Serializer footerSerializer, final boolean includeNullDelimiter,
210             final KeyValuePair[] additionalFields) {
211         super(config, charset, headerSerializer, footerSerializer);
212         this.objectWriter = objectWriter;
213         this.compact = compact;
214         this.complete = complete;
215         this.eol = compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL;
216         this.includeNullDelimiter = includeNullDelimiter;
217         this.additionalFields = prepareAdditionalFields(config, additionalFields);
218     }
219 
220     protected static boolean valueNeedsLookup(final String value) {
221         return value != null && value.contains("${");
222     }
223 
224     private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, final KeyValuePair[] additionalFields) {
225         if (additionalFields == null || additionalFields.length == 0) {
226             // No fields set
227             return new ResolvableKeyValuePair[0];
228         }
229 
230         // Convert to specific class which already determines whether values needs lookup during serialization
231         final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length];
232 
233         for (int i = 0; i < additionalFields.length; i++) {
234             ResolvableKeyValuePair resolvable = resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]);
235 
236             // Validate
237             if (config == null && resolvable.valueNeedsLookup) {
238                 throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables");
239             }
240         }
241 
242         return resolvableFields;
243     }
244 
245     /**
246      * Formats a {@link org.apache.logging.log4j.core.LogEvent}.
247      *
248      * @param event The LogEvent.
249      * @return The XML representation of the LogEvent.
250      */
251     @Override
252     public String toSerializable(final LogEvent event) {
253         final StringBuilderWriter writer = new StringBuilderWriter();
254         try {
255             toSerializable(event, writer);
256             return writer.toString();
257         } catch (final IOException e) {
258             // Should this be an ISE or IAE?
259             LOGGER.error(e);
260             return Strings.EMPTY;
261         }
262     }
263 
264     private static LogEvent convertMutableToLog4jEvent(final LogEvent event) {
265         // TODO Jackson-based layouts have certain filters set up for Log4jLogEvent.
266         // TODO Need to set up the same filters for MutableLogEvent but don't know how...
267         // This is a workaround.
268         return event instanceof MutableLogEvent
269                 ? ((MutableLogEvent) event).createMemento()
270                 : event;
271     }
272 
273     protected Object wrapLogEvent(final LogEvent event) {
274         if (additionalFields.length > 0) {
275             // Construct map for serialization - note that we are intentionally using original LogEvent
276             Map<String, String> additionalFieldsMap = resolveAdditionalFields(event);
277             // This class combines LogEvent with AdditionalFields during serialization
278             return new LogEventWithAdditionalFields(event, additionalFieldsMap);
279         } else {
280             // No additional fields, return original object
281             return event;
282         }
283     }
284 
285     private Map<String, String> resolveAdditionalFields(LogEvent logEvent) {
286         // Note: LinkedHashMap retains order
287         final Map<String, String> additionalFieldsMap = new LinkedHashMap<>(additionalFields.length);
288         final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor();
289 
290         // Go over each field
291         for (ResolvableKeyValuePair pair : additionalFields) {
292             if (pair.valueNeedsLookup) {
293                 // Resolve value
294                 additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value));
295             } else {
296                 // Plain text value
297                 additionalFieldsMap.put(pair.key, pair.value);
298             }
299         }
300 
301         return additionalFieldsMap;
302     }
303 
304     public void toSerializable(final LogEvent event, final Writer writer)
305             throws JsonGenerationException, JsonMappingException, IOException {
306         objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event)));
307         writer.write(eol);
308         if (includeNullDelimiter) {
309             writer.write('\0');
310         }
311         markEvent();
312     }
313 
314     @JsonRootName(XmlConstants.ELT_EVENT)
315     @JacksonXmlRootElement(namespace = XmlConstants.XML_NAMESPACE, localName = XmlConstants.ELT_EVENT)
316     public static class LogEventWithAdditionalFields {
317 
318         private final Object logEvent;
319         private final Map<String, String> additionalFields;
320 
321         public LogEventWithAdditionalFields(Object logEvent, Map<String, String> additionalFields) {
322             this.logEvent = logEvent;
323             this.additionalFields = additionalFields;
324         }
325 
326         @JsonUnwrapped
327         public Object getLogEvent() {
328             return logEvent;
329         }
330 
331         @JsonAnyGetter
332         @SuppressWarnings("unused")
333         public Map<String, String> getAdditionalFields() {
334             return additionalFields;
335         }
336     }
337 
338     protected static class ResolvableKeyValuePair {
339 
340         final String key;
341         final String value;
342         final boolean valueNeedsLookup;
343 
344         ResolvableKeyValuePair(KeyValuePair pair) {
345             this.key = pair.getKey();
346             this.value = pair.getValue();
347             this.valueNeedsLookup = AbstractJacksonLayout.valueNeedsLookup(this.value);
348         }
349     }
350 }