rgoers commented on code in PR #2385: URL: https://github.com/apache/logging-log4j2/pull/2385#discussion_r1533310760
########## log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java: ########## @@ -0,0 +1,264 @@ +/* + * 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; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Supplier; +import org.apache.logging.log4j.internal.ScopedContextAnchor; + +/** + * Context that can be used for data to be logged in a block of code. + * + * While this is influenced by ScopedValues from Java 21 it does not share the same API. While it can perform a + * similar function as a set of ScopedValues it is really meant to allow a block of code to include a set of keys and + * values in all the log events within that block. The underlying implementation must provide support for + * logging the ScopedContext for that to happen. + * + * The ScopedContext will not be bound to the current thread until either a run or call method is invoked. The + * contexts are nested so creating and running or calling via a second ScopedContext will result in the first + * ScopedContext being hidden until the call is returned. Thus the values from the first ScopedContext need to + * be added to the second to be included. + * + * @since 2.24.0 + */ +public class ScopedContext { + + public static final ScopedContext INITIAL_CONTEXT = new ScopedContext(); + + /** + * @hidden + * Returns an unmodifiable copy of the current ScopedContext Map. This method should + * only be used by implementations of Log4j API. + * @return the Map of Renderable objects. + */ + public static Map<String, Renderable> getContextMap() { + Optional<ScopedContext> context = ScopedContextAnchor.getContext(); + if (context.isPresent() + && context.get().contextMap != null + && !context.get().contextMap.isEmpty()) { + return Collections.unmodifiableMap(context.get().contextMap); + } + return Collections.emptyMap(); + } + + /** + * Return the key from the current ScopedContext, if there is one and the key exists. + * @param key The key. + * @return The value of the key in the current ScopedContext. + */ + @SuppressWarnings("unchecked") + public static <T> T get(String key) { + Optional<ScopedContext> context = ScopedContextAnchor.getContext(); + if (context.isPresent()) { + Renderable renderable = context.get().contextMap.get(key); + if (renderable != null) { + return (T) renderable.getObject(); + } + } + return null; + } + + /** + * Returns an Optional holding the active ScopedContext. + * @return an Optional containing the active ScopedContext, if there is one. + */ + public static Optional<ScopedContext> current() { + return ScopedContextAnchor.getContext(); + } + + private final ScopedContext parent; + private final String key; + private final Renderable value; + private final Map<String, Renderable> contextMap; + + private ScopedContext() { + this.parent = null; + this.key = null; + this.value = null; + this.contextMap = null; + } + + private ScopedContext(Map<String, Renderable> map) { + this.parent = null; + this.key = null; + this.value = null; + this.contextMap = map; + } + + private ScopedContext(ScopedContext parent, String key, Renderable value) { + this.parent = parent; + this.key = key; + this.value = value; + this.contextMap = null; + } + + /** + * Adds a key/value pair to the ScopedContext being constructed. + * @param key the key to add. + * @param value the value associated with the key. + * @return the ScopedContext being constructed. + */ + public ScopedContext where(String key, Object value) { + return addObject(key, value); + } + + /** + * Adds a key/value pair to the ScopedContext being constructed. + * @param key the key to add. + * @param supplier the function to generate the value. + * @return the ScopedContext being constructed. + */ + public ScopedContext where(String key, Supplier<Object> supplier) { + return addObject(key, supplier.get()); + } + + private ScopedContext addObject(String key, Object obj) { + if (obj != null) { + Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj); + return new ScopedContext(this, key, renderable); + } + return this; + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * @param op the code block to execute. + */ + public void run(Runnable op) { + new ScopedContextRunner(this, op).run(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * @param op the code block to execute. + * @return the return value from the code block. + */ + public <R> R call(Callable<R> op) throws Exception { + return new ScopedContextCaller<R>(this, op).call(); + } + + private static class ScopedContextRunner implements Runnable { + private final Map<String, Renderable> contextMap = new HashMap<>(); + private final ScopedContext context; + private final Runnable op; + + public ScopedContextRunner(ScopedContext context, Runnable op) { + this.context = context; + this.op = op; + } + + @Override + public void run() { + ScopedContext scopedContext = context; + // If the current context has a Map then we can just use it. + if (context.contextMap == null) { + do { + if (scopedContext.contextMap != null) { + // Once we hit a scope with an already populated Map we won't need to go any further. + contextMap.putAll(scopedContext.contextMap); + break; + } else if (scopedContext.key != null) { + contextMap.putIfAbsent(scopedContext.key, scopedContext.value); + } + scopedContext = scopedContext.parent; + } while (scopedContext != null); + scopedContext = new ScopedContext(contextMap); + } + ScopedContextAnchor.addScopedContext(scopedContext); + try { + op.run(); + } finally { + ScopedContextAnchor.removeScopedContext(); + } + } + } + + private static class ScopedContextCaller<R> implements Callable<R> { + private final Map<String, Renderable> contextMap = new HashMap<>(); + private final ScopedContext context; + private final Callable<R> op; + + public ScopedContextCaller(ScopedContext context, Callable<R> op) { + this.context = context; + this.op = op; + } + + @Override + public R call() throws Exception { + ScopedContext scopedContext = context; + // If the current context has a Map then we can just use it. + if (context.contextMap == null) { + do { + if (scopedContext.contextMap != null) { + // Once we hit a scope with an already populated Map we won't need to go any further. + contextMap.putAll(scopedContext.contextMap); + break; + } else if (scopedContext.key != null) { + contextMap.putIfAbsent(scopedContext.key, scopedContext.value); + } + scopedContext = scopedContext.parent; + } while (scopedContext != null); + scopedContext = new ScopedContext(contextMap); + } + ScopedContextAnchor.addScopedContext(scopedContext); + try { + return op.call(); + } finally { + ScopedContextAnchor.removeScopedContext(); + } + } + } + + /** + * Interface for converting Objects stored in the ContextScope to Strings for logging. + */ + public static interface Renderable { Review Comment: With regard to the get() method. This API supports Objects - something users have been requesting for a long time. I added them here because they can be safely used the way this is implemented. There is no point in accepting an object if user's cannot query them since logging will NEVER log an object, only its string representation. How? There are no StringBuilders at all in ScopedContext. It is used to convert an object into its String representation so it can be included in the LogEvent's ContextMap as a single String item. While the Object's render method certainly might use a StringBuilder internally the code storing the data into the Map certainly doesn't want one. Furthermore, if you look at the default implementation the render method simply calls toString() on the object. For the 90+% of use cases where the object is already a String this is effectively a no-op as it just returns "this". Using a StringBuilder would require much more overhead. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: notifications-unsubscr...@logging.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org