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 2024/06/17 08:50:43 UTC

(logging-log4j2) 01/01: Fix property substitution for `PatternResolver` (#2666)

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch fix/2.x/JTL-prop-subs
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 5de88c8d10dcd12f24e487b2cc15bc9529e22733
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Mon Jun 17 10:50:49 2024 +0200

    Fix property substitution for `PatternResolver` (#2666)
---
 .../PatternResolverPropertySubstitutionTest.java   | 96 ++++++++++++++++++++++
 .../template/json/resolver/PatternResolver.java    | 17 +++-
 .../json/resolver/TemplateResolverContext.java     |  2 +-
 .../template/json/resolver/package-info.java       |  2 +-
 .../.2.x.x/fix-PatternResolver-lookup-handling.xml |  8 ++
 .../modules/ROOT/pages/manual/garbagefree.adoc     |  4 +-
 .../ROOT/pages/manual/json-template-layout.adoc    | 31 ++++++-
 .../antora/modules/ROOT/pages/manual/messages.adoc |  6 +-
 .../modules/ROOT/pages/manual/pattern-layout.adoc  | 10 ++-
 9 files changed, 163 insertions(+), 13 deletions(-)

diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverPropertySubstitutionTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverPropertySubstitutionTest.java
new file mode 100644
index 0000000000..f4912cb42c
--- /dev/null
+++ b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolverPropertySubstitutionTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.asMap;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.usingSerializedLogEventAccessor;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.writeJson;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
+import org.apache.logging.log4j.core.config.builder.impl.DefaultConfigurationBuilder;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class PatternResolverPropertySubstitutionTest {
+
+    private static final String DEFAULT_PROPERTY_NAME = "DEFAULT";
+
+    private static final String FALLBACK_PROPERTY_NAME = "FALLBACK";
+
+    private static final class TestCase {
+
+        private final Map<String, String> properties;
+
+        private final LogEvent logEvent;
+
+        private final String expectedOutput;
+
+        private TestCase(final Map<String, String> properties, final LogEvent logEvent, final String expectedOutput) {
+            this.properties = properties;
+            this.logEvent = logEvent;
+            this.expectedOutput = expectedOutput;
+        }
+    }
+
+    static Stream<TestCase> testCases() {
+        return Stream.of(
+                new TestCase(
+                        Collections.singletonMap(DEFAULT_PROPERTY_NAME, "%m"),
+                        Log4jLogEvent.newBuilder()
+                                .setMessage(new SimpleMessage("foo"))
+                                .build(),
+                        "foo"),
+                new TestCase(
+                        Collections.singletonMap(FALLBACK_PROPERTY_NAME, "%p"),
+                        Log4jLogEvent.newBuilder().setLevel(Level.DEBUG).build(),
+                        "DEBUG"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testCases")
+    void test_property_substitution(final TestCase testCase) {
+
+        // Create the event template
+        final String pattern = String.format("${%s:-${%s}}", DEFAULT_PROPERTY_NAME, FALLBACK_PROPERTY_NAME);
+        final String eventTemplate = writeJson(asMap("output", asMap("$resolver", "pattern", "pattern", pattern)));
+
+        // Create configuration with properties
+        final DefaultConfigurationBuilder<BuiltConfiguration> configBuilder = new DefaultConfigurationBuilder<>();
+        testCase.properties.forEach(configBuilder::addProperty);
+        final Configuration config = configBuilder.build();
+
+        // Create the layout
+        final JsonTemplateLayout layout = JsonTemplateLayout.newBuilder()
+                .setConfiguration(config)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, testCase.logEvent, accessor -> assertThat(accessor.getString("output"))
+                .isEqualTo(testCase.expectedOutput));
+    }
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
index 249c24a44a..6d698e37d0 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/PatternResolver.java
@@ -35,6 +35,8 @@ import org.apache.logging.log4j.util.Strings;
  * stackTraceEnabled = "stackTraceEnabled" -> boolean
  * </pre>
  *
+ * Lookups (e.g., <tt>${java:version}</tt>, <tt>${env:USER}</tt>, <tt>${date:MM-dd-yyyy}</tt>) found in the <tt>pattern</tt> will be resolved.
+ *
  * The default value of <tt>stackTraceEnabled</tt> is inherited from the parent
  * {@link JsonTemplateLayout}.
  *
@@ -55,10 +57,7 @@ public final class PatternResolver implements EventResolver {
     private final BiConsumer<StringBuilder, LogEvent> emitter;
 
     PatternResolver(final EventResolverContext context, final TemplateResolverConfig config) {
-        final String pattern = config.getString("pattern");
-        if (Strings.isBlank(pattern)) {
-            throw new IllegalArgumentException("blank pattern: " + config);
-        }
+        final String pattern = readPattern(context.getSubstitutor(), config);
         final boolean stackTraceEnabled =
                 Optional.ofNullable(config.getBoolean("stackTraceEnabled")).orElse(context.isStackTraceEnabled());
         final PatternLayout patternLayout = PatternLayout.newBuilder()
@@ -71,6 +70,16 @@ public final class PatternResolver implements EventResolver {
                 patternLayout.serialize(logEvent, stringBuilder);
     }
 
+    private static String readPattern(
+            final EventResolverStringSubstitutor substitutor, final TemplateResolverConfig config) {
+        final String pattern = config.getString("pattern");
+        if (Strings.isBlank(pattern)) {
+            throw new IllegalArgumentException("blank pattern: " + config);
+        }
+        final boolean substitutionNeeded = pattern.contains("${");
+        return substitutionNeeded ? substitutor.replace(null, pattern) : pattern;
+    }
+
     static String getName() {
         return "pattern";
     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
index 74ca965887..e7522e3da9 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverContext.java
@@ -30,7 +30,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
  *
  * @see TemplateResolverFactory
  */
-interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
+public interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
 
     Class<C> getContextClass();
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/package-info.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/package-info.java
index 6398bdffb3..376a67bd33 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/package-info.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/package-info.java
@@ -16,7 +16,7 @@
  */
 @Export
 @Open("org.apache.logging.log4j.core")
-@Version("2.20.1")
+@Version("2.21.0")
 package org.apache.logging.log4j.layout.template.json.resolver;
 
 import aQute.bnd.annotation.jpms.Open;
diff --git a/src/changelog/.2.x.x/fix-PatternResolver-lookup-handling.xml b/src/changelog/.2.x.x/fix-PatternResolver-lookup-handling.xml
new file mode 100644
index 0000000000..2a78de2fab
--- /dev/null
+++ b/src/changelog/.2.x.x/fix-PatternResolver-lookup-handling.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="2666" link="https://github.com/apache/logging-log4j2/issues/2666"/>
+  <description format="asciidoc">Fix property substitution for `PatternResolver` of `JsonTemplateLayout`</description>
+</entry>
diff --git a/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc b/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
index c8736c8ef1..5bc6438cfa 100644
--- a/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/garbagefree.adoc
@@ -57,7 +57,7 @@ If not for yours, keep on reading.
 In order to have a garbage-free Log4j Core, you need to
 
 * <<core-properties,configure it using properties>>,
-* and employ garbage-free <<Layouts[layouts], xref:Appenders[appenders], and xref:#Filters,filters>>.
+* and employ garbage-free <<Layouts,layouts>>, <<Appenders,appenders>>, and <<Filters,filters>>.
 
 [#core-properties]
 === Properties
@@ -211,7 +211,7 @@ Not all Log4j API feature set is garbage-free, specifically:
 * The `ThreadContext` map (aka. MDC) is not garbage-free by default, but can be configured to be garbage-free by setting <<log4j2.garbagefreeThreadContextMap,the `log4j2.garbagefreeThreadContextMap` system property>> to `true`.
 * The `ThreadContext` stack (aka. NDC) is not garbage-free.
 * xref:manual/scoped-context.adoc[] is not garbage-free.
-* Logging very large messages (i.e., more than <<log4j2.maxReusableMsgSize[`log4j2.maxReusableMsgSize`] characters, which defaults to 518), when all loggers are xref:manual/async.adoc,asynchronous loggers>>, will cause the internal `StringBuilder` in the
+* Logging very large messages (i.e., more than <<log4j2.maxReusableMsgSize>> characters, which defaults to 518), when all loggers are xref:manual/async.adoc[asynchronous loggers], will cause the internal `StringBuilder` in the
 `RingBuffer` to be trimmed back to their configured maximum size.
 * Logging messages containing `$\{variable}` substitutions creates temporary objects.
 * Logging a lambda as a parameter:
diff --git a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
index 016450092c..29ccb826d5 100644
--- a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
@@ -16,7 +16,7 @@
 ////
 = JSON Template Layout
 
-`JsonTemplateLayout` is a customizable, <<performance,efficient>>, and xref:#faq-garbage-free,garbage-free>> JSON generating layout.
+`JsonTemplateLayout` is a customizable, <<performance,efficient>>, and <<faq-garbage-free,garbage-free>> JSON generating layout.
 It encodes ``LogEvent``s according to the structure described by the JSON template provided.
 In a nutshell, it shines with its
 
@@ -1208,6 +1208,8 @@ pattern           = "pattern" -> string
 stackTraceEnabled = "stackTraceEnabled" -> boolean
 ----
 
+xref:manual/lookups.adoc[] (e.g., `${java:version}`, `${env:USER}`, `${date:MM-dd-yyyy}`) found in the `pattern` will be resolved.
+
 The default value of `stackTraceEnabled` is inherited from the parent JSON Template Layout.
 
 [NOTE]
@@ -2055,6 +2057,33 @@ To get the most out of it, mind the following checklist:
 === Are lookups supported in templates?
 
 Yes, xref:manual/lookups.adoc[lookups] (e.g., `${java:version}`, `${env:USER}`, `${date:MM-dd-yyyy}`) are supported in string literals of templates.
+Though this doesn't apply to literals located in _configuration object of resolvers_, unless the associated resolver explicitly does so.
+Consider the following:
+
+[source,json]
+----
+{
+    "java-version": "${java:version}", //<1>
+    "userId": {
+      "$resolver": "mdc",
+      "key": "${env:FOO:-userId}" //<2>
+    },
+    "pid": {
+      "$resolver": "pattern",
+      "pattern": "${env:NO_SUCH_KEY:-%pid}" //<3>
+    }
+}
+----
+<1> This works. `${java:version}` will be replaced with the corresponding value _once_ while compiling the event template.
+<2> This won't work! That is, `${env:FOO:-userId}` literal will not get substituted, since it is located in a _configuration object of a resolver_, and <<event-template-resolver-mdc,the `mdc` resolver>> does not explicitly do any substitutions either.
+<3> This works, even though it is located in a _configuration object of a resolver_, since <<event-template-resolver-pattern,the `pattern` resolver>> explicitly perform substitutions on the `pattern` argument.
+
+[IMPORTANT]
+====
+Lookups are intended as a very generic, convenience utility to perform string interpolation for, in particular, configuration files and components (e.g., layouts) lacking this mechanism.
+*JSON Template Layout has powerful directives to inject external values, and you should always prefer those whenever possible over lookups!*
+For example, always prefer <<event-template-resolver-map, the `map` resolver>> over xref:manual/lookups.adoc#MapLookup[the `map` lookup].
+====
 
 include::partial$manual/layouts-gcfree-lookup.adoc[]
 
diff --git a/src/site/antora/modules/ROOT/pages/manual/messages.adoc b/src/site/antora/modules/ROOT/pages/manual/messages.adoc
index 023dda25a7..7e7d3f7bb0 100644
--- a/src/site/antora/modules/ROOT/pages/manual/messages.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/messages.adoc
@@ -161,19 +161,19 @@ It formats the message such that each `{}` placeholder in the pattern is replace
 [#ReusableObjectMessage]
 === `ReusableObjectMessage`
 
-link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableObjectMessage.html[`ReusableObjectMessage`] provides functionally equivalent to <<ObjectMessage[], plus methods to replace its content to enable xref:manual/garbagefree.adoc>>.
+link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableObjectMessage.html[`ReusableObjectMessage`] provides functionally equivalent to <<ObjectMessage>>, plus methods to replace its content to enable xref:manual/garbagefree.adoc[].
 When garbage-free logging is enabled, xref:manual/api.adoc#loggers[loggers] will use this instead of <<ObjectMessage>>.
 
 [#ReusableParameterizedMessage]
 === `ReusableParameterizedMessage`
 
-link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableParameterizedMessage.html[`ReusableParameterizedMessage`] provides functionally equivalent to <<ParameterizedMessage[], plus methods to replace its content to enable xref:manual/garbagefree.adoc>>.
+link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableParameterizedMessage.html[`ReusableParameterizedMessage`] provides functionally equivalent to <<ParameterizedMessage>>, plus methods to replace its content to enable xref:manual/garbagefree.adoc[].
 When garbage-free logging is enabled, xref:manual/api.adoc#loggers[loggers] will use this instead of <<ParameterizedMessage>>.
 
 [#ReusableSimpleMessage]
 === `ReusableSimpleMessage`
 
-link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableSimpleMessage.html[`ReusableSimpleMessage`] provides functionally equivalent to <<SimpleMessage[], plus methods to replace its content to enable xref:manual/garbagefree.adoc>>.
+link:../javadoc/log4j-api/org/apache/logging/log4j/message/ReusableSimpleMessage.html[`ReusableSimpleMessage`] provides functionally equivalent to <<SimpleMessage>>, plus methods to replace its content to enable xref:manual/garbagefree.adoc[].
 When garbage-free logging is enabled, xref:manual/api.adoc#loggers[loggers] will use this instead of <<SimpleMessage>>.
 
 [#SimpleMessage]
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index fac5648b2a..2135426298 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -17,7 +17,7 @@
 
 = Pattern Layout
 
-`PatternLayout` is a customizable, <<performance[efficient], xref:#garbage-free,garbage-free>>, and human-readable string generating layout using a user-provided pattern.
+`PatternLayout` is a customizable, <<performance,efficient>>, <<garbage-free,garbage-free>>, and human-readable string generating layout using a user-provided pattern.
 It is analogous to `String#format()` with specialized directives on injecting certain properties of a `LogEvent`.
 
 [IMPORTANT]
@@ -1683,7 +1683,15 @@ Format modifiers to control such things as field width, padding, left, and right
 
 |literal text
 a|Garbage-free, but care is needed for xref:manual/lookups.adoc[].
+
 include::partial$manual/layouts-gcfree-lookup.adoc[]
+
+[IMPORTANT]
+====
+Lookups are intended as a very generic, convenience utility to perform string interpolation for, in particular, configuration files and components (e.g., layouts) lacking this mechanism.
+*Pattern Layout has powerful directives to inject external values, and you should always prefer those whenever possible over lookups!*
+For example, always prefer <<converter-map, the `map` converter>> over xref:manual/lookups.adoc#MapLookup[the `map` lookup].
+====
 |===
 
 Patterns containing regular expressions and xref:manual/layouts.adoc#LocationInformation[location information] are not garbage-free.