Skip to content

Commit ee821b9

Browse files
committed
Merge branch 'main' of github.com:newrelic/newrelic-java-agent into http-request-method
2 parents a006d10 + 91fe8ff commit ee821b9

45 files changed

Lines changed: 609 additions & 294 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.fleetControl/schemaGeneration/reference-newrelic.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@ common: &default_settings
690690
# Default: 10
691691
gc_cpu_threshold: 10
692692

693+
# JDBC Statements are cached in a weak key map by default. Set enabled:false to use a vanilla ConcurrentHashMap instead.
694+
# Users who set enabled:false MUST call Statement.close() on every JDBC Statement.
695+
jdbc_statement_weak_key_caching:
696+
enabled: true
697+
693698
# Class transformer can be used to disable all agent instrumentation or specific instrumentation modules.
694699
# All instrumentation modules can be found here: https://github.com/newrelic/newrelic-java-agent/tree/main/instrumentation
695700
class_transformer:

.fleetControl/schemas/config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,17 @@
11971197
"additionalProperties": true,
11981198
"description": "Circuit breaker protection settings."
11991199
},
1200+
"jdbc_statement_weak_key_caching": {
1201+
"type": "object",
1202+
"properties": {
1203+
"enabled": {
1204+
"type": "boolean",
1205+
"default": true
1206+
}
1207+
},
1208+
"additionalProperties": true,
1209+
"description": "JDBC Statements are cached in a weak key map by default. Set enabled:false to use a vanilla ConcurrentHashMap instead. Users who set enabled:false MUST call Statement.close() on every JDBC Statement."
1210+
},
12001211
"class_transformer": {
12011212
"type": "object",
12021213
"properties": {

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ agentmain(String agentArgs, Instrumentation inst) ← dynamic attach
6868
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
6969
| `newrelic-agent` | Main agent implementation; ServiceManager, configuration, harvest cycle, data transport. Built as a shadow JAR with all dependencies relocated. |
7070
| `newrelic-weaver` | Bytecode weaving engine using ASM. Matches weave classes to target classes and applies transformations at class-load time. |
71-
| `newrelic-weaver-api` | Annotations for authoring weave classes (`@Weave`, `@WeaveAllImplementations`, `@NewField`, `@WeaveWithAnnotation`). |
71+
| `newrelic-weaver-api` | Annotations for authoring weave classes (`@Weave`, `@NewField`, `@WeaveWithAnnotation`). |
7272
| `newrelic-api` | Public API for custom instrumentation (`@Trace`, `NewRelic.getAgent()`, custom events/metrics). Ships with no-op implementations. |
7373
| `agent-bridge` | Runtime bridge between instrumentation modules and agent core. Provides `AgentBridge` static facade with volatile references swapped in when agent loads. |
7474
| `agent-bridge-datastore` | Datastore-specific bridge interfaces (connection URL parsing, vendor detection, instance metrics). |
@@ -172,4 +172,4 @@ Require the agent JAR to be built first (`./gradlew jar`).
172172
- Single module: `./gradlew :instrumentation:akka-http-core-10.0.11:test --parallel`
173173
- Single test: `./gradlew :instrumentation:vertx-web-3.2.0:test --tests com.nr.vertx.instrumentation.RoutingTest --parallel`
174174
- Scala module: `./gradlew -PincludeScala :instrumentation:sttp-2.13_2.2.3:test --parallel`
175-
- Java 17+ module: `./gradlew -Ptest17 :instrumentation:module-name:test --parallel`
175+
- Java 17+ module: `./gradlew -Ptest17 :instrumentation:module-name:test --parallel`

COMPATIBILITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ This version of the Java Agent supports Java versions 8 - 26.
7575
* Scala 2.13: 1.0.0 to latest
7676
* Scala 3: 1.0.0 to latest
7777
* Play 2.4.0-M3 to latest
78+
* Portlet 2.0.0 to latest
7879
* Quartz Scheduler 1.7.2 to latest
7980
* RESTEasy 2.2-RC-1 to latest
8081
* Servlet 2.3 to latest

agent-bridge-datastore/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
implementation(project(":agent-bridge"))
4141

4242
testImplementation('org.awaitility:awaitility:4.2.0')
43+
testImplementation("org.mockito:mockito-inline:4.11.0")
4344
testImplementation(fileTree(dir: 'src/test/resources/com/newrelic/agent/bridge', include: '*.jar'))
4445
}
4546

agent-bridge-datastore/src/main/java/com/newrelic/agent/bridge/datastore/JdbcHelper.java

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package com.newrelic.agent.bridge.datastore;
99

1010
import com.newrelic.agent.bridge.AgentBridge;
11-
import com.newrelic.agent.bridge.NoOpTransaction;
1211
import com.newrelic.api.agent.Logger;
1312
import com.newrelic.api.agent.NewRelic;
1413

@@ -41,10 +40,25 @@ protected Boolean initialValue() {
4140
}
4241
};
4342

