99import java .io .UnsupportedEncodingException ;
1010import java .net .URLEncoder ;
1111import java .nio .charset .StandardCharsets ;
12+ import java .util .concurrent .ConcurrentHashMap ;
1213import org .slf4j .Logger ;
1314import org .slf4j .LoggerFactory ;
1415
@@ -32,6 +33,28 @@ public class SharedDBCommenter {
3233 private static final String TRACEPARENT = encode ("traceparent" );
3334 private static final String DD_SERVICE_HASH = encode ("ddsh" );
3435
36+ /**
37+ * Cached static suffix: "dde='<env>',ddpv='<version>'" Computed lazily on first use
38+ * since Config may not be available at class-load time.
39+ */
40+ private static volatile String cachedStaticSuffix ;
41+
42+ /** Cached URL-encoded service name from Config. */
43+ private static volatile String cachedEncodedServiceName ;
44+
45+ /** The Config instance used to compute the cached values, for staleness detection. */
46+ private static volatile Config cachedConfigInstance ;
47+
48+ /**
49+ * Cached per-connection prefix fragments keyed by (dbService, hostname, dbName). Each entry holds
50+ * the pre-encoded "ddps='...',dddbs='...',ddh='...',dddb='...'" fragment, combining the static
51+ * service name with per-connection values to preserve the correct field ordering.
52+ */
53+ private static final int MAX_CONNECTION_CACHE_SIZE = 256 ;
54+
55+ private static final ConcurrentHashMap <ConnectionKey , String > connectionPrefixCache =
56+ new ConcurrentHashMap <>();
57+
3558 // Used by SQLCommenter and MongoCommentInjector to avoid duplicate comment injection
3659 public static boolean containsTraceComment (String commentContent ) {
3760 return commentContent .contains (PARENT_SERVICE + "=" )
@@ -50,25 +73,111 @@ public static String buildComment(
5073 String dbService , String dbType , String hostname , String dbName , String traceParent ) {
5174
5275 Config config = Config .get ();
53- StringBuilder sb = new StringBuilder ();
5476
55- int initSize = 0 ; // No initial content for pure comment
56- append (sb , PARENT_SERVICE , config .getServiceName (), initSize );
57- append (sb , DATABASE_SERVICE , dbService , initSize );
58- append (sb , DD_HOSTNAME , hostname , initSize );
59- append (sb , DD_DB_NAME , dbName , initSize );
60- append (sb , DD_PEER_SERVICE , getPeerService (), initSize );
61- append (sb , DD_ENV , config .getEnv (), initSize );
62- append (sb , DD_VERSION , config .getVersion (), initSize );
63- append (sb , TRACEPARENT , traceParent , initSize );
77+ // Ensure static values are initialized
78+ ensureStaticValuesInitialized (config );
79+
80+ // Get or compute the prefix: ddps='...',dddbs='...',ddh='...',dddb='...'
81+ String prefix = getConnectionPrefix (dbService , hostname , dbName );
82+
83+ // Get the cached static suffix: dde='...',ddpv='...'
84+ String suffix = cachedStaticSuffix ;
6485
65- if (config .isDbmInjectSqlBaseHash () && config .isExperimentalPropagateProcessTagsEnabled ()) {
66- append (sb , DD_SERVICE_HASH , BaseHash .getBaseHashStr (), initSize );
86+ // Dynamic values that change per-call
87+ String peerService = getPeerService ();
88+ boolean injectBaseHash =
89+ config .isDbmInjectSqlBaseHash () && config .isExperimentalPropagateProcessTagsEnabled ();
90+ String baseHash = injectBaseHash ? BaseHash .getBaseHashStr () : null ;
91+
92+ // Estimate capacity
93+ int capacity =
94+ (prefix != null ? prefix .length () : 0 )
95+ + (suffix != null ? suffix .length () : 0 )
96+ + 128 ; // generous estimate for dynamic parts
97+ StringBuilder sb = new StringBuilder (capacity );
98+
99+ // Append prefix: ddps, dddbs, ddh, dddb (preserves original field order)
100+ if (prefix != null ) {
101+ sb .append (prefix );
102+ }
103+
104+ // Append dynamic: ddprs
105+ appendEncoded (sb , DD_PEER_SERVICE , peerService );
106+
107+ // Append cached static suffix: dde, ddpv
108+ if (suffix != null ) {
109+ if (sb .length () > 0 ) {
110+ sb .append (COMMA );
111+ }
112+ sb .append (suffix );
113+ }
114+
115+ // Append dynamic: traceparent
116+ appendEncoded (sb , TRACEPARENT , traceParent );
117+
118+ // Append conditional: ddsh
119+ if (injectBaseHash ) {
120+ appendEncoded (sb , DD_SERVICE_HASH , baseHash );
67121 }
68122
69123 return sb .length () > 0 ? sb .toString () : null ;
70124 }
71125
126+ /**
127+ * Initializes the cached static values (service name, env, version) from Config. These never
128+ * change for the agent's lifetime. Identity-checks the Config instance to handle test scenarios
129+ * where Config is rebuilt.
130+ */
131+ private static void ensureStaticValuesInitialized (Config config ) {
132+ if (cachedConfigInstance == config ) {
133+ return ;
134+ }
135+ // Config instance changed (or first call) -- recompute all cached values
136+ // URL-encode the service name
137+ String serviceName = config .getServiceName ();
138+ cachedEncodedServiceName = encodeValue (serviceName );
139+
140+ // Build the static suffix: dde='...', ddpv='...'
141+ StringBuilder sb = new StringBuilder (48 );
142+ appendEncoded (sb , DD_ENV , config .getEnv ());
143+ appendEncoded (sb , DD_VERSION , config .getVersion ());
144+ cachedStaticSuffix = sb .length () > 0 ? sb .toString () : null ;
145+
146+ // Clear connection prefix cache since it includes the service name
147+ connectionPrefixCache .clear ();
148+
149+ // Publish the config instance last so other threads see all updates
150+ cachedConfigInstance = config ;
151+ }
152+
153+ /**
154+ * Returns a cached prefix fragment combining ddps (static service name) with dddbs, ddh, and dddb
155+ * (per-connection values). This preserves the original field ordering:
156+ * ddps='...',dddbs='...',ddh='...',dddb='...'
157+ */
158+ private static String getConnectionPrefix (String dbService , String hostname , String dbName ) {
159+ ConnectionKey key = new ConnectionKey (dbService , hostname , dbName );
160+ String fragment = connectionPrefixCache .get (key );
161+ if (fragment != null ) {
162+ return fragment ;
163+ }
164+ // Build the prefix fragment preserving original order
165+ StringBuilder sb = new StringBuilder (96 );
166+ appendPreEncoded (sb , PARENT_SERVICE , cachedEncodedServiceName );
167+ appendEncoded (sb , DATABASE_SERVICE , dbService );
168+ appendEncoded (sb , DD_HOSTNAME , hostname );
169+ appendEncoded (sb , DD_DB_NAME , dbName );
170+ fragment = sb .length () > 0 ? sb .toString () : null ;
171+ if (fragment != null ) {
172+ // Evict all entries if cache grows too large to bound memory
173+ if (connectionPrefixCache .size () >= MAX_CONNECTION_CACHE_SIZE ) {
174+ connectionPrefixCache .clear ();
175+ }
176+ connectionPrefixCache .put (key , fragment );
177+ }
178+ return fragment ;
179+ }
180+
72181 private static String getPeerService () {
73182 AgentSpan span = activeSpan ();
74183 Object peerService = null ;
@@ -89,7 +198,36 @@ private static String encode(String val) {
89198 return val ;
90199 }
91200
92- private static void append (StringBuilder sb , String key , String value , int initSize ) {
201+ /** URL-encodes a value, returning null if the input is null or empty. */
202+ private static String encodeValue (String value ) {
203+ if (value == null || value .isEmpty ()) {
204+ return null ;
205+ }
206+ try {
207+ return URLEncoder .encode (value , UTF8 );
208+ } catch (UnsupportedEncodingException e ) {
209+ return value ;
210+ }
211+ }
212+
213+ /**
214+ * Appends a key='already-encoded-value' pair to the StringBuilder. Used for values that have
215+ * already been URL-encoded and cached.
216+ */
217+ private static void appendPreEncoded (StringBuilder sb , String key , String encodedValue ) {
218+ if (null == encodedValue || encodedValue .isEmpty ()) {
219+ return ;
220+ }
221+ if (sb .length () > 0 ) {
222+ sb .append (COMMA );
223+ }
224+ sb .append (key ).append (EQUALS ).append (QUOTE ).append (encodedValue ).append (QUOTE );
225+ }
226+
227+ /**
228+ * Appends an encoded key='value' pair to the StringBuilder if value is non-null and non-empty.
229+ */
230+ private static void appendEncoded (StringBuilder sb , String key , String value ) {
93231 if (null == value || value .isEmpty ()) {
94232 return ;
95233 }
@@ -99,9 +237,59 @@ private static void append(StringBuilder sb, String key, String value, int initS
99237 } catch (UnsupportedEncodingException e ) {
100238 encodedValue = value ;
101239 }
102- if (sb .length () > initSize ) {
240+ if (sb .length () > 0 ) {
103241 sb .append (COMMA );
104242 }
105243 sb .append (key ).append (EQUALS ).append (QUOTE ).append (encodedValue ).append (QUOTE );
106244 }
245+
246+ /** Resets cached state. Visible for testing. */
247+ static void resetCache () {
248+ cachedConfigInstance = null ;
249+ cachedStaticSuffix = null ;
250+ cachedEncodedServiceName = null ;
251+ connectionPrefixCache .clear ();
252+ }
253+
254+ /**
255+ * Composite key for per-connection prefix cache. Holds the raw (unencoded) dbService, hostname,
256+ * and dbName values.
257+ */
258+ private static final class ConnectionKey {
259+ private final String dbService ;
260+ private final String hostname ;
261+ private final String dbName ;
262+ private final int hashCode ;
263+
264+ ConnectionKey (String dbService , String hostname , String dbName ) {
265+ this .dbService = dbService ;
266+ this .hostname = hostname ;
267+ this .dbName = dbName ;
268+ // Pre-compute hash code since this is an immutable key
269+ int h = 17 ;
270+ h = 31 * h + (dbService != null ? dbService .hashCode () : 0 );
271+ h = 31 * h + (hostname != null ? hostname .hashCode () : 0 );
272+ h = 31 * h + (dbName != null ? dbName .hashCode () : 0 );
273+ this .hashCode = h ;
274+ }
275+
276+ @ Override
277+ public boolean equals (Object o ) {
278+ if (this == o ) return true ;
279+ if (!(o instanceof ConnectionKey )) return false ;
280+ ConnectionKey that = (ConnectionKey ) o ;
281+ return equals (dbService , that .dbService )
282+ && equals (hostname , that .hostname )
283+ && equals (dbName , that .dbName );
284+ }
285+
286+ private static boolean equals (String a , String b ) {
287+ return a == null ? b == null : a .equals (b );
288+ }
289+
290+ @ Override
291+ public int hashCode () {
292+ return hashCode ;
293+ }
294+ }
107295}
0 commit comments