This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 3067dca1e34795964e72b76b40bad75954010ff4 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Sun Aug 28 23:05:28 2022 +0200 [LOG4J2-3583] Implements stack-valued MDC This PR implements SLF4J-531 by storing the result of `pushByKey(key, value)` in the usual thread context map and restoring the previous value, when a `popByKey(key)` call occurs. --- log4j-slf4j20-impl/pom.xml | 8 +++ .../org/apache/logging/slf4j/Log4jMDCAdapter.java | 84 +++++++++++++++++++--- .../apache/logging/slf4j/Log4jMDCAdapterTest.java | 63 ++++++++++++++++ 3 files changed, 147 insertions(+), 8 deletions(-) diff --git a/log4j-slf4j20-impl/pom.xml b/log4j-slf4j20-impl/pom.xml index 5f72a38614..d52ee1b592 100644 --- a/log4j-slf4j20-impl/pom.xml +++ b/log4j-slf4j20-impl/pom.xml @@ -91,6 +91,14 @@ <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + </dependency> </dependencies> <build> <plugins> diff --git a/log4j-slf4j20-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java b/log4j-slf4j20-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java index 11f52de5b0..71e5a0463a 100644 --- a/log4j-slf4j20-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java +++ b/log4j-slf4j20-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java @@ -16,10 +16,16 @@ */ package org.apache.logging.slf4j; +import java.util.ArrayDeque; import java.util.Deque; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.ThreadContext.ContextStack; +import org.apache.logging.log4j.status.StatusLogger; import org.slf4j.spi.MDCAdapter; /** @@ -27,6 +33,10 @@ import org.slf4j.spi.MDCAdapter; */ public class Log4jMDCAdapter implements MDCAdapter { + private static Logger LOGGER = StatusLogger.getLogger(); + + private final ThreadLocalMapOfStacks mapOfStacks = new ThreadLocalMapOfStacks(); + @Override public void put(final String key, final String val) { ThreadContext.put(key, val); @@ -53,31 +63,89 @@ public class Log4jMDCAdapter implements MDCAdapter { } @Override - @SuppressWarnings("unchecked") // nothing we can do about this, restricted by SLF4J API - public void setContextMap(@SuppressWarnings("rawtypes") final Map map) { + public void setContextMap(final Map<String, String> map) { ThreadContext.clearMap(); ThreadContext.putAll(map); } @Override public void pushByKey(String key, String value) { - // not implemented yet + if (key == null) { + ThreadContext.push(value); + } else { + final String oldValue = mapOfStacks.peekByKey(key); + if (!Objects.equals(ThreadContext.get(key), oldValue)) { + LOGGER.warn("The key {} was used in both the string and stack-valued MDC.", key); + } + mapOfStacks.pushByKey(key, value); + ThreadContext.put(key, value); + } } @Override public String popByKey(String key) { - // not implemented yet - return null; + if (key == null) { + return ThreadContext.getDepth() > 0 ? ThreadContext.pop() : null; + } + final String value = mapOfStacks.popByKey(key); + if (!Objects.equals(ThreadContext.get(key), value)) { + LOGGER.warn("The key {} was used in both the string and stack-valued MDC.", key); + } + ThreadContext.put(key, mapOfStacks.peekByKey(key)); + return value; } @Override public Deque<String> getCopyOfDequeByKey(String key) { - // not implemented yet - return null; + if (key == null) { + final ContextStack stack = ThreadContext.getImmutableStack(); + final Deque<String> copy = new ArrayDeque<>(stack.size()); + stack.forEach(copy::push); + return copy; + } + return mapOfStacks.getCopyOfDequeByKey(key); } @Override public void clearDequeByKey(String key) { - // not implemented yet + if (key == null) { + ThreadContext.clearStack(); + } else { + mapOfStacks.clearByKey(key); + ThreadContext.put(key, null); + } + } + + private static class ThreadLocalMapOfStacks { + + private final ThreadLocal<Map<String, Deque<String>>> tlMapOfStacks = ThreadLocal.withInitial(HashMap::new); + + public void pushByKey(String key, String value) { + tlMapOfStacks.get() + .computeIfAbsent(key, ignored -> new ArrayDeque<>()) + .push(value); + } + + public String popByKey(String key) { + final Deque<String> deque = tlMapOfStacks.get().get(key); + return deque != null ? deque.poll() : null; + } + + public Deque<String> getCopyOfDequeByKey(String key) { + final Deque<String> deque = tlMapOfStacks.get().get(key); + return deque != null ? new ArrayDeque<>(deque) : null; + } + + public void clearByKey(String key) { + final Deque<String> deque = tlMapOfStacks.get().get(key); + if (deque != null) { + deque.clear(); + } + } + + public String peekByKey(String key) { + final Deque<String> deque = tlMapOfStacks.get().get(key); + return deque != null ? deque.peek() : null; + } } } diff --git a/log4j-slf4j20-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java b/log4j-slf4j20-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java new file mode 100644 index 0000000000..4fa9038be1 --- /dev/null +++ b/log4j-slf4j20-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java @@ -0,0 +1,63 @@ +/* + * 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.slf4j; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class Log4jMDCAdapterTest { + + private static final Log4jMDCAdapter MDC_ADAPTER = new Log4jMDCAdapter(); + private static final String KEY = "Log4j2"; + + private static Deque<String> createDeque(int size) { + final Deque<String> result = new ArrayDeque<>(size); + IntStream.range(0, size).mapToObj(Integer::toString).forEach(result::addLast); + return result; + } + + private static Deque<String> popDeque(String key) { + final Deque<String> result = new ArrayDeque<>(); + String value; + while ((value = MDC_ADAPTER.popByKey(key)) != null) { + result.addLast(value); + } + return result; + } + + static Stream<String> keys() { + return Stream.of(KEY, "", null); + } + + @ParameterizedTest + @MethodSource("keys") + public void testPushPopByKey(final String key) { + MDC_ADAPTER.clearDequeByKey(key); + final Deque<String> expectedValues = createDeque(100); + expectedValues.descendingIterator().forEachRemaining(v -> MDC_ADAPTER.pushByKey(key, v)); + assertThat(MDC_ADAPTER.getCopyOfDequeByKey(key)).containsExactlyElementsOf(expectedValues); + assertThat(popDeque(key)).containsExactlyElementsOf(expectedValues); + } + +}
