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

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


The following commit(s) were added to refs/heads/2.x by this push:
     new e8f7ec059a Add literal Context Data (MDC) filtering to JSON Template 
Layout (#4186)
e8f7ec059a is described below

commit e8f7ec059a49c19b67b404e18c561128d7af110d
Author: Ramanathan <[email protected]>
AuthorDate: Fri Sep 11 18:51:47 2026 +0530

    Add literal Context Data (MDC) filtering to JSON Template Layout (#4186)
    
    Co-authored-by: Piotr P. Karwasz <[email protected]>
    Co-authored-by: Volkan Yazıcı <[email protected]>
---
 .../resolver/ReadOnlyStringMapResolverTest.java    | 121 +++++++++++++++++++++
 log4j-layout-template-json/pom.xml                 |   8 ++
 .../json/resolver/ReadOnlyStringMapResolver.java   |  66 ++++++++++-
 .../src/main/resources/EcsLayout.json              |  14 ++-
 .../log4j/perf/jmh/MdcKeyFilterBenchmark.java      |  90 +++++++++++++++
 src/changelog/.2.x.x/4166_add_mdc_key_filter.xml   |  14 +++
 .../ROOT/pages/manual/json-template-layout.adoc    |  34 +++++-
 7 files changed, 342 insertions(+), 5 deletions(-)

diff --git 
a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
 
b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
index 422514e9f4..10b749c115 100644
--- 
a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
+++ 
b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolverTest.java
@@ -26,6 +26,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.regex.PatternSyntaxException;
 import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
 import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
 import org.apache.logging.log4j.message.Message;
@@ -386,4 +387,124 @@ class ReadOnlyStringMapResolverTest {
             
assertThat(accessor.getString("stringifiedValue")).isEqualTo(String.valueOf(value));
         });
     }