43+
//statement cache settings
44+
/**
45+
* In extreme high-throughput scenarios, weak keyed eviction may throttle the CPU when it tries to perform maintenance on statement caches.
46+
* Setting -Dnewrelic.config.jdbc_statement_weak_key_caching.enabled=false will convert the caches to ordinary ConcurrentHashMaps
47+
* to alleviate this maintenance overhead.
48+
* <p>
49+
* Statements are only removed from the caches on Statement.close(), so this config MUST be enabled with caution. If the user does not properly close their
50+
* statements, setting -Dnewrelic.config.jdbc_statement_weak_key_caching.enabled=false will cause memory issues. The user MUST explicitly call
51+
* Statement.close() (not the implicit Statement.closeOnCompletion()) for cleanup to be guaranteed.
52+
*/
53+
private static final String JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED = "jdbc_statement_weak_key_caching.enabled";
54+
private static final boolean JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED_DEFAULT = Boolean.TRUE;
55+
private static final int INITIAL_STATEMENT_CACHE_CAPACITY = 1024;
56+
private static final Map<Statement, Object[]> statementToParams = initStatementCache("statementToParams");
57+
private static final Map<Statement, String> statementToSql = initStatementCache("statementToSql");
58+
4459
// This will contain every vendor type that we detected on the client system
4560
private static final Map<String, DatabaseVendor> typeToVendorLookup = new ConcurrentHashMap<>(10);
4661
private static final Map<Class<?>, DatabaseVendor> classToVendorLookup = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
47-
private static final Map<Statement, String> statementToSql = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
4862
private static final Map<Connection, String> connectionToIdentifier = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
4963
private static final Map<Connection, String> connectionToURL = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
5064
public static final String UNKNOWN = "unknown";
@@ -58,6 +72,31 @@ protected Boolean initialValue() {
5872
private static volatile Boolean isSqlMetadataCommentsEnabled = null;
5973
private static volatile String cachedServiceGuid = null;
6074

75+
public static Object[] getParams(Statement statement) {
76+
return statementToParams.get(statement);
77+
}
78+
79+
public static void putParams(Statement statement, Object[] params) {
80+
statementToParams.put(statement, params);
81+
}
82+
83+
public static String getSql(Statement statement) {
84+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting sql for statement: {0}", statement);
85+
return statementToSql.get(statement);
86+
}
87+
88+
public static void putSql(Statement statement, String sql) {
89+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing sql for statement: {0}", statement);
90+
statementToSql.put(statement, sql);
91+
}
92+
93+
public static void removeStatement(Statement statement) {
94+
if (statement != null) {
95+
statementToParams.remove(statement);
96+
statementToSql.remove(statement);
97+
}
98+
}
99+
61100
public static void putVendor(Class<?> driverOrDatastoreClass, DatabaseVendor databaseVendor) {
62101
classToVendorLookup.put(driverOrDatastoreClass, databaseVendor);
63102
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing class: {0}, vendor: {1}", driverOrDatastoreClass, databaseVendor);
@@ -68,7 +107,7 @@ public static void putVendor(Class<?> driverOrDatastoreClass, DatabaseVendor dat
68107

69108
public static DatabaseVendor getVendor(Class<?> driverOrDatastoreClass, String url) {
70109
DatabaseVendor vendor = classToVendorLookup.get(driverOrDatastoreClass);
71-
AgentBridge.getAgent().getLogger().log(Level.FINEST,"Getting class: {0}, url: {1}, vendor: {2}", driverOrDatastoreClass, url, vendor);
110+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting class: {0}, url: {1}, vendor: {2}", driverOrDatastoreClass, url, vendor);
72111

73112
if (vendor != null) {
74113
return vendor;
@@ -150,16 +189,6 @@ public static String getCachedDatabaseName(Connection connection) {
150189
return null;
151190
}
152191

153-
public static void putSql(Statement statement, String sql) {
154-
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing sql for statement: {0}", statement);
155-
statementToSql.put(statement, sql);
156-
}
157-
158-
public static String getSql(Statement statement) {
159-
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting sql for statement: {0}", statement);
160-
return statementToSql.get(statement);
161-
}
162-
163192
public static Object[] growParameterArray(Object[] params, int missingIndex) {
164193
int length = Math.max(10, (int) (missingIndex * 1.2));
165194
Object[] newParams = new Object[length];
@@ -224,7 +253,7 @@ public static String getConnectionURL(Connection connection) {
224253
*
225254
* @param connectionString URL connection string to parse.
226255
* @return identifier parsed from connection string if vendor is part of supported in-memory JDBC drivers,
227-
* {@link JdbcHelper#UNKNOWN} otherwise.
256+
* {@link JdbcHelper#UNKNOWN} otherwise.
228257
*/
229258
public static String parseInMemoryIdentifier(String connectionString) {
230259
if (connectionString == null) {
@@ -257,8 +286,6 @@ public static String parseInMemoryIdentifier(String connectionString) {
257286
return UNKNOWN;
258287
}
259288

260-
261-
262289
/**
263290
* Parse and cache identifier of in-memory database from Connection string url.
264291
*
@@ -325,7 +352,6 @@ public static void invalidateMetadataCommentConfig() {
325352
* the original SQL statement.
326353
*
327354
* @param sql the target SQL statement
328-
*
329355
* @return a SQL statement which might have the metadata comment prepended to it
330356
*/
331357
public static String addSqlMetadataCommentIfNeeded(String sql) {
@@ -412,6 +438,29 @@ private static Boolean isSqlMetadataCommentsEnabled() {
412438
return (isSqlMetadataCommentsEnabled != null) ? isSqlMetadataCommentsEnabled : Boolean.FALSE;
413439
}
414440

441+
/**
442+
* Checks the config to see whether weak key caching is enabled.
443+
* <p>
444+
* If weak key caching is enabled (default) a Caffeine-backed Weak Keyed cache will be returned.
445+
* If weak key caching is not enabled a vanilla Concurrent Hash Map will be returned.
446+
*/
447+
static <V> Map<Statement, V> initStatementCache(String cacheName) {
448+
boolean weakKeyCachingEnabled = NewRelic.getAgent()
449+
.getConfig()
450+
.getValue(JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED, JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED_DEFAULT);
451+
if (weakKeyCachingEnabled) {
452+
NewRelic.getAgent().getLogger().log(Level.INFO, "JDBC Statement weak key caching is enabled. Using default Weak Keyed Cache for {0}.", cacheName);
453+
return AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
454+
} else {
455+
NewRelic.getAgent()
456+
.getLogger()
457+
.log(Level.INFO,
458+
"JDBC Statement weak key caching is disabled. Using a ConcurrentHashMap for {0}. All JDBC Statements MUST be closed when this setting is enabled.",
459+
cacheName);
460+
return AgentBridge.collectionFactory.createVanillaJavaConcurrentHashMap(INITIAL_STATEMENT_CACHE_CAPACITY);
461+
}
462+
}
463+
415464
/**
416465
* Retrieves the cached entity GUID value and initialize the value on first access.
417466
* If the entity GUID is not yet set (empty string), this method will retry on subsequent

agent-bridge-datastore/src/test/java/com/newrelic/agent/bridge/datastore/JdbcHelperTest.java

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@
99

1010
import com.newrelic.agent.bridge.Agent;
1111
import com.newrelic.agent.bridge.AgentBridge;
12-
import org.junit.After;
1312
import org.junit.Assert;
14-
import org.junit.Before;
1513
import org.junit.Test;
14+
import com.newrelic.api.agent.NewRelic;
15+
import org.mockito.Answers;
16+
import org.mockito.MockedStatic;
1617
import org.mockito.Mockito;
1718

1819
import java.sql.Connection;
1920
import java.sql.DatabaseMetaData;
2021
import java.sql.Driver;
2122
import java.sql.SQLException;
23+
import java.sql.Statement;
24+
import java.util.Map;
2225

2326
import static org.junit.Assert.assertEquals;
2427
import static org.junit.Assert.assertFalse;
2528
import static org.junit.Assert.assertNull;
2629
import static org.junit.Assert.assertTrue;
30+
import static org.mockito.ArgumentMatchers.any;
31+
import static org.mockito.ArgumentMatchers.eq;
32+
import static org.mockito.Mockito.mockStatic;
2733

2834
public class JdbcHelperTest {
2935

@@ -313,4 +319,68 @@ public void testEntityGuidCaching_handlesNull() {
313319
JdbcHelper.resetEntityGuidCache();
314320
}
315321
}
322+
323+
@Test
324+
public void testInitStatementCachesReturnsDefaultCacheType() {
325+
Class<?> expectedDefaultCacheType = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap().getClass();
326+
Class<?> actualDefaultCacheType = JdbcHelper.initStatementCache("test_cache").getClass();
327+
assertEquals("Default statement cache type should be a weak keyed cache", expectedDefaultCacheType, actualDefaultCacheType);
328+
}
329+
330+
@Test
331+
public void testInitStatementCacheWeakKeysDisabledReturnsVanillaCHM() {
332+
try (MockedStatic<NewRelic> nRMockedStatic = mockStatic(NewRelic.class, Answers.RETURNS_DEEP_STUBS)) {
333+
addDefaultConfigValToAgentMock(nRMockedStatic);
334+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
335+
eq("jdbc_statement_weak_key_caching.enabled"), any(Boolean.class)))
336+
.thenReturn(false);
337+
338+
Map<Statement, String> cache = JdbcHelper.initStatementCache("test_cache_disabled");
339+
340+
Class<?> expectedCacheType = AgentBridge.collectionFactory.createVanillaJavaConcurrentHashMap(1024).getClass();
341+
assertEquals("When weak key caching is disabled, should return a ConcurrentHashMap",
342+
expectedCacheType, cache.getClass());
343+
}
344+
}
345+
346+
@Test
347+
public void testInitStatementCacheWeakKeysEnabledReturnsDefaultCacheType() {
348+
try (MockedStatic<NewRelic> nRMockedStatic = mockStatic(NewRelic.class, Answers.RETURNS_DEEP_STUBS)) {
349+
addDefaultConfigValToAgentMock(nRMockedStatic);
350+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
351+
eq("jdbc_statement_weak_key_caching.enabled"), any(Boolean.class)))
352+
.thenReturn(true);
353+
354+
Map<Statement, String> cache = JdbcHelper.initStatementCache("test_cache_enabled");
355+
356+
Class<?> expectedCacheType = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap().getClass();
357+
assertEquals("When weak key caching is enabled, should return a weak keyed cache",
358+
expectedCacheType, cache.getClass());
359+
}
360+
}
361+
362+
@Test
363+
public void testRemoveStatementClearsBothCaches() throws SQLException {
364+
Statement st = Mockito.mock(Statement.class);
365+
String fakeSql = "SELECT * FROM foo";
366+
Object[] fakeParams = {"bar", 6};
367+
368+
//Add the st to both caches and make sure it's in there.
369+
JdbcHelper.putSql(st, fakeSql);
370+
JdbcHelper.putParams(st, fakeParams);
371+
assertEquals(fakeSql, JdbcHelper.getSql(st));
372+
assertEquals(fakeParams, JdbcHelper.getParams(st));
373+
374+
//Remove should remove from both caches.
375+
JdbcHelper.removeStatement(st);
376+
assertNull(JdbcHelper.getSql(st));
377+
assertNull(JdbcHelper.getParams(st));
378+
}
379+
380+
//This config setting is read by JdbcHelper when it is initialized, so it needs to be added to the static mock.
381+
private void addDefaultConfigValToAgentMock(MockedStatic<NewRelic> nRMockedStatic){
382+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
383+
eq("jdbc_helper_cache_expire_time"), any()))
384+
.thenReturn(7200);
385+
}
316386
}

buildSrc/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ dependencies {
1717
implementation("org.ow2.asm:asm-commons:9.9.1")
1818

1919
// Shadow is used here because several classes implement the Transformer interface
20-
implementation("com.github.jengelman.gradle.plugins:shadow:6.0.0")
20+
implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.0")
21+
implementation("org.apache.ant:ant:1.10.14")
2122

2223
// Reflections and GSON are used for building the manifest of annotated classes.
2324
implementation("org.reflections:reflections:0.9.11")

buildSrc/src/main/java/com/nr/builder/DependencyPatcher.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import org.objectweb.asm.ClassVisitor;
1919
import org.objectweb.asm.ClassWriter;
2020
import org.slf4j.Logger;
21-
import shadow.org.apache.tools.zip.ZipEntry;
22-
import shadow.org.apache.tools.zip.ZipOutputStream;
21+
import org.apache.tools.zip.ZipEntry;
22+
import org.apache.tools.zip.ZipOutputStream;
2323

2424
import java.io.ByteArrayInputStream;
2525
import java.io.File;
@@ -53,6 +53,11 @@ public class DependencyPatcher implements Transformer {
5353

5454
private final List<TransformedFile> transformedFiles = new LinkedList<>();
5555

56+
@Override
57+
public String getName() {
58+
return "DependencyPatcher";
59+
}
60+
5661
/**
5762
* This method should return true for any file we might transform. Note that
5863
* even if we <i>don't</i> decide to transform it after all, we <i>still</i>

buildSrc/src/main/java/com/nr/builder/Log4j2PluginFileMover.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import org.gradle.api.file.FileTreeElement;
1313
import org.gradle.api.logging.Logging;
1414
import org.slf4j.Logger;
15-
import shadow.org.apache.tools.zip.ZipEntry;
16-
import shadow.org.apache.tools.zip.ZipOutputStream;
15+
import org.apache.tools.zip.ZipEntry;
16+
import org.apache.tools.zip.ZipOutputStream;
1717

1818
import java.io.File;
1919
import java.io.FileInputStream;
@@ -35,6 +35,11 @@ public class Log4j2PluginFileMover implements Transformer {
3535
private long lastModified;
3636
private static final Logger logger = Logging.getLogger(Log4j2PluginFileMover.class);
3737

38+
@Override
39+
public String getName() {
40+
return "Log4j2PluginFileMover";
41+
}
42+
3843
@Override
3944
public boolean canTransformResource(FileTreeElement element) {
4045
if (element.getPath().equals(LOG4J2PLUGINS_ORIGINAL_LOCATION)) {

0 commit comments

Comments
 (0)