Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud API #2081

Merged
merged 4 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down
31 changes: 31 additions & 0 deletions agent-bridge/src/main/java/com/newrelic/agent/bridge/CloudApi.java
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +74,11 @@ public AiMonitoring getAiMonitoring() {
return NoOpAiMonitoring.INSTANCE;
}

@Override
public Cloud getCloud() {
return NoOpCloud.INSTANCE;
}

@Override
public ErrorApi getErrorApi() {
return NoOpErrorApi.INSTANCE;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,11 @@ public AiMonitoring getAiMonitoring() {
return null;
}

@Override
public Cloud getCloud() {
return null;
}

@Override
public ErrorApi getErrorApi() { throw new RuntimeException(); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
*
* * 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;
import java.util.logging.Level;

/**
* This class implements the account info methods from the Cloud API.
*/
public class CloudAccountInfoCache {
private final LoadingCache<Object, Map<CloudAccountInfo, String>> 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) {
if (sdkClient == null) {
return;
}
if (value == null) {
Map<CloudAccountInfo, String> accountInfo = cache.getIfPresent(sdkClient);
if (accountInfo != null) {
accountInfo.remove(cloudAccountInfo);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a short-circuit return here or is it ok to have a cloudAccountInfo with a null value?

Copy link
Contributor Author

@meiao meiao Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it should short circuit.

It is also ok to have null values. But short circuiting prevents some overhead.

if (CloudAccountInfoValidator.validate(cloudAccountInfo, value)) {
Map<CloudAccountInfo, String> accountInfo = cache.get(sdkClient);
accountInfo.put(cloudAccountInfo, value);
}
}

public String getAccountInfo(CloudAccountInfo cloudAccountInfo) {
Map<CloudAccountInfo, String> 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<CloudAccountInfo, String> accountInfo = cache.getIfPresent(sdkClient);
if (accountInfo == null) {
return getAccountInfo(cloudAccountInfo);
}
return accountInfo.get(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) {
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());
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,6 +71,7 @@ protected void doStart() {
private void initializeBridgeApis() {
NewRelicApiImplementation.initialize();
PrivateApiImpl.initialize(Agent.LOG);
CloudApiImpl.initialize();
}

/**
Expand Down
Loading
Loading