You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2020/06/07 20:25:12 UTC
[logging-log4j2] 01/02: #335 Replace inline resolver DSL with JSON
structure.
This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 0dccbdbfbf5130a0616cf569c8a47c5d38b8bb89
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sun Jun 7 21:10:16 2020 +0200
#335 Replace inline resolver DSL with JSON structure.
This major change replaces the custom ${json:*} resolver syntax
with configuration passed using the JSON structure. This enables
a way more versatile configuration syntax without parsing issues.
The class documentation and the manual is improved extensively
as well.
The default template is switched from JsonLayout to EcsLayout.
---
.../layout/json/template/JsonTemplateLayout.java | 149 ++-
.../json/template/JsonTemplateLayoutDefaults.java | 2 +-
.../resolver/ContextStackResolverFactory.java | 39 -
.../resolver/EndOfBatchResolverFactory.java | 4 +-
.../template/resolver/EventResolverContext.java | 10 +-
.../template/resolver/EventResolverFactories.java | 4 +-
.../resolver/ExceptionInternalResolverFactory.java | 49 +-
.../json/template/resolver/ExceptionResolver.java | 23 +-
.../resolver/ExceptionResolverFactory.java | 12 +-
.../resolver/ExceptionRootCauseResolver.java | 23 +-
.../ExceptionRootCauseResolverFactory.java | 6 +-
.../json/template/resolver/LevelResolver.java | 85 +-
.../template/resolver/LevelResolverFactory.java | 6 +-
.../json/template/resolver/LoggerResolver.java | 41 +-
.../template/resolver/LoggerResolverFactory.java | 6 +-
.../json/template/resolver/MainMapResolver.java | 49 +-
.../template/resolver/MainMapResolverFactory.java | 6 +-
.../layout/json/template/resolver/MapResolver.java | 41 +-
.../json/template/resolver/MapResolverFactory.java | 6 +-
.../json/template/resolver/MarkerResolver.java | 32 +-
.../template/resolver/MarkerResolverFactory.java | 6 +-
.../json/template/resolver/MessageResolver.java | 62 +-
.../template/resolver/MessageResolverFactory.java | 6 +-
.../json/template/resolver/PatternResolver.java | 30 +-
.../template/resolver/PatternResolverFactory.java | 6 +-
.../json/template/resolver/SourceResolver.java | 41 +-
.../template/resolver/SourceResolverFactory.java | 6 +-
.../resolver/StackTraceElementObjectResolver.java | 36 +-
.../StackTraceElementObjectResolverFactory.java | 4 +-
...Resolver.java => StackTraceStringResolver.java} | 4 +-
...verFactory.java => TemplateResolverConfig.java} | 10 +-
.../template/resolver/TemplateResolverFactory.java | 2 +-
.../json/template/resolver/TemplateResolvers.java | 169 ++--
...esolver.java => ThreadContextDataResolver.java} | 223 +++--
....java => ThreadContextDataResolverFactory.java} | 18 +-
...solver.java => ThreadContextStackResolver.java} | 65 +-
...java => ThreadContextStackResolverFactory.java} | 18 +-
.../json/template/resolver/ThreadResolver.java | 34 +-
.../template/resolver/ThreadResolverFactory.java | 6 +-
.../json/template/resolver/TimestampResolver.java | 511 +++++-----
.../resolver/TimestampResolverFactory.java | 4 +-
.../layout/json/template/util/MapAccessor.java | 139 +++
.../src/main/resources/EcsLayout.json | 56 +-
.../src/main/resources/GelfLayout.json | 44 +-
.../src/main/resources/JsonLayout.json | 95 +-
.../main/resources/LogstashJsonEventLayoutV1.json | 65 +-
.../main/resources/StackTraceElementLayout.json | 20 +-
.../log4j/layout/json/template/EcsLayoutTest.java | 29 +-
.../log4j/layout/json/template/GelfLayoutTest.java | 53 +-
.../log4j/layout/json/template/JacksonFixture.java | 7 -
.../log4j/layout/json/template/JsonLayoutTest.java | 14 +-
.../json/template/JsonTemplateLayoutTest.java | 1010 +++++++++++---------
.../json/template/LayoutComparisonHelpers.java | 6 +-
.../log4j/layout/json/template/LogstashIT.java | 24 +-
.../src/test/resources/testJsonTemplateLayout.json | 79 +-
.../template/JsonTemplateLayoutBenchmarkState.java | 31 +-
src/site/asciidoc/manual/json-template-layout.adoc | 917 +++++++++++++-----
src/site/asciidoc/manual/layouts.adoc | 65 +-
src/site/markdown/manual/cloud.md | 32 +-
59 files changed, 3050 insertions(+), 1490 deletions(-)
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
index 557741d..a5118c2 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -44,6 +44,7 @@ import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.util.Strings;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
@@ -51,8 +52,7 @@ import java.util.function.Supplier;
@Plugin(name = "JsonTemplateLayout",
category = Node.CATEGORY,
- elementType = Layout.ELEMENT_TYPE,
- printObject = true)
+ elementType = Layout.ELEMENT_TYPE)
public class JsonTemplateLayout implements StringLayout {
private static final Map<String, String> CONTENT_FORMAT =
@@ -485,6 +485,11 @@ public class JsonTemplateLayout implements StringLayout {
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");
}
@@ -494,21 +499,43 @@ public class JsonTemplateLayout implements StringLayout {
// We need this ugly model and its builder just to be able to allow
// key-value pairs in a dedicated element.
@SuppressWarnings({"unused", "WeakerAccess"})
- @Plugin(name = "EventTemplateAdditionalFields", category = Node.CATEGORY, printObject = true)
+ @Plugin(name = "EventTemplateAdditionalFields",
+ category = Node.CATEGORY,
+ printObject = true)
public static final class EventTemplateAdditionalFields {
private static final EventTemplateAdditionalFields EMPTY = newBuilder().build();
- private final KeyValuePair[] additionalFields;
+ private final EventTemplateAdditionalField[] additionalFields;
private EventTemplateAdditionalFields(final Builder builder) {
- this.additionalFields = builder.additionalFields;
+ this.additionalFields = builder.additionalFields != null
+ ? builder.additionalFields
+ : new EventTemplateAdditionalField[0];
}
- public KeyValuePair[] getAdditionalFields() {
+ public EventTemplateAdditionalField[] getAdditionalFields() {
return additionalFields;
}
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) return true;
+ if (object == null || getClass() != object.getClass()) return false;
+ EventTemplateAdditionalFields that = (EventTemplateAdditionalFields) object;
+ return Arrays.equals(additionalFields, that.additionalFields);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(additionalFields);
+ }
+
+ @Override
+ public String toString() {
+ return Arrays.toString(additionalFields);
+ }
+
@PluginBuilderFactory
public static Builder newBuilder() {
return new Builder();
@@ -518,15 +545,16 @@ public class JsonTemplateLayout implements StringLayout {
implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalFields> {
@PluginElement("AdditionalField")
- private KeyValuePair[] additionalFields;
+ private EventTemplateAdditionalField[] additionalFields;
private Builder() {}
- public KeyValuePair[] getAdditionalFields() {
+ public EventTemplateAdditionalField[] getAdditionalFields() {
return additionalFields;
}
- public Builder setAdditionalFields(final KeyValuePair[] additionalFields) {
+ public Builder setAdditionalFields(
+ final EventTemplateAdditionalField[] additionalFields) {
this.additionalFields = additionalFields;
return this;
}
@@ -540,4 +568,107 @@ public class JsonTemplateLayout implements StringLayout {
}
+ @Plugin(name = "EventTemplateAdditionalField",
+ category = Node.CATEGORY,
+ printObject = true)
+ public static final class EventTemplateAdditionalField {
+
+ public enum Type { STRING, JSON }
+
+ private final String key;
+
+ private final String value;
+
+ private final Type type;
+
+ private EventTemplateAdditionalField(final Builder builder) {
+ this.key = builder.key;
+ this.value = builder.value;
+ this.type = builder.type;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ @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) &&
+ type == that.type;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value, type);
+ }
+
+ @Override
+ public String toString() {
+ final String formattedValue = Type.STRING.equals(type)
+ ? 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> {
+
+ private String key;
+
+ private String value;
+
+ private Type type = Type.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 setType(final Type type) {
+ this.type = type;
+ 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(type, "type");
+ }
+
+ }
+
+ }
+
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
index c156e69..adfe760 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
@@ -57,7 +57,7 @@ public enum JsonTemplateLayoutDefaults {;
private static final String EVENT_TEMPLATE_URI =
PROPERTIES.getStringProperty(
"log4j.layout.jsonTemplate.eventTemplateUri",
- "classpath:JsonLayout.json");
+ "classpath:EcsLayout.json");
private static final String STACK_TRACE_ELEMENT_TEMPLATE =
PROPERTIES.getStringProperty(
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
deleted file mode 100644
index 0db832a..0000000
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.json.template.resolver;
-
-final class ContextStackResolverFactory implements EventResolverFactory<ContextStackResolver> {
-
- private static final ContextStackResolverFactory INSTANCE = new ContextStackResolverFactory();
-
- private ContextStackResolverFactory() {}
-
- static ContextStackResolverFactory getInstance() {
- return INSTANCE;
- }
-
- @Override
- public String getName() {
- return ContextStackResolver.getName();
- }
-
- @Override
- public ContextStackResolver create(final EventResolverContext context, final String key) {
- return new ContextStackResolver(key);
- }
-
-}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
index c55fc58..0f013a4 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
@@ -32,7 +32,9 @@ final class EndOfBatchResolverFactory implements EventResolverFactory<EndOfBatch
}
@Override
- public EndOfBatchResolver create(final EventResolverContext context, String key) {
+ public EndOfBatchResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
return EndOfBatchResolver.getInstance();
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
index e178a66..a83b2c9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
@@ -20,6 +20,8 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
@@ -47,7 +49,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
private final TemplateResolver<Throwable> stackTraceObjectResolver;
- private final KeyValuePair[] additionalFields;
+ private final EventTemplateAdditionalField[] additionalFields;
private EventResolverContext(final Builder builder) {
this.configuration = builder.configuration;
@@ -112,7 +114,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
return stackTraceObjectResolver;
}
- KeyValuePair[] getAdditionalFields() {
+ EventTemplateAdditionalField[] getAdditionalFields() {
return additionalFields;
}
@@ -140,7 +142,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
private TemplateResolver<StackTraceElement> stackTraceElementObjectResolver;
- private KeyValuePair[] eventTemplateAdditionalFields;
+ private EventTemplateAdditionalField[] eventTemplateAdditionalFields;
private Builder() {
// Do nothing.
@@ -193,7 +195,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
}
public Builder setEventTemplateAdditionalFields(
- final KeyValuePair[] eventTemplateAdditionalFields) {
+ final EventTemplateAdditionalField[] eventTemplateAdditionalFields) {
this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
return this;
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
index 86ea6bb..fc8c6e9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
@@ -33,8 +33,8 @@ enum EventResolverFactories {;
// Collect resolver factories.
final List<EventResolverFactory<? extends EventResolver>> resolverFactories = Arrays.asList(
- ContextDataResolverFactory.getInstance(),
- ContextStackResolverFactory.getInstance(),
+ ThreadContextDataResolverFactory.getInstance(),
+ ThreadContextStackResolverFactory.getInstance(),
EndOfBatchResolverFactory.getInstance(),
ExceptionResolverFactory.getInstance(),
ExceptionRootCauseResolverFactory.getInstance(),
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
index de1ec31..b6e5ff8 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
@@ -16,6 +16,17 @@
*/
package org.apache.logging.log4j.layout.json.template.resolver;
+/**
+ * Exception resolver factory.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = field , [ stringified ]
+ * field = "field" -> ( "className" | "message" | "stackTrace" )
+ * stringified = "stringified" -> boolean
+ * </pre>
+ */
abstract class ExceptionInternalResolverFactory {
private static final EventResolver NULL_RESOLVER =
@@ -23,48 +34,34 @@ abstract class ExceptionInternalResolverFactory {
EventResolver createInternalResolver(
final EventResolverContext context,
- final String key) {
-
- // Split the key into its major and minor components.
- String majorKey;
- String minorKey;
- final int colonIndex = key.indexOf(':');
- if (colonIndex >= 0) {
- majorKey = key.substring(0, colonIndex);
- minorKey = key.substring(colonIndex + 1);
- } else {
- majorKey = key;
- minorKey = "";
- }
-
- // Create the resolver.
- switch (majorKey) {
+ final TemplateResolverConfig config) {
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
case "className": return createClassNameResolver();
case "message": return createMessageResolver(context);
- case "stackTrace": return createStackTraceResolver(context, minorKey);
+ case "stackTrace": return createStackTraceResolver(context, config);
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
abstract EventResolver createClassNameResolver();
- abstract EventResolver createMessageResolver(final EventResolverContext context);
+ abstract EventResolver createMessageResolver(EventResolverContext context);
private EventResolver createStackTraceResolver(
final EventResolverContext context,
- final String minorKey) {
+ final TemplateResolverConfig config) {
if (!context.isStackTraceEnabled()) {
return NULL_RESOLVER;
}
- switch (minorKey) {
- case "text": return createStackTraceTextResolver(context);
- case "": return createStackTraceObjectResolver(context);
- }
- throw new IllegalArgumentException("unknown minor key: " + minorKey);
+ final boolean stringified = config.getBoolean("stringified", false);
+ return stringified
+ ? createStackTraceStringResolver(context)
+ : createStackTraceObjectResolver(context);
}
- abstract EventResolver createStackTraceTextResolver(EventResolverContext context);
+ abstract EventResolver createStackTraceStringResolver(EventResolverContext context);
abstract EventResolver createStackTraceObjectResolver(EventResolverContext context);
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
index a5ab172..140cc42 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
@@ -19,6 +19,14 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * Exception resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
class ExceptionResolver implements EventResolver {
private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
@@ -51,15 +59,15 @@ class ExceptionResolver implements EventResolver {
}
@Override
- EventResolver createStackTraceTextResolver(final EventResolverContext context) {
- StackTraceTextResolver stackTraceTextResolver =
- new StackTraceTextResolver(context);
+ EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+ StackTraceStringResolver stackTraceStringResolver =
+ new StackTraceStringResolver(context);
return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
final Throwable exception = logEvent.getThrown();
if (exception == null) {
jsonWriter.writeNull();
} else {
- stackTraceTextResolver.resolve(exception, jsonWriter);
+ stackTraceStringResolver.resolve(exception, jsonWriter);
}
};
}
@@ -82,9 +90,12 @@ class ExceptionResolver implements EventResolver {
private final EventResolver internalResolver;
- ExceptionResolver(final EventResolverContext context, final String key) {
+ ExceptionResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
this.stackTraceEnabled = context.isStackTraceEnabled();
- this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+ this.internalResolver = INTERNAL_RESOLVER_FACTORY
+ .createInternalResolver(context, config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
index 8a70bfd..7ca79b0 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
@@ -16,9 +16,11 @@
*/
package org.apache.logging.log4j.layout.json.template.resolver;
-final class ExceptionResolverFactory implements EventResolverFactory<ExceptionResolver> {
+final class ExceptionResolverFactory
+ implements EventResolverFactory<ExceptionResolver> {
- private static final ExceptionResolverFactory INSTANCE = new ExceptionResolverFactory();
+ private static final ExceptionResolverFactory INSTANCE =
+ new ExceptionResolverFactory();
private ExceptionResolverFactory() {}
@@ -32,8 +34,10 @@ final class ExceptionResolverFactory implements EventResolverFactory<ExceptionRe
}
@Override
- public ExceptionResolver create(final EventResolverContext context, final String key) {
- return new ExceptionResolver(context, key);
+ public ExceptionResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new ExceptionResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
index 230ac59..f3d4705 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
@@ -20,6 +20,14 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.util.Throwables;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * Exception root cause resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
final class ExceptionRootCauseResolver implements EventResolver {
private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
@@ -54,16 +62,16 @@ final class ExceptionRootCauseResolver implements EventResolver {
}
@Override
- EventResolver createStackTraceTextResolver(final EventResolverContext context) {
- final StackTraceTextResolver stackTraceTextResolver =
- new StackTraceTextResolver(context);
+ EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+ final StackTraceStringResolver stackTraceStringResolver =
+ new StackTraceStringResolver(context);
return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
final Throwable exception = logEvent.getThrown();
if (exception == null) {
jsonWriter.writeNull();
} else {
final Throwable rootCause = Throwables.getRootCause(exception);
- stackTraceTextResolver.resolve(rootCause, jsonWriter);
+ stackTraceStringResolver.resolve(rootCause, jsonWriter);
}
};
}
@@ -87,9 +95,12 @@ final class ExceptionRootCauseResolver implements EventResolver {
private final EventResolver internalResolver;
- ExceptionRootCauseResolver(final EventResolverContext context, final String key) {
+ ExceptionRootCauseResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
this.stackTraceEnabled = context.isStackTraceEnabled();
- this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+ this.internalResolver = INTERNAL_RESOLVER_FACTORY
+ .createInternalResolver(context, config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
index 1f625b0..e511f0d 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
@@ -32,8 +32,10 @@ final class ExceptionRootCauseResolverFactory implements EventResolverFactory<Ex
}
@Override
- public ExceptionRootCauseResolver create(final EventResolverContext context, final String key) {
- return new ExceptionRootCauseResolver(context, key);
+ public ExceptionRootCauseResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new ExceptionRootCauseResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
index ad33a0a..422e445 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
@@ -26,6 +26,52 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
+/**
+ * Level resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = field , [ severity ]
+ * field = "field" -> ( "name" | "severity" )
+ * severity = severity-field
+ * severity-field = "field" -> ( "keyword" | "code" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the level name:
+ *
+ * <pre>
+ * {
+ * "$resolver": "level",
+ * "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the severity keyword:
+ *
+ * <pre>
+ * {
+ * "$resolver": "level",
+ * "field": "severity",
+ * "severity": {
+ * "field": "keyword"
+ * }
+ * }
+ *
+ * Resolve the severity code:
+ *
+ * <pre>
+ * {
+ * "$resolver": "level",
+ * "field": "severity",
+ * "severity": {
+ * "field": "code"
+ * }
+ * }
+ * </pre>
+ */
final class LevelResolver implements EventResolver {
private static String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
@@ -56,16 +102,31 @@ final class LevelResolver implements EventResolver {
private final EventResolver internalResolver;
- LevelResolver(final EventResolverContext context, final String key) {
+ LevelResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ this.internalResolver = createResolver(context, config);
+ }
+
+ private static EventResolver createResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
final JsonWriter jsonWriter = context.getJsonWriter();
- if (key == null) {
- internalResolver = createNameResolver(jsonWriter);
- } else if ("severity".equals(key)) {
- internalResolver = createSeverityNameResolver(jsonWriter);
- } else if ("severity:code".equals(key)) {
- internalResolver = SEVERITY_CODE_RESOLVER;
- } else {
- throw new IllegalArgumentException("unknown key: " + key);
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
+ case "name": return createNameResolver(jsonWriter);
+ case "severity": {
+ final String severityFieldName =
+ config.getString(new String[]{"severity", "field"});
+ switch (severityFieldName) {
+ case "keyword": return createSeverityKeywordResolver(jsonWriter);
+ case "code": return SEVERITY_CODE_RESOLVER;
+ default:
+ throw new IllegalArgumentException(
+ "unknown severity field: " + config);
+ }
+ }
+ default: throw new IllegalArgumentException("unknown field: " + config);
}
}
@@ -85,15 +146,15 @@ final class LevelResolver implements EventResolver {
};
}
- private static EventResolver createSeverityNameResolver(
+ private static EventResolver createSeverityKeywordResolver(
final JsonWriter contextJsonWriter) {
final Map<Level, String> resolutionByLevel = Arrays
.stream(Level.values())
.collect(Collectors.toMap(
Function.identity(),
(final Level level) -> contextJsonWriter.use(() -> {
- final String severityName = Severity.getSeverity(level).name();
- contextJsonWriter.writeString(severityName);
+ final String severityKeyword = Severity.getSeverity(level).name();
+ contextJsonWriter.writeString(severityKeyword);
})));
return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
final String resolution = resolutionByLevel.get(logEvent.getLevel());
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
index e28745b..f5ee519 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
@@ -32,8 +32,10 @@ final class LevelResolverFactory implements EventResolverFactory<LevelResolver>
}
@Override
- public LevelResolver create(final EventResolverContext context, final String key) {
- return new LevelResolver(context, key);
+ public LevelResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new LevelResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
index b88bd0d..66f1f87 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
@@ -19,6 +19,35 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * Logger resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "fqcn" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the logger name:
+ *
+ * <pre>
+ * {
+ * "$resolver": "logger",
+ * "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the logger's fully qualified class name:
+ *
+ * <pre>
+ * {
+ * "$resolver": "logger",
+ * "field": "fqcn"
+ * }
+ * </pre>
+ */
final class LoggerResolver implements EventResolver {
private static final EventResolver NAME_RESOLVER =
@@ -35,16 +64,18 @@ final class LoggerResolver implements EventResolver {
private final EventResolver internalResolver;
- LoggerResolver(final String key) {
- this.internalResolver = createInternalResolver(key);
+ LoggerResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createInternalResolver(config);
}
- private static EventResolver createInternalResolver(final String key) {
- switch (key) {
+ private static EventResolver createInternalResolver(
+ final TemplateResolverConfig config) {
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
case "name": return NAME_RESOLVER;
case "fqcn": return FQCN_RESOLVER;
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
index 10d4ba7..5539f6e 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
@@ -32,8 +32,10 @@ final class LoggerResolverFactory implements EventResolverFactory<LoggerResolver
}
@Override
- public LoggerResolver create(final EventResolverContext context, final String key) {
- return new LoggerResolver(key);
+ public LoggerResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new LoggerResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
index 29be9e8..b12821c 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
@@ -20,6 +20,39 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.lookup.MainMapLookup;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * An index-based resolver for the <tt>main()</tt> method arguments.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = index | key
+ * index = "index" -> number
+ * key = "key" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the 1st <tt>main()</tt> method argument:
+ *
+ * <pre>
+ * {
+ * "$resolver": "main",
+ * "index": 0
+ * }
+ * </pre>
+ *
+ * Resolve the argument coming right after <tt>--userId</tt>:
+ *
+ * <pre>
+ * {
+ * "$resolver": "main",
+ * "key": "--userId"
+ * }
+ * </pre>
+ *
+ * @see MainMapResolver
+ */
final class MainMapResolver implements EventResolver {
private static final MainMapLookup MAIN_MAP_LOOKUP = new MainMapLookup();
@@ -30,8 +63,20 @@ final class MainMapResolver implements EventResolver {
return "main";
}
- MainMapResolver(final String key) {
- this.key = key;
+ MainMapResolver(final TemplateResolverConfig config) {
+ final String key = config.getString("key");
+ final Integer index = config.getInteger("index");
+ if (key != null && index != null) {
+ throw new IllegalArgumentException(
+ "provided both key and index: " + config);
+ }
+ if (key == null && index == null) {
+ throw new IllegalArgumentException(
+ "either key or index must be provided: " + config);
+ }
+ this.key = index != null
+ ? String.valueOf(index)
+ : key;
}
@Override
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
index 60262d4..83b93a1 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
@@ -32,8 +32,10 @@ final class MainMapResolverFactory implements EventResolverFactory<MainMapResolv
}
@Override
- public MainMapResolver create(final EventResolverContext context, final String key) {
- return new MainMapResolver(key);
+ public MainMapResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new MainMapResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
index 1c71619..622d8f3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
@@ -21,6 +21,26 @@ import org.apache.logging.log4j.core.lookup.MapLookup;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
import org.apache.logging.log4j.message.MapMessage;
+/**
+ * {@link MapMessage} field resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "key" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> field of the message:
+ *
+ * <pre>
+ * {
+ * "$resolver": "map",
+ * "key": "userRole"
+ * }
+ * </pre>
+ */
final class MapResolver implements EventResolver {
private static final MapLookup MAP_LOOKUP = new MapLookup();
@@ -31,7 +51,11 @@ final class MapResolver implements EventResolver {
return "map";
}
- MapResolver(final String key) {
+ MapResolver(final TemplateResolverConfig config) {
+ final String key = config.getString("key");
+ if (key == null) {
+ throw new IllegalArgumentException("missing key: " + config);
+ }
this.key = key;
}
@@ -44,19 +68,8 @@ final class MapResolver implements EventResolver {
public void resolve(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
-
- // If the event message is not of type MapMessage, then do not even try
- // to perform the map lookup.
- if (!(logEvent.getMessage() instanceof MapMessage)) {
- jsonWriter.writeNull();
- }
-
- // Perform the map lookup against Log4j.
- else {
- final String resolvedValue = MAP_LOOKUP.lookup(logEvent, key);
- jsonWriter.writeString(resolvedValue);
- }
-
+ final String resolvedValue = MAP_LOOKUP.lookup(logEvent, key);
+ jsonWriter.writeString(resolvedValue);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
index 6e2e8d6..df57601 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
@@ -32,8 +32,10 @@ final class MapResolverFactory implements EventResolverFactory<MapResolver> {
}
@Override
- public MapResolver create(final EventResolverContext context, final String key) {
- return new MapResolver(key);
+ public MapResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new MapResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
index 86a352f..0bef3ff 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
@@ -20,6 +20,26 @@ import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * A {@link Marker} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> "name"
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the marker name:
+ *
+ * <pre>
+ * {
+ * "$resolver": "marker",
+ * "field": "name"
+ * }
+ * </pre>
+ */
final class MarkerResolver implements EventResolver {
private static final TemplateResolver<LogEvent> NAME_RESOLVER =
@@ -34,15 +54,17 @@ final class MarkerResolver implements EventResolver {
private final TemplateResolver<LogEvent> internalResolver;
- MarkerResolver(final String key) {
- this.internalResolver = createInternalResolver(key);
+ MarkerResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createInternalResolver(config);
}
- private TemplateResolver<LogEvent> createInternalResolver(final String key) {
- if ("name".equals(key)) {
+ private TemplateResolver<LogEvent> createInternalResolver(
+ final TemplateResolverConfig config) {
+ final String fieldName = config.getString("field");
+ if ("name".equals(fieldName)) {
return NAME_RESOLVER;
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
index 42f20c5..2d4a2cb 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
@@ -32,8 +32,10 @@ final class MarkerResolverFactory implements EventResolverFactory<MarkerResolver
}
@Override
- public MarkerResolver create(final EventResolverContext context, final String key) {
- return new MarkerResolver(key);
+ public MarkerResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new MarkerResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
index b2953df..54daefe 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -23,30 +23,58 @@ import org.apache.logging.log4j.message.MultiformatMessage;
import org.apache.logging.log4j.message.ObjectMessage;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.util.StringBuilderFormattable;
-import org.apache.logging.log4j.util.Strings;
+/**
+ * {@link Message} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = [ stringified ]
+ * stringified = "stringified" -> boolean
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the message into a string:
+ *
+ * <pre>
+ * {
+ * "$resolver": "message",
+ * "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve the message such that if it is a {@link ObjectMessage} or {@link
+ * MultiformatMessage} with JSON support, its emitted JSON type (string, list,
+ * object, etc.) will be retained:
+ *
+ * <pre>
+ * {
+ * "$resolver": "message"
+ * }
+ * </pre>
+ */
final class MessageResolver implements EventResolver {
private static final String[] FORMATS = { "JSON" };
private final EventResolver internalResolver;
- MessageResolver(final String key) {
- this.internalResolver = createInternalResolver(key);
+ MessageResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createInternalResolver(config);
}
static String getName() {
return "message";
}
- private static EventResolver createInternalResolver(final String key) {
- if (Strings.isBlank(key)) {
- return MessageResolver::resolveText;
- } else if (FORMATS[0].equalsIgnoreCase(key)) {
- return MessageResolver::resolveJson;
- } else {
- throw new IllegalArgumentException("unknown key: " + key);
- }
+ private static EventResolver createInternalResolver(
+ final TemplateResolverConfig config) {
+ final boolean stringified = config.getBoolean("stringified", false);
+ return stringified
+ ? MessageResolver::resolveString
+ : MessageResolver::resolveObject;
}
@Override
@@ -56,14 +84,14 @@ final class MessageResolver implements EventResolver {
internalResolver.resolve(logEvent, jsonWriter);
}
- private static void resolveText(
+ private static void resolveString(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
final Message message = logEvent.getMessage();
- resolveText(message, jsonWriter);
+ resolveString(message, jsonWriter);
}
- private static void resolveText(
+ private static void resolveString(
final Message message,
final JsonWriter jsonWriter) {
if (message instanceof StringBuilderFormattable) {
@@ -76,7 +104,7 @@ final class MessageResolver implements EventResolver {
}
}
- private static void resolveJson(
+ private static void resolveObject(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
@@ -97,7 +125,7 @@ final class MessageResolver implements EventResolver {
}
// Fallback to plain Object write.
- resolveText(logEvent, jsonWriter);
+ resolveString(logEvent, jsonWriter);
}
@@ -146,7 +174,7 @@ final class MessageResolver implements EventResolver {
}
// Fallback to the default message formatter.
- resolveText((LogEvent) message, jsonWriter);
+ resolveString((LogEvent) message, jsonWriter);
return true;
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
index 9032d9e..4d46bb5 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
@@ -32,8 +32,10 @@ final class MessageResolverFactory implements EventResolverFactory<MessageResolv
}
@Override
- public MessageResolver create(final EventResolverContext context, final String key) {
- return new MessageResolver(key);
+ public MessageResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new MessageResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
index 1b2fba0..727a8e8 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
@@ -20,19 +20,45 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.Strings;
+/**
+ * Resolver delegating to {@link PatternLayout}.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = pattern
+ * pattern = "pattern" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Inject string produced by <tt>%p %c{1.} [%t] %X{userId} %X %m%ex</tt> pattern:
+ *
+ * <pre>
+ * {
+ * "$resolver": "pattern",
+ * "pattern": "%p %c{1.} [%t] %X{userId} %X %m%ex"
+ * }
+ * </pre>
+ */
final class PatternResolver implements EventResolver {
private final BiConsumer<StringBuilder, LogEvent> emitter;
PatternResolver(
final EventResolverContext context,
- final String key) {
+ final TemplateResolverConfig config) {
+ final String pattern = config.getString("pattern");
+ if (Strings.isBlank(pattern)) {
+ throw new IllegalArgumentException("blank pattern: " + config);
+ }
final PatternLayout patternLayout = PatternLayout
.newBuilder()
.setConfiguration(context.getConfiguration())
.setCharset(context.getCharset())
- .setPattern(key)
+ .setPattern(pattern)
.build();
this.emitter = (final StringBuilder stringBuilder, final LogEvent logEvent) ->
patternLayout.serialize(logEvent, stringBuilder);
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
index c47f850..e3ddaf9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
@@ -32,8 +32,10 @@ final class PatternResolverFactory implements EventResolverFactory<PatternResolv
}
@Override
- public PatternResolver create(final EventResolverContext context, final String key) {
- return new PatternResolver(context, key);
+ public PatternResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new PatternResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
index 7a029f1..8f857d0 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
@@ -19,6 +19,34 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * Resolver for the {@link StackTraceElement} returned by {@link LogEvent#getSource()}.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setLocationInfoEnabled(boolean)}
+ * method.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ * "className" |
+ * "fileName" |
+ * "methodName" |
+ * "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ * "$resolver": "source",
+ * "field": "lineNumber"
+ * }
+ * </pre>
+ */
final class SourceResolver implements EventResolver {
private static final EventResolver NULL_RESOLVER =
@@ -73,24 +101,27 @@ final class SourceResolver implements EventResolver {
private final EventResolver internalResolver;
- SourceResolver(final EventResolverContext context, final String key) {
+ SourceResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
this.locationInfoEnabled = context.isLocationInfoEnabled();
- this.internalResolver = createInternalResolver(context, key);
+ this.internalResolver = createInternalResolver(context, config);
}
private static EventResolver createInternalResolver(
final EventResolverContext context,
- final String key) {
+ final TemplateResolverConfig config) {
if (!context.isLocationInfoEnabled()) {
return NULL_RESOLVER;
}
- switch (key) {
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
case "className": return CLASS_NAME_RESOLVER;
case "fileName": return FILE_NAME_RESOLVER;
case "lineNumber": return LINE_NUMBER_RESOLVER;
case "methodName": return METHOD_NAME_RESOLVER;
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
index 0c5c2b5..3f1e957 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
@@ -32,8 +32,10 @@ final class SourceResolverFactory implements EventResolverFactory<SourceResolver
}
@Override
- public SourceResolver create(final EventResolverContext context, final String key) {
- return new SourceResolver(context, key);
+ public SourceResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new SourceResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
index 1e30204..1c8a483 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
@@ -18,6 +18,30 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * {@link StackTraceElement} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ * "className" |
+ * "fileName" |
+ * "methodName" |
+ * "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ * "$resolver": "source",
+ * "field": "lineNumber"
+ * }
+ * </pre>
+ */
final class StackTraceElementObjectResolver implements TemplateResolver<StackTraceElement> {
private static final TemplateResolver<StackTraceElement> CLASS_NAME_RESOLVER =
@@ -38,18 +62,20 @@ final class StackTraceElementObjectResolver implements TemplateResolver<StackTra
private final TemplateResolver<StackTraceElement> internalResolver;
- StackTraceElementObjectResolver(final String key) {
- this.internalResolver = createInternalResolver(key);
+ StackTraceElementObjectResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createInternalResolver(config);
}
- private TemplateResolver<StackTraceElement> createInternalResolver(final String key) {
- switch (key) {
+ private TemplateResolver<StackTraceElement> createInternalResolver(
+ final TemplateResolverConfig config) {
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
case "className": return CLASS_NAME_RESOLVER;
case "methodName": return METHOD_NAME_RESOLVER;
case "fileName": return FILE_NAME_RESOLVER;
case "lineNumber": return LINE_NUMBER_RESOLVER;
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
index 84456e7..a07694c 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
@@ -36,8 +36,8 @@ final class StackTraceElementObjectResolverFactory
@Override
public StackTraceElementObjectResolver create(
final StackTraceElementObjectResolverContext context,
- final String key) {
- return new StackTraceElementObjectResolver(key);
+ final TemplateResolverConfig config) {
+ return new StackTraceElementObjectResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
similarity index 93%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
index 17f9ac1..d744070 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
@@ -22,11 +22,11 @@ import org.apache.logging.log4j.layout.json.template.util.Recycler;
import java.util.function.Supplier;
-final class StackTraceTextResolver implements StackTraceResolver {
+final class StackTraceStringResolver implements StackTraceResolver {
private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
- StackTraceTextResolver(final EventResolverContext context) {
+ StackTraceStringResolver(final EventResolverContext context) {
final Supplier<TruncatingBufferedPrintWriter> writerSupplier =
() -> TruncatingBufferedPrintWriter.ofCapacity(
context.getMaxStringByteCount());
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
similarity index 79%
copy from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
index dacc07b..a83fffa 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
@@ -16,10 +16,14 @@
*/
package org.apache.logging.log4j.layout.json.template.resolver;
-interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
- String getName();
+import java.util.Map;
- R create(C context, String key);
+class TemplateResolverConfig extends MapAccessor {
+
+ TemplateResolverConfig(final Map<String, Object> map) {
+ super(map);
+ }
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
index dacc07b..3e3c8ef 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
@@ -20,6 +20,6 @@ interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R
String getName();
- R create(C context, String key);
+ R create(C context, TemplateResolverConfig config);
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
index 0ba10ce..ca304b7 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
@@ -17,7 +17,7 @@
package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.apache.logging.log4j.layout.json.template.util.JsonReader;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
@@ -28,6 +28,8 @@ import java.util.stream.Collectors;
public enum TemplateResolvers {;
+ private static final String RESOLVER_FIELD_NAME = "$resolver";
+
private static final TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
(final Object ignored, final JsonWriter jsonWriter) -> {
jsonWriter.writeArrayStart();
@@ -60,28 +62,8 @@ public enum TemplateResolvers {;
// Append the additional fields.
if (context instanceof EventResolverContext) {
final EventResolverContext eventResolverContext = (EventResolverContext) context;
- final KeyValuePair[] additionalFields = eventResolverContext.getAdditionalFields();
- if (additionalFields != null) {
-
- // Check that the root is an object node.
- final Map<String, Object> objectNode;
- try {
- @SuppressWarnings("unchecked")
- final Map<String, Object> map = (Map<String, Object>) node;
- objectNode = map;
- } catch (final ClassCastException error) {
- final String message = String.format(
- "was expecting an object to merge additional fields (class=%s)",
- node.getClass().getName());
- throw new IllegalArgumentException(message);
- }
-
- // Merge additional fields.
- for (final KeyValuePair additionalField : additionalFields) {
- objectNode.put(additionalField.getKey(), additionalField.getValue());
- }
-
- }
+ final EventTemplateAdditionalField[] additionalFields = eventResolverContext.getAdditionalFields();
+ appendAdditionalFields(node, additionalFields);
}
// Resolve the template.
@@ -89,6 +71,55 @@ public enum TemplateResolvers {;
}
+ private static void appendAdditionalFields(
+ final Object node,
+ EventTemplateAdditionalField[] additionalFields) {
+ if (additionalFields.length > 0) {
+
+ // Check that the root is an object node.
+ final Map<String, Object> objectNode;
+ try {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> map = (Map<String, Object>) node;
+ objectNode = map;
+ } catch (final ClassCastException error) {
+ final String message = String.format(
+ "was expecting an object to merge additional fields: %s",
+ node.getClass().getName());
+ throw new IllegalArgumentException(message);
+ }
+
+ // Merge additional fields.
+ for (final EventTemplateAdditionalField additionalField : additionalFields) {
+ final String additionalFieldKey = additionalField.getKey();
+ final Object additionalFieldValue;
+ switch (additionalField.getType()) {
+ case STRING:
+ additionalFieldValue = additionalField.getValue();
+ break;
+ case JSON:
+ try {
+ additionalFieldValue = JsonReader.read(additionalField.getValue());
+ } catch (final Exception error) {
+ final String message = String.format(
+ "failed reading JSON provided by additional field: %s",
+ additionalFieldKey);
+ throw new IllegalArgumentException(message, error);
+ }
+ break;
+ default: {
+ final String message = String.format(
+ "unknown type %s for additional field: %s",
+ additionalFieldKey, additionalField.getType());
+ throw new IllegalArgumentException(message);
+ }
+ }
+ objectNode.put(additionalFieldKey, additionalFieldValue);
+ }
+
+ }
+ }
+
private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
final C context,
final Object object) {
@@ -167,6 +198,11 @@ public enum TemplateResolvers {;
final C context,
final Map<String, Object> map) {
+ // Check if this is a resolver request.
+ if (map.containsKey(RESOLVER_FIELD_NAME)) {
+ return ofResolver(context, map);
+ }
+
// Create resolver for each object field.
final List<String> fieldNames = new ArrayList<>();
final List<TemplateResolver<V>> fieldResolvers = new ArrayList<>();
@@ -211,7 +247,8 @@ public enum TemplateResolvers {;
continue;
}
final boolean succeedingEntry = resolvedFieldCount > 0;
- if (fieldResolver.isFlattening()) {
+ final boolean flattening = fieldResolver.isFlattening();
+ if (flattening) {
final int initLength = jsonWriterStringBuilder.length();
fieldResolver.resolve(value, jsonWriter, succeedingEntry);
final boolean resolved = jsonWriterStringBuilder.length() > initLength;
@@ -233,25 +270,32 @@ public enum TemplateResolvers {;
}
- private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+ private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofResolver(
final C context,
- final String fieldValue) {
+ final Map<String, Object> map) {
- // Try to resolve the directive as a ${json:xxx} parameter.
- final TemplateResolverRequest resolverRequest = readResolverRequest(fieldValue);
- if (resolverRequest != null) {
- final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
- context.getResolverFactoryByName().get(resolverRequest.resolverName);
- if (resolverFactory != null) {
- return resolverFactory.create(context, resolverRequest.resolverKey);
- }
+ // Extract the resolver name.
+ final Object resolverNameObject = map.get(RESOLVER_FIELD_NAME);
+ if (!(resolverNameObject instanceof String)) {
+ throw new IllegalArgumentException(
+ "invalid resolver name: " + resolverNameObject);
+ }
+ final String resolverName = (String) resolverNameObject;
+
+ // Retrieve the resolver.
+ final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
+ context.getResolverFactoryByName().get(resolverName);
+ if (resolverFactory == null) {
+ throw new IllegalArgumentException("unknown resolver: " + resolverName);
}
+ final TemplateResolverConfig resolverConfig = new TemplateResolverConfig(map);
+ return resolverFactory.create(context, resolverConfig);
+
+ }
- // The rest is the fallback template resolver that delegates every other
- // substitution to Log4j. This will be the case for every template value
- // that does not use directives of pattern ${json:xxx}. This
- // additionally serves as a mechanism to resolve values at runtime when
- // this layout misses certain resolvers.
+ private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+ final C context,
+ final String fieldValue) {
// Check if substitution needed at all. (Copied logic from
// AbstractJacksonLayout.valueNeedsLookup() method.)
@@ -264,11 +308,7 @@ public enum TemplateResolvers {;
return (final V value, final JsonWriter jsonWriter) -> {
final LogEvent logEvent = (LogEvent) value;
final String replacedText = context.getSubstitutor().replace(logEvent, fieldValue);
- if (replacedText == null) {
- jsonWriter.writeNull();
- } else {
- jsonWriter.writeString(replacedText);
- }
+ jsonWriter.writeString(replacedText);
};
}
@@ -291,7 +331,7 @@ public enum TemplateResolvers {;
}
- // Write the field value as is. (Blank value check has already been done at the top.)
+ // Write the field value as is.
else {
final String escapedFieldValue =
contextJsonWriter.use(() ->
@@ -302,45 +342,6 @@ public enum TemplateResolvers {;
}
- private static TemplateResolverRequest readResolverRequest(final String fieldValue) {
-
- // Bail-out if cannot spot the template signature.
- if (!fieldValue.startsWith("${json:") || !fieldValue.endsWith("}")) {
- return null;
- }
-
- // Try to read both resolver name and key.
- final int resolverNameStartIndex = 7;
- final int fieldNameSeparatorIndex = fieldValue.indexOf(':', resolverNameStartIndex);
- if (fieldNameSeparatorIndex < 0) {
- final int resolverNameEndIndex = fieldValue.length() - 1;
- final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
- return new TemplateResolverRequest(resolverName, null);
- } else {
- @SuppressWarnings("UnnecessaryLocalVariable")
- final int resolverNameEndIndex = fieldNameSeparatorIndex;
- final int resolverKeyStartIndex = fieldNameSeparatorIndex + 1;
- final int resolverKeyEndIndex = fieldValue.length() - 1;
- final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
- final String resolverKey = fieldValue.substring(resolverKeyStartIndex, resolverKeyEndIndex);
- return new TemplateResolverRequest(resolverName, resolverKey);
- }
-
- }
-
- private static final class TemplateResolverRequest {
-
- private final String resolverName;
-
- private final String resolverKey;
-
- private TemplateResolverRequest(final String resolverName, final String resolverKey) {
- this.resolverName = resolverName;
- this.resolverKey = resolverKey;
- }
-
- }
-
private static <V> TemplateResolver<V> ofNumber(final Number number) {
final String numberString = String.valueOf(number);
return (final V ignored, final JsonWriter jsonWriter) ->
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
similarity index 63%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
index 71c973b..a2671fb 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -19,67 +19,143 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
import org.apache.logging.log4j.layout.json.template.util.Recycler;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.TriConsumer;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
import java.util.Map;
-import java.util.Set;
import java.util.regex.Pattern;
/**
- * Add Mapped Diagnostic Context (MDC).
+ * Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = singleAccess | multiAccess
+ *
+ * singleAccess = key , [ stringified ]
+ * key = "key" -> string
+ * stringified = "stringified" -> boolean
+ *
+ * multiAccess = [ pattern ] , [ flatten ] , [ stringified ]
+ * pattern = "pattern" -> string
+ * flatten = "flatten" -> ( boolean | flattenConfig )
+ * flattenConfig = [ flattenPrefix ]
+ * flattenPrefix = "prefix" -> string
+ * </pre>
+ *
+ * Note that <tt>singleAccess</tt> resolves the MDC value as is, whilst
+ * <tt>multiAccess</tt> resolves a multitude of MDC values. If <tt>flatten</tt>
+ * is provided, <tt>multiAccess</tt> merges the values with the parent,
+ * otherwise creates a new JSON object containing the values.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> MDC value:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc",
+ * "key": "userRole"
+ * }
+ * </pre>
+ *
+ * Resolve the string representation of the <tt>userRank</tt> MDC value:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc",
+ * "key": "userRank",
+ * "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc"
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object such that values are converted to
+ * string:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc",
+ * "stringified": true
+ * }
+ * </pre>
+ *
+ * Merge all MDC entries whose keys are matching with the
+ * <tt>user(Role|Rank)</tt> regex into the parent:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc",
+ * "flatten": true,
+ * "pattern": "user(Role|Rank)"
+ * }
+ * </pre>
+ *
+ * After converting the corresponding entries to string, merge all MDC entries
+ * to parent such that keys are prefixed with <tt>_</tt>:
+ *
+ * <pre>
+ * {
+ * "$resolver": "mdc",
+ * "stringified": true,
+ * "flatten": {
+ * "prefix": "_"
+ * }
+ * }
+ * </pre>
*/
-final class ContextDataResolver implements EventResolver {
-
- private enum Param {;
-
- private static final String FLATTEN = "flatten";
-
- private static final String PATTERN = "pattern";
-
- private static final String KEY = "key";
-
- private static final String STRINGIFY = "stringify";
-
- }
-
- private static final Set<String> PARAMS =
- new LinkedHashSet<>(Arrays.asList(
- Param.FLATTEN,
- Param.PATTERN,
- Param.KEY,
- Param.STRINGIFY));
+final class ThreadContextDataResolver implements EventResolver {
private final EventResolver internalResolver;
- ContextDataResolver(final EventResolverContext context, final String spec) {
- this.internalResolver = createResolver(context, spec);
+ ThreadContextDataResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ this.internalResolver = createResolver(context, config);
}
private static EventResolver createResolver(
final EventResolverContext context,
- final String spec) {
- final Map<String, StringParameterParser.Value> params = StringParameterParser.parse(spec, PARAMS);
- final StringParameterParser.Value keyValue = params.get(Param.KEY);
- if (keyValue != null) {
- if (params.size() != 1) {
+ final TemplateResolverConfig config) {
+ final Object flattenObject = config.getObject("flatten");
+ final boolean flatten;
+ if (flattenObject == null) {
+ flatten = false;
+ } else if (flattenObject instanceof Boolean) {
+ flatten = (boolean) flattenObject;
+ } else if (flattenObject instanceof Map) {
+ flatten = true;
+ } else {
+ throw new IllegalArgumentException("invalid flatten option: " + config);
+ }
+ final String key = config.getString("key");
+ final String prefix = config.getString(new String[] {"flatten", "prefix"});
+ final String pattern = config.getString("pattern");
+ final boolean stringified = config.getBoolean("stringified", false);
+ if (key != null) {
+ if (flatten) {
throw new IllegalArgumentException(
- "MDC key access doesn't take arguments: " + spec);
- }
- if (keyValue instanceof StringParameterParser.NullValue) {
- throw new IllegalArgumentException("missing MDC key: " + spec);
+ "both key and flatten options cannot be supplied: " + config);
}
- final String key = keyValue.toString();
- return createKeyResolver(key);
+ return createKeyResolver(key, stringified);
} else {
- return createResolver(context, spec, params);
+ final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
+ return createResolver(recyclerFactory, flatten, prefix, pattern, stringified);
}
}
- private static EventResolver createKeyResolver(final String key) {
+ private static EventResolver createKeyResolver(
+ final String key,
+ final boolean stringified) {
return new EventResolver() {
@Override
@@ -92,62 +168,41 @@ final class ContextDataResolver implements EventResolver {
public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
final ReadOnlyStringMap contextData = logEvent.getContextData();
final Object value = contextData == null ? null : contextData.getValue(key);
- jsonWriter.writeValue(value);
+ if (stringified) {
+ // TODO Write the value as string without extra allocations.
+ final String valueString = String.valueOf(value);
+ jsonWriter.writeString(valueString);
+ } else {
+ jsonWriter.writeValue(value);
+ }
}
};
}
private static EventResolver createResolver(
- final EventResolverContext context,
- final String spec,
- final Map<String, StringParameterParser.Value> params) {
-
- // Read the flatten prefix.
- final StringParameterParser.Value flattenValue = params.get(Param.FLATTEN);
- final boolean flatten;
- final String prefix;
- if (flattenValue != null) {
- flatten = true;
- prefix = flattenValue.toString();
- } else {
- flatten = false;
- prefix = null;
- }
-
- // Read the pattern.
- final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
- final Pattern pattern;
- if (patternValue == null) {
- pattern = null;
- } else if (patternValue instanceof StringParameterParser.NullValue) {
- throw new IllegalArgumentException("missing MDC pattern: " + spec);
- } else {
- pattern = Pattern.compile(patternValue.toString());
- }
+ final RecyclerFactory recyclerFactory,
+ final boolean flatten,
+ final String prefix,
+ final String pattern,
+ final boolean stringified) {
- // Read the stringify flag.
- final StringParameterParser.Value stringifyValue = params.get(Param.STRINGIFY);
- final boolean stringify;
- if (stringifyValue == null) {
- stringify = false;
- } else if (!(stringifyValue instanceof StringParameterParser.NullValue)) {
- throw new IllegalArgumentException(
- "MDC stringify directive doesn't take parameters: " + spec);
- } else {
- stringify = true;
- }
+ // Compile the pattern.
+ final Pattern compiledPattern =
+ pattern == null
+ ? null
+ : Pattern.compile(pattern);
// Create the recycler for the loop context.
final Recycler<LoopContext> loopContextRecycler =
- context.getRecyclerFactory().create(() -> {
+ recyclerFactory.create(() -> {
final LoopContext loopContext = new LoopContext();
if (prefix != null) {
loopContext.prefix = prefix;
loopContext.prefixedKey = new StringBuilder(prefix);
}
- loopContext.pattern = pattern;
- loopContext.stringify = stringify;
+ loopContext.pattern = compiledPattern;
+ loopContext.stringified = stringified;
return loopContext;
});
@@ -222,7 +277,7 @@ final class ContextDataResolver implements EventResolver {
private Pattern pattern;
- private boolean stringify;
+ private boolean stringified;
private JsonWriter jsonWriter;
@@ -259,7 +314,7 @@ final class ContextDataResolver implements EventResolver {
loopContext.prefixedKey.append(key);
loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
}
- if (loopContext.stringify && !(value instanceof String)) {
+ if (loopContext.stringified && !(value instanceof String)) {
final String valueString = String.valueOf(value);
loopContext.jsonWriter.writeString(valueString);
} else {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
similarity index 61%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
index c18c2a6..3ef164d 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
@@ -16,24 +16,28 @@
*/
package org.apache.logging.log4j.layout.json.template.resolver;
-final class ContextDataResolverFactory implements EventResolverFactory<ContextDataResolver> {
+final class ThreadContextDataResolverFactory
+ implements EventResolverFactory<ThreadContextDataResolver> {
- private static final ContextDataResolverFactory INSTANCE = new ContextDataResolverFactory();
+ private static final ThreadContextDataResolverFactory INSTANCE =
+ new ThreadContextDataResolverFactory();
- private ContextDataResolverFactory() {}
+ private ThreadContextDataResolverFactory() {}
- static ContextDataResolverFactory getInstance() {
+ static ThreadContextDataResolverFactory getInstance() {
return INSTANCE;
}
@Override
public String getName() {
- return ContextDataResolver.getName();
+ return ThreadContextDataResolver.getName();
}
@Override
- public ContextDataResolver create(final EventResolverContext context, final String key) {
- return new ContextDataResolver(context, key);
+ public ThreadContextDataResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new ThreadContextDataResolver(context, config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
similarity index 64%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
index 1b5fb9f..6a9af12 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -19,43 +19,48 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
-import java.util.Collections;
-import java.util.Map;
+import java.util.Optional;
import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
/**
- * Add Nested Diagnostic Context (NDC).
+ * Nested Diagnostic Context (NDC), aka. Thread Context Stack, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = [ pattern ]
+ * pattern = "pattern" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve all NDC values into a list:
+ *
+ * <pre>
+ * {
+ * "$resolver": "ndc"
+ * }
+ * </pre>
+ *
+ * Resolve all NDC values matching with the <tt>pattern</tt> regex:
+ *
+ * <pre>
+ * {
+ * "$resolver": "ndc",
+ * "pattern": "user(Role|Rank):\\w+"
+ * }
+ * </pre>
*/
-final class ContextStackResolver implements EventResolver {
+final class ThreadContextStackResolver implements EventResolver {
private final Pattern itemPattern;
- private enum Param {;
-
- private static final String PATTERN = "pattern";
-
- }
-
- ContextStackResolver(final String spec) {
- final Map<String, StringParameterParser.Value> params =
- StringParameterParser.parse(spec, Collections.singleton(Param.PATTERN));
- final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
- if (patternValue == null) {
- this.itemPattern = null;
- } else if (patternValue instanceof StringParameterParser.NullValue) {
- throw new IllegalArgumentException("missing NDC pattern: " + spec);
- } else {
- final String pattern = patternValue.toString();
- try {
- this.itemPattern = Pattern.compile(pattern);
- } catch (final PatternSyntaxException error) {
- throw new IllegalArgumentException(
- "invalid NDC pattern: " + spec, error);
- }
- }
+ ThreadContextStackResolver(final TemplateResolverConfig config) {
+ this.itemPattern = Optional
+ .ofNullable(config.getString("pattern"))
+ .map(Pattern::compile)
+ .orElse(null);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
similarity index 64%
copy from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
index 638df83..82a5c23 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
@@ -16,26 +16,28 @@
*/
package org.apache.logging.log4j.layout.json.template.resolver;
-final class TimestampResolverFactory implements EventResolverFactory<TimestampResolver> {
+final class ThreadContextStackResolverFactory
+ implements EventResolverFactory<ThreadContextStackResolver> {
- private static final TimestampResolverFactory INSTANCE = new TimestampResolverFactory();
+ private static final ThreadContextStackResolverFactory INSTANCE
+ = new ThreadContextStackResolverFactory();
- private TimestampResolverFactory() {}
+ private ThreadContextStackResolverFactory() {}
- static TimestampResolverFactory getInstance() {
+ static ThreadContextStackResolverFactory getInstance() {
return INSTANCE;
}
@Override
public String getName() {
- return TimestampResolver.getName();
+ return ThreadContextStackResolver.getName();
}
@Override
- public TimestampResolver create(
+ public ThreadContextStackResolver create(
final EventResolverContext context,
- final String key) {
- return new TimestampResolver(key);
+ final TemplateResolverConfig config) {
+ return new ThreadContextStackResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
index afe8ecb..a316afe 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -19,6 +19,26 @@ package org.apache.logging.log4j.layout.json.template.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+/**
+ * Thread resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "id" | "priority" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the thread name:
+ *
+ * <pre>
+ * {
+ * "$resolver": "thread",
+ * "field": "name"
+ * }
+ * </pre>
+ */
final class ThreadResolver implements EventResolver {
private static final EventResolver NAME_RESOLVER =
@@ -41,17 +61,19 @@ final class ThreadResolver implements EventResolver {
private final EventResolver internalResolver;
- ThreadResolver(final String key) {
- this.internalResolver = createInternalResolver(key);
+ ThreadResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createInternalResolver(config);
}
- private static EventResolver createInternalResolver(final String key) {
- switch (key) {
+ private static EventResolver createInternalResolver(
+ final TemplateResolverConfig config) {
+ final String fieldName = config.getString("field");
+ switch (fieldName) {
case "name": return NAME_RESOLVER;
case "id": return ID_RESOLVER;
case "priority": return PRIORITY_RESOLVER;
}
- throw new IllegalArgumentException("unknown key: " + key);
+ throw new IllegalArgumentException("unknown field: " + config);
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
index 266c982..75df1e3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
@@ -32,8 +32,10 @@ final class ThreadResolverFactory implements EventResolverFactory<ThreadResolver
}
@Override
- public ThreadResolver create(final EventResolverContext context, final String key) {
- return new ThreadResolver(key);
+ public ThreadResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new ThreadResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
index c7c32b0..3d023b3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
@@ -21,24 +21,195 @@ import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
import org.apache.logging.log4j.layout.json.template.JsonTemplateLayoutDefaults;
import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
-import java.util.Arrays;
import java.util.Calendar;
-import java.util.LinkedHashSet;
import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
import java.util.TimeZone;
+/**
+ * Timestamp resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = [ patternConfig | epochConfig ]
+ *
+ * patternConfig = "pattern" -> ( [ format ] , [ timeZone ] , [ locale ] )
+ * format = "format" -> string
+ * timeZone = "timeZone" -> string
+ * locale = "locale" -> (
+ * language |
+ * ( language , "_" , country ) |
+ * ( language , "_" , country , "_" , variant )
+ * )
+ *
+ * epochConfig = "epoch" -> ( unit , [ rounded ] )
+ * unit = "unit" -> (
+ * "nanos" |
+ * "millis" |
+ * "secs" |
+ * "millis.nanos" |
+ * "secs.nanos" |
+ * )
+ * rounded = "rounded" -> boolean
+ * </pre>
+ *
+ * If no configuration options are provided, <tt>pattern-config</tt> is
+ * employed. There {@link
+ * JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link
+ * JsonTemplateLayoutDefaults#getTimeZone()}, {@link
+ * JsonTemplateLayoutDefaults#getLocale()} are used as defaults for
+ * <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ *
+ * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
+ * for the fractional component in nanoseconds.
+ *
+ * <h3>Examples</h3>
+ *
+ * <table>
+ * <tr>
+ * <td>Configuration</td>
+ * <td>Output</td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp"
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 2020-02-07T13:38:47.098+02:00
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "pattern": {
+ * "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ * "timeZone": "UTC",
+ * "locale": "en_US"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 2020-02-07T13:38:47.098Z
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "secs"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 1581082727.982123456
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "secs",
+ * "rounded": true
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 1581082727
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "secs.nanos"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 982123456
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "millis"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 1581082727982.123456
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "millis",
+ * "rounded": true
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 1581082727982
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "millis.nanos"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 123456
+ * </pre></td>
+ * </tr>
+ * <tr>
+ * <td><pre>
+ * {
+ * "$resolver": "timestamp",
+ * "epoch": {
+ * "unit": "nanos"
+ * }
+ * }
+ * </pre></td>
+ * <td><pre>
+ * 1581082727982123456
+ * </pre></td>
+ * </tr>
+ * </table>
+ */
final class TimestampResolver implements EventResolver {
private final EventResolver internalResolver;
- TimestampResolver(final String key) {
- this.internalResolver = (key != null && key.startsWith("epoch:"))
- ? createEpochResolver(key)
- : createFormatResolver(key);
+ TimestampResolver(final TemplateResolverConfig config) {
+ this.internalResolver = createResolver(config);
+ }
+
+ private static EventResolver createResolver(
+ final TemplateResolverConfig config) {
+ final boolean patternProvided = config.exists("pattern");
+ final boolean epochProvided = config.exists("epoch");
+ if (patternProvided && epochProvided) {
+ throw new IllegalArgumentException(
+ "conflicting configuration options are provided: " + config);
+ }
+ return epochProvided
+ ? createEpochResolver(config)
+ : createFormatResolver(config);
}
/**
@@ -46,20 +217,6 @@ final class TimestampResolver implements EventResolver {
*/
private static final class FormatResolverContext {
- private enum Key {;
-
- private static final String PATTERN = "pattern";
-
- private static final String TIME_ZONE = "timeZone";
-
- private static final String LOCALE = "locale";
-
- }
-
- private static final Set<String> KEYS =
- new LinkedHashSet<>(Arrays.asList(
- Key.PATTERN, Key.TIME_ZONE, Key.LOCALE));
-
private final FastDateFormat timestampFormat;
private final Calendar calendar;
@@ -76,41 +233,36 @@ final class TimestampResolver implements EventResolver {
timestampFormat.format(calendar, formattedTimestampBuilder);
}
- private static FormatResolverContext fromKey(final String key) {
- final Map<String, StringParameterParser.Value> keys =
- StringParameterParser.parse(key, KEYS);
- final String pattern = readPattern(keys);
- final TimeZone timeZone = readTimeZone(keys);
- final Locale locale = readLocale(keys);
+ private static FormatResolverContext fromConfig(
+ final TemplateResolverConfig config) {
+ final String format = readFormat(config);
+ final TimeZone timeZone = readTimeZone(config);
+ final Locale locale = readLocale(config);
final FastDateFormat fastDateFormat =
- FastDateFormat.getInstance(pattern, timeZone, locale);
+ FastDateFormat.getInstance(format, timeZone, locale);
return new FormatResolverContext(timeZone, locale, fastDateFormat);
}
- private static String readPattern(
- final Map<String, StringParameterParser.Value> keys) {
- final StringParameterParser.Value patternValue = keys.get(Key.PATTERN);
- if (patternValue == null || patternValue instanceof StringParameterParser.NullValue) {
+ private static String readFormat(final TemplateResolverConfig config) {
+ final String format = config.getString(new String[]{"pattern", "format"});
+ if (format == null) {
return JsonTemplateLayoutDefaults.getTimestampFormatPattern();
}
- final String pattern = patternValue.toString();
try {
- FastDateFormat.getInstance(pattern);
+ FastDateFormat.getInstance(format);
} catch (final IllegalArgumentException error) {
throw new IllegalArgumentException(
- "invalid pattern in timestamp key: " + pattern,
+ "invalid timestamp format: " + config,
error);
}
- return pattern;
+ return format;
}
- private static TimeZone readTimeZone(
- final Map<String, StringParameterParser.Value> keys) {
- final StringParameterParser.Value timeZoneValue = keys.get(Key.TIME_ZONE);
- if (timeZoneValue == null || timeZoneValue instanceof StringParameterParser.NullValue) {
+ private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+ final String timeZoneId = config.getString(new String[]{"pattern", "timeZone"});
+ if (timeZoneId == null) {
return JsonTemplateLayoutDefaults.getTimeZone();
}
- final String timeZoneId = timeZoneValue.toString();
boolean found = false;
for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
@@ -120,27 +272,23 @@ final class TimestampResolver implements EventResolver {
}
if (!found) {
throw new IllegalArgumentException(
- "invalid time zone in timestamp key: " + timeZoneId);
+ "invalid timestamp time zone: " + config);
}
return TimeZone.getTimeZone(timeZoneId);
}
- private static Locale readLocale(
- final Map<String, StringParameterParser.Value> keys) {
- final StringParameterParser.Value localeValue = keys.get(Key.LOCALE);
- if (localeValue == null || localeValue instanceof StringParameterParser.NullValue) {
+ private static Locale readLocale(final TemplateResolverConfig config) {
+ final String locale = config.getString(new String[]{"pattern", "locale"});
+ if (locale == null) {
return JsonTemplateLayoutDefaults.getLocale();
}
- final String locale = localeValue.toString();
final String[] localeFields = locale.split("_", 3);
switch (localeFields.length) {
case 1: return new Locale(localeFields[0]);
case 2: return new Locale(localeFields[0], localeFields[1]);
case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]);
- default:
- throw new IllegalArgumentException(
- "invalid locale in timestamp key: " + locale);
}
+ throw new IllegalArgumentException("invalid timestamp locale: " + config);
}
}
@@ -196,54 +344,36 @@ final class TimestampResolver implements EventResolver {
}
- private static EventResolver createFormatResolver(final String key) {
+ private static EventResolver createFormatResolver(
+ final TemplateResolverConfig config) {
final FormatResolverContext formatResolverContext =
- FormatResolverContext.fromKey(key);
+ FormatResolverContext.fromConfig(config);
return new FormatResolver(formatResolverContext);
}
- private static EventResolver createEpochResolver(final String key) {
- switch (key) {
- case "epoch:nanos":
- return createEpochNanosResolver();
- case "epoch:micros":
- return createEpochMicrosDoubleResolver();
- case "epoch:micros,integral":
- return createEpochMicrosLongResolver();
- case "epoch:millis":
- return createEpochMillisDoubleResolver();
- case "epoch:millis,integral":
- return createEpochMillisLongResolver();
- case "epoch:secs":
- return createEpochSecsDoubleResolver();
- case "epoch:secs,integral":
- return createEpochSecsLongResolver();
- case "epoch:micros.nanos":
- return createEpochMicrosNanosResolver();
- case "epoch:millis.nanos":
- return createEpochMillisNanosResolver();
- case "epoch:millis.micros":
- return createEpochMillisMicrosResolver();
- case "epoch:secs.nanos":
- return createEpochSecsNanosResolver();
- case "epoch:secs.micros":
- return createEpochSecsMicrosResolver();
- case "epoch:secs.millis":
- return createEpochSecsMillisResolver();
- default:
- throw new IllegalArgumentException(
- "was expecting an epoch key: " + key);
+ private static EventResolver createEpochResolver(
+ final TemplateResolverConfig config) {
+ final String unit = config.getString(new String[]{"epoch", "unit"});
+ final Boolean rounded = config.getBoolean(new String[]{"epoch", "rounded"});
+ if ("nanos".equals(unit) && !Boolean.FALSE.equals(rounded)) {
+ return EPOCH_NANOS_RESOLVER;
+ } else if ("millis".equals(unit)) {
+ return !Boolean.TRUE.equals(rounded)
+ ? EPOCH_MILLIS_RESOLVER
+ : EPOCH_MILLIS_ROUNDED_RESOLVER;
+ } else if ("millis.nanos".equals(unit) && rounded == null) {
+ return EPOCH_MILLIS_NANOS_RESOLVER;
+ } else if ("secs".equals(unit)) {
+ return !Boolean.TRUE.equals(rounded)
+ ? EPOCH_SECS_RESOLVER
+ : EPOCH_SECS_ROUNDED_RESOLVER;
+ } else if ("secs.nanos".equals(unit) && rounded == null) {
+ return EPOCH_SECS_NANOS_RESOLVER;
}
+ throw new IllegalArgumentException(
+ "invalid epoch configuration: " + config);
}
- private static final int MICROS_PER_SEC = 1_000_000;
-
- private static final int NANOS_PER_SEC = 1_000_000_000;
-
- private static final int NANOS_PER_MILLI = 1_000_000;
-
- private static final int NANOS_PER_MICRO = 1_000;
-
private static final class EpochResolutionRecord {
private static final int MAX_LONG_LENGTH =
@@ -292,144 +422,73 @@ final class TimestampResolver implements EventResolver {
}
- private static EventResolver createEpochNanosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- jsonWriter.writeNumber(nanos);
- }
- };
- }
-
- private static EventResolver createEpochMicrosDoubleResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long secs = logEventInstant.getEpochSecond();
- final int nanosOfSecs = logEventInstant.getNanoOfSecond();
- final long micros = MICROS_PER_SEC * secs + nanosOfSecs / NANOS_PER_MICRO;
- final int nanosOfMicros = nanosOfSecs - nanosOfSecs % NANOS_PER_MICRO;
- jsonWriter.writeNumber(micros, nanosOfMicros);
- }
- };
- }
-
- private static EventResolver createEpochMicrosLongResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final long nanos = epochNanos(logEventInstant);
- final long micros = nanos / NANOS_PER_MICRO;
- jsonWriter.writeNumber(micros);
- }
- };
- }
-
- private static EventResolver createEpochMillisDoubleResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(
- logEventInstant.getEpochMillisecond(),
- logEventInstant.getNanoOfMillisecond());
- }
- };
- }
-
- private static EventResolver createEpochMillisLongResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
- }
- };
- }
-
- private static EventResolver createEpochSecsDoubleResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(
- logEventInstant.getEpochSecond(),
- logEventInstant.getNanoOfSecond());
- }
- };
- }
-
- private static EventResolver createEpochSecsLongResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getEpochSecond());
- }
- };
- }
-
- private static EventResolver createEpochMicrosNanosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final int nanosOfSecs = logEventInstant.getNanoOfSecond();
- final int nanosOfMicros = nanosOfSecs % NANOS_PER_MICRO;
- jsonWriter.writeNumber(nanosOfMicros);
- }
- };
- }
-
- private static EventResolver createEpochMillisNanosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getNanoOfMillisecond());
- }
- };
- }
-
- private static EventResolver createEpochMillisMicrosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final int nanosOfMillis = logEventInstant.getNanoOfMillisecond();
- final int microsOfMillis = nanosOfMillis / NANOS_PER_MICRO;
- jsonWriter.writeNumber(microsOfMillis);
- }
- };
- }
+ private static final EventResolver EPOCH_NANOS_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final long nanos = epochNanos(logEventInstant);
+ jsonWriter.writeNumber(nanos);
+ }
+ };
+
+ private static final EventResolver EPOCH_MILLIS_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+ final long nanos = epochNanos(logEventInstant);
+ jsonWriterStringBuilder.append(nanos);
+ jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
+ }
+ };
- private static EventResolver createEpochSecsNanosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
- }
- };
- }
+ private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
+ }
+ };
+
+ private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final long nanos = epochNanos(logEventInstant);
+ final long fraction = nanos % 1_000_000L;
+ jsonWriter.writeNumber(fraction);
+ }
+ };
+
+ private static final EventResolver EPOCH_SECS_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+ final long nanos = epochNanos(logEventInstant);
+ jsonWriterStringBuilder.append(nanos);
+ jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
+ }
+ };
- private static EventResolver createEpochSecsMicrosResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final int nanosOfSecs = logEventInstant.getNanoOfSecond();
- final int microsOfSecs = nanosOfSecs / NANOS_PER_MICRO;
- jsonWriter.writeNumber(microsOfSecs);
- }
- };
- }
+ private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getEpochSecond());
+ }
+ };
- private static EventResolver createEpochSecsMillisResolver() {
- return new EpochResolver() {
- @Override
- void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
- final int nanosOfSecs = logEventInstant.getNanoOfSecond();
- final int millisOfSecs = nanosOfSecs / NANOS_PER_MILLI;
- jsonWriter.writeNumber(millisOfSecs);
- }
- };
- }
+ private static final EventResolver EPOCH_SECS_NANOS_RESOLVER =
+ new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
+ }
+ };
private static long epochNanos(Instant instant) {
- return NANOS_PER_SEC * instant.getEpochSecond() + instant.getNanoOfSecond();
+ return 1_000_000_000L * instant.getEpochSecond() + instant.getNanoOfSecond();
}
static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
index 638df83..f1547f2 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
@@ -34,8 +34,8 @@ final class TimestampResolverFactory implements EventResolverFactory<TimestampRe
@Override
public TimestampResolver create(
final EventResolverContext context,
- final String key) {
- return new TimestampResolver(key);
+ final TemplateResolverConfig config) {
+ return new TimestampResolver(config);
}
}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
new file mode 100644
index 0000000..a4c140f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
@@ -0,0 +1,139 @@
+/*
+ * 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.json.template.util;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+public class MapAccessor {
+
+ private final Map<String, Object> map;
+
+ public MapAccessor(final Map<String, Object> map) {
+ this.map = Objects.requireNonNull(map, "map");
+ }
+
+ public String getString(final String key) {
+ final String[] path = {key};
+ return getObject(path, String.class);
+ }
+
+ public String getString(final String[] path) {
+ return getObject(path, String.class);
+ }
+
+ public boolean getBoolean(final String key, final boolean defaultValue) {
+ final String[] path = {key};
+ return getBoolean(path, defaultValue);
+ }
+
+ public boolean getBoolean(final String[] path, final boolean defaultValue) {
+ final Boolean value = getObject(path, Boolean.class);
+ return value == null ? defaultValue : value;
+ }
+
+ public Boolean getBoolean(final String key) {
+ final String[] path = {key};
+ return getObject(path, Boolean.class);
+ }
+
+ public Boolean getBoolean(final String[] path) {
+ return getObject(path, Boolean.class);
+ }
+
+ public Integer getInteger(final String key) {
+ final String[] path = {key};
+ return getInteger(path);
+ }
+
+ public Integer getInteger(final String[] path) {
+ return getObject(path, Integer.class);
+ }
+
+ public boolean exists(final String key) {
+ final String[] path = {key};
+ return exists(path);
+ }
+
+ public boolean exists(final String[] path) {
+ final Object value = getObject(path, Object.class);
+ return value != null;
+ }
+
+ public Object getObject(final String key) {
+ final String[] path = {key};
+ return getObject(path, Object.class);
+ }
+
+ public <T> T getObject(final String key, final Class<T> clazz) {
+ final String[] path = {key};
+ return getObject(path, clazz);
+ }
+
+ public Object getObject(final String[] path) {
+ return getObject(path, Object.class);
+ }
+
+ public <T> T getObject(final String[] path, final Class<T> clazz) {
+ Objects.requireNonNull(path, "path");
+ Objects.requireNonNull(clazz, "clazz");
+ if (path.length == 0) {
+ throw new IllegalArgumentException("empty path");
+ }
+ Object parent = map;
+ for (final String key : path) {
+ if (!(parent instanceof Map)) {
+ return null;
+ }
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> parentMap = (Map<String, Object>) parent;
+ parent = parentMap.get(key);
+ }
+ if (parent != null && !clazz.isInstance(parent)) {
+ final String message = String.format(
+ "was expecting %s at path %s: %s (of type %s)",
+ clazz.getSimpleName(),
+ Arrays.asList(path),
+ parent,
+ parent.getClass().getCanonicalName());
+ throw new IllegalArgumentException(message);
+ }
+ @SuppressWarnings("unchecked")
+ final T typedValue = (T) parent;
+ return typedValue;
+ }
+
+ @Override
+ public boolean equals(final Object instance) {
+ if (this == instance) return true;
+ if (instance == null || getClass() != instance.getClass()) return false;
+ final MapAccessor that = (MapAccessor) instance;
+ return map.equals(that.map);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(map);
+ }
+
+ @Override
+ public String toString() {
+ return map.toString();
+ }
+
+}
diff --git a/log4j-layout-json-template/src/main/resources/EcsLayout.json b/log4j-layout-json-template/src/main/resources/EcsLayout.json
index 7004e54..f5f957b 100644
--- a/log4j-layout-json-template/src/main/resources/EcsLayout.json
+++ b/log4j-layout-json-template/src/main/resources/EcsLayout.json
@@ -1,12 +1,48 @@
{
- "@timestamp": "${json:timestamp:pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z',timeZone=UTC}",
- "log.level": "${json:level}",
- "message": "${json:message}",
- "process.thread.name": "${json:thread:name}",
- "log.logger": "${json:logger:name}",
- "labels": "${json:mdc:flatten=labels.,stringify}",
- "tags": "${json:ndc}",
- "error.type": "${json:exception:className}",
- "error.message": "${json:exception:message}",
- "error.stack_trace": "${json:exception:stackTrace:text}"
+ "@timestamp": {
+ "$resolver": "timestamp",
+ "pattern": {
+ "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ "timeZone": "UTC"
+ }
+ },
+ "log.level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "process.thread.name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "log.logger": {
+ "$resolver": "logger",
+ "field": "name"
+ },
+ "labels": {
+ "$resolver": "mdc",
+ "flatten": {
+ "prefix": "labels."
+ },
+ "stringified": true
+ },
+ "tags": {
+ "$resolver": "ndc"
+ },
+ "error.type": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "error.message": {
+ "$resolver": "exception",
+ "field": "message"
+ },
+ "error.stack_trace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ }
}
diff --git a/log4j-layout-json-template/src/main/resources/GelfLayout.json b/log4j-layout-json-template/src/main/resources/GelfLayout.json
index bcea9ee..dd43cc8 100644
--- a/log4j-layout-json-template/src/main/resources/GelfLayout.json
+++ b/log4j-layout-json-template/src/main/resources/GelfLayout.json
@@ -1,11 +1,41 @@
{
"version": "1.1",
"host": "${hostName}",
- "short_message": "${json:message}",
- "full_message": "${json:exception:stackTrace:text}",
- "timestamp": "${json:timestamp:epoch:secs}",
- "level": "${json:level:severity:code}",
- "_logger": "${json:logger:name}",
- "_thread": "${json:thread:name}",
- "_mdc": "${json:mdc:flatten=_,stringify}"
+ "short_message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "full_message": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ },
+ "timestamp": {
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs"
+ }
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "severity",
+ "severity": {
+ "field": "code"
+ }
+ },
+ "_logger": {
+ "$resolver": "logger",
+ "field": "name"
+ },
+ "_thread": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "_mdc": {
+ "$resolver": "mdc",
+ "flatten": {
+ "prefix": "_"
+ },
+ "stringified": true
+ }
}
diff --git a/log4j-layout-json-template/src/main/resources/JsonLayout.json b/log4j-layout-json-template/src/main/resources/JsonLayout.json
index f31ec63..503e2cd 100644
--- a/log4j-layout-json-template/src/main/resources/JsonLayout.json
+++ b/log4j-layout-json-template/src/main/resources/JsonLayout.json
@@ -1,26 +1,83 @@
{
"instant": {
- "epochSecond": "${json:timestamp:epoch:secs,integral}",
- "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+ "epochSecond": {
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs",
+ "rounded": true
+ }
+ },
+ "nanoOfSecond": {
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs.nanos"
+ }
+ }
+ },
+ "thread": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "loggerName": {
+ "$resolver": "logger",
+ "field": "name"
+ },
+ "message": {
+ "$resolver": "message",
+ "stringified": true
},
- "thread": "${json:thread:name}",
- "level": "${json:level}",
- "loggerName": "${json:logger:name}",
- "message": "${json:message}",
"thrown": {
- "message": "${json:exception:message}",
- "name": "${json:exception:className}",
- "extendedStackTrace": "${json:exception:stackTrace}"
- },
- "contextStack": "${json:ndc}",
- "endOfBatch": "${json:endOfBatch}",
- "loggerFqcn": "${json:logger:fqcn}",
- "threadId": "${json:thread:id}",
- "threadPriority": "${json:thread:priority}",
+ "message": {
+ "$resolver": "exception",
+ "field": "message"
+ },
+ "name": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "extendedStackTrace": {
+ "$resolver": "exception",
+ "field": "stackTrace"
+ }
+ },
+ "contextStack": {
+ "$resolver": "ndc"
+ },
+ "endOfBatch": {
+ "$resolver": "endOfBatch"
+ },
+ "loggerFqcn": {
+ "$resolver": "logger",
+ "field": "fqcn"
+ },
+ "threadId": {
+ "$resolver": "thread",
+ "field": "id"
+ },
+ "threadPriority": {
+ "$resolver": "thread",
+ "field": "priority"
+ },
"source": {
- "class": "${json:source:className}",
- "method": "${json:source:methodName}",
- "file": "${json:source:fileName}",
- "line": "${json:source:lineNumber}"
+ "class": {
+ "$resolver": "source",
+ "field": "className"
+ },
+ "method": {
+ "$resolver": "source",
+ "field": "methodName"
+ },
+ "file": {
+ "$resolver": "source",
+ "field": "fileName"
+ },
+ "line": {
+ "$resolver": "source",
+ "field": "lineNumber"
+ }
}
}
diff --git a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
index 62d4c20..3225930 100644
--- a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
+++ b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
@@ -1,19 +1,58 @@
{
- "mdc": "${json:mdc}",
+ "mdc": {
+ "$resolver": "mdc"
+ },
"exception": {
- "exception_class": "${json:exception:className}",
- "exception_message": "${json:exception:message}",
- "stacktrace": "${json:exception:stackTrace:text}"
+ "exception_class": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "exception_message": {
+ "$resolver": "exception",
+ "field": "message",
+ "stringified": true
+ },
+ "stacktrace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ }
+ },
+ "line_number": {
+ "$resolver": "source",
+ "field": "lineNumber"
+ },
+ "class": {
+ "$resolver": "source",
+ "field": "className"
},
- "line_number": "${json:source:lineNumber}",
- "class": "${json:source:className}",
"@version": 1,
"source_host": "${hostName}",
- "message": "${json:message}",
- "thread_name": "${json:thread:name}",
- "@timestamp": "${json:timestamp}",
- "level": "${json:level}",
- "file": "${json:source:fileName}",
- "method": "${json:source:methodName}",
- "logger_name": "${json:logger:name}"
+ "message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "thread_name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "@timestamp": {
+ "$resolver": "timestamp"
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "file": {
+ "$resolver": "source",
+ "field": "fileName"
+ },
+ "method": {
+ "$resolver": "source",
+ "field": "methodName"
+ },
+ "logger_name": {
+ "$resolver": "logger",
+ "field": "name"
+ }
}
diff --git a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
index 3faacd4..218a01a 100644
--- a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
+++ b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
@@ -1,6 +1,18 @@
{
- "class": "${json:stackTraceElement:className}",
- "method": "${json:stackTraceElement:methodName}",
- "file": "${json:stackTraceElement:fileName}",
- "line": "${json:stackTraceElement:lineNumber}"
+ "class": {
+ "$resolver": "stackTraceElement",
+ "field": "className"
+ },
+ "method": {
+ "$resolver": "stackTraceElement",
+ "field": "methodName"
+ },
+ "file": {
+ "$resolver": "stackTraceElement",
+ "field": "fileName"
+ },
+ "line": {
+ "$resolver": "stackTraceElement",
+ "field": "lineNumber"
+ }
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
index ec96407..262ddb1 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
@@ -5,6 +5,7 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.assertj.core.api.Assertions;
import org.junit.Test;
@@ -18,6 +19,8 @@ public class EcsLayoutTest {
private static final Configuration CONFIGURATION = new DefaultConfiguration();
+ private static final String SERVICE_NAME = "test";
+
private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
.newBuilder()
.setConfiguration(CONFIGURATION)
@@ -27,10 +30,12 @@ public class EcsLayoutTest {
.EventTemplateAdditionalFields
.newBuilder()
.setAdditionalFields(
- new KeyValuePair[]{
- new KeyValuePair(
- "service.name",
- "test")
+ new EventTemplateAdditionalField[]{
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("service.name")
+ .setValue(SERVICE_NAME)
+ .build()
})
.build())
.build();
@@ -38,42 +43,40 @@ public class EcsLayoutTest {
private static final EcsLayout ECS_LAYOUT = EcsLayout
.newBuilder()
.setConfiguration(CONFIGURATION)
- .setServiceName("test")
+ .setServiceName(SERVICE_NAME)
.build();
@Test
- public void test_lite_log_events() throws Exception {
+ public void test_lite_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
test(logEvents);
}
@Test
- public void test_full_log_events() throws Exception {
+ public void test_full_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
test(logEvents);
}
- private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ private static void test(final Collection<LogEvent> logEvents) {
for (final LogEvent logEvent : logEvents) {
test(logEvent);
}
}
- private static void test(final LogEvent logEvent) throws Exception {
+ private static void test(final LogEvent logEvent) {
final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
final Map<String, Object> ecsLayoutMap = renderUsingEcsLayout(logEvent);
Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(ecsLayoutMap);
}
private static Map<String, Object> renderUsingJsonTemplateLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
}
private static Map<String, Object> renderUsingEcsLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
return renderUsing(logEvent, ECS_LAYOUT);
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
index b561ea6..795546b 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
@@ -4,11 +4,12 @@ import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.layout.GelfLayout;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.assertj.core.api.Assertions;
-import org.assertj.core.data.Percentage;
import org.junit.Test;
+import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -30,10 +31,12 @@ public class GelfLayoutTest {
.EventTemplateAdditionalFields
.newBuilder()
.setAdditionalFields(
- new KeyValuePair[]{
- new KeyValuePair(
- "host",
- HOST_NAME)
+ new EventTemplateAdditionalField[]{
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("host")
+ .setValue(HOST_NAME)
+ .build()
})
.build())
.build();
@@ -46,39 +49,37 @@ public class GelfLayoutTest {
.build();
@Test
- public void test_lite_log_events() throws Exception {
+ public void test_lite_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
test(logEvents);
}
@Test
- public void test_full_log_events() throws Exception {
+ public void test_full_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
test(logEvents);
}
- private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ private static void test(final Collection<LogEvent> logEvents) {
for (final LogEvent logEvent : logEvents) {
test(logEvent);
}
}
- private static void test(final LogEvent logEvent) throws Exception {
+ private static void test(final LogEvent logEvent) {
final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
final Map<String, Object> gelfLayoutMap = renderUsingGelfLayout(logEvent);
- verifyTimestamp(jsonTemplateLayoutMap, gelfLayoutMap);
+ verifyTimestamp(logEvent.getInstant(), jsonTemplateLayoutMap, gelfLayoutMap);
Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(gelfLayoutMap);
}
private static Map<String, Object> renderUsingJsonTemplateLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
}
private static Map<String, Object> renderUsingGelfLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
return renderUsing(logEvent, GELF_LAYOUT);
}
@@ -86,17 +87,23 @@ public class GelfLayoutTest {
* Handle timestamps individually to avoid floating-point comparison hiccups.
*/
private static void verifyTimestamp(
+ final Instant logEventInstant,
final Map<String, Object> jsonTemplateLayoutMap,
final Map<String, Object> gelfLayoutMap) {
- final Number jsonTemplateLayoutTimestamp =
- (Number) jsonTemplateLayoutMap.remove("timestamp");
- final Number gelfLayoutTimestamp =
- (Number) gelfLayoutMap.remove("timestamp");
+ final BigDecimal jsonTemplateLayoutTimestamp =
+ (BigDecimal) jsonTemplateLayoutMap.remove("timestamp");
+ final BigDecimal gelfLayoutTimestamp =
+ (BigDecimal) gelfLayoutMap.remove("timestamp");
+ final String description = String.format(
+ "instantEpochSecs=%d.%d, jsonTemplateLayoutTimestamp=%s, gelfLayoutTimestamp=%s",
+ logEventInstant.getEpochSecond(),
+ logEventInstant.getNanoOfSecond(),
+ jsonTemplateLayoutTimestamp,
+ gelfLayoutTimestamp);
Assertions
- .assertThat(jsonTemplateLayoutTimestamp.doubleValue())
- .isCloseTo(
- gelfLayoutTimestamp.doubleValue(),
- Percentage.withPercentage(0.01));
+ .assertThat(jsonTemplateLayoutTimestamp.compareTo(gelfLayoutTimestamp))
+ .as(description)
+ .isEqualTo(0);
}
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
index 0eb077f..a2ebe6f 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -16,21 +16,14 @@
*/
package org.apache.logging.log4j.layout.json.template;
-import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
public enum JacksonFixture {;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
- private static final JsonFactory JSON_FACTORY = OBJECT_MAPPER.getFactory();
-
public static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
- public static JsonFactory getJsonFactory() {
- return JSON_FACTORY;
- }
-
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
index 7bbd9a9..ec8458e 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
@@ -32,32 +32,31 @@ public class JsonLayoutTest {
.build();
@Test
- public void test_lite_log_events() throws Exception {
+ public void test_lite_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
test(logEvents);
}
@Test
- public void test_full_log_events() throws Exception {
+ public void test_full_log_events() {
final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
test(logEvents);
}
- private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ private static void test(final Collection<LogEvent> logEvents) {
for (final LogEvent logEvent : logEvents) {
test(logEvent);
}
}
- private static void test(final LogEvent logEvent) throws Exception {
+ private static void test(final LogEvent logEvent) {
final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
final Map<String, Object> jsonLayoutMap = renderUsingJsonLayout(logEvent);
Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(jsonLayoutMap);
}
private static Map<String, Object> renderUsingJsonTemplateLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
final Map<String, Object> map = renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
final Map<String, Object> emptySourceExcludedMap = removeEmptyObject(map, "source");
// JsonLayout blindly serializes the Throwable as a POJO, this is,
@@ -69,8 +68,7 @@ public class JsonLayoutTest {
}
private static Map<String, Object> renderUsingJsonLayout(
- final LogEvent logEvent)
- throws Exception {
+ final LogEvent logEvent) {
final Map<String, Object> map = renderUsing(logEvent, JSON_LAYOUT);
// JsonLayout blindly serializes the Throwable as a POJO, this is,
// to say the least, quite wrong, and I ain't gonna try to emulate
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
index 4e330f9..315e50f 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
@@ -19,13 +19,10 @@ package org.apache.logging.log4j.layout.json.template;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.MissingNode;
-import com.fasterxml.jackson.databind.node.NullNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.SocketAppender;
import org.apache.logging.log4j.core.config.Configuration;
@@ -36,7 +33,11 @@ import org.apache.logging.log4j.core.layout.ByteBufferDestination;
import org.apache.logging.log4j.core.lookup.MainMapLookup;
import org.apache.logging.log4j.core.net.Severity;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
import org.apache.logging.log4j.message.ObjectMessage;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.message.StringMapMessage;
@@ -44,7 +45,7 @@ import org.apache.logging.log4j.test.AvailablePortFinder;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
import org.apache.logging.log4j.util.Strings;
-import org.assertj.core.data.Percentage;
+import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -53,6 +54,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
@@ -60,12 +62,15 @@ import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -78,7 +83,11 @@ public class JsonTemplateLayoutTest {
private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
- private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance;
+ private static final JsonWriter JSON_WRITER = JsonWriter
+ .newBuilder()
+ .setMaxStringLength(10_000)
+ .setTruncatedStringSuffix("…")
+ .build();
private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
@@ -210,12 +219,14 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("@timestamp", "${json:timestamp:timeZone=Europe/Amsterdam}");
+ final String timestampFieldName = "@timestamp";
final String staticFieldName = "staticFieldName";
final String staticFieldValue = "staticFieldValue";
- eventTemplateRootNode.put(staticFieldName, staticFieldValue);
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ timestampFieldName, Map(
+ "$resolver", "timestamp",
+ "pattern", Map("timeZone", "Europe/Amsterdam")),
+ staticFieldName, staticFieldValue));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -225,28 +236,23 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "@timestamp").asText()).isEqualTo(timestamp);
- assertThat(point(rootNode, staticFieldName).asText()).isEqualTo(staticFieldValue);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(timestampFieldName)).isEqualTo(timestamp);
+ assertThat(accessor.getString(staticFieldName)).isEqualTo(staticFieldValue);
+ });
}
@Test
- public void test_log4j_deferred_runtime_resolver_for_MapMessage() throws Exception {
+ public void test_log4j_deferred_runtime_resolver_for_MapMessage() {
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("mapValue3", "${json:message:json}");
- eventTemplateRootNode.put("mapValue1", "${map:key1}");
- eventTemplateRootNode.put("mapValue2", "${map:key2}");
- eventTemplateRootNode.put(
- "nestedLookupEmptyValue",
- "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}");
- eventTemplateRootNode.put(
- "nestedLookupStaticValue",
- "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "mapValue3", Map("$resolver", "message"),
+ "mapValue1", "${map:key1}",
+ "mapValue2", "${map:key2}",
+ "nestedLookupEmptyValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}",
+ "nestedLookupStaticValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}"));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -269,22 +275,21 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
- assertThat(point(rootNode, "mapValue2").asText()).isEqualTo("val2");
- assertThat(point(rootNode, "nestedLookupEmptyValue").asText()).isEmpty();
- assertThat(point(rootNode, "nestedLookupStaticValue").asText()).isEqualTo("Static Value");
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+ assertThat(accessor.getString("mapValue2")).isEqualTo("val2");
+ assertThat(accessor.getString("nestedLookupEmptyValue")).isEmpty();
+ assertThat(accessor.getString("nestedLookupStaticValue")).isEqualTo("Static Value");
+ });
}
@Test
- public void test_MapMessage_serialization() throws Exception {
+ public void test_MapMessage_serialization() {
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:message:json}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map("$resolver", "message")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -307,16 +312,16 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "message", "key1").asText()).isEqualTo("val1");
- assertThat(point(rootNode, "message", "key2").asLong()).isEqualTo(0xDEADBEEF);
- assertThat(point(rootNode, "message", "key3", "key3.1").asText()).isEqualTo("val3.1");
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(new String[]{"message", "key1"})).isEqualTo("val1");
+ assertThat(accessor.getInteger(new String[]{"message", "key2"})).isEqualTo(0xDEADBEEF);
+ assertThat(accessor.getString(new String[]{"message", "key3", "key3.1"})).isEqualTo("val3.1");
+ });
}
@Test
- public void test_property_injection() throws Exception {
+ public void test_property_injection() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World");
@@ -328,10 +333,9 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template with property.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String propertyName = "propertyName";
- eventTemplateRootNode.put(propertyName, "${" + propertyName + "}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ propertyName, "${" + propertyName + "}"));
// Create the layout with property.
final String propertyValue = "propertyValue";
@@ -346,14 +350,13 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, propertyName).asText()).isEqualTo(propertyValue);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+ assertThat(accessor.getString(propertyName)).isEqualTo(propertyValue));
}
@Test
- public void test_empty_root_cause() throws Exception {
+ public void test_empty_root_cause() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -367,14 +370,27 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("ex_class", "${json:exception:className}");
- eventTemplateRootNode.put("ex_message", "${json:exception:message}");
- eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
- eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
- eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
- eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "ex_class", Map(
+ "$resolver", "exception",
+ "field", "className"),
+ "ex_message", Map(
+ "$resolver", "exception",
+ "field", "message"),
+ "ex_stacktrace", Map(
+ "$resolver", "exception",
+ "field", "stackTrace",
+ "stringified", true),
+ "root_ex_class", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "className"),
+ "root_ex_message", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "message"),
+ "root_ex_stacktrace", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "stackTrace",
+ "stringified", true)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -385,25 +401,25 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "ex_class").asText())
- .isEqualTo(exception.getClass().getCanonicalName());
- assertThat(point(rootNode, "ex_message").asText())
- .isEqualTo(exception.getMessage());
- assertThat(point(rootNode, "ex_stacktrace").asText())
- .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
- assertThat(point(rootNode, "root_ex_class").asText())
- .isEqualTo(point(rootNode, "ex_class").asText());
- assertThat(point(rootNode, "root_ex_message").asText())
- .isEqualTo(point(rootNode, "ex_message").asText());
- assertThat(point(rootNode, "root_ex_stacktrace").asText())
- .isEqualTo(point(rootNode, "ex_stacktrace").asText());
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString("ex_class"))
+ .isEqualTo(exception.getClass().getCanonicalName());
+ assertThat(accessor.getString("ex_message"))
+ .isEqualTo(exception.getMessage());
+ assertThat(accessor.getString("ex_stacktrace"))
+ .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+ assertThat(accessor.getString("root_ex_class"))
+ .isEqualTo(accessor.getString("ex_class"));
+ assertThat(accessor.getString("root_ex_message"))
+ .isEqualTo(accessor.getString("ex_message"));
+ assertThat(accessor.getString("root_ex_stacktrace"))
+ .isEqualTo(accessor.getString("ex_stacktrace"));
+ });
}
@Test
- public void test_root_cause() throws Exception {
+ public void test_root_cause() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -418,14 +434,27 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("ex_class", "${json:exception:className}");
- eventTemplateRootNode.put("ex_message", "${json:exception:message}");
- eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
- eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
- eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
- eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "ex_class", Map(
+ "$resolver", "exception",
+ "field", "className"),
+ "ex_message", Map(
+ "$resolver", "exception",
+ "field", "message"),
+ "ex_stacktrace", Map(
+ "$resolver", "exception",
+ "field", "stackTrace",
+ "stringified", true),
+ "root_ex_class", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "className"),
+ "root_ex_message", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "message"),
+ "root_ex_stacktrace", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "stackTrace",
+ "stringified", true)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -436,25 +465,25 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "ex_class").asText())
- .isEqualTo(exception.getClass().getCanonicalName());
- assertThat(point(rootNode, "ex_message").asText())
- .isEqualTo(exception.getMessage());
- assertThat(point(rootNode, "ex_stacktrace").asText())
- .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
- assertThat(point(rootNode, "root_ex_class").asText())
- .isEqualTo(exceptionCause.getClass().getCanonicalName());
- assertThat(point(rootNode, "root_ex_message").asText())
- .isEqualTo(exceptionCause.getMessage());
- assertThat(point(rootNode, "root_ex_stacktrace").asText())
- .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString("ex_class"))
+ .isEqualTo(exception.getClass().getCanonicalName());
+ assertThat(accessor.getString("ex_message"))
+ .isEqualTo(exception.getMessage());
+ assertThat(accessor.getString("ex_stacktrace"))
+ .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+ assertThat(accessor.getString("root_ex_class"))
+ .isEqualTo(exceptionCause.getClass().getCanonicalName());
+ assertThat(accessor.getString("root_ex_message"))
+ .isEqualTo(exceptionCause.getMessage());
+ assertThat(accessor.getString("root_ex_stacktrace"))
+ .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+ });
}
@Test
- public void test_marker_name() throws IOException {
+ public void test_marker_name() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -469,12 +498,13 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String messageKey = "message";
- eventTemplateRootNode.put(messageKey, "${json:message}");
final String markerNameKey = "marker";
- eventTemplateRootNode.put(markerNameKey, "${json:marker:name}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map("$resolver", "message"),
+ "marker", Map(
+ "$resolver", "marker",
+ "field", "name")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -484,10 +514,10 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, messageKey).asText()).isEqualTo(message.getFormattedMessage());
- assertThat(point(rootNode, markerNameKey).asText()).isEqualTo(markerName);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(messageKey)).isEqualTo(message.getFormattedMessage());
+ assertThat(accessor.getString(markerNameKey)).isEqualTo(markerName);
+ });
}
@@ -528,7 +558,7 @@ public class JsonTemplateLayoutTest {
}
@Test
- public void test_main_key_access() throws IOException {
+ public void test_main_key_access() {
// Set main() arguments.
final String kwKey = "--name";
@@ -548,11 +578,16 @@ public class JsonTemplateLayoutTest {
.build();
// Create the template.
- final ObjectNode templateRootNode = JSON_NODE_FACTORY.objectNode();
- templateRootNode.put("name", String.format("${json:main:%s}", kwKey));
- templateRootNode.put("positionArg", "${json:main:2}");
- templateRootNode.put("notFoundArg", String.format("${json:main:%s}", missingKwKey));
- final String template = templateRootNode.toString();
+ final String template = writeJson(Map(
+ "name", Map(
+ "$resolver", "main",
+ "key", kwKey),
+ "positionArg", Map(
+ "$resolver", "main",
+ "index", 2),
+ "notFoundArg", Map(
+ "$resolver", "main",
+ "key", missingKwKey)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -562,16 +597,16 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "name").asText()).isEqualTo(kwVal);
- assertThat(point(rootNode, "positionArg").asText()).isEqualTo(positionArg);
- assertThat(point(rootNode, "notFoundArg")).isInstanceOf(NullNode.class);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString("name")).isEqualTo(kwVal);
+ assertThat(accessor.getString("positionArg")).isEqualTo(positionArg);
+ assertThat(accessor.exists("notFoundArg")).isFalse();
+ });
}
@Test
- public void test_mdc_key_access() throws IOException {
+ public void test_mdc_key_access() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -592,14 +627,13 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put(
- mdcDirectlyAccessedKey,
- String.format("${json:mdc:key=%s}", mdcDirectlyAccessedKey));
- eventTemplateRootNode.put(
- mdcDirectlyAccessedNullPropertyKey,
- String.format("${json:mdc:key=%s}", mdcDirectlyAccessedNullPropertyKey));
- String eventTemplate = eventTemplateRootNode.toString();
+ String eventTemplate = writeJson(Map(
+ mdcDirectlyAccessedKey, Map(
+ "$resolver", "mdc",
+ "key", mdcDirectlyAccessedKey),
+ mdcDirectlyAccessedNullPropertyKey, Map(
+ "$resolver", "mdc",
+ "key", mdcDirectlyAccessedNullPropertyKey)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -610,15 +644,15 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, mdcDirectlyAccessedKey).asText()).isEqualTo(mdcDirectlyAccessedValue);
- assertThat(point(rootNode, mdcDirectlyAccessedNullPropertyKey)).isInstanceOf(NullNode.class);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(mdcDirectlyAccessedKey)).isEqualTo(mdcDirectlyAccessedValue);
+ assertThat(accessor.getString(mdcDirectlyAccessedNullPropertyKey)).isNull();
+ });
}
@Test
- public void test_mdc_pattern() throws IOException {
+ public void test_mdc_pattern() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -638,10 +672,11 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String mdcFieldName = "mdc";
- eventTemplateRootNode.put(mdcFieldName, "${json:mdc:pattern=" + mdcPatternMatchedKey + "}");
- String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ mdcFieldName, Map(
+ "$resolver", "mdc",
+ "pattern", mdcPatternMatchedKey)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -652,15 +687,15 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, mdcFieldName, mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
- assertThat(point(rootNode, mdcFieldName, mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(new String[]{mdcFieldName, mdcPatternMatchedKey})).isEqualTo(mdcPatternMatchedValue);
+ assertThat(accessor.exists(new String[]{mdcFieldName, mdcPatternMismatchedKey})).isFalse();
+ });
}
@Test
- public void test_mdc_flatten() throws IOException {
+ public void test_mdc_flatten() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -680,12 +715,12 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String mdcPrefix = "_mdc.";
- eventTemplateRootNode.put(
- mdcPrefix,
- "${json:mdc:flatten=" + mdcPrefix + ",pattern=" + mdcPatternMatchedKey + "}");
- String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "ignoredFieldName", Map(
+ "$resolver", "mdc",
+ "pattern", mdcPatternMatchedKey,
+ "flatten", Map("prefix", mdcPrefix))));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -696,15 +731,15 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, mdcPrefix + mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
- assertThat(point(rootNode, mdcPrefix + mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(mdcPrefix + mdcPatternMatchedKey)).isEqualTo(mdcPatternMatchedValue);
+ assertThat(accessor.exists(mdcPrefix + mdcPatternMismatchedKey)).isFalse();
+ });
}
@Test
- public void test_MapResolver() throws IOException {
+ public void test_MapResolver() {
// Create the log event.
final StringMapMessage message = new StringMapMessage().with("key1", "val1");
@@ -716,10 +751,13 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template node with map values.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("mapValue1", "${json:map:key1}");
- eventTemplateRootNode.put("mapValue2", "${json:map:noExist}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "mapValue1", Map(
+ "$resolver", "map",
+ "key", "key1"),
+ "mapValue2", Map(
+ "$resolver", "map",
+ "key", "key?")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -729,15 +767,15 @@ public class JsonTemplateLayoutTest {
.build();
// Check serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
- assertThat(point(rootNode, "mapValue2")).isInstanceOf(NullNode.class);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+ assertThat(accessor.getString("mapValue2")).isNull();
+ });
}
@Test
- public void test_message_json() throws IOException {
+ public void test_StringMapMessage() {
// Create the log event.
final StringMapMessage message = new StringMapMessage();
@@ -751,42 +789,8 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:message:json}");
- final String eventTemplate = eventTemplateRootNode.toString();
-
- // Create the layout.
- final JsonTemplateLayout layout = JsonTemplateLayout
- .newBuilder()
- .setConfiguration(CONFIGURATION)
- .setStackTraceEnabled(true)
- .setEventTemplate(eventTemplate)
- .build();
-
- // Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "message", "message").asText()).isEqualTo("Hello, World!");
- assertThat(point(rootNode, "message", "bottle").asText()).isEqualTo("Kickapoo Joy Juice");
-
- }
-
- @Test
- public void test_message_json_fallback() throws IOException {
-
- // Create the log event.
- final SimpleMessage message = new SimpleMessage("Hello, World!");
- final LogEvent logEvent = Log4jLogEvent
- .newBuilder()
- .setLoggerName(LOGGER_NAME)
- .setLevel(Level.INFO)
- .setMessage(message)
- .build();
-
- // Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:message:json}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map("$resolver", "message")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -797,14 +801,15 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "message").asText()).isEqualTo("Hello, World!");
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getString(new String[]{"message", "message"})).isEqualTo("Hello, World!");
+ assertThat(accessor.getString(new String[]{"message", "bottle"})).isEqualTo("Kickapoo Joy Juice");
+ });
}
@Test
- public void test_message_object() throws IOException {
+ public void test_ObjectMessage() {
// Create the log event.
final int id = 0xDEADBEEF;
@@ -822,9 +827,8 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:message:json}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map("$resolver", "message")));
// Create the layout.
JsonTemplateLayout layout = JsonTemplateLayout
@@ -835,41 +839,41 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "message", "id").asInt()).isEqualTo(id);
- assertThat(point(rootNode, "message", "name").asText()).isEqualTo(name);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getInteger(new String[]{"message", "id"})).isEqualTo(id);
+ assertThat(accessor.getString(new String[]{"message", "name"})).isEqualTo(name);
+ });
}
@Test
- public void test_StackTraceElement_template() throws IOException {
+ public void test_StackTraceElement_template() {
// Create the stack trace element template.
- final ObjectNode stackTraceElementTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String classNameFieldName = "className";
- stackTraceElementTemplateRootNode.put(
- classNameFieldName,
- "${json:stackTraceElement:className}");
final String methodNameFieldName = "methodName";
- stackTraceElementTemplateRootNode.put(
- methodNameFieldName,
- "${json:stackTraceElement:methodName}");
final String fileNameFieldName = "fileName";
- stackTraceElementTemplateRootNode.put(
- fileNameFieldName,
- "${json:stackTraceElement:fileName}");
final String lineNumberFieldName = "lineNumber";
- stackTraceElementTemplateRootNode.put(
- lineNumberFieldName,
- "${json:stackTraceElement:lineNumber}");
- final String stackTraceElementTemplate = stackTraceElementTemplateRootNode.toString();
+ final String stackTraceElementTemplate = writeJson(Map(
+ classNameFieldName, Map(
+ "$resolver", "stackTraceElement",
+ "field", "className"),
+ methodNameFieldName, Map(
+ "$resolver", "stackTraceElement",
+ "field", "methodName"),
+ fileNameFieldName, Map(
+ "$resolver", "stackTraceElement",
+ "field", "fileName"),
+ lineNumberFieldName, Map(
+ "$resolver", "stackTraceElement",
+ "field", "lineNumber")));
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String stackTraceFieldName = "stackTrace";
- eventTemplateRootNode.put(stackTraceFieldName, "${json:exception:stackTrace}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ stackTraceFieldName, Map(
+ "$resolver", "exception",
+ "field", "stackTrace")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -893,27 +897,29 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- final JsonNode stackTraceNode = point(rootNode, stackTraceFieldName);
- assertThat(stackTraceNode.isArray()).isTrue();
- final StackTraceElement[] stackTraceElements = exception.getStackTrace();
- assertThat(stackTraceNode.size()).isEqualTo(stackTraceElements.length);
- for (int stackTraceElementIndex = 0;
- stackTraceElementIndex < stackTraceElements.length;
- stackTraceElementIndex++) {
- final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
- final JsonNode stackTraceElementNode = stackTraceNode.get(stackTraceElementIndex);
- assertThat(stackTraceElementNode.size()).isEqualTo(4);
- assertThat(point(stackTraceElementNode, classNameFieldName).asText())
- .isEqualTo(stackTraceElement.getClassName());
- assertThat(point(stackTraceElementNode, methodNameFieldName).asText())
- .isEqualTo(stackTraceElement.getMethodName());
- assertThat(point(stackTraceElementNode, fileNameFieldName).asText())
- .isEqualTo(stackTraceElement.getFileName());
- assertThat(point(stackTraceElementNode, lineNumberFieldName).asInt())
- .isEqualTo(stackTraceElement.getLineNumber());
- }
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.exists(stackTraceFieldName)).isTrue();
+ @SuppressWarnings("unchecked")
+ final List<Map<String, Object>> deserializedStackTraceElements =
+ accessor.getObject(stackTraceFieldName, List.class);
+ final StackTraceElement[] stackTraceElements = exception.getStackTrace();
+ assertThat(deserializedStackTraceElements.size()).isEqualTo(stackTraceElements.length);
+ for (int stackTraceElementIndex = 0;
+ stackTraceElementIndex < stackTraceElements.length;
+ stackTraceElementIndex++) {
+ final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
+ final Map<String, Object> deserializedStackTraceElement = deserializedStackTraceElements.get(stackTraceElementIndex);
+ assertThat(deserializedStackTraceElement.size()).isEqualTo(4);
+ assertThat(deserializedStackTraceElement.get(classNameFieldName))
+ .isEqualTo(stackTraceElement.getClassName());
+ assertThat(deserializedStackTraceElement.get(methodNameFieldName))
+ .isEqualTo(stackTraceElement.getMethodName());
+ assertThat(deserializedStackTraceElement.get(fileNameFieldName))
+ .isEqualTo(stackTraceElement.getFileName());
+ assertThat(deserializedStackTraceElement.get(lineNumberFieldName))
+ .isEqualTo(stackTraceElement.getLineNumber());
+ }
+ });
}
@@ -981,7 +987,7 @@ public class JsonTemplateLayoutTest {
}
@Test
- public void test_maxStringLength() throws IOException {
+ public void test_maxStringLength() {
// Create the log event.
final int maxStringLength = 30;
@@ -997,15 +1003,16 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template node with map values.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
final String messageKey = "message";
- eventTemplateRootNode.put(messageKey, "${json:message}");
final String excessiveKey = Strings.repeat("k", maxStringLength) + 'K';
final String excessiveValue = Strings.repeat("v", maxStringLength) + 'V';
- eventTemplateRootNode.put(excessiveKey, excessiveValue);
final String nullValueKey = "nullValueKey";
- eventTemplateRootNode.put(nullValueKey, "${json:exception:message}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ messageKey, Map("$resolver", "message"),
+ excessiveKey, excessiveValue,
+ nullValueKey, Map(
+ "$resolver", "exception",
+ "field", "message")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1016,22 +1023,22 @@ public class JsonTemplateLayoutTest {
.build();
// Check serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- final String truncatedStringSuffix =
- JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
- final String truncatedMessageString =
- excessiveMessageString.substring(0, maxStringLength) +
- truncatedStringSuffix;
- assertThat(point(rootNode, messageKey).asText()).isEqualTo(truncatedMessageString);
- final String truncatedKey =
- excessiveKey.substring(0, maxStringLength) +
- truncatedStringSuffix;
- final String truncatedValue =
- excessiveValue.substring(0, maxStringLength) +
- truncatedStringSuffix;
- assertThat(point(rootNode, truncatedKey).asText()).isEqualTo(truncatedValue);
- assertThat(point(rootNode, nullValueKey).isNull()).isTrue();
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ final String truncatedStringSuffix =
+ JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
+ final String truncatedMessageString =
+ excessiveMessageString.substring(0, maxStringLength) +
+ truncatedStringSuffix;
+ assertThat(accessor.getString(messageKey)).isEqualTo(truncatedMessageString);
+ final String truncatedKey =
+ excessiveKey.substring(0, maxStringLength) +
+ truncatedStringSuffix;
+ final String truncatedValue =
+ excessiveValue.substring(0, maxStringLength) +
+ truncatedStringSuffix;
+ assertThat(accessor.getString(truncatedKey)).isEqualTo(truncatedValue);
+ assertThat(accessor.getString(nullValueKey)).isNull();
+ });
}
@@ -1066,7 +1073,7 @@ public class JsonTemplateLayoutTest {
}
@Test
- public void test_exception_with_nonAscii_utf8_method_name() throws IOException {
+ public void test_exception_with_nonAscii_utf8_method_name() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -1080,9 +1087,11 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "ex_stacktrace", Map(
+ "$resolver", "exception",
+ "field", "stackTrace",
+ "stringified", true)));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1093,15 +1102,14 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "ex_stacktrace").asText())
- .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+ assertThat(accessor.getString("ex_stacktrace"))
+ .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT));
}
@Test
- public void test_event_template_additional_fields() throws IOException {
+ public void test_event_template_additional_fields() {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -1116,16 +1124,29 @@ public class JsonTemplateLayoutTest {
.build();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("level", "${json:level}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = "{}";
// Create the layout.
- final KeyValuePair additionalField1 = new KeyValuePair("message", "${json:message}");
- final KeyValuePair additionalField2 = new KeyValuePair("@version", "1");
- final KeyValuePair[] additionalFieldPairs = {additionalField1, additionalField2};
- final JsonTemplateLayout.EventTemplateAdditionalFields additionalFields = JsonTemplateLayout
- .EventTemplateAdditionalFields
+ final EventTemplateAdditionalField[] additionalFieldPairs = {
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("number")
+ .setValue("1")
+ .setType(EventTemplateAdditionalField.Type.JSON)
+ .build(),
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("string")
+ .setValue("foo")
+ .build(),
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("level")
+ .setValue("{\"$resolver\": \"level\", \"field\": \"name\"}")
+ .setType(EventTemplateAdditionalField.Type.JSON)
+ .build()
+ };
+ final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
.newBuilder()
.setAdditionalFields(additionalFieldPairs)
.build();
@@ -1138,53 +1159,63 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "level").asText()).isEqualTo(level.name());
- assertThat(point(rootNode, additionalField1.getKey()).asText()).isEqualTo(message.getFormattedMessage());
- assertThat(point(rootNode, additionalField2.getKey()).asText()).isEqualTo(additionalField2.getValue());
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getInteger("number")).isEqualTo(1);
+ assertThat(accessor.getString("string")).isEqualTo("foo");
+ assertThat(accessor.getString("level")).isEqualTo(level.name());
+ });
}
@Test
@SuppressWarnings("FloatingPointLiteralPrecision")
- public void test_timestamp_epoch_accessor() throws IOException {
-
- // Create the log event.
- final SimpleMessage message = new SimpleMessage("Hello, World!");
- final Level level = Level.ERROR;
- final MutableInstant instant = new MutableInstant();
- final long instantEpochSecond = 1581082727L;
- final int instantEpochSecondNano = 982123456;
- instant.initFromEpochSecond(instantEpochSecond, instantEpochSecondNano);
- final LogEvent logEvent = Log4jLogEvent
- .newBuilder()
- .setLoggerName(LOGGER_NAME)
- .setLevel(level)
- .setMessage(message)
- .setInstant(instant)
- .build();
+ public void test_timestamp_epoch_resolvers() {
+
+ final List<Map<String, Object>> testCases = Arrays.asList(
+ Map(
+ "epochSecs", new BigDecimal("1581082727.982123456"),
+ "epochSecsRounded", 1581082727,
+ "epochSecsNanos", 982123456,
+ "epochMillis", new BigDecimal("1581082727982.123456"),
+ "epochMillisRounded", 1581082727982L,
+ "epochMillisNanos", 123456,
+ "epochNanos", 1581082727982123456L),
+ Map(
+ "epochSecs", new BigDecimal("1591177590.005000001"),
+ "epochSecsRounded", 1591177590,
+ "epochSecsNanos", 5000001,
+ "epochMillis", new BigDecimal("1591177590005.000001"),
+ "epochMillisRounded", 1591177590005L,
+ "epochMillisNanos", 1,
+ "epochNanos", 1591177590005000001L));
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- final ObjectNode epochSecsNode = eventTemplateRootNode.putObject("epochSecs");
- epochSecsNode.put("double", "${json:timestamp:epoch:secs}");
- epochSecsNode.put("long", "${json:timestamp:epoch:secs,integral}");
- epochSecsNode.put("nanos", "${json:timestamp:epoch:secs.nanos}");
- epochSecsNode.put("micros", "${json:timestamp:epoch:secs.micros}");
- epochSecsNode.put("millis", "${json:timestamp:epoch:secs.millis}");
- final ObjectNode epochMillisNode = eventTemplateRootNode.putObject("epochMillis");
- epochMillisNode.put("double", "${json:timestamp:epoch:millis}");
- epochMillisNode.put("long", "${json:timestamp:epoch:millis,integral}");
- epochMillisNode.put("nanos", "${json:timestamp:epoch:millis.nanos}");
- epochMillisNode.put("micros", "${json:timestamp:epoch:millis.micros}");
- final ObjectNode epochMicrosNode = eventTemplateRootNode.putObject("epochMicros");
- epochMicrosNode.put("double", "${json:timestamp:epoch:micros}");
- epochMicrosNode.put("long", "${json:timestamp:epoch:micros,integral}");
- epochMicrosNode.put("nanos", "${json:timestamp:epoch:micros.nanos}");
- final ObjectNode epochNanosNode = eventTemplateRootNode.putObject("epochNanos");
- epochNanosNode.put("long", "${json:timestamp:epoch:nanos}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "epochSecs", Map(
+ "$resolver", "timestamp",
+ "epoch", Map("unit", "secs")),
+ "epochSecsRounded", Map(
+ "$resolver", "timestamp",
+ "epoch", Map(
+ "unit", "secs",
+ "rounded", true)),
+ "epochSecsNanos", Map(
+ "$resolver", "timestamp",
+ "epoch", Map("unit", "secs.nanos")),
+ "epochMillis", Map(
+ "$resolver", "timestamp",
+ "epoch", Map("unit", "millis")),
+ "epochMillisRounded", Map(
+ "$resolver", "timestamp",
+ "epoch", Map(
+ "unit", "millis",
+ "rounded", true)),
+ "epochMillisNanos", Map(
+ "$resolver", "timestamp",
+ "epoch", Map("unit", "millis.nanos")),
+ "epochNanos", Map(
+ "$resolver", "timestamp",
+ "epoch", Map("unit", "nanos"))));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1193,120 +1224,40 @@ public class JsonTemplateLayoutTest {
.setEventTemplate(eventTemplate)
.build();
- // Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- final Percentage errorMargin = Percentage.withPercentage(0.001D);
- assertThat(point(rootNode, "epochSecs", "double").asDouble())
- .isCloseTo(1581082727.982123456D, errorMargin);
- assertThat(point(rootNode, "epochSecs", "long").asLong())
- .isEqualTo(1581082727L);
- assertThat(point(rootNode, "epochSecs", "nanos").asInt())
- .isEqualTo(982123456L);
- assertThat(point(rootNode, "epochSecs", "micros").asInt())
- .isEqualTo(982123L);
- assertThat(point(rootNode, "epochSecs", "millis").asInt())
- .isEqualTo(982L);
- assertThat(point(rootNode, "epochMillis", "double").asDouble())
- .isCloseTo(1581082727982.123456D, errorMargin);
- assertThat(point(rootNode, "epochMillis", "long").asLong())
- .isEqualTo(1581082727982L);
- assertThat(point(rootNode, "epochMillis", "nanos").asInt())
- .isEqualTo(123456);
- assertThat(point(rootNode, "epochMillis", "micros").asInt())
- .isEqualTo(123);
- assertThat(point(rootNode, "epochMicros", "double").asDouble())
- .isCloseTo(1581082727982123.456D, errorMargin);
- assertThat(point(rootNode, "epochMicros", "long").asLong())
- .isEqualTo(1581082727982123L);
- assertThat(point(rootNode, "epochMicros", "nanos").asInt())
- .isEqualTo(456);
- assertThat(point(rootNode, "epochNanos", "long").asLong())
- .isEqualTo(1581082727982123456L);
-
- }
-
- @Test
- public void test_level_severity() throws IOException {
-
- // Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("severity", "${json:level:severity}");
- eventTemplateRootNode.put("severityCode", "${json:level:severity:code}");
- final String eventTemplate = eventTemplateRootNode.toString();
-
- // Create the layout.
- final JsonTemplateLayout layout = JsonTemplateLayout
- .newBuilder()
- .setConfiguration(CONFIGURATION)
- .setEventTemplate(eventTemplate)
- .build();
-
- for (final Level level : Level.values()) {
+ testCases.forEach(testCase -> {
// Create the log event.
final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final Level level = Level.ERROR;
+ final MutableInstant instant = new MutableInstant();
+ final Object instantSecsObject = testCase.get("epochSecsRounded");
+ final long instantSecs = instantSecsObject instanceof Long
+ ? (long) instantSecsObject
+ : (int) instantSecsObject;
+ final int instantSecsNanos = (int) testCase.get("epochSecsNanos");
+ instant.initFromEpochSecond(instantSecs, instantSecsNanos);
final LogEvent logEvent = Log4jLogEvent
.newBuilder()
.setLoggerName(LOGGER_NAME)
.setLevel(level)
.setMessage(message)
+ .setInstant(instant)
.build();
- // Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- final Severity expectedSeverity = Severity.getSeverity(level);
- final String expectedSeverityName = expectedSeverity.name();
- final int expectedSeverityCode = expectedSeverity.getCode();
- assertThat(point(rootNode, "severity").asText()).isEqualTo(expectedSeverityName);
- assertThat(point(rootNode, "severityCode").asInt()).isEqualTo(expectedSeverityCode);
-
- }
-
- }
-
- @Test
- public void test_exception_resolvers_against_no_exceptions() throws IOException {
-
- // Create the log event.
- final SimpleMessage message = new SimpleMessage("Hello, World!");
- final LogEvent logEvent = Log4jLogEvent
- .newBuilder()
- .setLoggerName(LOGGER_NAME)
- .setMessage(message)
- .build();
-
- // Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("exceptionStackTrace", "${json:exception:stackTrace}");
- eventTemplateRootNode.put("exceptionStackTraceText", "${json:exception:stackTrace:text}");
- eventTemplateRootNode.put("exceptionRootCauseStackTrace", "${json:exceptionRootCause:stackTrace}");
- eventTemplateRootNode.put("exceptionRootCauseStackTraceText", "${json:exceptionRootCause:stackTrace:text}");
- eventTemplateRootNode.put("requiredFieldTriggeringError", true);
- final String eventTemplate = eventTemplateRootNode.toString();
+ // Verify the test case.
+ usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+ testCase.forEach((key, expectedValue) ->
+ Assertions
+ .assertThat(accessor.getObject(key))
+ .describedAs("key=%s", key)
+ .isEqualTo(expectedValue)));
- // Create the layout.
- final JsonTemplateLayout layout = JsonTemplateLayout
- .newBuilder()
- .setConfiguration(CONFIGURATION)
- .setEventTemplate(eventTemplate)
- .setStackTraceEnabled(true)
- .build();
-
- // Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "exceptionStackTrace")).isInstanceOf(MissingNode.class);
- assertThat(point(rootNode, "exceptionStackTraceText")).isInstanceOf(MissingNode.class);
- assertThat(point(rootNode, "exceptionRootCauseStackTrace")).isInstanceOf(MissingNode.class);
- assertThat(point(rootNode, "exceptionRootCauseStackTraceText")).isInstanceOf(MissingNode.class);
- assertThat(point(rootNode, "requiredFieldTriggeringError").asBoolean()).isTrue();
+ });
}
@Test
- public void test_timestamp_resolver() throws IOException {
+ public void test_timestamp_pattern_resolver() {
// Create log events.
final String logEvent1FormattedInstant = "2019-01-02T09:34:11Z";
@@ -1320,14 +1271,12 @@ public class JsonTemplateLayoutTest {
final LogEvent logEvent4 = createLogEventAtInstant(logEvent4FormattedInstant);
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put(
- "timestamp",
- "${json:timestamp:" +
- "pattern=yyyy-MM-dd'T'HH:mm:ss'Z'," +
- "timeZone=UTC" +
- "}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "timestamp", Map(
+ "$resolver", "timestamp",
+ "pattern", Map(
+ "format", "yyyy-MM-dd'T'HH:mm:ss'Z'",
+ "timeZone", "UTC"))));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1337,24 +1286,24 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized 1st event.
- final String serializedLogEvent1 = layout.toSerializable(logEvent1);
- final JsonNode rootNode1 = OBJECT_MAPPER.readTree(serializedLogEvent1);
- assertThat(point(rootNode1, "timestamp").asText()).isEqualTo(logEvent1FormattedInstant);
+ usingSerializedLogEventAccessor(layout, logEvent1, accessor ->
+ assertThat(accessor.getString("timestamp"))
+ .isEqualTo(logEvent1FormattedInstant));
// Check the serialized 2nd event.
- final String serializedLogEvent2 = layout.toSerializable(logEvent2);
- final JsonNode rootNode2 = OBJECT_MAPPER.readTree(serializedLogEvent2);
- assertThat(point(rootNode2, "timestamp").asText()).isEqualTo(logEvent2FormattedInstant);
+ usingSerializedLogEventAccessor(layout, logEvent2, accessor ->
+ assertThat(accessor.getString("timestamp"))
+ .isEqualTo(logEvent2FormattedInstant));
// Check the serialized 3rd event.
- final String serializedLogEvent3 = layout.toSerializable(logEvent3);
- final JsonNode rootNode3 = OBJECT_MAPPER.readTree(serializedLogEvent3);
- assertThat(point(rootNode3, "timestamp").asText()).isEqualTo(logEvent3FormattedInstant);
+ usingSerializedLogEventAccessor(layout, logEvent3, accessor ->
+ assertThat(accessor.getString("timestamp"))
+ .isEqualTo(logEvent3FormattedInstant));
// Check the serialized 4th event.
- final String serializedLogEvent4 = layout.toSerializable(logEvent4);
- final JsonNode rootNode4 = OBJECT_MAPPER.readTree(serializedLogEvent4);
- assertThat(point(rootNode4, "timestamp").asText()).isEqualTo(logEvent4FormattedInstant);
+ usingSerializedLogEventAccessor(layout, logEvent4, accessor ->
+ assertThat(accessor.getString("timestamp"))
+ .isEqualTo(logEvent4FormattedInstant));
}
@@ -1372,12 +1321,107 @@ public class JsonTemplateLayoutTest {
}
@Test
- public void test_StackTraceTextResolver_with_maxStringLength() throws Exception {
+ public void test_level_severity() {
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("stackTrace", "${json:exception:stackTrace:text}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "severityKeyword", Map(
+ "$resolver", "level",
+ "field", "severity",
+ "severity", Map("field", "keyword")),
+ "severityCode", Map(
+ "$resolver", "level",
+ "field", "severity",
+ "severity", Map("field", "code"))));
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ for (final Level level : Level.values()) {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(level)
+ .setMessage(message)
+ .build();
+
+ // Check the serialized event.
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ final Severity expectedSeverity = Severity.getSeverity(level);
+ final String expectedSeverityKeyword = expectedSeverity.name();
+ final int expectedSeverityCode = expectedSeverity.getCode();
+ assertThat(accessor.getString("severityKeyword")).isEqualTo(expectedSeverityKeyword);
+ assertThat(accessor.getInteger("severityCode")).isEqualTo(expectedSeverityCode);
+ });
+
+ }
+
+ }
+
+ @Test
+ public void test_exception_resolvers_against_no_exceptions() {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setMessage(message)
+ .build();
+
+ // Create the event template.
+ final String eventTemplate = writeJson(Map(
+ "exStackTrace", Map(
+ "$resolver", "exception",
+ "field", "stackTrace"),
+ "exStackTraceString", Map(
+ "$resolver", "exception",
+ "field", "stackTrace",
+ "stringified", true),
+ "exRootCauseStackTrace", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "stackTrace"),
+ "exRootCauseStackTraceString", Map(
+ "$resolver", "exceptionRootCause",
+ "field", "stackTrace",
+ "stringified", true),
+ "requiredFieldTriggeringError", true));
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .setStackTraceEnabled(true)
+ .build();
+
+ // Check the serialized event.
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ assertThat(accessor.getObject("exStackTrace")).isNull();
+ assertThat(accessor.getObject("exStackTraceString")).isNull();
+ assertThat(accessor.getObject("exRootCauseStackTrace")).isNull();
+ assertThat(accessor.getObject("exRootCauseStackTraceString")).isNull();
+ assertThat(accessor.getBoolean("requiredFieldTriggeringError")).isTrue();
+ });
+
+ }
+
+ @Test
+ public void test_StackTraceTextResolver_with_maxStringLength() {
+
+ // Create the event template.
+ final String eventTemplate = writeJson(Map(
+ "stackTrace", Map(
+ "$resolver", "exception",
+ "field", "stackTrace",
+ "stringified", true)));
// Create the layout.
final int maxStringLength = eventTemplate.length();
@@ -1399,9 +1443,11 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- assertThat(point(rootNode, "stackTrace").asText()).isNotBlank();
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ final int expectedLength = maxStringLength +
+ JsonTemplateLayoutDefaults.getTruncatedStringSuffix().length();
+ assertThat(accessor.getString("stackTrace").length()).isEqualTo(expectedLength);
+ });
}
@@ -1409,9 +1455,7 @@ public class JsonTemplateLayoutTest {
public void test_null_eventDelimiter() {
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("key", "val");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map("key", "val"));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1443,9 +1487,8 @@ public class JsonTemplateLayoutTest {
final List<LogEvent> logEvents = createNastyLogEvents();
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:message}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map("$resolver", "message")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1616,12 +1659,13 @@ public class JsonTemplateLayoutTest {
}
@Test
- public void test_PatternResolver() throws IOException {
+ public void test_PatternResolver() {
// Create the event template.
- final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
- eventTemplateRootNode.put("message", "${json:pattern:%p:%m}");
- final String eventTemplate = eventTemplateRootNode.toString();
+ final String eventTemplate = writeJson(Map(
+ "message", Map(
+ "$resolver", "pattern",
+ "pattern", "%p:%m")));
// Create the layout.
final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1641,13 +1685,53 @@ public class JsonTemplateLayoutTest {
.build();
// Check the serialized event.
- final String serializedLogEvent = layout.toSerializable(logEvent);
- final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
- final String expectedMessage = String.format(
- "%s:%s",
- level, message.getFormattedMessage());
- assertThat(point(rootNode, "message").asText()).isEqualTo(expectedMessage);
+ usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+ final String expectedMessage = String.format(
+ "%s:%s",
+ level, message.getFormattedMessage());
+ assertThat(accessor.getString("message")).isEqualTo(expectedMessage);
+ });
+
+ }
+ private static String writeJson(final Object value) {
+ final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
+ stringBuilder.setLength(0);
+ try {
+ JSON_WRITER.writeValue(value);
+ return stringBuilder.toString();
+ } finally {
+ stringBuilder.setLength(0);
+ }
+ }
+
+ private static void usingSerializedLogEventAccessor(
+ final Layout<String> layout,
+ final LogEvent logEvent,
+ final Consumer<MapAccessor> accessorConsumer) {
+ final String serializedLogEventJson = layout.toSerializable(logEvent);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> serializedLogEvent =
+ (Map<String, Object>) readJson(serializedLogEventJson);
+ final MapAccessor serializedLogEventAccessor = new MapAccessor(serializedLogEvent);
+ accessorConsumer.accept(serializedLogEventAccessor);
+ }
+
+ private static Object readJson(final String json) {
+ return JsonReader.read(json);
+ }
+
+ private static Map<String, Object> Map(final Object... pairs) {
+ final Map<String, Object> map = new LinkedHashMap<>();
+ if (pairs.length % 2 != 0) {
+ throw new IllegalArgumentException("odd number of arguments");
+ }
+ for (int i = 0; i < pairs.length; i += 2) {
+ final String key = (String) pairs[i];
+ final Object value = pairs[i + 1];
+ map.put(key, value);
+ }
+ return map;
}
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
index 62ad35d..6f0a6c0 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
@@ -2,6 +2,7 @@ package org.apache.logging.log4j.layout.json.template;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
import java.util.Map;
@@ -10,10 +11,9 @@ enum LayoutComparisonHelpers {;
@SuppressWarnings("unchecked")
static Map<String, Object> renderUsing(
final LogEvent logEvent,
- final Layout<String> layout)
- throws Exception {
+ final Layout<String> layout) {
final String json = layout.toSerializable(logEvent);
- return JacksonFixture.getObjectMapper().readValue(json, Map.class);
+ return (Map<String, Object>) JsonReader.read(json);
}
}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
index 00571b7..9c30be8 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
@@ -10,8 +10,8 @@ import org.apache.logging.log4j.core.appender.SocketAppender;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.layout.GelfLayout;
-import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.core.util.NetUtils;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.status.StatusLogger;
@@ -81,9 +81,14 @@ public class LogstashIT {
.setEventTemplateAdditionalFields(JsonTemplateLayout
.EventTemplateAdditionalFields
.newBuilder()
- .setAdditionalFields(new KeyValuePair[]{
- new KeyValuePair("host", HOST_NAME)
- })
+ .setAdditionalFields(
+ new EventTemplateAdditionalField[]{
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("host")
+ .setValue(HOST_NAME)
+ .build()
+ })
.build())
.build();
@@ -103,9 +108,14 @@ public class LogstashIT {
.setEventTemplateAdditionalFields(JsonTemplateLayout
.EventTemplateAdditionalFields
.newBuilder()
- .setAdditionalFields(new KeyValuePair[]{
- new KeyValuePair("service.name", SERVICE_NAME)
- })
+ .setAdditionalFields(
+ new EventTemplateAdditionalField[]{
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("service.name")
+ .setValue(SERVICE_NAME)
+ .build()
+ })
.build())
.build();
diff --git a/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
index c1cc187..daf455e 100644
--- a/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
+++ b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
@@ -1,21 +1,68 @@
{
- "exception_class": "${json:exception:className}",
- "exception_message": "${json:exception:message}",
- "stacktrace": "${json:exception:stackTrace:text}",
- "line_number": "${json:source:lineNumber}",
- "class": "${json:source:className}",
+ "exception_class": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "exception_message": {
+ "$resolver": "exception",
+ "field": "message"
+ },
+ "stacktrace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ },
+ "line_number": {
+ "$resolver": "source",
+ "field": "lineNumber"
+ },
+ "class": {
+ "$resolver": "source",
+ "field": "className"
+ },
"@version": 1,
"source_host": "${hostName}",
- "message": "${json:message}",
- "thread_id": "${json:thread:id}",
- "thread_name": "${json:thread:name}",
- "thread_priority": "${json:thread:priority}",
- "@timestamp": "${json:timestamp}",
- "level": "${json:level}",
- "file": "${json:source:fileName}",
- "method": "${json:source:methodName}",
- "logger_fqcn": "${json:logger:fqcn}",
- "logger_name": "${json:logger:name}",
- "end_of_batch": "${json:endOfBatch}",
+ "message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "thread_id": {
+ "$resolver": "thread",
+ "field": "id"
+ },
+ "thread_name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "thread_priority": {
+ "$resolver": "thread",
+ "field": "priority"
+ },
+ "@timestamp": {
+ "$resolver": "timestamp"
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "file": {
+ "$resolver": "source",
+ "field": "fileName"
+ },
+ "method": {
+ "$resolver": "source",
+ "field": "methodName"
+ },
+ "logger_fqcn": {
+ "$resolver": "logger",
+ "field": "fqcn"
+ },
+ "logger_name": {
+ "$resolver": "logger",
+ "field": "name"
+ },
+ "end_of_batch": {
+ "$resolver": "endOfBatch"
+ },
"lookup_test_key": "${sys:lookup_test_key}"
}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
index c175f1f..dc8dcd1 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
@@ -25,6 +25,7 @@ import org.apache.logging.log4j.core.layout.GelfLayout;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.core.util.NetUtils;
import org.apache.logging.log4j.jackson.json.layout.JsonLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
import org.openjdk.jmh.annotations.Scope;
@@ -88,9 +89,14 @@ public class JsonTemplateLayoutBenchmarkState {
private static JsonTemplateLayout createJsonTemplateLayout4EcsLayout() {
final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
.newBuilder()
- .setAdditionalFields(new KeyValuePair[]{
- new KeyValuePair("service.name", "benchmark")
- })
+ .setAdditionalFields(
+ new EventTemplateAdditionalField[]{
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("service.name")
+ .setValue("benchmark")
+ .build()
+ })
.build();
return JsonTemplateLayout
.newBuilder()
@@ -111,12 +117,17 @@ public class JsonTemplateLayoutBenchmarkState {
.setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
.setEventTemplateAdditionalFields(EventTemplateAdditionalFields
.newBuilder()
- .setAdditionalFields(new KeyValuePair[]{
- // Adding "host" as a constant rather than using
- // the "hostName" property lookup at runtime, which
- // is what GelfLayout does as well.
- new KeyValuePair("host", NetUtils.getLocalHostname())
- })
+ .setAdditionalFields(
+ new EventTemplateAdditionalField[]{
+ // Adding "host" as a constant rather than using
+ // the "hostName" property lookup at runtime, which
+ // is what GelfLayout does as well.
+ EventTemplateAdditionalField
+ .newBuilder()
+ .setKey("host")
+ .setValue(NetUtils.getLocalHostname())
+ .build()
+ })
.build())
.build();
}
@@ -135,7 +146,7 @@ public class JsonTemplateLayoutBenchmarkState {
.setConfiguration(CONFIGURATION)
.setCharset(CHARSET)
.setAdditionalFields(new KeyValuePair[]{
- new KeyValuePair("@version", "1")
+ new KeyValuePair("@version", "\"1\"")
})
.build();
}
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc b/src/site/asciidoc/manual/json-template-layout.adoc
index 29db221..d541dc2 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc
+++ b/src/site/asciidoc/manual/json-template-layout.adoc
@@ -29,8 +29,8 @@ by the JSON template provided. In a nutshell, it shines with its
[#usage]
== Usage
-Adding `log4j-layout-json-template` artifact to your dependencies is enough to
-enable access to `JsonTemplateLayout` in your Log4j configuration:
+Adding `log4j-layout-json-template` artifact to your list of dependencies is
+enough to enable access to `JsonTemplateLayout` in your Log4j configuration:
[source,xml]
----
@@ -43,28 +43,67 @@ enable access to `JsonTemplateLayout` in your Log4j configuration:
For instance, given the following JSON template modelling the
https://github.com/logstash/log4j-jsonevent-layout[the official Logstash
-`JSONEventLayoutV1`]
+`JSONEventLayoutV1`] (accessible via `classpath:LogstashJsonEventLayoutV1.json`)
[source,json]
----
{
- "mdc": "${json:mdc}",
+ "mdc": {
+ "$resolver": "mdc"
+ },
"exception": {
- "exception_class": "${json:exception:className}",
- "exception_message": "${json:exception:message}",
- "stacktrace": "${json:exception:stackTrace:text}"
+ "exception_class": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "exception_message": {
+ "$resolver": "exception",
+ "field": "message",
+ "stringified": true
+ },
+ "stacktrace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ }
+ },
+ "line_number": {
+ "$resolver": "source",
+ "field": "lineNumber"
+ },
+ "class": {
+ "$resolver": "source",
+ "field": "className"
},
- "line_number": "${json:source:lineNumber}",
- "class": "${json:source:className}",
"@version": 1,
"source_host": "${hostName}",
- "message": "${json:message}",
- "thread_name": "${json:thread:name}",
- "@timestamp": "${json:timestamp}",
- "level": "${json:level}",
- "file": "${json:source:fileName}",
- "method": "${json:source:methodName}",
- "logger_name": "${json:logger:name}"
+ "message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "thread_name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "@timestamp": {
+ "$resolver": "timestamp"
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "file": {
+ "$resolver": "source",
+ "field": "fileName"
+ },
+ "method": {
+ "$resolver": "source",
+ "field": "methodName"
+ },
+ "logger_name": {
+ "$resolver": "logger",
+ "field": "name"
+ }
}
----
@@ -125,12 +164,13 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
| locationInfoEnabled
| boolean
-| includes the filename and line number in the output (defaults to `false` set
- by `log4j.layout.jsonTemplate.locationInfoEnabled` property)
+| toggles access to the `LogEvent` source; file name, line number, etc.
+ (defaults to `false` set by `log4j.layout.jsonTemplate.locationInfoEnabled`
+ property)
| stackTraceEnabled
| boolean
-| includes stack traces (defaults to `true` set by
+| toggles access to the stack traces (defaults to `true` set by
`log4j.layout.jsonTemplate.stackTraceEnabled` property)
| eventTemplate
@@ -142,7 +182,7 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
| eventTemplateUri
| String
| URI pointing to the JSON template for rendering ``LogEvent``s (defaults to
- `classpath:JsonLayout.json` set by `log4j.layout.jsonTemplate.eventTemplateUri`
+ `classpath:EcsLayout.json` set by `log4j.layout.jsonTemplate.eventTemplateUri`
property)
| eventTemplateAdditionalFields
@@ -174,8 +214,9 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
| truncatedStringSuffix
| String
-| suffix to append to the truncated strings (defaults to `…` set by
- `log4j.layout.jsonTemplate.truncatedStringSuffix` property)
+| suffix to append to strings truncated due to exceeding `maxStringLength`
+ (defaults to `…` set by `log4j.layout.jsonTemplate.truncatedStringSuffix`
+ property)
| recyclerFactory
| RecyclerFactory
@@ -193,12 +234,30 @@ One can configure additional event template fields via
----
<JsonTemplateLayout ...>
<EventTemplateAdditionalFields>
- <KeyValuePair key="serviceName" value="auth-service"/>
- <KeyValuePair key="containerId" value="6ede3f0ca7d9"/>
+ <EventTemplateAdditionalField key="serviceName" value="auth-service"/>
+ <EventTemplateAdditionalField key="containerId" value="6ede3f0ca7d9"/>
</EventTemplateAdditionalFields>
</JsonTemplateLayout>
----
+One can also pass JSON literals into additional fields:
+
+[source,xml]
+----
+<EventTemplateAdditionalField
+ key="marker"
+ type="JSON"
+ value='{"$resolver": "marker", "field": "name"}'/>
+<EventTemplateAdditionalField
+ key="aNumber"
+ type="JSON"
+ value="1"/>
+<EventTemplateAdditionalField
+ key="aList"
+ type="JSON"
+ value='[1,2,"string"]'/>
+----
+
[#recycling-strategy]
=== Recycling strategy
@@ -245,7 +304,7 @@ parameters:
- `eventTemplate[Uri]` (for serializing ``LogEvent``s)
- `stackTraceElementTemplate[Uri]` (for serializing ``StackStraceElement``s)
-- `eventTemplateAdditionalFields` (for extending the used `LogEvent` template)
+- `eventTemplateAdditionalFields` (for extending the used event template)
[#event-templates]
=== Event Templates
@@ -253,38 +312,60 @@ parameters:
`eventTemplate[Uri]` describes the JSON structure `JsonTemplateLayout` uses to
serialize ``LogEvent``s. The default configuration (accessible by
`log4j.layout.jsonTemplate.eventTemplate[Uri]` property) is set to
-`classpath:JsonLayout.json` provided by the `log4j-layout-json-template`
+`classpath:EcsLayout.json` provided by the `log4j-layout-json-template`
artifact:
[source,json]
----
{
- "instant": {
- "epochSecond": "${json:timestamp:epoch:secs,integral}",
- "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+ "@timestamp": {
+ "$resolver": "timestamp",
+ "pattern": {
+ "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ "timeZone": "UTC"
+ }
+ },
+ "log.level": {
+ "$resolver": "level",
+ "field": "name"
},
- "thread": "${json:thread:name}",
- "level": "${json:level}",
- "loggerName": "${json:logger:name}",
- "message": "${json:message}",
- "thrown": {
- "message": "${json:exception:message}",
- "name": "${json:exception:className}",
- "extendedStackTrace": "${json:exception:stackTrace}"
+ "message": {
+ "$resolver": "message",
+ "stringified": true
},
- "contextStack": "${json:ndc}",
- "endOfBatch": "${json:endOfBatch}",
- "loggerFqcn": "${json:logger:fqcn}",
- "contextMap": "${json:mdc}",
- "threadId": "${json:thread:id}",
- "threadPriority": "${json:thread:priority}",
- "source": {
- "class": "${json:source:className}",
- "method": "${json:source:methodName}",
- "file": "${json:source:fileName}",
- "line": "${json:source:lineNumber}"
+ "process.thread.name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "log.logger": {
+ "$resolver": "logger",
+ "field": "name"
+ },
+ "labels": {
+ "$resolver": "mdc",
+ "flatten": {
+ "prefix": "labels."
+ },
+ "stringified": true
+ },
+ "tags": {
+ "$resolver": "ndc"
+ },
+ "error.type": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "error.message": {
+ "$resolver": "exception",
+ "field": "message"
+ },
+ "error.stack_trace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
}
}
+
----
`log4j-layout-json-template` artifact contains the following predefined event
@@ -294,7 +375,8 @@ templates:
described by https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[the Elastic Common Schema (ECS) specification]
- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json[`LogstashJsonEventLayoutV1.json`]
- described in https://github.com/logstash/log4j-jsonevent-layout[log4j-jsonevent-layout]
+ described in https://github.com/logstash/log4j-jsonevent-layout[Logstash
+ `json_event` pattern for log4j]
- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/GelfLayout.json[`GelfLayout.json`]
described by https://docs.graylog.org/en/3.1/pages/gelf.html#gelf-payload-specification[the
@@ -308,202 +390,600 @@ templates:
with the exception of `thrown` field. (`JsonLayout` serializes the `Throwable`
as is via Jackson `ObjectMapper`, whereas `JsonLayout.json` template of
`JsonTemplateLayout` employs the `StackTraceElementLayout.json` template
- for stack traces to generate an always document-store-friendly flat structure.)
+ for stack traces to generate a document-store-friendly flat structure.)
-Below is the list of supported event template variables:
+Below is the list of supported event template resolvers:
-.`LogEvent` template variables
-[cols="1m,4"]
+[#event-template-resolvers]
+.`LogEvent` template resolvers
+[cols="1m,3,2,2,4"]
|===
-| Variable Name
+| Resolver Name
+| Syntax
| Description
+| Garbage Footprint
+| Examples
| endOfBatch
+|
| `logEvent.isEndOfBatch()`
+| none
+a|
+[source,json]
+----
+{
+ "$resolver": "endOfBatch"
+}
+----
+
+| exception
+a|
+[source]
+----
+config = field , [ stringified ]
+field = "field" -> (
+ "className" \|
+ "message" \|
+ "stackTrace" )
+stringified = "stringified" -> boolean
+----
+a|
+Resolves fields of the `Throwable` returned by `logEvent.getThrown()`.
+
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
+ access to (and hence rendering of) stack traces are not garbage-free.
+a|
+Resolve `logEvent.getThrown().getClass().getCanonicalName()`:
-| exception:className
-| `logEvent.getThrown().getClass().getCanonicalName()`
+[source,json]
+----
+{
+ "$resolver": "exception",
+ "field": "className"
+}
+----
-| exception:message
-| `logEvent.getThrown().getMessage()`
+Resolve the stack trace into a list of `StackTraceElement` objects:
-| exception:stackTrace
-| `logEvent.getThrown().getStackTrace()` (inactive when `stackTraceEnabled=false`)
+[source,json]
+----
+{
+ "$resolver": "exception",
+ "field": "stackTrace"
+}
+----
-| exception:stackTrace:text
-| `logEvent.getThrown().printStackTrace()` (inactive when `stackTraceEnabled=false`)
+Resolve the stack trace into a string field:
-| exceptionRootCause:className
-| the innermost `exception:className` in causal chain
+[source,json]
+----
+{
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+}
+----
-| exceptionRootCause:message
-| the innermost `exception:message` in causal chain
+| exceptionRootCause
+| identical to `exception` resolver
+a|
+Resolves the fields of the innermost `Throwable` returned by
+`logEvent.getThrown()`.
-| exceptionRootCause:stackTrace[:text]
-| the innermost `exception:stackTrace[:text]` in causal chain
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| identical to `exception` resolver
+| identical to `exception` resolver
| level
-| `logEvent.getLevel()`
+a|
+[source]
+----
+config = field , [ severity ]
+field = "field" -> ( "name" \| "severity" )
+severity = severity-field
+severity-field = "field" -> ( "keyword" \| "code" )
+----
+| resolves the fields of the `logEvent.getLevel()`
+| none
+a|
+Resolve the level name:
+
+[source,json]
+----
+{
+ "$resolver": "level",
+ "field": "name"
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+keyword:
+
+[source,json]
+----
+{
+ "$resolver": "level",
+ "field": "severity",
+ "severity": {
+ "field": "keyword"
+ }
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+code:
+
+[source,json]
+----
+{
+ "$resolver": "level",
+ "field": "severity",
+ "severity": {
+ "field": "code"
+ }
+}
+----
-| level:severity
-| https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity] keyword
- of `logEvent.getLevel()`
+| logger
+a|
+[source]
+----
+config = "field" -> ( "name" \| "fqcn" )
+----
+| resolves `logEvent.getLoggerFqcn()` and `logEvent.getLoggerName()`
+| none
+a|
+Resolve the logger name:
-| level:severity:code
-| https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity] code of
- `logEvent.getLevel()`
+[source,json]
+----
+{
+ "$resolver": "logger",
+ "field": "name"
+}
+----
-| logger:fqcn
-| `logEvent.getLoggerFqcn()`
+Resolve the logger's fully qualified class name:
-| logger:name
-| `logEvent.getLoggerName()`
+[source,json]
+----
+{
+ "$resolver": "logger",
+ "field": "fqcn"
+}
+----
| main:<key>
+a|
+[source]
+----
+config = ( index \| key )
+index = "index" -> number
+key = "key" -> string
+----
| performs link:lookups.html#AppMainArgsLookup[Main Argument Lookup] for the
- given `key`
+ given `index` or `key`
+| none
+a|
+Resolve the 1st `main()` method argument:
+
+[source,json]
+----
+{
+ "$resolver": "main",
+ "index": 0
+}
+----
+
+Resolve the argument coming right after `--userId`:
+
+[source,json]
+----
+{
+ "$resolver": "main",
+ "key": "--userId"
+}
+----
-| map:<key>
+| map
+a|
+[source]
+----
+config = "key" -> string
+----
| performs link:lookups.html#MapLookup[Map Lookup] for the given `key`
+| none
+a|
+Resolve the `userRole` field of the message:
+
+[source,json]
+----
+{
+ "$resolver": "map",
+ "key": "userRole"
+}
+----
-| marker:name
-| `logEvent.getMarker.getName()`
+| marker
+a|
+[source]
+----
+config = "field" -> "name"
+----
+| `logEvent.getMarker().getName()`
+| none
+a|
+Resolve the marker name:
+
+[source,json]
+----
+{
+ "$resolver": "marker",
+ "field": "name"
+}
+----
| mdc
-.4+| Mapped Diagnostic Context `Map<String, String>` returned by
- `logEvent.getContextData()`, where one can merge it with the parent JSON
- object via `flatten[=<prefix>]`, filter keys matching a regex `pattern`, just
- extract a certain `key`, or `stringify` values
+a|
+[source]
+----
+config = singleAccess \| multiAccess
-| mdc:flatten[=<prefix>][,stringify]
+singleAccess = key , [ stringified ]
+key = "key" -> string
+stringified = "stringified" -> boolean
-| mdc:pattern=<pattern>[,flatten=<prefix>][,stringify]
+multi-access = [ pattern ] , [ flatten ] , [ stringified ]
+pattern = "pattern" -> string
+flatten = "flatten" -> ( boolean \| flattenConfig )
+flattenConfig = [ flattenPrefix ]
+flattenPrefix = "prefix" -> string
+----
+a| Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
-| mdc:key=<key>[,stringify]
+`singleAccess` resolves the MDC value as is, whilst `multiAccess` resolves a
+multitude of MDC values. If `flatten` is provided, `multiAccess` merges the
+values with the parent, otherwise creates a new JSON object containing the
+values.
-| message
-| `logEvent.getFormattedMessage()`
+Enabling `stringified` flag converts each value to its string representation.
-| message:json
-| if `logEvent.getMessage()` is of type `MultiformatMessage` and supports JSON,
- its read value; if is of type `ObjectMessage`, its serialized output via
- Jackson `ObjectMapper`; otherwise, `{"message": <formattedMessage>}` object
+Regex provided in the `pattern` is used to match against the keys.
+a|
+`log4j2.garbagefreeThreadContextMap` flag needs to be turned on to iterate
+the map without allocations.
-| ndc[:pattern=<pattern>]
-| Nested Diagnostic Context `String[]` returned by `logEvent.getContextStack()`,
- where filtering is supported via the supplied regex `pattern`
+`stringify` allocates a new `String` for values that are not of type `String`.
-| pattern:<pattern>
-| injects link:layouts.html#PatternLayout[`PatternLayout`] serialized string
- described by the `pattern` parameter
+Writing certain non-primitive values (e.g., `BigDecimal`, `Set`, etc.) to JSON
+generates garbage, though most (e.g., `int`, `long`, `String`, `List`,
+`boolean[]`, etc.) don't.
+a|
+Resolve the `userRole` MDC value:
-| source:className
-| `logEvent.getSource().getClassName()`
+[source,json]
+----
+{
+ "$resolver": "mdc",
+ "key": "userRole"
+}
+----
-| source:fileName
-| `logEvent.getSource().getFileName()` (inactive when `locationInfoEnabled=false`)
+Resolve the string representation of the `userRank` MDC value:
-| source:lineNumber
-| `logEvent.getSource().getLineNumber()` (inactive when `locationInfoEnabled=false`)
+[source,json]
+----
+{
+ "$resolver": "mdc",
+ "key": "userRank",
+ "stringified": true
+}
+----
-| source:methodName
-| `logEvent.getSource().getMethodName()`
+Resolve all MDC entries into an object:
-| thread:id
-| `logEvent.getThreadId()`
+[source,json]
+----
+{
+ "$resolver": "mdc"
+}
+----
-| thread:name
-| `logEvent.getThreadName()`
+Resolve all MDC entries into an object such that values are converted to string:
-| thread:priority
-| `logEvent.getThreadPriority()`
+[source,json]
+----
+{
+ "$resolver": "mdc",
+ "stringified": true
+}
+----
-| timestamp
-.4+| `logEvent.getInstant()` formatted using optional
- `pattern` (defaults to `yyyy-MM-dd'T'HH:mm:ss.SSSZZZ` set by
- `log4j.layout.jsonTemplate.timestampFormatPattern` property), `timeZone`
- (defaults to `TimeZone.getDefault()` set by
- `log4j.layout.jsonTemplate.timeZone` property), and `locale` (represented by
- `language[_country[_variant]]` pattern, defaults to `Locale.getDefault()` set
- by `log4j.layout.jsonTemplate.locale` property) parameters
+Merge all MDC entries whose keys are matching with the `user(Role\|Rank)` regex
+into the parent:
-| timestamp:pattern=<pattern>
+[source,json]
+----
+{
+ "$resolver": "mdc",
+ "flatten": true,
+ "pattern": "user(Role\|Rank)"
+}
+----
-| timestamp:timeZone=<timeZone>
+After converting the corresponding entries to string, merge all MDC entries to
+parent such that keys are prefixed with `_`:
-| timestamp:locale=<locale>
+[source,json]
+----
+{
+ "$resolver": "mdc",
+ "stringified": true,
+ "flatten": {
+ "prefix": "_"
+ }
+}
+----
-| timestamp:epoch:nanos
-| UTC epoch nanoseconds (of type `long`) derived from `logEvent.getInstant()`
+| message
+a|
+[source]
+----
+config = [ stringified ]
+stringified = "stringified" -> boolean
+----
+a| `logEvent.getMessage()`
+| For simple string messages, the resolution is performed without allocations.
+ For `ObjectMessage`s and `MultiformatMessage`s, it depends.
+a|
+Resolve the message into a string:
-| timestamp:epoch:<secs\|micros\|millis>[,integral]
-| UTC epoch seconds, microseconds, or milliseconds (of type `double`) derived from
- `logEvent.getInstant()` and, if `integral` is provided, cast to `long`
+[source,json]
+----
+{
+ "$resolver": "message",
+ "stringified": true
+}
+----
-| timestamp:epoch:secs.<micros\|millis\|nanos>
-.3+| UTC epoch fractions (of type `long`) derived from `logEvent.getInstant()`;
- `secs.micros` denotes the "fractional part of epoch seconds, in microseconds",
- `micros.millis` denotes the "fractional part of epoch microseconds, in
-milliseconds", etc.
+Resolve the message such that if it is an `ObjectMessage` or a
+`MultiformatMessage` with JSON support, its type (string, list, object, etc.)
+will be retained:
-| timestamp:epoch:micros.<millis\|nanos>
+[source,json]
+----
+{
+ "$resolver": "message"
+}
+----
-| timestamp:epoch:millis.nanos
-|===
+| ndc
+a|
+[source]
+----
+config = [ pattern ]
+pattern = "pattern" -> string
+----
+| Resolves the Nested Diagnostic Context (NDC), aka. Thread Context Stack,
+ `String[]` returned by `logEvent.getContextStack()`
+| none
+a|
+Resolve all NDC values into a list:
-In the following table, timestamp template variables are illustrated by
-examples:
+[source,json]
+----
+{
+ "$resolver": "ndc"
+}
+----
-.`timestamp` template variable examples
-[cols="1m,4m"]
-|===
-| Variable Name
-| Output
+Resolve all NDC values matching with the `pattern` regex:
-|timestamp
-|2020-02-07T13:38:47.098+02:00
+[source,json]
+----
+{
+ "$resolver": "ndc",
+ "pattern": "user(Role\|Rank):\\w+"
+}
+----
-|timestamp:pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z',timeZone=UTC,locale=en_US
-|2020-02-07T13:38:47.098Z
+| source
+a|
+[source]
+----
+config = "field" -> (
+ "className" \|
+ "fileName" \|
+ "methodName" \|
+ "lineNumber" )
+----
+a|
+Resolves the fields of the `StackTraceElement` returned by
+`logEvent.getSource()`.
-|timestamp:epoch:secs
-|1581082727.982123456
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.locationInfoEnabled` property.
+| none
+a|
+Resolve the line number:
-|timestamp:epoch:secs,integral
-|1581082727
+[source,json]
+----
+{
+ "$resolver": "source",
+ "field": "lineNumber"
+}
+----
-|timestamp:epoch:millis
-|1581082727982.123456
+| thread
+a|
+[source]
+----
+config = "field" -> ( "name" \| "id" \| "priority" )
+----
+| resolves `logEvent.getThreadId()`, `logEvent.getThreadName()`,
+ `logEvent.getThreadPriority()`
+| none
+a|
+Resolve the thread name:
-|timestamp:epoch:millis,integral
-|1581082727982
+[source,json]
+----
+{
+ "$resolver": "thread",
+ "field": "name"
+}
+----
-|timestamp:epoch:micros
-|1581082727982123.456
+| timestamp
+a|
+[source]
+----
+config = [ patternConfig \| epochConfig ]
+
+patternConfig = "pattern" -> (
+ [ format ] ,
+ [ timeZone ] ,
+ [ locale ] )
+format = "format" -> string
+timeZone = "timeZone" -> string
+locale = "locale" -> (
+ language \|
+ ( language , "_" , country ) \|
+ ( language , "_" , country , "_" , variant )
+ )
+
+epochConfig = "epoch" -> ( unit , [ rounded ] )
+unit = "unit" -> (
+ "nanos" \|
+ "millis" \|
+ "secs" \|
+ "millis.nanos" \|
+ "secs.nanos" \|
+ )
+rounded = "rounded" -> boolean
+----
+| resolves `logEvent.getInstant()` in various forms
+| none
+a|
+.`timestamp` template resolver examples
+[cols="5,2m"]
+!===
+! Configuration
+! Output
+
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp"
+}
+----
+! 2020-02-07T13:38:47.098+02:00
-|timestamp:epoch:millis,integral
-|1581082727982123
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "pattern": {
+ "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ "timeZone": "UTC",
+ "locale": "en_US"
+ }
+}
+----
+! 2020-02-07T13:38:47.098Z
-|timestamp:epoch:nanos
-|1581082727982123456
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs"
+ }
+}
+----
+! 1581082727.982123456
-|timestamp:epoch:secs.millis
-|0000000000982
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs",
+ "rounded": true
+ }
+}
+----
+! 1581082727
-|timestamp:epoch:secs.micros
-|0000000000982123
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "secs.nanos"
+ }
+}
+----
+! 982123456
-|timestamp:epoch:secs.nanos
-|0000000000982123456
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "millis"
+ }
+}
+----
+! 1581082727982.123456
-|timestamp:epoch:millis.micros
-|0000000000000123
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "millis",
+ "rounded": true
+ }
+}
+----
+! 1581082727982
-|timestamp:epoch:millis.nanos
-|0000000000000123456
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "millis.nanos"
+ }
+}
+----
+! 123456
-|timestamp:epoch:micros.nanos
-|0000000000000000456
+a!
+[source,json]
+----
+{
+ "$resolver": "timestamp",
+ "epoch": {
+ "unit": "nanos"
+ }
+}
+----
+! 1581082727982123456
+!===
|===
[#stack-trace-element-templates]
@@ -518,46 +998,37 @@ to format ``StackTraceElement``s. The default configuration (accessible by
[source,json]
----
{
- "class": "${json:stackTraceElement:className}",
- "method": "${json:stackTraceElement:methodName}",
- "file": "${json:stackTraceElement:fileName}",
- "line": "${json:stackTraceElement:lineNumber}"
+ "class": {
+ "$resolver": "stackTraceElement",
+ "field": "className"
+ },
+ "method": {
+ "$resolver": "stackTraceElement",
+ "field": "methodName"
+ },
+ "file": {
+ "$resolver": "stackTraceElement",
+ "field": "fileName"
+ },
+ "line": {
+ "$resolver": "stackTraceElement",
+ "field": "lineNumber"
+ }
}
----
-Below is the list of supported stack trace element template variables:
-
-.`StackTraceElement` template variables
-[cols="1m,4m"]
-|===
-| Variable Name
-| Description
+The allowed template configuration syntax is as follows:
-| stackTraceElement:className
-| stackTraceElement.getClassName()
-
-| stackTraceElement:methodName
-| stackTraceElement.getMethodName()
-
-| stackTraceElement:fileName
-| stackTraceElement.getFileName()
-
-| stackTraceElement:lineNumber
-| stackTraceElement.getLineNumber()
-|===
-
-[#template-variables]
-=== Template Variables
-
-JSON field lookups are performed using the `${json:<variable-name>}` scheme
-where `<variable-name>` is defined as `<resolver-name>[:<resolver-key>]`.
-Characters following colon (`:`) are treated as the `resolver-key`.
+[source]
+----
+config = "field" -> (
+ "className" |
+ "fileName" |
+ "methodName" |
+ "lineNumber" )
+----
-link:lookups.html[Lookups] (e.g., `${java:version}`, `${env:USER}`,
-`${date:MM-dd-yyyy}`) are supported in templates too. Though note that while
-`${json:...}` template variables are expected to occupy an entire field, that
-is, `"level": "${json:level}"`, a lookup can be mixed within a regular string as
-in `"greeting": "Hello, ${env:USER}!"`.
+All above accesses to `StackTraceElement` is garbage-free.
[#features]
== Features
@@ -638,22 +1109,32 @@ alternatives.
[#faq]
== F.A.Q.
+[#faq-lookups]
+=== Are lookups supported in templates?
+
+Yes, link:lookups.html[lookups] (e.g., `${java:version}`, `${env:USER}`,
+`${date:MM-dd-yyyy}`) are supported in string literals of templates. Though note
+that they are not garbage-free.
+
[#faq-garbage-free]
=== Is `JsonTemplateLayout` garbage-free?
-Given the garbage-free layout behaviour enabler properties
-`log4j2.enableDirectEncoders` and `log4j2.garbagefreeThreadContextMap` are set
-to `true`, `JsonTemplateLayout` is garbage-free with the following exceptions:
+Yes, if the garbage-free layout behaviour toggling properties
+`log4j2.enableDirectEncoders` and `log4j2.garbagefreeThreadContextMap` are
+enabled. Take into account the following caveats:
-* When recycling strategy is either `dummy` or `queue` but access concurrency
- exceeds the configured `capacity`, then the recycler is not garbage-free.
+* The configured link:#recycling-strategy[recycling strategy] might not be
+ garbage-free.
* Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
access to (and hence rendering of) stack traces are not garbage-free.
-* Serialization of ``ObjectMessage``s via `${json:message:json}` is mostly
+* Serialization of ``MapMessage``s and ``ObjectMessage``s are mostly
garbage-free except for certain types (e.g., `BigDecimal`, `BigInteger`,
``Collection``s with the exception of `List`).
* link:lookups.html[Lookups] (that is, `${...}` variables, excluding
`${json:...}` ones) are not garbage-free.
+
+Don't forget to checkout link:#event-template-resolvers[the notes on garbage footprint of resolvers]
+you employ in templates.
diff --git a/src/site/asciidoc/manual/layouts.adoc b/src/site/asciidoc/manual/layouts.adoc
index fc842bf..a1d540f 100644
--- a/src/site/asciidoc/manual/layouts.adoc
+++ b/src/site/asciidoc/manual/layouts.adoc
@@ -502,23 +502,62 @@ Logstash `JSONEventLayoutV1`]
[source,json]
----
{
- "mdc": "${json:mdc}",
+ "mdc": {
+ "$resolver": "mdc"
+ },
"exception": {
- "exception_class": "${json:exception:className}",
- "exception_message": "${json:exception:message}",
- "stacktrace": "${json:exception:stackTrace:text}"
+ "exception_class": {
+ "$resolver": "exception",
+ "field": "className"
+ },
+ "exception_message": {
+ "$resolver": "exception",
+ "field": "message",
+ "stringified": true
+ },
+ "stacktrace": {
+ "$resolver": "exception",
+ "field": "stackTrace",
+ "stringified": true
+ }
+ },
+ "line_number": {
+ "$resolver": "source",
+ "field": "lineNumber"
+ },
+ "class": {
+ "$resolver": "source",
+ "field": "className"
},
- "line_number": "${json:source:lineNumber}",
- "class": "${json:source:className}",
"@version": 1,
"source_host": "${hostName}",
- "message": "${json:message}",
- "thread_name": "${json:thread:name}",
- "@timestamp": "${json:timestamp}",
- "level": "${json:level}",
- "file": "${json:source:fileName}",
- "method": "${json:source:methodName}",
- "logger_name": "${json:logger:name}"
+ "message": {
+ "$resolver": "message",
+ "stringified": true
+ },
+ "thread_name": {
+ "$resolver": "thread",
+ "field": "name"
+ },
+ "@timestamp": {
+ "$resolver": "timestamp"
+ },
+ "level": {
+ "$resolver": "level",
+ "field": "name"
+ },
+ "file": {
+ "$resolver": "source",
+ "field": "fileName"
+ },
+ "method": {
+ "$resolver": "source",
+ "field": "methodName"
+ },
+ "logger_name": {
+ "$resolver": "logger",
+ "field": "name"
+ }
}
----
diff --git a/src/site/markdown/manual/cloud.md b/src/site/markdown/manual/cloud.md
index bbca8db..3df1d33 100644
--- a/src/site/markdown/manual/cloud.md
+++ b/src/site/markdown/manual/cloud.md
@@ -138,22 +138,22 @@ great fit for the bill.
bufferedIo="true">
<JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json">
<EventTemplateAdditionalFields>
- <KeyValuePair key="containerId" value="${docker:containerId:-}"/>
- <KeyValuePair key="application" value="$${lower:${spring:spring.application.name:-spring}}"/>
- <KeyValuePair key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
- <KeyValuePair key="kubernetes.containerId" value="${k8s:containerId:-}"/>
- <KeyValuePair key="kubernetes.containerName" value="${k8s:containerName:-}"/>
- <KeyValuePair key="kubernetes.host" value="${k8s:host:-}"/>
- <KeyValuePair key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
- <KeyValuePair key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
- <KeyValuePair key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
- <KeyValuePair key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
- <KeyValuePair key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
- <KeyValuePair key="kubernetes.podID" value="${k8s:podId:-}"/>
- <KeyValuePair key="kubernetes.podIP" value="${k8s:podIp:-}"/>
- <KeyValuePair key="kubernetes.podName" value="${k8s:podName:-}"/>
- <KeyValuePair key="kubernetes.imageId" value="${k8s:imageId:-}"/>
- <KeyValuePair key="kubernetes.imageName" value="${k8s:imageName:-}"/>
+ <EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
+ <EventTemplateAdditionalField key="application" value="$${lower:${spring:spring.application.name:-spring}}"/>
+ <EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
+ <EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
</EventTemplateAdditionalFields>
</JsonTemplateLayout>
</Socket>