+
+    @Test
+    void test_key_filter_disallowed() {
+
+        final String eventTemplate = "" + "{\n"
+                + "  \"$resolver\": \"mdc\",\n"
+                + "  \"key\": {\n"
+                + "    \"disallowed\": [\"@timestamp\", \"message\", 
\"log.logger\"]\n"
+                + "  }\n"
+                + "}";
+
+        final String serializedJson = serializeContextData(eventTemplate);
+
+        assertThat(serializedJson).contains("\"allowedKey1\":\"value1\"");
+        assertThat(serializedJson).contains("\"allowedKey2\":\"value2\"");
+        assertThat(serializedJson).doesNotContain("\"message\"");
+        assertThat(serializedJson).doesNotContain("\"@timestamp\"");
+        assertThat(serializedJson).doesNotContain("this should be hidden");
+    }
+
+    @Test
+    void test_key_filter_allowed() {
+
+        final String eventTemplate = "" + "{\n"
+                + "  \"$resolver\": \"mdc\",\n"
+                + "  \"key\": {\n"
+                + "    \"allowed\": [\"allowedKey1\", \"allowedKey2\"]\n"
+                + "  }\n"
+                + "}";
+
+        final String serializedJson = serializeContextData(eventTemplate);
+
+        assertThat(serializedJson).contains("\"allowedKey1\":\"value1\"");
+        assertThat(serializedJson).contains("\"allowedKey2\":\"value2\"");
+        assertThat(serializedJson).doesNotContain("\"message\"");
+        assertThat(serializedJson).doesNotContain("\"@timestamp\"");
+        assertThat(serializedJson).doesNotContain("this should be hidden");
+    }
+
+    @Test
+    void test_key_filter_allowed_and_disallowed() {
+
+        final String eventTemplate = "" + "{\n"
+                + "  \"$resolver\": \"mdc\",\n"
+                + "  \"key\": {\n"
+                + "    \"allowed\": [\"allowedKey1\", \"allowedKey2\"],\n"
+                + "    \"disallowed\": [\"allowedKey2\"]\n"
+                + "  }\n"
+                + "}";
+
+        final String serializedJson = serializeContextData(eventTemplate);
+
+        assertThat(serializedJson).contains("\"allowedKey1\":\"value1\"");
+        assertThat(serializedJson).doesNotContain("\"allowedKey2\"");
+    }
+
+    @Test
+    void test_key_filter_combined_with_pattern() {
+
+        final String eventTemplate = "" + "{\n"
+                + "  \"$resolver\": \"mdc\",\n"
+                + "  \"pattern\": \"allowedKey(1|2)\",\n"
+                + "  \"replacement\": \"key$1\",\n"
+                + "  \"key\": {\n"
+                + "    \"disallowed\": [\"allowedKey2\"]\n"
+                + "  }\n"
+                + "}";
+
+        final String serializedJson = serializeContextData(eventTemplate);
+
+        assertThat(serializedJson).contains("\"key1\":\"value1\"");
+        assertThat(serializedJson).doesNotContain("key2");
+        assertThat(serializedJson).doesNotContain("\"message\"");
+    }
+
+    @Test
+    void test_key_filter_combined_with_flatten() {
+
+        final String eventTemplate = "" + "{\n"
+                + "  \"$resolver\": \"mdc\",\n"
+                + "  \"flatten\": true,\n"
+                + "  \"key\": {\n"
+                + "    \"disallowed\": [\"message\"]\n"
+                + "  }\n"
+                + "}";
+
+        final String serializedJson = serializeContextData(eventTemplate);
+
+        assertThat(serializedJson).contains("\"allowedKey1\":\"value1\"");
+        assertThat(serializedJson).doesNotContain("\"message\"");
+    }
+
+    @Test
+    void test_key_filter_invalid() {
+
+        final String eventTemplate = "" + "{\n" + "  \"$resolver\": 
\"mdc\",\n" + "  \"key\": [\"message\"]\n" + "}";
+
+        Assertions.assertThatThrownBy(() -> 
serializeContextData(eventTemplate))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("invalid key option");
+    }
+
+    private static String serializeContextData(final String eventTemplate) {
+
+        final StringMap contextData = new SortedArrayStringMap();
+        contextData.putValue("allowedKey1", "value1");
+        contextData.putValue("message", "this should be hidden");
+        contextData.putValue("@timestamp", "this should also be hidden");
+        contextData.putValue("allowedKey2", "value2");
+
+        final LogEvent logEvent =
+                Log4jLogEvent.newBuilder().setContextData(contextData).build();
+
+        final JsonTemplateLayout layout = JsonTemplateLayout.newBuilder()
+                .setConfiguration(new DefaultConfiguration())
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        return layout.toSerializable(logEvent);
+    }
 }
