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 java.util.*;
020import java.util.concurrent.atomic.*;
021
022/**
023 * Identity-keyed side table that carries a {@link LogContext} snapshot alongside a {@link java.util.logging.LogRecord}.
024 *
025 * <p>
026 * The context snapshot is attached in a side table (not a field on any {@link java.util.logging.LogRecord} type) so it is
027 * visible regardless of whether the record is a base JUL {@link java.util.logging.LogRecord} (as both debug pipelines
028 * construct) or the commons {@link LogRecord} subclass.
029 *
030 * <h5 class='section'>Identity-key invariant (load-bearing):</h5>
031 * <p>
032 * The backing {@link WeakHashMap} behaves as an identity map <b>only because</b> neither base JUL
033 * {@link java.util.logging.LogRecord} nor the commons {@link LogRecord} subclass overrides {@code equals}/{@code hashCode}
034 * (both use {@link Object} identity).  If a future change gives either value-equality, the side table silently corrupts
035 * (two distinct records comparing equal would share/overwrite one entry).  A unit test guards this by asserting both
036 * record types remain on {@link Object} equality.
037 *
038 * <h5 class='section'>Framework attach points:</h5>
039 * <p>
040 * The two {@code attachIfAbsent} overloads are called by {@link RichLogger#log(java.util.logging.LogRecord)} and by the
041 * two REST debug pipelines to pre-seed a record's correlation context.  They are <b>not</b> intended as a general
042 * application-facing mutator.
043 *
044 * <h5 class='section'>See Also:</h5><ul>
045 *    <li class='jc'>{@link LogContext}
046 *    <li class='jc'>{@link RichLogger#log(java.util.logging.LogRecord)}
047 * </ul>
048 */
049public final class LogRecordContext {
050
051   private static final Map<java.util.logging.LogRecord,Map<String,Object>> TABLE =
052      Collections.synchronizedMap(new WeakHashMap<>());
053
054   /** White-box counter of side-table put attempts; incremented only when a non-empty map reaches the table. */
055   private static final AtomicLong PUT_COUNT = new AtomicLong();
056
057   private LogRecordContext() {}
058
059   /**
060    * Returns the context snapshot attached to the specified record.
061    *
062    * @param rec The record.  Must not be <jk>null</jk>.
063    * @return The attached snapshot, or the shared empty {@link Map#of()} singleton if none is attached.  Never
064    *    <jk>null</jk>.
065    */
066   public static Map<String,Object> of(java.util.logging.LogRecord rec) {
067      var m = TABLE.get(rec);
068      return m == null ? Map.of() : m;
069   }
070
071   /**
072    * Attaches the current thread's live {@link LogContext} snapshot to the specified record, if no snapshot is already
073    * attached.
074    *
075    * <p>
076    * Framework attach point (called as the first statement of {@link RichLogger#log(java.util.logging.LogRecord)}).  When
077    * the live context is empty this returns immediately, before touching the synchronized side table, so callers who
078    * never use {@link LogContext} pay only a thread-local read and an {@code isEmpty()} check &mdash; never a lock.
079    *
080    * @param rec The record to attach to.  Must not be <jk>null</jk>.
081    */
082   public static void attachIfAbsent(java.util.logging.LogRecord rec) {
083      attachIfAbsent(rec, LogContext.INSTANCE.snapshot());
084   }
085
086   /**
087    * Attaches the supplied context snapshot to the specified record, if no snapshot is already attached.
088    *
089    * <p>
090    * Framework attach point used by the debug pipelines to pre-seed a record from a snapshot carried across an async
091    * boundary.  When the supplied map is empty this returns immediately, before touching the synchronized side table
092    * &mdash; symmetric with the one-arg overload &mdash; so a later one-arg call from an empty emitting thread cannot
093    * clobber a previously pre-seeded snapshot.
094    *
095    * @param rec The record to attach to.  Must not be <jk>null</jk>.
096    * @param ctx The context snapshot to attach.  Must not be <jk>null</jk>.
097    */
098   public static void attachIfAbsent(java.util.logging.LogRecord rec, Map<String,Object> ctx) {
099      if (ctx.isEmpty())
100         return;
101      PUT_COUNT.incrementAndGet();
102      TABLE.putIfAbsent(rec, ctx);
103   }
104
105   /**
106    * White-box accessor: the number of live entries in the side table.  For tests only.
107    *
108    * @return The current side-table size.
109    */
110   static int tableSize() {
111      return TABLE.size();
112   }
113
114   /**
115    * White-box accessor: the number of times a non-empty context reached the synchronized side table.  For tests only.
116    *
117    * @return The cumulative put-attempt count.
118    */
119   static long putCount() {
120      return PUT_COUNT.get();
121   }
122}