| /* |
| * 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.layout.template.json; |
| |
| import org.apache.logging.log4j.core.Layout; |
| import org.apache.logging.log4j.core.LogEvent; |
| import org.apache.logging.log4j.core.StringLayout; |
| import org.apache.logging.log4j.core.config.Configuration; |
| import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; |
| import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; |
| import org.apache.logging.log4j.core.layout.ByteBufferDestination; |
| import org.apache.logging.log4j.core.layout.Encoder; |
| import org.apache.logging.log4j.core.layout.LockingStringBuilderEncoder; |
| import org.apache.logging.log4j.core.util.Constants; |
| import org.apache.logging.log4j.core.util.StringEncoder; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactories; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptor; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptors; |
| import org.apache.logging.log4j.layout.template.json.resolver.EventResolverStringSubstitutor; |
| import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver; |
| import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolvers; |
| import org.apache.logging.log4j.layout.template.json.util.JsonWriter; |
| import org.apache.logging.log4j.layout.template.json.util.Recycler; |
| import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory; |
| import org.apache.logging.log4j.layout.template.json.util.Uris; |
| import org.apache.logging.log4j.plugins.Node; |
| import org.apache.logging.log4j.plugins.Plugin; |
| import org.apache.logging.log4j.plugins.PluginBuilderAttribute; |
| import org.apache.logging.log4j.plugins.PluginElement; |
| import org.apache.logging.log4j.util.Strings; |
| |
| import java.nio.charset.Charset; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.function.Supplier; |
| |
| @Plugin(name = "JsonTemplateLayout", |
| category = Node.CATEGORY, |
| elementType = Layout.ELEMENT_TYPE) |
| public class JsonTemplateLayout implements StringLayout { |
| |
| private static final Map<String, String> CONTENT_FORMAT = |
| Collections.singletonMap("version", "1"); |
| |
| private final Charset charset; |
| |
| private final String contentType; |
| |
| private final TemplateResolver<LogEvent> eventResolver; |
| |
| private final String eventDelimiter; |
| |
| private final Recycler<Context> contextRecycler; |
| |
| // The class and fields are visible for tests. |
| static final class Context implements AutoCloseable { |
| |
| final JsonWriter jsonWriter; |
| |
| final Encoder<StringBuilder> encoder; |
| |
| private Context( |
| final JsonWriter jsonWriter, |
| final Encoder<StringBuilder> encoder) { |
| this.jsonWriter = jsonWriter; |
| this.encoder = encoder; |
| } |
| |
| @Override |
| public void close() { |
| jsonWriter.close(); |
| } |
| |
| } |
| |
| private JsonTemplateLayout(final Builder builder) { |
| this.charset = builder.charset; |
| this.contentType = "application/json; charset=" + charset; |
| final String eventDelimiterSuffix = builder.isNullEventDelimiterEnabled() ? "\0" : ""; |
| this.eventDelimiter = builder.eventDelimiter + eventDelimiterSuffix; |
| final Configuration configuration = builder.configuration; |
| final JsonWriter jsonWriter = JsonWriter |
| .newBuilder() |
| .setMaxStringLength(builder.maxStringLength) |
| .setTruncatedStringSuffix(builder.truncatedStringSuffix) |
| .build(); |
| this.eventResolver = createEventResolver( |
| builder, |
| configuration, |
| charset, |
| jsonWriter); |
| this.contextRecycler = createContextRecycler(builder, jsonWriter); |
| } |
| |
| private TemplateResolver<LogEvent> createEventResolver( |
| final Builder builder, |
| final Configuration configuration, |
| final Charset charset, |
| final JsonWriter jsonWriter) { |
| |
| // Inject resolver factory and interceptor plugins. |
| final List<String> pluginPackages = configuration.getPluginPackages(); |
| final Map<String, EventResolverFactory> resolverFactoryByName = |
| EventResolverFactories.populateResolverFactoryByName(pluginPackages); |
| final List<EventResolverInterceptor> resolverInterceptors = |
| EventResolverInterceptors.populateInterceptors(pluginPackages); |
| final EventResolverStringSubstitutor substitutor = |
| new EventResolverStringSubstitutor(configuration.getStrSubstitutor()); |
| |
| // Read event and stack trace element templates. |
| final String eventTemplate = readEventTemplate(builder); |
| final String stackTraceElementTemplate = readStackTraceElementTemplate(builder); |
| |
| // Determine the max. string byte count. |
| final float maxByteCountPerChar = builder.charset.newEncoder().maxBytesPerChar(); |
| final int maxStringByteCount = |
| Math.toIntExact(Math.round(Math.ceil( |
| maxByteCountPerChar * builder.maxStringLength))); |
| |
| // Replace null event template additional fields with an empty array. |
| final EventTemplateAdditionalField[] eventTemplateAdditionalFields = |
| builder.eventTemplateAdditionalFields != null |
| ? builder.eventTemplateAdditionalFields |
| : new EventTemplateAdditionalField[0]; |
| |
| // Create the resolver context. |
| final EventResolverContext resolverContext = EventResolverContext |
| .newBuilder() |
| .setConfiguration(configuration) |
| .setResolverFactoryByName(resolverFactoryByName) |
| .setResolverInterceptors(resolverInterceptors) |
| .setSubstitutor(substitutor) |
| .setCharset(charset) |
| .setJsonWriter(jsonWriter) |
| .setRecyclerFactory(builder.recyclerFactory) |
| .setMaxStringByteCount(maxStringByteCount) |
| .setTruncatedStringSuffix(builder.truncatedStringSuffix) |
| .setLocationInfoEnabled(builder.locationInfoEnabled) |
| .setStackTraceEnabled(builder.stackTraceEnabled) |
| .setStackTraceElementTemplate(stackTraceElementTemplate) |
| .setEventTemplateRootObjectKey(builder.eventTemplateRootObjectKey) |
| .setEventTemplateAdditionalFields(eventTemplateAdditionalFields) |
| .build(); |
| |
| // Compile the resolver template. |
| return TemplateResolvers.ofTemplate(resolverContext, eventTemplate); |
| |
| } |
| |
| private static String readEventTemplate(final Builder builder) { |
| return readTemplate( |
| builder.eventTemplate, |
| builder.eventTemplateUri, |
| builder.charset); |
| } |
| |
| private static String readStackTraceElementTemplate(final Builder builder) { |
| return readTemplate( |
| builder.stackTraceElementTemplate, |
| builder.stackTraceElementTemplateUri, |
| builder.charset); |
| } |
| |
| private static String readTemplate( |
| final String template, |
| final String templateUri, |
| final Charset charset) { |
| return Strings.isBlank(template) |
| ? Uris.readUri(templateUri, charset) |
| : template; |
| } |
| |
| private static Recycler<Context> createContextRecycler( |
| final Builder builder, |
| final JsonWriter jsonWriter) { |
| final Supplier<Context> supplier = |
| createContextSupplier(builder.charset, jsonWriter); |
| return builder |
| .recyclerFactory |
| .create(supplier, Context::close); |
| } |
| |
| private static Supplier<Context> createContextSupplier( |
| final Charset charset, |
| final JsonWriter jsonWriter) { |
| return () -> { |
| final JsonWriter clonedJsonWriter = jsonWriter.clone(); |
| final Encoder<StringBuilder> encoder = |
| Constants.ENABLE_DIRECT_ENCODERS |
| ? new LockingStringBuilderEncoder(charset) |
| : null; |
| return new Context(clonedJsonWriter, encoder); |
| }; |
| } |
| |
| @Override |
| public byte[] toByteArray(final LogEvent event) { |
| final String eventJson = toSerializable(event); |
| return StringEncoder.toBytes(eventJson, charset); |
| } |
| |
| @Override |
| public String toSerializable(final LogEvent event) { |
| final Context context = acquireContext(); |
| final JsonWriter jsonWriter = context.jsonWriter; |
| final StringBuilder stringBuilder = jsonWriter.getStringBuilder(); |
| try { |
| eventResolver.resolve(event, jsonWriter); |
| stringBuilder.append(eventDelimiter); |
| return stringBuilder.toString(); |
| } finally { |
| contextRecycler.release(context); |
| } |
| } |
| |
| @Override |
| public void encode(final LogEvent event, final ByteBufferDestination destination) { |
| |
| // Acquire a context. |
| final Context context = acquireContext(); |
| final JsonWriter jsonWriter = context.jsonWriter; |
| final StringBuilder stringBuilder = jsonWriter.getStringBuilder(); |
| final Encoder<StringBuilder> encoder = context.encoder; |
| |
| try { |
| |
| // Render the JSON. |
| eventResolver.resolve(event, jsonWriter); |
| stringBuilder.append(eventDelimiter); |
| |
| // Write to the destination. |
| if (encoder == null) { |
| final String eventJson = stringBuilder.toString(); |
| final byte[] eventJsonBytes = StringEncoder.toBytes(eventJson, charset); |
| destination.writeBytes(eventJsonBytes, 0, eventJsonBytes.length); |
| } else { |
| encoder.encode(stringBuilder, destination); |
| } |
| |
| } |
| |
| // Release the context. |
| finally { |
| contextRecycler.release(context); |
| } |
| |
| } |
| |
| // Visible for tests. |
| Context acquireContext() { |
| return contextRecycler.acquire(); |
| } |
| |
| @Override |
| public byte[] getFooter() { |
| return null; |
| } |
| |
| @Override |
| public byte[] getHeader() { |
| return null; |
| } |
| |
| @Override |
| public Charset getCharset() { |
| return charset; |
| } |
| |
| @Override |
| public String getContentType() { |
| return contentType; |
| } |
| |
| @Override |
| public Map<String, String> getContentFormat() { |
| return CONTENT_FORMAT; |
| } |
| |
| @PluginBuilderFactory |
| @SuppressWarnings("WeakerAccess") |
| public static Builder newBuilder() { |
| return new Builder(); |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public static final class Builder |
| implements org.apache.logging.log4j.core.util.Builder<JsonTemplateLayout> { |
| |
| @PluginConfiguration |
| private Configuration configuration; |
| |
| @PluginBuilderAttribute |
| private Charset charset = JsonTemplateLayoutDefaults.getCharset(); |
| |
| @PluginBuilderAttribute |
| private boolean locationInfoEnabled = |
| JsonTemplateLayoutDefaults.isLocationInfoEnabled(); |
| |
| @PluginBuilderAttribute |
| private boolean stackTraceEnabled = |
| JsonTemplateLayoutDefaults.isStackTraceEnabled(); |
| |
| @PluginBuilderAttribute |
| private String eventTemplate = JsonTemplateLayoutDefaults.getEventTemplate(); |
| |
| @PluginBuilderAttribute |
| private String eventTemplateUri = |
| JsonTemplateLayoutDefaults.getEventTemplateUri(); |
| |
| @PluginBuilderAttribute |
| private String eventTemplateRootObjectKey = |
| JsonTemplateLayoutDefaults.getEventTemplateRootObjectKey(); |
| |
| @PluginElement("EventTemplateAdditionalField") |
| private EventTemplateAdditionalField[] eventTemplateAdditionalFields; |
| |
| @PluginBuilderAttribute |
| private String stackTraceElementTemplate = |
| JsonTemplateLayoutDefaults.getStackTraceElementTemplate(); |
| |
| @PluginBuilderAttribute |
| private String stackTraceElementTemplateUri = |
| JsonTemplateLayoutDefaults.getStackTraceElementTemplateUri(); |
| |
| @PluginBuilderAttribute |
| private String eventDelimiter = JsonTemplateLayoutDefaults.getEventDelimiter(); |
| |
| @PluginBuilderAttribute |
| private boolean nullEventDelimiterEnabled = |
| JsonTemplateLayoutDefaults.isNullEventDelimiterEnabled(); |
| |
| @PluginBuilderAttribute |
| private int maxStringLength = JsonTemplateLayoutDefaults.getMaxStringLength(); |
| |
| @PluginBuilderAttribute |
| private String truncatedStringSuffix = |
| JsonTemplateLayoutDefaults.getTruncatedStringSuffix(); |
| |
| @PluginBuilderAttribute |
| private RecyclerFactory recyclerFactory = |
| JsonTemplateLayoutDefaults.getRecyclerFactory(); |
| |
| private Builder() { |
| // Do nothing. |
| } |
| |
| public Configuration getConfiguration() { |
| return configuration; |
| } |
| |
| public Builder setConfiguration(final Configuration configuration) { |
| this.configuration = configuration; |
| return this; |
| } |
| |
| public Charset getCharset() { |
| return charset; |
| } |
| |
| public Builder setCharset(final Charset charset) { |
| this.charset = charset; |
| return this; |
| } |
| |
| public boolean isLocationInfoEnabled() { |
| return locationInfoEnabled; |
| } |
| |
| public Builder setLocationInfoEnabled(final boolean locationInfoEnabled) { |
| this.locationInfoEnabled = locationInfoEnabled; |
| return this; |
| } |
| |
| public boolean isStackTraceEnabled() { |
| return stackTraceEnabled; |
| } |
| |
| public Builder setStackTraceEnabled(final boolean stackTraceEnabled) { |
| this.stackTraceEnabled = stackTraceEnabled; |
| return this; |
| } |
| |
| public String getEventTemplate() { |
| return eventTemplate; |
| } |
| |
| public Builder setEventTemplate(final String eventTemplate) { |
| this.eventTemplate = eventTemplate; |
| return this; |
| } |
| |
| public String getEventTemplateUri() { |
| return eventTemplateUri; |
| } |
| |
| public Builder setEventTemplateUri(final String eventTemplateUri) { |
| this.eventTemplateUri = eventTemplateUri; |
| return this; |
| } |
| |
| public String getEventTemplateRootObjectKey() { |
| return eventTemplateRootObjectKey; |
| } |
| |
| public Builder setEventTemplateRootObjectKey(String eventTemplateRootObjectKey) { |
| this.eventTemplateRootObjectKey = eventTemplateRootObjectKey; |
| return this; |
| } |
| |
| public EventTemplateAdditionalField[] getEventTemplateAdditionalFields() { |
| return eventTemplateAdditionalFields; |
| } |
| |
| public Builder setEventTemplateAdditionalFields( |
| final EventTemplateAdditionalField[] eventTemplateAdditionalFields) { |
| this.eventTemplateAdditionalFields = eventTemplateAdditionalFields; |
| return this; |
| } |
| |
| public String getStackTraceElementTemplate() { |
| return stackTraceElementTemplate; |
| } |
| |
| public Builder setStackTraceElementTemplate( |
| final String stackTraceElementTemplate) { |
| this.stackTraceElementTemplate = stackTraceElementTemplate; |
| return this; |
| } |
| |
| public String getStackTraceElementTemplateUri() { |
| return stackTraceElementTemplateUri; |
| } |
| |
| public Builder setStackTraceElementTemplateUri( |
| final String stackTraceElementTemplateUri) { |
| this.stackTraceElementTemplateUri = stackTraceElementTemplateUri; |
| return this; |
| } |
| |
| public String getEventDelimiter() { |
| return eventDelimiter; |
| } |
| |
| public Builder setEventDelimiter(final String eventDelimiter) { |
| this.eventDelimiter = eventDelimiter; |
| return this; |
| } |
| |
| public boolean isNullEventDelimiterEnabled() { |
| return nullEventDelimiterEnabled; |
| } |
| |
| public Builder setNullEventDelimiterEnabled( |
| final boolean nullEventDelimiterEnabled) { |
| this.nullEventDelimiterEnabled = nullEventDelimiterEnabled; |
| return this; |
| } |
| |
| public int getMaxStringLength() { |
| return maxStringLength; |
| } |
| |
| public Builder setMaxStringLength(final int maxStringLength) { |
| this.maxStringLength = maxStringLength; |
| return this; |
| } |
| |
| public String getTruncatedStringSuffix() { |
| return truncatedStringSuffix; |
| } |
| |
| public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) { |
| this.truncatedStringSuffix = truncatedStringSuffix; |
| return this; |
| } |
| |
| public RecyclerFactory getRecyclerFactory() { |
| return recyclerFactory; |
| } |
| |
| public Builder setRecyclerFactory(final RecyclerFactory recyclerFactory) { |
| this.recyclerFactory = recyclerFactory; |
| return this; |
| } |
| |
| @Override |
| public JsonTemplateLayout build() { |
| validate(); |
| return new JsonTemplateLayout(this); |
| } |
| |
| private void validate() { |
| Objects.requireNonNull(configuration, "config"); |
| if (Strings.isBlank(eventTemplate) && Strings.isBlank(eventTemplateUri)) { |
| throw new IllegalArgumentException( |
| "both eventTemplate and eventTemplateUri are blank"); |
| } |
| if (stackTraceEnabled && |
| Strings.isBlank(stackTraceElementTemplate) |
| && Strings.isBlank(stackTraceElementTemplateUri)) { |
| throw new IllegalArgumentException( |
| "both stackTraceElementTemplate and stackTraceElementTemplateUri are blank"); |
| } |
| if (maxStringLength <= 0) { |
| throw new IllegalArgumentException( |
| "was expecting a non-zero positive maxStringLength: " + |
| maxStringLength); |
| } |
| Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix"); |
| Objects.requireNonNull(recyclerFactory, "recyclerFactory"); |
| } |
| |
| } |
| |
| @Plugin(name = "EventTemplateAdditionalField", |
| category = Node.CATEGORY, |
| printObject = true) |
| public static final class EventTemplateAdditionalField { |
| |
| public enum Format { STRING, JSON } |
| |
| private final String key; |
| |
| private final String value; |
| |
| private final Format format; |
| |
| private EventTemplateAdditionalField(final Builder builder) { |
| this.key = builder.key; |
| this.value = builder.value; |
| this.format = builder.format; |
| } |
| |
| public String getKey() { |
| return key; |
| } |
| |
| public String getValue() { |
| return value; |
| } |
| |
| public Format getFormat() { |
| return format; |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (this == object) { |
| return true; |
| } |
| if (object == null || getClass() != object.getClass()) { |
| return false; |
| } |
| EventTemplateAdditionalField that = (EventTemplateAdditionalField) object; |
| return key.equals(that.key) && |
| value.equals(that.value) && |
| format == that.format; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(key, value, format); |
| } |
| |
| @Override |
| public String toString() { |
| final String formattedValue = Format.STRING.equals(format) |
| ? String.format("\"%s\"", value) |
| : value; |
| return String.format("%s=%s", key, formattedValue); |
| } |
| |
| @PluginBuilderFactory |
| public static EventTemplateAdditionalField.Builder newBuilder() { |
| return new EventTemplateAdditionalField.Builder(); |
| } |
| |
| public static class Builder |
| implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalField> { |
| |
| @PluginBuilderAttribute |
| private String key; |
| |
| @PluginBuilderAttribute |
| private String value; |
| |
| @PluginBuilderAttribute |
| private Format format = Format.STRING; |
| |
| public Builder setKey(final String key) { |
| this.key = key; |
| return this; |
| } |
| |
| public Builder setValue(final String value) { |
| this.value = value; |
| return this; |
| } |
| |
| public Builder setFormat(final Format format) { |
| this.format = format; |
| return this; |
| } |
| |
| @Override |
| public EventTemplateAdditionalField build() { |
| validate(); |
| return new EventTemplateAdditionalField(this); |
| } |
| |
| private void validate() { |
| if (Strings.isBlank(key)) { |
| throw new IllegalArgumentException("blank key"); |
| } |
| if (Strings.isBlank(value)) { |
| throw new IllegalArgumentException("blank value"); |
| } |
| Objects.requireNonNull(format, "format"); |
| } |
| |
| } |
| |
| } |
| |
| } |