001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.logging;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * A thread-confined, MDC-style key/value context whose entries are auto-attached to every {@link java.util.logging.LogRecord}
025 * emitted by a {@link RichLogger} on the same thread while a scope is open.
026 *
027 * <p>
028 * There is exactly one context per thread, shared across every {@link RichLogger} name &mdash; attachment happens in the
029 * single central interception point ({@link RichLogger#log(java.util.logging.LogRecord)}), not per logger instance.  The
030 * "on {@link RichLogger}" framing is about API home ({@link RichLogger#context()}), not about per-instance scoping.
031 *
032 * <h5 class='section'>Usage:</h5>
033 * <p class='bjava'>
034 *    <jk>try</jk> (LogContext.Scope <jv>c</jv> = RichLogger.<jsm>context</jsm>().with(<js>"requestId"</js>, <jv>id</jv>)) {
035 *       <jc>// Every record logged in this scope automatically carries requestId.</jc>
036 *       <jv>logger</jv>.info(<js>"processing"</js>);
037 *    }
038 * </p>
039 *
040 * <h5 class='section'>Nesting semantics:</h5>
041 * <p>
042 * Classic MDC save/restore: {@link #with(String, Object)} records the key's <i>previous</i> value (or "absent") before
043 * overwriting, and {@link Scope#close()} restores exactly that prior state &mdash; never a blanket clear.  Nested scopes on
044 * the same key are safe: closing the inner scope restores the outer value, not the empty state.  {@link #with(Map)}
045 * restores all entries in one {@link Scope#close()}.
046 *
047 * <h5 class='section'>Null policy:</h5>
048 * <p>
049 * {@link #with(String, Object)} rejects a <jk>null</jk> key.  A <jk>null</jk> value means "remove this key for the scope's
050 * duration" &mdash; a <jk>null</jk> is never stored, so {@link #snapshot()} (an immutable {@link Map#copyOf(Map) copy}) never
051 * blows up on a null key or value.
052 *
053 * <h5 class='section'>Thread confinement (v1):</h5>
054 * <p>
055 * This context is <b>thread-confined only</b>.  It is not inherited by child threads, not propagated across executor or
056 * {@link java.util.concurrent.CompletableFuture} boundaries, and is <b>not</b> SLF4J MDC and is <b>not</b> restored by
057 * {@code MdcAsyncListener}.  Apps that rely on SLF4J MDC on an async-completion thread keep it; apps that rely on
058 * {@code LogContext} do not (except the REST debug-pipeline completion record, which carries it explicitly).
059 *
060 * <h5 class='section'>Scope discipline:</h5>
061 * <p>
062 * A {@link Scope} must be closed on the same thread that opened it (use try-with-resources).  A forgotten
063 * {@link Scope#close()} leaves the entry live on that thread and is visible on the next {@link #get(String)} on the same
064 * thread &mdash; the leak is the caller's, not the framework's.
065 *
066 * <h5 class='section'>See Also:</h5><ul>
067 *    <li class='jc'>{@link RichLogger#context()}
068 *    <li class='jc'>{@link LogRecordContext}
069 * </ul>
070 */
071@SuppressWarnings({
072   "resource" // Returns a Scope the caller must close via try-with-resources; Eclipse JDT @Owning warning is by design.
073})
074public final class LogContext {
075
076   /** The single shared instance.  There is exactly one thread-local map; sharing the instance is harmless. */
077   static final LogContext INSTANCE = new LogContext();
078
079   @SuppressWarnings({
080      "java:S5164" // Scope#close() restores/empties the per-thread map on every key; the cached empty map is harmless to retain for the thread's lifetime.
081   })
082   private static final ThreadLocal<Map<String,Object>> CONTEXT = ThreadLocal.withInitial(LinkedHashMap::new);
083
084   private LogContext() {}
085
086   /**
087    * Opens a scope that sets a single entry.
088    *
089    * @param key The entry key.  Must not be <jk>null</jk>.
090    * @param value The entry value.  A <jk>null</jk> value removes the key for the scope's duration.
091    * @return A scope that restores the prior state of this key on {@link Scope#close()}.  Never <jk>null</jk>.
092    */
093   public Scope with(String key, Object value) {
094      if (key == null)
095         throw iaex("Argument 'key' cannot be null.");
096      var map = CONTEXT.get();
097      var hadPrior = map.containsKey(key);
098      var prior = map.get(key);
099      apply(map, key, value);
100      return new SingleScope(key, hadPrior, prior);
101   }
102
103   /**
104    * Opens a scope that sets a batch of entries atomically.
105    *
106    * <p>
107    * All entries are restored to their prior state in one {@link Scope#close()} (not N chained scopes), so a batch
108    * caller does not need nested try-with-resources for multiple keys set together.
109    *
110    * @param entries The entries to set.  Must not be <jk>null</jk> and must not contain a <jk>null</jk> key.  A
111    *    <jk>null</jk> value removes that key for the scope's duration.
112    * @return A scope that restores the prior state of all supplied keys on {@link Scope#close()}.  Never <jk>null</jk>.
113    */
114   public Scope with(Map<String,Object> entries) {
115      if (entries == null)
116         throw iaex("Argument 'entries' cannot be null.");
117      for (var key : entries.keySet())
118         if (key == null)
119            throw iaex("Context key cannot be null.");
120      var map = CONTEXT.get();
121      var priors = new LinkedHashMap<String,Object>();
122      var hadPrior = new LinkedHashMap<String,Boolean>();
123      for (var e : entries.entrySet()) {
124         var key = e.getKey();
125         hadPrior.put(key, map.containsKey(key));
126         priors.put(key, map.get(key));
127      }
128      for (var e : entries.entrySet())
129         apply(map, e.getKey(), e.getValue());
130      return new BatchScope(priors, hadPrior);
131   }
132
133   /**
134    * Returns the current value for the specified key on this thread.
135    *
136    * @param key The entry key.
137    * @return The value, or <jk>null</jk> if not set on this thread.
138    */
139   public Object get(String key) {
140      return CONTEXT.get().get(key);
141   }
142
143   /**
144    * Returns an immutable snapshot of this thread's context at this instant.
145    *
146    * @return An immutable copy of the current entries, or the shared empty {@link Map#of()} singleton when empty.
147    */
148   public Map<String,Object> snapshot() {
149      var map = CONTEXT.get();
150      return map.isEmpty() ? Map.of() : Map.copyOf(map);
151   }
152
153   private static void apply(Map<String,Object> map, String key, Object value) {
154      if (value == null)
155         map.remove(key);
156      else
157         map.put(key, value);
158   }
159
160   private static void restore(Map<String,Object> map, String key, boolean hadPrior, Object prior) {
161      if (hadPrior)
162         map.put(key, prior);
163      else
164         map.remove(key);
165   }
166
167   /**
168    * An open context scope.  Closing it restores the thread's context to the state it had before the scope opened.
169    */
170   public interface Scope extends AutoCloseable {
171      @Override
172      void close();
173   }
174
175   private static final class SingleScope implements Scope {
176      private final String key;
177      private final boolean hadPrior;
178      private final Object prior;
179      private boolean closed;
180
181      SingleScope(String key, boolean hadPrior, Object prior) {
182         this.key = key;
183         this.hadPrior = hadPrior;
184         this.prior = prior;
185      }
186
187      @Override
188      public void close() {
189         if (closed)
190            return;
191         closed = true;
192         restore(CONTEXT.get(), key, hadPrior, prior);
193      }
194   }
195
196   private static final class BatchScope implements Scope {
197      private final Map<String,Object> priors;
198      private final Map<String,Boolean> hadPrior;
199      private boolean closed;
200
201      BatchScope(Map<String,Object> priors, Map<String,Boolean> hadPrior) {
202         this.priors = priors;
203         this.hadPrior = hadPrior;
204      }
205
206      @Override
207      public void close() {
208         if (closed)
209            return;
210         closed = true;
211         var map = CONTEXT.get();
212         for (var e : priors.entrySet())
213            restore(map, e.getKey(), hadPrior.get(e.getKey()), e.getValue());
214      }
215   }
216}