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 — 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 — 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" — 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 — 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}