| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache license, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the license for the specific language governing permissions and |
| * limitations under the license. |
| */ |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache license, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the license for the specific language governing permissions and |
| * limitations under the license. |
| */ |
| |
| package org.apache.logging.log4j.jackson.layout; |
| |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.nio.charset.Charset; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| import org.apache.logging.log4j.core.LogEvent; |
| import org.apache.logging.log4j.core.config.Configuration; |
| import org.apache.logging.log4j.plugins.PluginBuilderAttribute; |
| import org.apache.logging.log4j.plugins.PluginElement; |
| import org.apache.logging.log4j.core.impl.Log4jLogEvent; |
| import org.apache.logging.log4j.core.layout.AbstractStringLayout; |
| import org.apache.logging.log4j.core.lookup.StrSubstitutor; |
| import org.apache.logging.log4j.core.util.KeyValuePair; |
| import org.apache.logging.log4j.core.util.StringBuilderWriter; |
| import org.apache.logging.log4j.jackson.XmlConstants; |
| import org.apache.logging.log4j.util.Strings; |
| |
| import com.fasterxml.jackson.annotation.JsonAnyGetter; |
| import com.fasterxml.jackson.annotation.JsonRootName; |
| import com.fasterxml.jackson.annotation.JsonUnwrapped; |
| import com.fasterxml.jackson.core.JsonGenerationException; |
| import com.fasterxml.jackson.databind.JsonMappingException; |
| import com.fasterxml.jackson.databind.ObjectWriter; |
| |
| abstract class AbstractJacksonLayout extends AbstractStringLayout { |
| |
| public static abstract class Builder<B extends Builder<B>> extends AbstractStringLayout.Builder<B> { |
| |
| @PluginBuilderAttribute |
| private boolean eventEol; |
| |
| @PluginBuilderAttribute |
| private String endOfLine; |
| |
| @PluginBuilderAttribute |
| private boolean compact; |
| |
| @PluginBuilderAttribute |
| private boolean complete; |
| |
| @PluginBuilderAttribute |
| private boolean locationInfo; |
| |
| @PluginBuilderAttribute |
| private boolean properties; |
| |
| @PluginBuilderAttribute |
| private boolean includeStacktrace = true; |
| |
| @PluginBuilderAttribute |
| private boolean stacktraceAsString = false; |
| |
| @PluginBuilderAttribute |
| private boolean includeNullDelimiter = false; |
| |
| @PluginElement("AdditionalField") |
| private KeyValuePair[] additionalFields; |
| |
| public KeyValuePair[] getAdditionalFields() { |
| return additionalFields; |
| } |
| |
| public boolean getEventEol() { |
| return eventEol; |
| } |
| |
| public String getEndOfLine() { |
| return endOfLine; |
| } |
| |
| public boolean isCompact() { |
| return compact; |
| } |
| |
| public boolean isComplete() { |
| return complete; |
| } |
| |
| public boolean isIncludeNullDelimiter() { |
| return includeNullDelimiter; |
| } |
| |
| /** |
| * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". |
| * |
| * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". |
| */ |
| public boolean isIncludeStacktrace() { |
| return includeStacktrace; |
| } |
| |
| public boolean isLocationInfo() { |
| return locationInfo; |
| } |
| |
| public boolean isProperties() { |
| return properties; |
| } |
| |
| public boolean isStacktraceAsString() { |
| return stacktraceAsString; |
| } |
| |
| /** |
| * Additional fields to set on each log event. |
| * |
| * @return this builder |
| */ |
| public B setAdditionalFields(final KeyValuePair[] additionalFields) { |
| this.additionalFields = additionalFields; |
| return asBuilder(); |
| } |
| |
| public B setCompact(final boolean compact) { |
| this.compact = compact; |
| return asBuilder(); |
| } |
| |
| public B setComplete(final boolean complete) { |
| this.complete = complete; |
| return asBuilder(); |
| } |
| |
| public B setEventEol(final boolean eventEol) { |
| this.eventEol = eventEol; |
| return asBuilder(); |
| } |
| |
| public B setEndOfLine(final String endOfLine) { |
| this.endOfLine = endOfLine; |
| return asBuilder(); |
| } |
| |
| /** |
| * Whether to include NULL byte as delimiter after each event (optional, default to false). |
| * |
| * @return this builder |
| */ |
| public B setIncludeNullDelimiter(final boolean includeNullDelimiter) { |
| this.includeNullDelimiter = includeNullDelimiter; |
| return asBuilder(); |
| } |
| |
| /** |
| * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". |
| * |
| * @param includeStacktrace |
| * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". |
| * @return this builder |
| */ |
| public B setIncludeStacktrace(final boolean includeStacktrace) { |
| this.includeStacktrace = includeStacktrace; |
| return asBuilder(); |
| } |
| |
| public B setLocationInfo(final boolean locationInfo) { |
| this.locationInfo = locationInfo; |
| return asBuilder(); |
| } |
| |
| public B setProperties(final boolean properties) { |
| this.properties = properties; |
| return asBuilder(); |
| } |
| |
| /** |
| * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false). |
| * |
| * @return this builder |
| */ |
| public B setStacktraceAsString(final boolean stacktraceAsString) { |
| this.stacktraceAsString = stacktraceAsString; |
| return asBuilder(); |
| } |
| |
| protected String toStringOrNull(final byte[] header) { |
| return header == null ? null : new String(header, Charset.defaultCharset()); |
| } |
| } |
| @JsonRootName(XmlConstants.ELT_EVENT) |
| public static class LogEventWrapperWithAdditionalFields { |
| |
| private final Object logEvent; |
| private final Map<String, String> additionalFields; |
| |
| public LogEventWrapperWithAdditionalFields(final Object logEvent, final Map<String, String> additionalFields) { |
| this.logEvent = logEvent; |
| this.additionalFields = additionalFields; |
| } |
| |
| @JsonAnyGetter |
| @SuppressWarnings("unused") |
| public Map<String, String> getAdditionalFields() { |
| return additionalFields; |
| } |
| |
| @JsonUnwrapped |
| public Object getLogEvent() { |
| return logEvent; |
| } |
| } |
| |
| protected static class ResolvableKeyValuePair { |
| |
| final String key; |
| final String value; |
| final boolean valueNeedsLookup; |
| |
| ResolvableKeyValuePair(final KeyValuePair pair) { |
| this.key = pair.getKey(); |
| this.value = pair.getValue(); |
| this.valueNeedsLookup = AbstractJacksonLayout.valueNeedsLookup(this.value); |
| } |
| } |
| |
| protected static final String DEFAULT_EOL = "\r\n"; |
| |
| protected static final String COMPACT_EOL = Strings.EMPTY; |
| private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { |
| // TODO Jackson-based layouts have certain filters set up for Log4jLogEvent. |
| // TODO Need to set up the same filters for MutableLogEvent but don't know how... |
| // This is a workaround. |
| return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); |
| } |
| private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, |
| final KeyValuePair[] additionalFields) { |
| if (additionalFields == null || additionalFields.length == 0) { |
| // No fields set |
| return new ResolvableKeyValuePair[0]; |
| } |
| |
| // Convert to specific class which already determines whether values needs lookup during serialization |
| final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; |
| |
| for (int i = 0; i < additionalFields.length; i++) { |
| final ResolvableKeyValuePair resolvable = resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); |
| |
| // Validate |
| if (config == null && resolvable.valueNeedsLookup) { |
| throw new IllegalArgumentException( |
| "configuration needs to be set when there are additional fields with variables"); |
| } |
| } |
| |
| return resolvableFields; |
| } |
| protected static boolean valueNeedsLookup(final String value) { |
| return value != null && value.contains("${"); |
| } |
| protected final String eol; |
| protected final ObjectWriter objectWriter; |
| |
| protected final boolean compact; |
| |
| protected final boolean complete; |
| |
| protected final boolean includeNullDelimiter; |
| |
| protected final ResolvableKeyValuePair[] additionalFields; |
| |
| protected AbstractJacksonLayout(final Configuration config, final ObjectWriter objectWriter, final Charset charset, |
| final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer, |
| final Serializer footerSerializer, final boolean includeNullDelimiter, |
| final KeyValuePair[] additionalFields) { |
| this(config, objectWriter, charset, compact, complete, eventEol, null, |
| headerSerializer, footerSerializer, includeNullDelimiter, additionalFields); |
| } |
| |
| |
| protected AbstractJacksonLayout(final Configuration config, final ObjectWriter objectWriter, final Charset charset, |
| final boolean compact, final boolean complete, final boolean eventEol, String endOfLine, |
| final Serializer headerSerializer, final Serializer footerSerializer, final boolean includeNullDelimiter, |
| final KeyValuePair[] additionalFields) { |
| super(config, charset, headerSerializer, footerSerializer); |
| this.objectWriter = objectWriter; |
| this.compact = compact; |
| this.complete = complete; |
| this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; |
| this.includeNullDelimiter = includeNullDelimiter; |
| this.additionalFields = prepareAdditionalFields(config, additionalFields); |
| } |
| |
| protected LogEventWrapperWithAdditionalFields createLogEventWrapperWithAdditionalFields(final LogEvent event, |
| final Map<String, String> additionalFieldsMap) { |
| return new LogEventWrapperWithAdditionalFields(event, additionalFieldsMap); |
| } |
| |
| private Map<String, String> resolveAdditionalFields(final LogEvent logEvent) { |
| // Note: LinkedHashMap retains order |
| final Map<String, String> additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); |
| final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); |
| |
| // Go over each field |
| for (final ResolvableKeyValuePair pair : additionalFields) { |
| if (pair.valueNeedsLookup) { |
| // Resolve value |
| additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); |
| } else { |
| // Plain text value |
| additionalFieldsMap.put(pair.key, pair.value); |
| } |
| } |
| |
| return additionalFieldsMap; |
| } |
| |
| /** |
| * Formats a {@link org.apache.logging.log4j.core.LogEvent}. |
| * |
| * @param event |
| * The LogEvent. |
| * @return The XML representation of the LogEvent. |
| */ |
| @Override |
| public String toSerializable(final LogEvent event) { |
| final StringBuilderWriter writer = new StringBuilderWriter(); |
| try { |
| toSerializable(event, writer); |
| return writer.toString(); |
| } catch (final IOException e) { |
| // Should this be an ISE or IAE? |
| LOGGER.error(e); |
| return Strings.EMPTY; |
| } |
| } |
| |
| public void toSerializable(final LogEvent event, final Writer writer) |
| throws JsonGenerationException, JsonMappingException, IOException { |
| objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); |
| writer.write(eol); |
| if (includeNullDelimiter) { |
| writer.write('\0'); |
| } |
| markEvent(); |
| } |
| |
| protected Object wrapLogEvent(final LogEvent event) { |
| if (additionalFields.length > 0) { |
| // Construct map for serialization - note that we are intentionally using original LogEvent |
| final Map<String, String> additionalFieldsMap = resolveAdditionalFields(event); |
| // This class combines LogEvent with AdditionalFields during serialization |
| return createLogEventWrapperWithAdditionalFields(event, additionalFieldsMap); |
| } |
| // No additional fields, return original object |
| return event; |
| } |
| } |