diff --git a/log4j-layout-template-json/pom.xml 
b/log4j-layout-template-json/pom.xml
index 9b1351fae3..751f85fe09 100644
--- a/log4j-layout-template-json/pom.xml
+++ b/log4j-layout-template-json/pom.xml
@@ -44,7 +44,9 @@
     <bnd-extra-package-options>
       <!-- JCTools is optional (#1895) -->
       org.jctools.*;resolution:=optional,
+      org.jspecify.*;resolution:=optional
     </bnd-extra-package-options>
+    
<bnd-extra-module-options>org.jspecify;transitive=false</bnd-extra-module-options>
     <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
 
   </properties>
@@ -62,6 +64,12 @@
       <optional>true</optional>
     </dependency>
 
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git 
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
 
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
index 144525a06e..be2b20c918 100644
--- 
a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
+++ 
b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
@@ -16,7 +16,11 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -26,6 +30,7 @@ import 
org.apache.logging.log4j.layout.template.json.util.Recycler;
 import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
+import org.jspecify.annotations.NonNull;
 
 /**
  * {@link ReadOnlyStringMap} resolver.
@@ -39,7 +44,11 @@ import org.apache.logging.log4j.util.TriConsumer;
  * key           = "key" -> string
  * stringified   = "stringified" -> boolean
  *
- * multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified 
]
+ * multiAccess   = [ keyFilter ] , [ pattern ] , [ replacement ] , [ flatten ] 
, [ stringified ]
+ * keyFilter     = "key" -> keyConfig
+ * keyConfig     = [ allowed ] , [ disallowed ]
+ * allowed       = "allowed" -> array of strings
+ * disallowed    = "disallowed" -> array of strings
  * pattern       = "pattern" -> string
  * replacement   = "replacement" -> string
  * flatten       = "flatten" -> ( boolean | flattenConfig )
@@ -60,6 +69,13 @@ import org.apache.logging.log4j.util.TriConsumer;
  * These two are effectively equivalent to
  * <tt>Pattern.compile(pattern).matcher(key).matches()</tt> and
  * <tt>Pattern.compile(pattern).matcher(key).replaceAll(replacement)</tt> 
calls.
+ * <p>
+ * <tt>key</tt> given as an object filters keys against literal names rather
+ * than a regex. If <tt>allowed</tt> is provided, only the listed keys are
+ * resolved. Keys listed in <tt>disallowed</tt> are silently dropped, and take
+ * precedence over <tt>allowed</tt>. Both are matched against the key as found
+ * in the map, that is, before <tt>replacement</tt> is applied, and both can be
+ * combined with <tt>pattern</tt>, which a key must then satisfy as well.
  *
  * <h3>Garbage Footprint</h3>
  *
@@ -192,7 +208,21 @@ class ReadOnlyStringMapResolver implements EventResolver {
             throw new IllegalArgumentException("invalid flatten option: " + 
config);
         }
         final String prefix = config.getString(new String[] {"flatten", 
"prefix"});
-        final String key = config.getString("key");
+        final Object keyObject = config.getObject("key");
+        final String key;
+        final Set<String> allowedKeys;
+        final Set<String> disallowedKeys;
+        if (keyObject == null || keyObject instanceof Map) {
+            key = null;
+            allowedKeys = readKeyFilter(config, "allowed");
+            disallowedKeys = readKeyFilter(config, "disallowed");
+        } else if (keyObject instanceof String) {
+            key = (String) keyObject;
+            allowedKeys = Collections.emptySet();
+            disallowedKeys = Collections.emptySet();
+        } else {
+            throw new IllegalArgumentException("invalid key option: " + 
config);
+        }
         if (key != null && flatten) {
             throw new IllegalArgumentException("key and flatten options cannot 
be combined: " + config);
         }
@@ -209,10 +239,24 @@ class ReadOnlyStringMapResolver implements EventResolver {
             return createKeyResolver(key, stringified, mapAccessor);
         } else {
             final RecyclerFactory recyclerFactory = 
context.getRecyclerFactory();
-            return createResolver(recyclerFactory, flatten, prefix, pattern, 
replacement, stringified, mapAccessor);
+            return createResolver(
+                    recyclerFactory,
+                    flatten,
+                    prefix,
+                    pattern,
+                    replacement,
+                    allowedKeys,
+                    disallowedKeys,
+                    stringified,
+                    mapAccessor);
         }
     }
 
+    private static Set<String> readKeyFilter(final TemplateResolverConfig 
config, final String filterName) {
+        final List<String> keys = config.getList(new String[] {"key", 
filterName}, String.class);
+        return keys == null || keys.isEmpty() ? Collections.emptySet() : new 
HashSet<>(keys);
+    }
+
     private static EventResolver createKeyResolver(
             final String key, final boolean stringified, final 
Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
         return new EventResolver() {
@@ -243,6 +287,8 @@ class ReadOnlyStringMapResolver implements EventResolver {
             final String prefix,
             final String pattern,
             final String replacement,
+            final Set<String> allowedKeys,
+            final Set<String> disallowedKeys,
             final boolean stringified,
             final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
 
@@ -258,6 +304,8 @@ class ReadOnlyStringMapResolver implements EventResolver {
             }
             loopContext.pattern = compiledPattern;
             loopContext.replacement = replacement;
+            loopContext.allowedKeys = allowedKeys;
+            loopContext.disallowedKeys = disallowedKeys;
             loopContext.stringified = stringified;
             return loopContext;
         });
@@ -331,6 +379,12 @@ class ReadOnlyStringMapResolver implements EventResolver {
 
         private String replacement;
 
+        @NonNull
+        private Set<String> allowedKeys;
+
+        @NonNull
+        private Set<String> disallowedKeys;
+
         private boolean stringified;
 
         private JsonWriter jsonWriter;
@@ -345,6 +399,12 @@ class ReadOnlyStringMapResolver implements EventResolver {
 
         @Override
         public void accept(final String key, final Object value, final 
LoopContext loopContext) {
+            if (!loopContext.allowedKeys.isEmpty() && 
!loopContext.allowedKeys.contains(key)) {
+                return;
+            }
+            if (loopContext.disallowedKeys.contains(key)) {
+                return;
+            }
             final Matcher matcher = loopContext.pattern != null ? 
loopContext.pattern.matcher(key) : null;
             final boolean keyMatched = matcher == null || matcher.matches();
             if (keyMatched) {
diff --git a/log4j-layout-template-json/src/main/resources/EcsLayout.json 
b/log4j-layout-template-json/src/main/resources/EcsLayout.json
index 8d215ab56a..863e37aabc 100644
--- a/log4j-layout-template-json/src/main/resources/EcsLayout.json
+++ b/log4j-layout-template-json/src/main/resources/EcsLayout.json
@@ -26,7 +26,19 @@
   "labels": {
     "$resolver": "mdc",
     "flatten": true,
-    "stringified": true
+    "stringified": true,
+    "key": {
+      "disallowed": [
+        "@timestamp",
+        "message",
+        "log.logger",
+        "log.level",
+        "event.dataset",
+        "process.thread.name",
+        "process.thread.id",
+        "ecs.version"
+      ]
+    }
   },
   "tags": {
     "$resolver": "ndc"
diff --git 
a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/MdcKeyFilterBenchmark.java
 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/MdcKeyFilterBenchmark.java
new file mode 100644
index 0000000000..d04fc9cb1b
--- /dev/null
+++ 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/MdcKeyFilterBenchmark.java
@@ -0,0 +1,90 @@
+/*
+ * 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.perf.jmh;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@State(Scope.Benchmark)
+public class MdcKeyFilterBenchmark {
+
+    private JsonTemplateLayout patternLayout;
+    private JsonTemplateLayout keyFilterLayout;
+    private LogEvent logEvent;
+
+    @Setup
+    public void setup() {
+
+        SortedArrayStringMap contextData = new SortedArrayStringMap();
+        contextData.putValue("userId", "12345");
+        contextData.putValue("transactionId", "tx-98765");
+        contextData.putValue("userRole", "admin");
+        contextData.putValue("@timestamp", "2026-07-22T10:15:30.123Z");
+        contextData.putValue("message", "Tx completed");
+        contextData.putValue("log.level", "INFO");
+
+        logEvent = 
Log4jLogEvent.newBuilder().setContextData(contextData).build();
+
+        // Layout using regex pattern
+        String patternTemplate = "{" + "\"$resolver\": \"mdc\", "
+                + "\"pattern\": 
\"^(?!@timestamp|message|log\\\\.logger|log\\\\.level|event\\\\.dataset|process\\\\.thread\\\\.name|process\\\\.thread\\\\.id|ecs\\\\.version).*$\""
+                + "}";
+
+        patternLayout = JsonTemplateLayout.newBuilder()
+                .setConfiguration(new DefaultConfiguration())
+                .setEventTemplate(patternTemplate)
+                .build();
+
+        String keyFilterTemplate = "{" + "\"$resolver\": \"mdc\", "
+                + "\"key\": {\"disallowed\": [\"@timestamp\", \"message\", 
\"log.logger\", \"log.level\", \"event.dataset\", \"process.thread.name\", 
\"process.thread.id\", \"ecs.version\"]}"
+                + "}";
+
+        keyFilterLayout = JsonTemplateLayout.newBuilder()
+                .setConfiguration(new DefaultConfiguration())
+                .setEventTemplate(keyFilterTemplate)
+                .build();
+    }
+
+    @Benchmark
+    public String testPatternResolver() {
+        return patternLayout.toSerializable(logEvent);
+    }
+
+    @Benchmark
+    public String testKeyFilterResolver() {
+        return keyFilterLayout.toSerializable(logEvent);
+    }
+}
diff --git a/src/changelog/.2.x.x/4166_add_mdc_key_filter.xml 
b/src/changelog/.2.x.x/4166_add_mdc_key_filter.xml
new file mode 100644
index 0000000000..e9f6076563
--- /dev/null
+++ b/src/changelog/.2.x.x/4166_add_mdc_key_filter.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="added">
+    <issue id="4166" 
link="https://github.com/apache/logging-log4j2/issues/4166"/>
+    <issue id="4186" 
link="https://github.com/apache/logging-log4j2/pull/4186"/>
+    <description format="asciidoc">
+        Add `key` based literal filtering to JSON Template Layout's Context 
Data (MDC) and Map resolvers.
+        Harden `EcsLayout.json` using that.
+    </description>
+</entry>
\ No newline at end of file
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 8fc7b16de4..756a1116eb 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
@@ -1435,7 +1435,11 @@ singleAccess  = key , [ stringified ]
 key           = "key" -> string
 stringified   = "stringified" -> boolean
 
-multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified ]
+multiAccess   = [ keyFilter ] , [ pattern ] , [ replacement ] , [ flatten ] , 
[ stringified ]
+keyFilter     = "key" -> keyConfig
+keyConfig     = [ allowed ] , [ disallowed ]
+allowed       = "allowed" -> array of strings
+disallowed    = "disallowed" -> array of strings
 pattern       = "pattern" -> string
 replacement   = "replacement" -> string
 flatten       = "flatten" -> ( boolean | flattenConfig )
@@ -1452,6 +1456,11 @@ Regex provided in the `pattern` is used to match against 
the keys.
 If provided, `replacement` will be used to replace the matched keys.
 These two are analogous to `Pattern.compile(pattern).matcher(key).matches()` 
and `Pattern.compile(pattern).matcher(key).replaceAll(replacement)` calls.
 
+`key` given as an object filters keys against literal names rather than a 
regex.
+If `allowed` is provided, only the listed keys are resolved.
+Keys listed in `disallowed` are silently dropped, and take precedence over 
`allowed`.
+Both are matched against the key as found in the map, that is, before 
`replacement` is applied, and both can be combined with `pattern`, which a key 
must then satisfy as well.
+
 [WARNING]
 ====
 Regarding garbage footprint, `stringified` flag translates to 
`String.valueOf(value)`, hence mind values that are not `String`-typed.
@@ -1526,6 +1535,29 @@ Resolve all fields whose keys match with the 
`user:(role|rank)` regex into an ob
   "replacement": "$1"
 }
 ----
+Resolve all fields into an object, but silently drop `message` and `log.level`:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "key": {
+    "disallowed": ["message", "log.level"]
+  }
+}
+----
+
+Resolve only the `userId` and `userRole` fields into an object:
+
+[source,json]
+----
+{
+  "$resolver": "…",
+  "key": {
+    "allowed": ["userId", "userRole"]
+  }
+}
+----
 
 Merge all fields whose keys are matching with the `user:(role|rank)` regex 
into the parent:
 

Reply via email to