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 — 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 * — symmetric with the one-arg overload — 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}