From 2bb79e0b2ecafc04e8c24709736b1a617150db44 Mon Sep 17 00:00:00 2001 From: Andre Onuki Date: Thu, 19 Sep 2024 15:28:28 -0400 Subject: [PATCH 1/4] Cloud API --- .../newrelic/agent/bridge/AgentBridge.java | 2 + .../com/newrelic/agent/bridge/CloudApi.java | 31 +++++++ .../com/newrelic/agent/bridge/NoOpAgent.java | 6 ++ .../com/newrelic/agent/bridge/NoOpCloud.java | 37 ++++++++ .../agent/extension/FakeExtensionAgent.java | 6 ++ .../java/com/newrelic/agent/AgentImpl.java | 6 ++ .../java/com/newrelic/agent/MetricNames.java | 5 + .../agent/cloud/CloudAccountInfoCache.java | 74 +++++++++++++++ .../newrelic/agent/cloud/CloudApiImpl.java | 61 ++++++++++++ .../newrelic/agent/core/CoreServiceImpl.java | 2 + .../cloud/CloudAccountInfoCacheTest.java | 74 +++++++++++++++ .../agent/cloud/CloudApiImplTest.java | 92 +++++++++++++++++++ .../java/com/newrelic/api/agent/Agent.java | 7 ++ .../java/com/newrelic/api/agent/Cloud.java | 50 ++++++++++ .../newrelic/api/agent/CloudAccountInfo.java | 16 ++++ .../com/newrelic/api/agent/NoOpAgent.java | 13 +++ .../opentelemetry/OpenTelemetryAgent.java | 6 ++ 17 files changed, 488 insertions(+) create mode 100644 agent-bridge/src/main/java/com/newrelic/agent/bridge/CloudApi.java create mode 100644 agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpCloud.java create mode 100644 newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java create mode 100644 newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudApiImpl.java create mode 100644 newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java create mode 100644 newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java create mode 100644 newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java create mode 100644 newrelic-api/src/main/java/com/newrelic/api/agent/CloudAccountInfo.java diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/AgentBridge.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/AgentBridge.java index a5511e2d7c..18270c8789 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/AgentBridge.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/AgentBridge.java @@ -42,6 +42,8 @@ public final class AgentBridge { public static volatile AsyncApi asyncApi = new NoOpAsyncApi(); + public static volatile CloudApi cloud = NoOpCloud.INSTANCE; + public static volatile CollectionFactory collectionFactory = new DefaultCollectionFactory(); /** diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/CloudApi.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/CloudApi.java new file mode 100644 index 0000000000..529ad91415 --- /dev/null +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/CloudApi.java @@ -0,0 +1,31 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.bridge; + +import com.newrelic.api.agent.Cloud; +import com.newrelic.api.agent.CloudAccountInfo; + +/** + * Internal Cloud API. This extends the public Cloud API and adds methods + * for retrieving the data set by the public API methods. + */ +public interface CloudApi extends Cloud { + + /** + * Return the general account information of the provided type. + * This data is either set by {@link Cloud#setAccountInfo(CloudAccountInfo, String)} + * or the agent config. + */ + String getAccountInfo(CloudAccountInfo cloudAccountInfo); + + /** + * Retrieves the account information for a cloud service SDK client. + * If no data was recorded for the SDK client, the general account information will be returned. + */ + String getAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo); +} diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java index 904f3fdcfb..2d654f1a30 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java @@ -8,6 +8,7 @@ package com.newrelic.agent.bridge; import com.newrelic.api.agent.AiMonitoring; +import com.newrelic.api.agent.Cloud; import com.newrelic.api.agent.Config; import com.newrelic.api.agent.ErrorApi; import com.newrelic.api.agent.Insights; @@ -73,6 +74,11 @@ public AiMonitoring getAiMonitoring() { return NoOpAiMonitoring.INSTANCE; } + @Override + public Cloud getCloud() { + return NoOpCloud.INSTANCE; + } + @Override public ErrorApi getErrorApi() { return NoOpErrorApi.INSTANCE; diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpCloud.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpCloud.java new file mode 100644 index 0000000000..1b3ab7889a --- /dev/null +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpCloud.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.bridge; + +import com.newrelic.api.agent.CloudAccountInfo; + +public class NoOpCloud implements CloudApi { + + public static final CloudApi INSTANCE = new NoOpCloud(); + + private NoOpCloud() { + // only instance should be the INSTANCE + } + + @Override + public void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value) { + } + + @Override + public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value) { + } + + @Override + public String getAccountInfo(CloudAccountInfo cloudAccountInfo) { + return null; + } + + @Override + public String getAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo) { + return null; + } +} diff --git a/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java b/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java index e6bb7559d2..3201fe67d1 100644 --- a/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java +++ b/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java @@ -11,6 +11,7 @@ import com.newrelic.agent.bridge.TracedMethod; import com.newrelic.agent.bridge.Transaction; import com.newrelic.api.agent.AiMonitoring; +import com.newrelic.api.agent.Cloud; import com.newrelic.api.agent.Config; import com.newrelic.api.agent.ErrorApi; import com.newrelic.api.agent.Insights; @@ -44,6 +45,11 @@ public AiMonitoring getAiMonitoring() { return null; } + @Override + public Cloud getCloud() { + return null; + } + @Override public ErrorApi getErrorApi() { throw new RuntimeException(); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java index 2acd5efd7a..aa20d24c35 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java @@ -17,6 +17,7 @@ import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.tracers.Tracer; import com.newrelic.api.agent.AiMonitoring; +import com.newrelic.api.agent.Cloud; import com.newrelic.api.agent.ErrorApi; import com.newrelic.api.agent.Insights; import com.newrelic.api.agent.Logger; @@ -144,6 +145,11 @@ public AiMonitoring getAiMonitoring() { return new AiMonitoringImpl(); } + @Override + public Cloud getCloud() { + return AgentBridge.cloud; + } + @Override public Logs getLogSender() { return ServiceFactory.getServiceManager().getLogSenderService(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java index 0a426c81b7..3666a607cd 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java @@ -361,6 +361,11 @@ public class MetricNames { public static final String SUPPORTABILITY_API_SET_ACCOUNT_NAME = "SetAccountName"; public static final String SUPPORTABILITY_API_SET_USER_ID = "SetUserId"; + // Cloud API + public static final String SUPPORTABILITY_API_CLOUD_SET_ACCOUNT_INFO_CLIENT = "Cloud/SetAccountInfoClient/"; + public static final String SUPPORTABILITY_API_CLOUD_SET_ACCOUNT_INFO = "Cloud/SetAccountInfo/"; + public static final String SUPPORTABILITY_CONFIG_AWS_ACCOUNT_ID = "Supportability/Cloud/ConfigAccountInfo/aws_account_id"; + //Transaction supportability metrics public static final String SUPPORTABILITY_TRANSACTION_STARTED = "Supportability/Transaction/StartedCount"; public static final String SUPPORTABILITY_TRANSACTION_FINISHED = "Supportability/Transaction/FinishedCount"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java new file mode 100644 index 0000000000..4cf9f5c0be --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java @@ -0,0 +1,74 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.newrelic.agent.MetricNames; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.api.agent.CloudAccountInfo; +import com.newrelic.api.agent.NewRelic; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +/** + * This class implements the account info methods from the Cloud API. + */ +public class CloudAccountInfoCache { + private final LoadingCache> cache; + // this object is used to store data that is not related to a specific sdk client + private static final Object NULL_CLIENT = new Object(); + + CloudAccountInfoCache() { + cache = Caffeine.newBuilder() + .initialCapacity(4) + .weakKeys() + .executor(Runnable::run) + .build((key) -> Collections.synchronizedMap(new EnumMap<>(CloudAccountInfo.class))); + } + + public void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value) { + setAccountInfo(NULL_CLIENT, cloudAccountInfo, value); + } + + public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value) { + Map accountInfo = cache.get(sdkClient); + accountInfo.put(cloudAccountInfo, value); + } + + public String getAccountInfo(CloudAccountInfo cloudAccountInfo) { + Map accountInfo = cache.getIfPresent(NULL_CLIENT); + if (accountInfo == null) { + return null; + } + return accountInfo.get(cloudAccountInfo); + } + + public String getAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo) { + if (sdkClient == null) { + return getAccountInfo(cloudAccountInfo); + } + Map accountInfo = cache.getIfPresent(sdkClient); + if (accountInfo == null) { + return getAccountInfo(cloudAccountInfo); + } + return accountInfo.get(cloudAccountInfo); + } + + void retrieveDataFromConfig() { + AgentConfig agentConfig = ServiceFactory.getConfigService().getDefaultAgentConfig(); + Object awsAccountId = agentConfig.getValue("cloud.aws.account_id"); + if (awsAccountId != null) { + NewRelic.incrementCounter(MetricNames.SUPPORTABILITY_CONFIG_AWS_ACCOUNT_ID); + setAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID, awsAccountId.toString()); + } + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudApiImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudApiImpl.java new file mode 100644 index 0000000000..36c93d10c6 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudApiImpl.java @@ -0,0 +1,61 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.newrelic.agent.MetricNames; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.CloudApi; +import com.newrelic.api.agent.CloudAccountInfo; + +/** + * Facade for the Cloud API. + */ +public class CloudApiImpl implements CloudApi { + + private final CloudAccountInfoCache accountInfoCache; + + private CloudApiImpl() { + this(new CloudAccountInfoCache()); + accountInfoCache.retrieveDataFromConfig(); + } + + // for testing + CloudApiImpl(CloudAccountInfoCache accountInfoCache) { + this.accountInfoCache = accountInfoCache; + } + + // calling this method more than once will invalidate any Cloud API calls to set account info + public static void initialize() { + AgentBridge.cloud = new CloudApiImpl(); + } + + @Override + public void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value) { + MetricNames.recordApiSupportabilityMetric(MetricNames.SUPPORTABILITY_API_CLOUD_SET_ACCOUNT_INFO + cloudAccountInfo.toString()); + accountInfoCache.setAccountInfo(cloudAccountInfo, value); + } + + @Override + public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value) { + MetricNames.recordApiSupportabilityMetric(MetricNames.SUPPORTABILITY_API_CLOUD_SET_ACCOUNT_INFO_CLIENT + cloudAccountInfo.toString()); + accountInfoCache.setAccountInfo(sdkClient, cloudAccountInfo, value); + } + + @Override + public String getAccountInfo(CloudAccountInfo cloudAccountInfo) { + // not recording metrics because this is for the internal API + return accountInfoCache.getAccountInfo(cloudAccountInfo); + } + + @Override + public String getAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo) { + // not recording metrics because this is for the internal API + return accountInfoCache.getAccountInfo(sdkClient, cloudAccountInfo); + } + +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/core/CoreServiceImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/core/CoreServiceImpl.java index 481428c8fd..183f2df3b4 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/core/CoreServiceImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/core/CoreServiceImpl.java @@ -13,6 +13,7 @@ import com.newrelic.agent.MetricNames; import com.newrelic.agent.PrivateApiImpl; import com.newrelic.agent.TransactionService; +import com.newrelic.agent.cloud.CloudApiImpl; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.ConfigService; import com.newrelic.agent.logging.AgentLogManager; @@ -70,6 +71,7 @@ protected void doStart() { private void initializeBridgeApis() { NewRelicApiImplementation.initialize(); PrivateApiImpl.initialize(Agent.LOG); + CloudApiImpl.initialize(); } /** diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java new file mode 100644 index 0000000000..a7d3fb72a1 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java @@ -0,0 +1,74 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.service.ServiceManager; +import org.junit.Test; + +import static com.newrelic.api.agent.CloudAccountInfo.AWS_ACCOUNT_ID; +import static org.junit.Assert.*; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CloudAccountInfoCacheTest { + + @Test + public void accountInfo() { + CloudAccountInfoCache cache = new CloudAccountInfoCache(); + + assertNull(cache.getAccountInfo(AWS_ACCOUNT_ID)); + + String accountId = "123456789"; + cache.setAccountInfo(AWS_ACCOUNT_ID, accountId); + + assertEquals(accountId, cache.getAccountInfo(AWS_ACCOUNT_ID)); + } + + @Test + public void accountInfoClient() { + CloudAccountInfoCache cache = new CloudAccountInfoCache(); + Object sdkClient = new Object(); + + assertNull(cache.getAccountInfo(sdkClient, AWS_ACCOUNT_ID)); + + String accountId = "123456789"; + cache.setAccountInfo(sdkClient, AWS_ACCOUNT_ID, accountId); + + assertEquals(accountId, cache.getAccountInfo(sdkClient, AWS_ACCOUNT_ID)); + + Object anotherSdkClient = new Object(); + assertNull(cache.getAccountInfo(anotherSdkClient, AWS_ACCOUNT_ID)); + } + + @Test + public void accountInfoClientFallback() { + CloudAccountInfoCache cache = new CloudAccountInfoCache(); + String accountId = "123456789"; + cache.setAccountInfo(AWS_ACCOUNT_ID, accountId); + + Object sdkClient = new Object(); + assertEquals(accountId, cache.getAccountInfo(sdkClient, AWS_ACCOUNT_ID)); + } + + + @Test + public void retrieveDataFromConfigAccountInfo() { + CloudAccountInfoCache cache = new CloudAccountInfoCache(); + String accountId = "123456789"; + + ServiceManager serviceManager = mock(ServiceManager.class, RETURNS_DEEP_STUBS); + ServiceFactory.setServiceManager(serviceManager); + when(serviceManager.getConfigService().getDefaultAgentConfig().getValue("cloud.aws.account_id")) + .thenReturn(accountId); + cache.retrieveDataFromConfig(); + + assertEquals(accountId, cache.getAccountInfo(AWS_ACCOUNT_ID)); + } +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java new file mode 100644 index 0000000000..9569324c65 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java @@ -0,0 +1,92 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.newrelic.api.agent.CloudAccountInfo; +import com.newrelic.api.agent.NewRelic; +import org.junit.Test; +import org.mockito.MockedStatic; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CloudApiImplTest { + + @Test + public void setAccountInfo() { + CloudAccountInfoCache cache = mock(CloudAccountInfoCache.class); + CloudApiImpl cloudApi = new CloudApiImpl(cache); + + try (MockedStatic newRelic = mockStatic(NewRelic.class)) { + + String accountId = "1234567890"; + cloudApi.setAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID, accountId); + + newRelic.verify(() -> NewRelic.incrementCounter("Supportability/API/Cloud/SetAccountInfo/AWS_ACCOUNT_ID/API")); + newRelic.verifyNoMoreInteractions(); + + verify(cache).setAccountInfo(eq(CloudAccountInfo.AWS_ACCOUNT_ID), eq(accountId)); + verifyNoMoreInteractions(cache); + } + } + + @Test + public void setAccountInfoClient() { + CloudAccountInfoCache cache = mock(CloudAccountInfoCache.class); + CloudApiImpl cloudApi = new CloudApiImpl(cache); + + try (MockedStatic newRelic = mockStatic(NewRelic.class)) { + + String accountId = "1234567890"; + Object sdkClient = new Object(); + cloudApi.setAccountInfo(sdkClient, CloudAccountInfo.AWS_ACCOUNT_ID, accountId); + + newRelic.verify(() -> NewRelic.incrementCounter("Supportability/API/Cloud/SetAccountInfoClient/AWS_ACCOUNT_ID/API")); + newRelic.verifyNoMoreInteractions(); + + verify(cache).setAccountInfo(eq(sdkClient), eq(CloudAccountInfo.AWS_ACCOUNT_ID), eq(accountId)); + verifyNoMoreInteractions(cache); + } + } + + @Test + public void getAccountInfo() { + CloudAccountInfoCache cache = mock(CloudAccountInfoCache.class); + CloudApiImpl cloudApi = new CloudApiImpl(cache); + + try (MockedStatic newRelic = mockStatic(NewRelic.class)) { + + cloudApi.getAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID); + + newRelic.verifyNoInteractions(); + + verify(cache).getAccountInfo(eq(CloudAccountInfo.AWS_ACCOUNT_ID)); + verifyNoMoreInteractions(cache); + } + } + + @Test + public void getAccountInfoClient() { + CloudAccountInfoCache cache = mock(CloudAccountInfoCache.class); + CloudApiImpl cloudApi = new CloudApiImpl(cache); + + try (MockedStatic newRelic = mockStatic(NewRelic.class)) { + + Object sdkClient = new Object(); + cloudApi.getAccountInfo(sdkClient, CloudAccountInfo.AWS_ACCOUNT_ID); + + newRelic.verifyNoInteractions(); + + verify(cache).getAccountInfo(eq(sdkClient), eq(CloudAccountInfo.AWS_ACCOUNT_ID)); + verifyNoMoreInteractions(cache); + } + } +} \ No newline at end of file diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/Agent.java b/newrelic-api/src/main/java/com/newrelic/api/agent/Agent.java index 023c34d278..144aa5ef27 100644 --- a/newrelic-api/src/main/java/com/newrelic/api/agent/Agent.java +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/Agent.java @@ -70,6 +70,13 @@ public interface Agent { */ AiMonitoring getAiMonitoring(); + /** + * Provides access to the Cloud API. + * + * @since 8.15.0 + */ + Cloud getCloud(); + ErrorApi getErrorApi(); /** diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java b/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java new file mode 100644 index 0000000000..a4648f2d7e --- /dev/null +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java @@ -0,0 +1,50 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.api.agent; + +/** + * This interface defines methods to pass cloud services information to the agent. + */ +public interface Cloud { + + /** + *

+ * Sets the account information for a cloud service. + *

+ *

+ * This information is used by some instrumentation modules that cannot + * determine the resource id of the cloud service being invoked. + *

+ *

+ * The value provided to this method has priority over a value set in + * the agent configuration. + *

+ * @param cloudAccountInfo the type of account information being stored + * @param value the value to store + */ + void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value); + + + /** + *

+ * Sets the account information for a cloud service SDK client. + *

+ *

+ * This information is used by some instrumentation modules that cannot + * determine the resource id of the cloud service being invoked. + *

+ *

+ * The value provided to this method has priority over a value set in + * the agent configuration or a value set using {@link #setAccountInfo(CloudAccountInfo, String)}. + *

+ * @param sdkClient the SDK client object this account information is associated with + * @param cloudAccountInfo the type of account information being stored + * @param value the value to store + */ + void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value); +} diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/CloudAccountInfo.java b/newrelic-api/src/main/java/com/newrelic/api/agent/CloudAccountInfo.java new file mode 100644 index 0000000000..a142684106 --- /dev/null +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/CloudAccountInfo.java @@ -0,0 +1,16 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.api.agent; + +/** + * Identifier for the type of account information. + */ +public enum CloudAccountInfo { + AWS_ACCOUNT_ID, + ; +} diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/NoOpAgent.java b/newrelic-api/src/main/java/com/newrelic/api/agent/NoOpAgent.java index dc61aee4f2..b99efa7d5d 100644 --- a/newrelic-api/src/main/java/com/newrelic/api/agent/NoOpAgent.java +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/NoOpAgent.java @@ -368,6 +368,14 @@ public void recordLlmFeedbackEvent(Map llmFeedbackEventAttribute public void setLlmTokenCountCallback(LlmTokenCountCallback llmTokenCountCallback) {} }; + private static final Cloud CLOUD = new Cloud() { + @Override + public void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value) {} + + @Override + public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value) {} + }; + private static final Segment SEGMENT = new Segment() { @Override public void setMetricName(String... metricNameParts) { @@ -471,6 +479,11 @@ public AiMonitoring getAiMonitoring() { return AI_MONITORING; } + @Override + public Cloud getCloud() { + return CLOUD; + } + @Override public ErrorApi getErrorApi() { return ERROR_API; diff --git a/newrelic-opentelemetry-agent-extension/src/main/java/com/newrelic/opentelemetry/OpenTelemetryAgent.java b/newrelic-opentelemetry-agent-extension/src/main/java/com/newrelic/opentelemetry/OpenTelemetryAgent.java index ce1fdeb23a..d843179408 100644 --- a/newrelic-opentelemetry-agent-extension/src/main/java/com/newrelic/opentelemetry/OpenTelemetryAgent.java +++ b/newrelic-opentelemetry-agent-extension/src/main/java/com/newrelic/opentelemetry/OpenTelemetryAgent.java @@ -9,6 +9,7 @@ import com.newrelic.api.agent.Agent; import com.newrelic.api.agent.AiMonitoring; +import com.newrelic.api.agent.Cloud; import com.newrelic.api.agent.Config; import com.newrelic.api.agent.Insights; import com.newrelic.api.agent.Logger; @@ -79,6 +80,11 @@ public AiMonitoring getAiMonitoring() { return null; } + @Override + public Cloud getCloud() { + return null; + } + @Override public TraceMetadata getTraceMetadata() { OpenTelemetryNewRelic.logUnsupportedMethod("Agent", "getTraceMetadata"); From 02de31aa61795043193382b14017bdfd549c80b9 Mon Sep 17 00:00:00 2001 From: Andre Onuki Date: Wed, 9 Oct 2024 16:54:15 -0400 Subject: [PATCH 2/4] Cloud config: adding value validation --- .../agent/cloud/CloudAccountInfoCache.java | 29 ++++++++++-- .../cloud/CloudAccountInfoValidator.java | 47 +++++++++++++++++++ .../cloud/CloudAccountInfoValidatorTest.java | 32 +++++++++++++ .../agent/cloud/CloudApiImplTest.java | 4 +- 4 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoValidator.java create mode 100644 newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoValidatorTest.java diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java index 4cf9f5c0be..3b85f6f41c 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; +import java.util.logging.Level; /** * This class implements the account info methods from the Cloud API. @@ -40,8 +41,19 @@ public void setAccountInfo(CloudAccountInfo cloudAccountInfo, String value) { } public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, String value) { - Map accountInfo = cache.get(sdkClient); - accountInfo.put(cloudAccountInfo, value); + if (sdkClient == null) { + return; + } + if (value == null) { + Map accountInfo = cache.getIfPresent(sdkClient); + if (accountInfo != null) { + accountInfo.remove(cloudAccountInfo); + } + } + if (CloudAccountInfoValidator.validate(cloudAccountInfo, value)) { + Map accountInfo = cache.get(sdkClient); + accountInfo.put(cloudAccountInfo, value); + } } public String getAccountInfo(CloudAccountInfo cloudAccountInfo) { @@ -65,10 +77,17 @@ public String getAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo void retrieveDataFromConfig() { AgentConfig agentConfig = ServiceFactory.getConfigService().getDefaultAgentConfig(); + retrieveAwsAccountId(agentConfig); + } + + private void retrieveAwsAccountId(AgentConfig agentConfig) { Object awsAccountId = agentConfig.getValue("cloud.aws.account_id"); - if (awsAccountId != null) { - NewRelic.incrementCounter(MetricNames.SUPPORTABILITY_CONFIG_AWS_ACCOUNT_ID); - setAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID, awsAccountId.toString()); + if (awsAccountId == null) { + return; } + + NewRelic.getAgent().getLogger().log(Level.INFO, "Found AWS account ID configuration."); + NewRelic.incrementCounter(MetricNames.SUPPORTABILITY_CONFIG_AWS_ACCOUNT_ID); + setAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID, awsAccountId.toString()); } } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoValidator.java b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoValidator.java new file mode 100644 index 0000000000..936c55b5be --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoValidator.java @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.newrelic.api.agent.CloudAccountInfo; +import com.newrelic.api.agent.NewRelic; + +import java.util.logging.Level; +import java.util.regex.Pattern; + +public class CloudAccountInfoValidator { + + private static final Pattern AWS_ACCOUNT_ID_PATTERN = Pattern.compile("^\\d+$"); + private static Level awsAccountIdLogLevel = Level.WARNING; + + public static boolean validate(CloudAccountInfo cloudAccountInfo, String value) { + switch (cloudAccountInfo) { + case AWS_ACCOUNT_ID: + return validateAwsAccountId(value); + default: + return false; + } + } + + private static boolean validateAwsAccountId(String accountId) { + final int AWS_ACCOUNT_ID_LENGTH = 12; + if (accountId == null) { + return false; + } + boolean valid = accountId.length() == AWS_ACCOUNT_ID_LENGTH && + AWS_ACCOUNT_ID_PATTERN.matcher(accountId).matches(); + if (!valid) { + NewRelic.getAgent().getLogger().log(awsAccountIdLogLevel, "AWS account ID should be a 12-digit number."); + awsAccountIdLogLevel = Level.FINEST; + } + return valid; + } + + private CloudAccountInfoValidator() { + // prevents instantiation + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoValidatorTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoValidatorTest.java new file mode 100644 index 0000000000..c6e30c43a2 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoValidatorTest.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.cloud; + +import com.newrelic.api.agent.CloudAccountInfo; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CloudAccountInfoValidatorTest { + + @Test + public void testValidateAwsAccountId() { + assertFalse(CloudAccountInfoValidator.validate(CloudAccountInfo.AWS_ACCOUNT_ID, null)); + + // accountId is not 12 digits + assertFalse(CloudAccountInfoValidator.validate(CloudAccountInfo.AWS_ACCOUNT_ID, "12345678901")); + + // accountId is not a number + assertFalse(CloudAccountInfoValidator.validate(CloudAccountInfo.AWS_ACCOUNT_ID, "12345678901a")); + + // happy path + assertTrue(CloudAccountInfoValidator.validate(CloudAccountInfo.AWS_ACCOUNT_ID, "123456789012")); + } + +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java index 9569324c65..89d974059c 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudApiImplTest.java @@ -27,7 +27,7 @@ public void setAccountInfo() { try (MockedStatic newRelic = mockStatic(NewRelic.class)) { - String accountId = "1234567890"; + String accountId = "123456789012"; cloudApi.setAccountInfo(CloudAccountInfo.AWS_ACCOUNT_ID, accountId); newRelic.verify(() -> NewRelic.incrementCounter("Supportability/API/Cloud/SetAccountInfo/AWS_ACCOUNT_ID/API")); @@ -45,7 +45,7 @@ public void setAccountInfoClient() { try (MockedStatic newRelic = mockStatic(NewRelic.class)) { - String accountId = "1234567890"; + String accountId = "123456789012"; Object sdkClient = new Object(); cloudApi.setAccountInfo(sdkClient, CloudAccountInfo.AWS_ACCOUNT_ID, accountId); From 276dd236791cd1499cab37017c272309df819540 Mon Sep 17 00:00:00 2001 From: Andre Onuki Date: Wed, 9 Oct 2024 21:34:29 -0400 Subject: [PATCH 3/4] Fixing tests --- .../newrelic/agent/cloud/CloudAccountInfoCacheTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java index a7d3fb72a1..37a3bec94b 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/cloud/CloudAccountInfoCacheTest.java @@ -25,7 +25,7 @@ public void accountInfo() { assertNull(cache.getAccountInfo(AWS_ACCOUNT_ID)); - String accountId = "123456789"; + String accountId = "123456789012"; cache.setAccountInfo(AWS_ACCOUNT_ID, accountId); assertEquals(accountId, cache.getAccountInfo(AWS_ACCOUNT_ID)); @@ -38,7 +38,7 @@ public void accountInfoClient() { assertNull(cache.getAccountInfo(sdkClient, AWS_ACCOUNT_ID)); - String accountId = "123456789"; + String accountId = "123456789012"; cache.setAccountInfo(sdkClient, AWS_ACCOUNT_ID, accountId); assertEquals(accountId, cache.getAccountInfo(sdkClient, AWS_ACCOUNT_ID)); @@ -50,7 +50,7 @@ public void accountInfoClient() { @Test public void accountInfoClientFallback() { CloudAccountInfoCache cache = new CloudAccountInfoCache(); - String accountId = "123456789"; + String accountId = "123456789012"; cache.setAccountInfo(AWS_ACCOUNT_ID, accountId); Object sdkClient = new Object(); @@ -61,7 +61,7 @@ public void accountInfoClientFallback() { @Test public void retrieveDataFromConfigAccountInfo() { CloudAccountInfoCache cache = new CloudAccountInfoCache(); - String accountId = "123456789"; + String accountId = "123456789012"; ServiceManager serviceManager = mock(ServiceManager.class, RETURNS_DEEP_STUBS); ServiceFactory.setServiceManager(serviceManager); From 989a4ba14bea23080200e0cfdb1f215aa958ce1c Mon Sep 17 00:00:00 2001 From: Andre Onuki Date: Thu, 10 Oct 2024 15:34:47 -0400 Subject: [PATCH 4/4] Cloud config: short circuiting when removing account info Update documentation to inform the behavior --- .../com/newrelic/agent/cloud/CloudAccountInfoCache.java | 1 + .../src/main/java/com/newrelic/api/agent/Cloud.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java index 3b85f6f41c..7280433152 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java @@ -49,6 +49,7 @@ public void setAccountInfo(Object sdkClient, CloudAccountInfo cloudAccountInfo, if (accountInfo != null) { accountInfo.remove(cloudAccountInfo); } + return; } if (CloudAccountInfoValidator.validate(cloudAccountInfo, value)) { Map accountInfo = cache.get(sdkClient); diff --git a/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java b/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java index a4648f2d7e..1454dd42ca 100644 --- a/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java @@ -24,6 +24,9 @@ public interface Cloud { * The value provided to this method has priority over a value set in * the agent configuration. *

+ *

+ * Passing null as the value will remove the account information previously stored. + *

* @param cloudAccountInfo the type of account information being stored * @param value the value to store */ @@ -42,6 +45,9 @@ public interface Cloud { * The value provided to this method has priority over a value set in * the agent configuration or a value set using {@link #setAccountInfo(CloudAccountInfo, String)}. *

+ *

+ * Passing null as the value will remove the account information previously stored. + *

* @param sdkClient the SDK client object this account information is associated with * @param cloudAccountInfo the type of account information being stored * @param value the value to store