diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 0000000000..8594e55c58 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,50 @@ +# Workflow: Dependency Graph Submission and Vulnerability Reporting +# +# Trigger: This workflow runs on every merge to the main branch. +# +# Purpose: It generates and submits a dependency graph to the GitHub Dependency Submission API. The graph is used to +# trigger Dependabot Alerts for vulnerable dependencies, and to populate the Dependency Graph insights view in GitHub. +# +# Excludes: +# - Test-only dependencies + +name: Dependency Submission + +on: + push: + branches: [ 'main' ] + +permissions: + contents: write + +jobs: + dependency-submission: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4 + with: + ref: 'main' + + - name: Set gradle.properties Workaround + shell: bash + run: | + echo "jdk8=/tmp" >> gradle.properties + echo "jdk11=/tmp" >> gradle.properties + echo "jdk17=/tmp" >> gradle.properties + echo "jdk21=/tmp" >> gradle.properties + + - name: Setup Java + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # pin@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@d156388eb19639ec20ade50009f3d199ce1e2808 # pin@v4 + with: + dependency-graph-exclude-configurations: '.*[Tt]est(Compile|Runtime)Classpath' + dependency-graph-include-configurations: '.*(includeInJar|shadowIntoJar).*' + build-scan-publish: true + build-scan-terms-of-use-url: "https://gralde.com/help/legal-terms-of-use" + build-scan-terms-of-use-agree: "yes" diff --git a/.github/workflows/snyk_scan.yml b/.github/workflows/snyk_scan.yml deleted file mode 100644 index 1a8a096534..0000000000 --- a/.github/workflows/snyk_scan.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow automates the process of identifying potential security vulnerabilities -# in the agent's dependencies using Snyk. Vulnerability scans will be run on a -# weekly schedule, but can also be triggered manually. -name: Snyk Vulnerability Scan - -on: - workflow_dispatch: - schedule: - - cron: '00 15 * * 1' - -jobs: - security: - runs-on: ubuntu-latest - steps: - - name: Checkout Java Agent - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # pin@v4 - with: - ref: 'main' - - - name: Set gradle.properties Workaround - shell: bash - run: | - echo "jdk8=/tmp" >> gradle.properties - echo "jdk11=/tmp" >> gradle.properties - echo "jdk17=/tmp" >> gradle.properties - - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/gradle-jdk11@8349f9043a8b7f0f3ee8885bf28f0b388d2446e8 # pin@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - command: monitor - args: --all-sub-projects --org=java-agent --configuration-matching='(includeInJar)|(shadowIntoJar)' 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/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorCoreService.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorCoreService.java index 09ca42ccff..87c8ece7fc 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorCoreService.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorCoreService.java @@ -10,8 +10,10 @@ import com.newrelic.agent.InstrumentationProxy; import com.newrelic.agent.core.CoreService; import com.newrelic.agent.service.AbstractService; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; -class IntrospectorCoreService extends AbstractService implements CoreService { +class IntrospectorCoreService extends AbstractService implements CoreService, HealthDataProducer { private InstrumentationProxy instrumentation = null; public IntrospectorCoreService() { @@ -48,4 +50,7 @@ public boolean isEnabled() { return true; } + @Override + public void registerHealthDataChangeListener(HealthDataChangeListener listener) { + } } diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java index b0cc1659ac..7a6d7e74ec 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java @@ -20,6 +20,7 @@ import com.newrelic.agent.service.module.JarData; import com.newrelic.agent.sql.SqlTrace; import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.agent.trace.TransactionTrace; import com.newrelic.agent.transaction.TransactionNamingScheme; @@ -162,6 +163,11 @@ public void sendErrorEvents(int reservoirSize, int eventsSeen, Collection tracedErrors) { } + @Override + public HealthDataProducer getHttpDataSenderAsHealthDataProducer() { + return null; + } + @Override public String getEntityGuid() { return ""; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/Agent.java b/newrelic-agent/src/main/java/com/newrelic/agent/Agent.java index b31128bdd8..52bcf72233 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/Agent.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/Agent.java @@ -25,6 +25,8 @@ import com.newrelic.agent.service.ServiceManagerImpl; import com.newrelic.agent.stats.StatsService; import com.newrelic.agent.stats.StatsWorks; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.SuperAgentIntegrationUtils; import com.newrelic.agent.util.UnwindableInstrumentation; import com.newrelic.agent.util.UnwindableInstrumentationImpl; import com.newrelic.agent.util.asm.ClassStructure; @@ -290,12 +292,15 @@ private static boolean tryToInitializeServiceManager(Instrumentation inst) { ServiceManager serviceManager = new ServiceManagerImpl(coreService, configService); ServiceFactory.setServiceManager(serviceManager); - if (isLicenseKeyEmpty(serviceManager.getConfigService().getDefaultAgentConfig().getLicenseKey())) { + AgentConfig agentConfig = serviceManager.getConfigService().getDefaultAgentConfig(); + if (isLicenseKeyEmpty(agentConfig.getLicenseKey())) { + SuperAgentIntegrationUtils.reportUnhealthyStatusPriorToServiceStart(agentConfig, AgentHealth.Status.MISSING_LICENSE); LOG.error("license_key is empty in the config. Not starting New Relic Agent."); return false; } if (!serviceManager.getConfigService().getDefaultAgentConfig().isAgentEnabled()) { + SuperAgentIntegrationUtils.reportUnhealthyStatusPriorToServiceStart(agentConfig, AgentHealth.Status.AGENT_DISABLED); LOG.warning("agent_enabled is false in the config. Not starting New Relic Agent."); return false; } 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/IRPMService.java b/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java index aecb050f72..27fad1ae16 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java @@ -19,6 +19,7 @@ import com.newrelic.agent.service.module.JarData; import com.newrelic.agent.sql.SqlTrace; import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.agent.trace.TransactionTrace; import com.newrelic.agent.transaction.TransactionNamingScheme; @@ -90,4 +91,6 @@ public interface IRPMService extends Service { void sendSpanEvents(int reservoirSize, int eventsSeen, final Collection events) throws Exception; void sendErrorData(List tracedErrors) throws Exception; + + HealthDataProducer getHttpDataSenderAsHealthDataProducer(); } 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..554cfa825a 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java @@ -205,6 +205,8 @@ public class MetricNames { public static final String TIMEOUT_ASYNC = "Java/Timeout/asyncActivityNotStarted"; + public static final String SUPPORTABILITY_AZURE_SITE_EXT_INSTALL_TYPE = "Supportability/Java/InstallType"; + public static final String SUPPORTABILITY_JAVA_AGENTVERSION = "Supportability/Java/AgentVersion/{0}"; public static final String SUPPORTABILITY_HARVEST_SERVICE_RESPONSE_TIME = "Supportability/Harvest"; @@ -361,6 +363,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"; @@ -501,6 +508,9 @@ public class MetricNames { // AiMonitoring Callback Set public static final String SUPPORTABILITY_AI_MONITORING_TOKEN_COUNT_CALLBACK_SET = "Supportability/AiMonitoringTokenCountCallback/Set"; + // Super Agent Integration + public static final String SUPPORTABILITY_SUPERAGENT_HEALTH_REPORTING_ENABLED = "Supportability/SuperAgent/Health/enabled"; + /** * Utility method for adding supportability metrics to APIs * diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java b/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java index 9b9fe70d50..7b8a1a6714 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java @@ -36,6 +36,9 @@ import com.newrelic.agent.service.module.JarData; import com.newrelic.agent.sql.SqlTrace; import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.agent.trace.TransactionTrace; import com.newrelic.agent.transaction.TransactionNamingScheme; import com.newrelic.agent.transport.ConnectionResponse; @@ -58,6 +61,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -65,7 +69,8 @@ /** * The RPMService acts as a stub for communication between the agent and New Relic. */ -public class RPMService extends AbstractService implements IRPMService, EnvironmentChangeListener, AgentConfigListener { +public class RPMService extends AbstractService implements IRPMService, EnvironmentChangeListener, + AgentConfigListener { public static final String COLLECT_TRACES_KEY = "collect_traces"; public static final String COLLECT_ERRORS_KEY = "collect_errors"; @@ -986,6 +991,11 @@ private void removeHarvestablesFromServices(String appName) { ServiceFactory.getHarvestService().removeHarvestablesByAppName(appName); } + @Override + public HealthDataProducer getHttpDataSenderAsHealthDataProducer() { + return (HealthDataProducer) dataSender; + } + @Override public long getConnectionTimestamp() { return connectionTimestamp; @@ -1004,5 +1014,4 @@ public void configChanged(String appName, AgentConfig agentConfig) { // reset our error logging so that something will show up at info level if data failures persist last503Error.set(0); } - } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/circuitbreaker/CircuitBreakerService.java b/newrelic-agent/src/main/java/com/newrelic/agent/circuitbreaker/CircuitBreakerService.java index eb9cde0746..f208265da4 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/circuitbreaker/CircuitBreakerService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/circuitbreaker/CircuitBreakerService.java @@ -17,11 +17,17 @@ import com.newrelic.agent.service.AbstractService; import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; +import com.newrelic.agent.superagent.SuperAgentIntegrationUtils; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; @@ -43,13 +49,15 @@ *
* The default memory and gc thresholds are set in {@link CircuitBreakerConfig}. */ -public class CircuitBreakerService extends AbstractService implements HarvestListener, AgentConfigListener { +public class CircuitBreakerService extends AbstractService implements HarvestListener, AgentConfigListener, HealthDataProducer { private static final int TRACER_SAMPLING_RATE = 1000; private volatile int tripped = 0; private final CircuitBreakerConfig circuitBreakerConfig; private volatile GarbageCollectorMXBean oldGenGCBeanCached = null; private final ReentrantLock lock = new ReentrantLock(); + private final List healthDataChangeListeners = new CopyOnWriteArrayList<>(); + /** * Map of application names to booleans that indicates whether the data to be reported at harvest is incomplete for @@ -178,6 +186,10 @@ private boolean shouldTrip() { if (gcCpuTimePercentage >= gcCPUThreshold && percentageFreeMemory <= freeMemoryThreshold) { Agent.LOG.log(Level.WARNING, "Circuit breaker tripped at memory {0}% GC CPU time {1}%", percentageFreeMemory, gcCpuTimePercentage); + + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.GC_CIRCUIT_BREAKER, + String.valueOf(percentageFreeMemory), String.valueOf(gcCpuTimePercentage)); + return true; } return false; @@ -220,6 +232,7 @@ private void trip() { public void reset() { tripped = 0; Agent.LOG.log(Level.FINE, "Circuit breaker reset"); + SuperAgentIntegrationUtils.reportHealthyStatus(healthDataChangeListeners, AgentHealth.Category.CIRCUIT_BREAKER); logWarning.set(true); } @@ -363,4 +376,9 @@ public void setPreviousChecksForTesting(long newGCTimeNS, long newCpuTimeNS) { public static SamplingCounter createTracerSamplerCounter() { return new SamplingCounter(TRACER_SAMPLING_RATE); } + + @Override + public void registerHealthDataChangeListener(HealthDataChangeListener listener) { + healthDataChangeListeners.add(listener); + } } 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..7280433152 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/cloud/CloudAccountInfoCache.java @@ -0,0 +1,94 @@ +/* + * + * * 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> 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 accountInfo = cache.getIfPresent(sdkClient); + if (accountInfo != null) { + accountInfo.remove(cloudAccountInfo); + } + return; + } + if (CloudAccountInfoValidator.validate(cloudAccountInfo, 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(); + 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()); + } +} 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/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/config/AgentConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java index 886153c6bf..5aa0eb55ce 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java @@ -355,4 +355,6 @@ public interface AgentConfig extends com.newrelic.api.agent.Config, DataSenderCo SlowTransactionsConfig getSlowTransactionsConfig(); + SuperAgentIntegrationConfig getSuperAgentIntegrationConfig(); + } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java index 884ed2167b..9607461b41 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java @@ -271,6 +271,7 @@ public class AgentConfigImpl extends BaseConfig implements AgentConfig { private final SpanEventsConfig spanEventsConfig; private final SqlTraceConfig sqlTraceConfig; private final StripExceptionConfig stripExceptionConfig; + private final SuperAgentIntegrationConfig superAgentIntegrationConfig; private final ThreadProfilerConfig threadProfilerConfig; private final TransactionEventsConfig transactionEventsConfig; private final TransactionTracerConfigImpl transactionTracerConfig; @@ -374,6 +375,7 @@ private AgentConfigImpl(Map props) { normalizationRuleConfig = new NormalizationRuleConfig(props); slowTransactionsConfig = initSlowTransactionsConfig(); obfuscateJvmPropsConfig = initObfuscateJvmPropsConfig(); + superAgentIntegrationConfig = initSuperAgentHealthCheckConfig(); Map flattenedProps = new HashMap<>(); flatten("", props, flattenedProps); @@ -840,6 +842,10 @@ private SlowTransactionsConfig initSlowTransactionsConfig() { return new SlowTransactionsConfigImpl(props); } + private SuperAgentIntegrationConfig initSuperAgentHealthCheckConfig() { + return new SuperAgentIntegrationConfigImpl(nestedProps(SuperAgentIntegrationConfigImpl.ROOT)); + } + @Override public long getApdexTInMillis() { return apdexTInMillis; @@ -1061,6 +1067,11 @@ public SlowTransactionsConfig getSlowTransactionsConfig() { return slowTransactionsConfig; } + @Override + public SuperAgentIntegrationConfig getSuperAgentIntegrationConfig() { + return superAgentIntegrationConfig; + } + private Object findPropertyInMap(String[] property, Map map) { Object result = map; for (String component : property) { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigServiceFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigServiceFactory.java index 400a0c1ef4..828a57d1bf 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigServiceFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigServiceFactory.java @@ -9,6 +9,8 @@ import com.google.common.annotations.VisibleForTesting; import com.newrelic.agent.ForceDisconnectException; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.SuperAgentIntegrationUtils; import com.newrelic.api.agent.Logger; import java.io.File; @@ -73,9 +75,11 @@ private static File getConfigFile(Logger log) { @VisibleForTesting public static void validateConfig(AgentConfig config) throws ConfigurationException, ForceDisconnectException { if (config.getApplicationName() == null) { + SuperAgentIntegrationUtils.reportUnhealthyStatusPriorToServiceStart(config, AgentHealth.Status.MISSING_APP_NAME); throw new ConfigurationException("The agent requires an application name. Check the app_name setting in newrelic.yml"); } if (config.getApplicationNames().size() > 3) { + SuperAgentIntegrationUtils.reportUnhealthyStatusPriorToServiceStart(config, AgentHealth.Status.MAX_APP_NAMES_EXCEEDED); throw new ConfigurationException("The agent does not support more than three application names. Check the app_name setting in newrelic.yml"); } if (config.isHighSecurity() && config.laspEnabled()) { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfig.java new file mode 100644 index 0000000000..dd2a2d8025 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfig.java @@ -0,0 +1,46 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.config; + +import java.net.URI; + +public interface SuperAgentIntegrationConfig { + /** + * Check if the Super Agent integration service is enabled + * + * @return true if the Super Agent Health Check service is enabled, else false. + */ + boolean isEnabled(); + + /** + * Get the domain socket listener address + * + * @return the domain socket address for the health check + */ + URI getHealthDeliveryLocation(); + + /** + * Return the frequency of the health messages sent to the Super Agent, in seconds + * + * @return the health check frequency, in seconds + */ + int getHealthReportingFrequency(); + + /** + * Return the fleet id assigned by the super agent + * + * @return the fleet id, if available + */ + String getFleetId(); + + /** + * Return the health client type ("file" or "noop" for example) + * + * @return the client type + */ + String getHealthClientType(); +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfigImpl.java new file mode 100644 index 0000000000..071e0a0639 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationConfigImpl.java @@ -0,0 +1,76 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.config; + +import com.newrelic.agent.Agent; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Map; +import java.util.logging.Level; + +public class SuperAgentIntegrationConfigImpl extends BaseConfig implements SuperAgentIntegrationConfig { + public static final String ROOT = "superagent"; + public static final String SYSTEM_PROPERTY_ROOT = "newrelic.config.superagent."; + public static final String FLEET_ID = "fleet_id"; + + private final String fleetId; + + private final SuperAgentIntegrationHealthConfig superAgentIntegrationHealthConfig; + + public SuperAgentIntegrationConfigImpl(Map configProps) { + super(configProps, SYSTEM_PROPERTY_ROOT); + + if (configProps == null) { + configProps = Collections.emptyMap(); + } + + superAgentIntegrationHealthConfig = createHealthConfig(); + fleetId = superAgentIntegrationHealthConfig == null ? null : getProperty(FLEET_ID); + } + + private SuperAgentIntegrationHealthConfig createHealthConfig() { + Map healthProps = getProperty(SuperAgentIntegrationHealthConfig.ROOT, Collections.emptyMap()); + SuperAgentIntegrationHealthConfig superAgentIntegrationHealthConfig; + + superAgentIntegrationHealthConfig = new SuperAgentIntegrationHealthConfig(healthProps, SYSTEM_PROPERTY_ROOT); + + if (superAgentIntegrationHealthConfig.getHealthDeliveryLocation() == null) { + Agent.LOG.log(Level.WARNING, "Configured Super Agent health delivery location is not a valid URI; " + + "SuperAgent integration service will not be started"); + superAgentIntegrationHealthConfig = null; + } + + return superAgentIntegrationHealthConfig; + } + + @Override + public boolean isEnabled() { + return fleetId != null; + } + + @Override + public URI getHealthDeliveryLocation() { + return superAgentIntegrationHealthConfig == null ? null : superAgentIntegrationHealthConfig.getHealthDeliveryLocation(); + } + + @Override + public int getHealthReportingFrequency() { + return superAgentIntegrationHealthConfig == null ? 0 : superAgentIntegrationHealthConfig.getHealthReportingFrequency(); + } + + @Override + public String getFleetId() { + return fleetId; + } + + @Override + public String getHealthClientType() { + return superAgentIntegrationHealthConfig == null ? null : superAgentIntegrationHealthConfig.getHealthClientType(); + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationHealthConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationHealthConfig.java new file mode 100644 index 0000000000..1ac2276cd4 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/SuperAgentIntegrationHealthConfig.java @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.config; + +import org.apache.commons.lang3.StringUtils; + +import java.net.URI; +import java.util.Map; + +public class SuperAgentIntegrationHealthConfig extends BaseConfig { + public static final String ROOT = "health"; + public static final String FREQUENCY = "frequency"; + public static final int FREQUENCY_DEFAULT = 5; // In seconds + public static final String LOCATION = "delivery_location"; //URI Format; ex: file://opt/tmp/health.yml + + private final int frequency; + private URI deliveryLocation; + private String healthClientType; + + public SuperAgentIntegrationHealthConfig(Map props, String systemPropertyPrefix) { + super(props, systemPropertyPrefix + ROOT + "."); + frequency = getProperty(FREQUENCY, FREQUENCY_DEFAULT); + + // Location is in URI format; the client type is then derived from the URI scheme (file, http..) + validateAndAssignLocationUri(getProperty(LOCATION)); + } + + public int getHealthReportingFrequency() { + return frequency; + } + + public URI getHealthDeliveryLocation() { + return deliveryLocation; + } + + public String getHealthClientType() { + return healthClientType; + } + + private void validateAndAssignLocationUri(String locationAsUri) { + if (StringUtils.isNotEmpty(locationAsUri)) { + try { + deliveryLocation = new URI(locationAsUri); + } catch (Exception e) { + deliveryLocation = null; + return; + } + + healthClientType = deliveryLocation.getScheme(); + + // Ensure the URI contains the scheme and path + if (StringUtils.isAnyEmpty(healthClientType, deliveryLocation.getPath())) { + deliveryLocation = null; + } + } + + } +} 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..ba858227f3 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,12 +13,17 @@ 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; import com.newrelic.agent.service.AbstractService; import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.stats.StatsService; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; +import com.newrelic.agent.superagent.SuperAgentIntegrationUtils; import com.newrelic.api.agent.NewRelicApiImplementation; import java.lang.instrument.Instrumentation; @@ -26,12 +31,15 @@ import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; -public class CoreServiceImpl extends AbstractService implements CoreService { +public class CoreServiceImpl extends AbstractService implements CoreService, HealthDataProducer { private volatile boolean enabled = true; private final Instrumentation instrumentation; private volatile InstrumentationProxy instrumentationProxy; + private final List healthDataChangeListeners = new CopyOnWriteArrayList<>(); + public CoreServiceImpl(Instrumentation instrumentation) { super(CoreService.class.getName()); @@ -70,6 +78,7 @@ protected void doStart() { private void initializeBridgeApis() { NewRelicApiImplementation.initialize(); PrivateApiImpl.initialize(Agent.LOG); + CloudApiImpl.initialize(); } /** @@ -149,5 +158,8 @@ public InstrumentationProxy getInstrumentation() { return instrumentationProxy; } - + @Override + public void registerHealthDataChangeListener(HealthDataChangeListener listener) { + healthDataChangeListeners.add(listener); + } } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/environment/Environment.java b/newrelic-agent/src/main/java/com/newrelic/agent/environment/Environment.java index 54488e051d..561d33a8ca 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/environment/Environment.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/environment/Environment.java @@ -11,9 +11,12 @@ import com.newrelic.agent.Agent; import com.newrelic.agent.attributes.ExcludeIncludeFilter; import com.newrelic.agent.attributes.ExcludeIncludeFilterImpl; +import com.newrelic.agent.MetricNames; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.ObfuscateJvmPropsConfig; import com.newrelic.agent.samplers.MemorySampler; +import com.newrelic.api.agent.NewRelic; +import org.apache.commons.lang3.StringUtils; import org.json.simple.JSONArray; import org.json.simple.JSONStreamAware; @@ -42,6 +45,7 @@ public class Environment implements JSONStreamAware, Cloneable { private static final String LOGICAL_CORE_KEY = "Logical Processors"; private static final String TOTAL_MEMORY_MB = "Total Physical Memory (MB)"; private static final String SOLR_VERSION_KEY = "Solr Version"; + private static final String AZURE_SITE_EXT_INSTALL_TYPE = "AzureSiteExtension"; private static final Pattern JSON_WORKAROUND = Pattern.compile("\\\\+$"); private static final String OBFUSCATED = "=obfuscated"; @@ -128,6 +132,13 @@ public Environment(AgentConfig config, String logFilePath) { addVariable("Framework", "java"); + if (isAzureSiteExtenstionInstall()) { + // Yes this is intentional, since we only have a single installation type for the site extension + addVariable(AZURE_SITE_EXT_INSTALL_TYPE, AZURE_SITE_EXT_INSTALL_TYPE); + NewRelic.getAgent().getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_AZURE_SITE_EXT_INSTALL_TYPE + "/" + + AZURE_SITE_EXT_INSTALL_TYPE); + } + Number appServerPort = config.getProperty("appserver_port"); Integer serverPort = null; if (appServerPort != null) { @@ -310,4 +321,11 @@ public void setServerInfo(String serverInfo) { setServerInfo(info[0], info[1]); } } + + private boolean isAzureSiteExtenstionInstall() { + // Currently, the only "install type" we support is the Azure site extension, which is detected + // by the presence of a specific environment variable with a non-null/non-empty value + final String AZURE_SITE_EXT_VAR = "NEW_RELIC_METADATA_AZURE_APP_SERVICE_NAME"; + return StringUtils.isNotBlank(System.getenv(AZURE_SITE_EXT_VAR)); + } } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java index ca83e0322f..105f76ed20 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java @@ -15,6 +15,7 @@ import com.newrelic.agent.GCService; import com.newrelic.agent.HarvestService; import com.newrelic.agent.HarvestServiceImpl; +import com.newrelic.agent.IRPMService; import com.newrelic.agent.RPMServiceManager; import com.newrelic.agent.RPMServiceManagerImpl; import com.newrelic.agent.ThreadService; @@ -78,6 +79,10 @@ import com.newrelic.agent.stats.StatsService; import com.newrelic.agent.stats.StatsServiceImpl; import com.newrelic.agent.stats.StatsWork; +import com.newrelic.agent.superagent.HealthDataProducer; +import com.newrelic.agent.superagent.SuperAgentIntegrationClientFactory; +import com.newrelic.agent.superagent.SuperAgentIntegrationHealthClient; +import com.newrelic.agent.superagent.SuperAgentIntegrationService; import com.newrelic.agent.trace.TransactionTraceService; import com.newrelic.agent.tracing.DistributedTraceService; import com.newrelic.agent.tracing.DistributedTraceServiceImpl; @@ -88,6 +93,7 @@ import com.newrelic.api.agent.NewRelic; import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; @@ -149,6 +155,7 @@ public class ServiceManagerImpl extends AbstractService implements ServiceManage private volatile SourceLanguageService sourceLanguageService; private volatile ExpirationService expirationService; private volatile SlowTransactionService slowTransactionService; + private volatile SuperAgentIntegrationService superAgentIntegrationService; public ServiceManagerImpl(CoreService coreService, ConfigService configService) { super(ServiceManagerImpl.class.getSimpleName()); @@ -281,6 +288,8 @@ protected synchronized void doStart() throws Exception { slowTransactionService = new SlowTransactionService(config); + superAgentIntegrationService = buildSuperAgentIntegrationService(config); + asyncTxService.start(); threadService.start(); statsService.start(); @@ -314,6 +323,7 @@ protected synchronized void doStart() throws Exception { distributedTraceService.start(); spanEventsService.start(); slowTransactionService.start(); + superAgentIntegrationService.start(); startServices(); @@ -344,6 +354,21 @@ private InfiniteTracing buildInfiniteTracing(ConfigService configService) { return InfiniteTracing.initialize(infiniteTracingConfig, NewRelic.getAgent().getMetricAggregator()); } + private SuperAgentIntegrationService buildSuperAgentIntegrationService(AgentConfig config) { + SuperAgentIntegrationHealthClient healthClient = + SuperAgentIntegrationClientFactory.createHealthClient(config.getSuperAgentIntegrationConfig()); + + ArrayList healthDataProducers = new ArrayList<>(); + healthDataProducers.add(circuitBreakerService); + healthDataProducers.add((HealthDataProducer) coreService); + for (IRPMService service : ServiceFactory.getRPMServiceManager().getRPMServices()) { + healthDataProducers.add(service.getHttpDataSenderAsHealthDataProducer()); + } + + return new SuperAgentIntegrationService(healthClient, config, + healthDataProducers.toArray(new HealthDataProducer[]{})); + } + @Override protected synchronized void doStop() throws Exception { insightsService.stop(); @@ -385,6 +410,7 @@ protected synchronized void doStop() throws Exception { distributedTraceService.stop(); spanEventsService.stop(); slowTransactionService.stop(); + superAgentIntegrationService.stop(); stopServices(); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/AgentHealth.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/AgentHealth.java new file mode 100644 index 0000000000..6c2f96ba7a --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/AgentHealth.java @@ -0,0 +1,96 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public class AgentHealth { + public enum Category { + AGENT, CONFIG, HARVEST, CIRCUIT_BREAKER, + } + + public enum Status { + HEALTHY("NR-APM-000", "Healthy", Category.AGENT), + INVALID_LICENSE("NR-APM-001", "Invalid license key (HTTP status code 401)", Category.CONFIG), + MISSING_LICENSE("NR-APM-002", "License key missing in configuration", Category.CONFIG), + FORCED_DISCONNECT("NR-APM-003", "Forced disconnect received from New Relic (HTTP status code 410)", Category.HARVEST), + HTTP_ERROR("NR-APM-004", "HTTP error response code [%s] received from New Relic while sending data type [%s]", Category.HARVEST), + MISSING_APP_NAME("NR-APM-005", "Missing application name in agent configuration", Category.CONFIG), + MAX_APP_NAMES_EXCEEDED("NR-APM-006", "The maximum number of configured app names (3) exceeded", Category.CONFIG), + PROXY_ERROR("NR-APM-007", "HTTP Proxy configuration error; response code [%s]", Category.HARVEST), + AGENT_DISABLED("NR-APM-008", "Agent is disabled via configuration", Category.CONFIG), + SHUTDOWN("NR-APM-099", "Agent has shutdown", Category.AGENT), + GC_CIRCUIT_BREAKER("NR-APM-100", "Garbage collection circuit breaker triggered: Percent free memory %s; GC CPU time: %s", Category.CIRCUIT_BREAKER),; + + private final String code; + private final String description; + private String [] additionalInfo; + private final Category category; + + Status(String code, String description, Category category) { + this.code = code; + this.description = description; + this.category = category; + } + + public String getCode() { + return code; + } + + public String getDescription() { + String finalDescription; + if (additionalInfo != null && additionalInfo.length > 0) { + finalDescription = String.format(description, (Object[]) additionalInfo); + } else { + finalDescription = description; + } + return finalDescription; + } + + public boolean isHealthStatus() { + return this == HEALTHY; + } + + void setAdditionalInfo(String[] additionalInfo) { + this.additionalInfo = additionalInfo; + } + } + + private final long startTimeNanos; + private Status status; + + AgentHealth(long startTimeNanos) { + this.startTimeNanos = startTimeNanos; + status = Status.HEALTHY; + } + + void setUnhealthyStatus(Status newStatus, String... additionalInfo) { + status = newStatus; + status.setAdditionalInfo(additionalInfo); + } + + void setHealthyStatus(Category category) { + //Only set the status to healthy if the category matches the current status category + if (category == status.category) { + status = Status.HEALTHY; + } + } + + public boolean isHealthy() { + return status.isHealthStatus(); + } + + public String getLastError() { + return status.getCode(); + } + + public String getCurrentStatus() { + return status.getDescription(); + } + + public long getStartTimeNanos() { + return startTimeNanos; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataChangeListener.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataChangeListener.java new file mode 100644 index 0000000000..d5554bc974 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataChangeListener.java @@ -0,0 +1,13 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public interface HealthDataChangeListener { + void onUnhealthyStatus(AgentHealth.Status newStatus, String... additionalInfo); + + void onHealthyStatus(AgentHealth.Category... category); +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataProducer.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataProducer.java new file mode 100644 index 0000000000..a2329cbfa7 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/HealthDataProducer.java @@ -0,0 +1,11 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public interface HealthDataProducer { + void registerHealthDataChangeListener(HealthDataChangeListener listener); +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentHealthNoOpClient.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentHealthNoOpClient.java new file mode 100644 index 0000000000..a16451863f --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentHealthNoOpClient.java @@ -0,0 +1,14 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public class SuperAgentHealthNoOpClient implements SuperAgentIntegrationHealthClient { + @Override + public void sendHealthMessage(AgentHealth agentHealth) { + + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactory.java new file mode 100644 index 0000000000..e8a8133186 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactory.java @@ -0,0 +1,44 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.Agent; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; + +import java.util.logging.Level; + +public class SuperAgentIntegrationClientFactory { + private static final SuperAgentIntegrationHealthClient NO_OP_INSTANCE = new SuperAgentHealthNoOpClient(); + + public enum HealthClientType { + noop, + file, + } + public static SuperAgentIntegrationHealthClient createHealthClient(SuperAgentIntegrationConfig config) { + SuperAgentIntegrationHealthClient client; + + try { + HealthClientType healthClientType = HealthClientType.valueOf(config.getHealthClientType()); + Agent.LOG.log(Level.INFO, "Generating SuperAgent Health Client type: {0}", healthClientType); + + switch (healthClientType) { + case file: + client = new SuperAgentIntegrationHealthFileBasedClient(config); + break; + + default: + client = NO_OP_INSTANCE; + break; + } + } catch (Exception e) { + Agent.LOG.log(Level.WARNING, "Invalid health client type: {0}; returning NoOp implementation", config.getHealthClientType()); + client = NO_OP_INSTANCE; + } + + return client; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthClient.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthClient.java new file mode 100644 index 0000000000..105fea535a --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthClient.java @@ -0,0 +1,11 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public interface SuperAgentIntegrationHealthClient { + void sendHealthMessage(AgentHealth agentHealth); +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClient.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClient.java new file mode 100644 index 0000000000..66fc2bafed --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClient.java @@ -0,0 +1,73 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.Agent; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +public class SuperAgentIntegrationHealthFileBasedClient implements SuperAgentIntegrationHealthClient { + private Yaml yamlWriter; + + private File healthFile = null; + + public SuperAgentIntegrationHealthFileBasedClient(SuperAgentIntegrationConfig config) { + URI locationFromConfig = config.getHealthDeliveryLocation(); + + if (locationFromConfig != null) { + this.healthFile = new File(locationFromConfig); + + DumperOptions yamlDumperOptions = new DumperOptions(); + yamlDumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + yamlDumperOptions.setPrettyFlow(true); + yamlWriter = new Yaml(yamlDumperOptions); + } else { + Agent.LOG.log(Level.WARNING, "superagent.health.delivery_location is not set. " + + "Health messages will not be generated"); + } + } + + @Override + public void sendHealthMessage(AgentHealth agentHealth) { + if (healthFile != null) { + try { + FileWriter fw = new FileWriter(healthFile); + yamlWriter.dump(createHeathMessageMap(agentHealth), fw); + fw.close(); + + if (Agent.LOG.isFinestEnabled() && Agent.isDebugEnabled()) { + Agent.LOG.log(Level.FINEST, "Wrote SA health file: {0}", healthFile.getAbsolutePath()); + } + } catch (IOException e) { + Agent.LOG.log(Level.WARNING, "Error writing health message to file: {0}", e.getMessage()); + } + } + } + + private Map createHeathMessageMap(AgentHealth agentHealth) { + Map healthMap = new HashMap<>(); + + healthMap.put("healthy", agentHealth.isHealthy()); + healthMap.put("status", agentHealth.getCurrentStatus()); + healthMap.put("start_time_unix_nano", agentHealth.getStartTimeNanos()); + healthMap.put("status_time_unix_nano", SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + if (!agentHealth.isHealthy()) { + healthMap.put("last_error", agentHealth.getLastError()); + } + + return healthMap; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationService.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationService.java new file mode 100644 index 0000000000..b890737f4b --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationService.java @@ -0,0 +1,90 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.Agent; +import com.newrelic.agent.MetricNames; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.AgentConfigListener; +import com.newrelic.agent.service.AbstractService; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.util.DefaultThreadFactory; +import com.newrelic.api.agent.NewRelic; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class SuperAgentIntegrationService extends AbstractService implements HealthDataChangeListener { + private final String SA_INTEGRATION_THREAD_NAME = "New Relic Super Agent Integration Service"; + + private final AgentConfig agentConfig; + private final SuperAgentIntegrationHealthClient client; + private final AgentHealth agentHealth; + + private ScheduledExecutorService scheduler; + + public SuperAgentIntegrationService(SuperAgentIntegrationHealthClient client, AgentConfig agentConfig, + HealthDataProducer... healthProducers) { + super(SuperAgentIntegrationService.class.getSimpleName()); + + this.agentConfig = agentConfig; + this.client = client; + this.agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + + for (HealthDataProducer healthProducer : healthProducers) { + healthProducer.registerHealthDataChangeListener(this); + } + } + + @Override + protected void doStart() throws Exception { + if (isEnabled()) { + Agent.LOG.log(Level.INFO, "SuperAgentIntegrationService starting: Health file location: {0} Frequency: {1} Scheme: {2}", + agentConfig.getSuperAgentIntegrationConfig().getHealthDeliveryLocation(), + agentConfig.getSuperAgentIntegrationConfig().getHealthReportingFrequency(), + agentConfig.getSuperAgentIntegrationConfig().getHealthClientType()); + NewRelic.getAgent().getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_SUPERAGENT_HEALTH_REPORTING_ENABLED); + + int messageSendFrequency = agentConfig.getSuperAgentIntegrationConfig().getHealthReportingFrequency(); //Used for both repeat frequency and initial delay + + this.scheduler = Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory(SA_INTEGRATION_THREAD_NAME, true)); + this.scheduler.scheduleWithFixedDelay(() -> client.sendHealthMessage(agentHealth), messageSendFrequency, messageSendFrequency, TimeUnit.SECONDS); + } + } + + @Override + protected void doStop() throws Exception { + if (isEnabled()) { + scheduler.shutdown(); + agentHealth.setUnhealthyStatus(AgentHealth.Status.SHUTDOWN); + client.sendHealthMessage(agentHealth); + } + } + + @Override + public boolean isEnabled() { + return agentConfig.getSuperAgentIntegrationConfig().isEnabled(); + } + + @Override + public void onUnhealthyStatus(AgentHealth.Status newStatus, String... additionalInfo) { + if (isEnabled()) { + agentHealth.setUnhealthyStatus(newStatus, additionalInfo); + } + } + + @Override + public void onHealthyStatus(AgentHealth.Category... categories) { + if (isEnabled()) { + for (AgentHealth.Category category : categories) { + agentHealth.setHealthyStatus(category); + } + } + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtils.java b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtils.java new file mode 100644 index 0000000000..fff9516526 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtils.java @@ -0,0 +1,43 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; + +import java.util.List; + +public class SuperAgentIntegrationUtils { + public static long getPseudoCurrentTimeNanos() { + // The message expects the time in nanoseconds. Since this is a practical impossibility on most hardware, + // simply get the current ms and multiply. + return System.currentTimeMillis() * 1000000; + } + + public static void reportUnhealthyStatus(List healthDataChangeListeners, + AgentHealth.Status newStatus, String... additionalInfo) { + for (HealthDataChangeListener listener : healthDataChangeListeners) { + listener.onUnhealthyStatus(newStatus, additionalInfo); + } + } + + public static void reportHealthyStatus(List healthDataChangeListeners, AgentHealth.Category... categories) { + for (HealthDataChangeListener listener : healthDataChangeListeners) { + listener.onHealthyStatus(categories); + } + } + + public static void reportUnhealthyStatusPriorToServiceStart(AgentConfig config, AgentHealth.Status status) { + SuperAgentIntegrationConfig superAgentIntegrationConfig = config.getSuperAgentIntegrationConfig(); + if (superAgentIntegrationConfig.isEnabled()) { + SuperAgentIntegrationHealthClient client = SuperAgentIntegrationClientFactory.createHealthClient(superAgentIntegrationConfig); + AgentHealth agentHealth = new AgentHealth(getPseudoCurrentTimeNanos()); + agentHealth.setUnhealthyStatus(status); + client.sendHealthMessage(agentHealth); + } + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java index 8793af0fab..76a55ac87e 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java @@ -31,6 +31,10 @@ import com.newrelic.agent.sql.SqlTrace; import com.newrelic.agent.stats.StatsService; import com.newrelic.agent.stats.StatsWorks; +import com.newrelic.agent.superagent.AgentHealth; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; +import com.newrelic.agent.superagent.SuperAgentIntegrationUtils; import com.newrelic.agent.trace.TransactionTrace; import org.json.simple.JSONObject; import org.json.simple.JSONStreamAware; @@ -56,6 +60,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; @@ -68,7 +73,7 @@ * * This class is thread-safe. */ -public class DataSenderImpl implements DataSender { +public class DataSenderImpl implements DataSender, HealthDataProducer { private static final String MODULE_TYPE = "Jars"; private static final int PROTOCOL_VERSION = 17; @@ -124,6 +129,9 @@ public class DataSenderImpl implements DataSender { private volatile int maxPayloadSizeInBytes = DEFAULT_MAX_PAYLOAD_SIZE_IN_BYTES; private volatile Map requestMetadata; private volatile Map metadata; + private final List healthDataChangeListeners = new CopyOnWriteArrayList<>(); + private final boolean isSuperAgentEnabled; + public DataSenderImpl( DataSenderConfig config, HttpClientWrapper httpClientWrapper, @@ -155,6 +163,7 @@ public DataSenderImpl( } this.httpClientWrapper = httpClientWrapper; + this.isSuperAgentEnabled = configService.getDefaultAgentConfig().getSuperAgentIntegrationConfig().isEnabled(); } private void checkAuditMode() { @@ -616,6 +625,8 @@ private ReadResult connectAndSend(String host, String method, String encoding, S recordDataUsageMetrics(method, payloadJsonSent, payloadJsonReceived); + SuperAgentIntegrationUtils.reportHealthyStatus(healthDataChangeListeners, AgentHealth.Category.HARVEST, AgentHealth.Category.CONFIG); + if (dataSenderListener != null) { dataSenderListener.dataSent(method, encoding, uri, data); } @@ -661,6 +672,8 @@ private void throwExceptionFromStatusCode(String method, ReadResult result, byte switch (result.getStatusCode()) { case HttpResponseCode.PROXY_AUTHENTICATION_REQUIRED: // agent receives a 407 response due to a misconfigured proxy (not from NR backend), throw exception + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.PROXY_ERROR, + Integer.toString(result.getStatusCode()), method); final String authField = result.getProxyAuthenticateHeader(); if (authField != null) { throw new HttpError("Proxy Authentication Mechanism Failed: " + authField, result.getStatusCode(), data.length); @@ -669,20 +682,30 @@ private void throwExceptionFromStatusCode(String method, ReadResult result, byte } case HttpResponseCode.UNAUTHORIZED: // received 401 Unauthorized, throw exception instead of parsing LicenseException from 200 response body + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.INVALID_LICENSE); throw new LicenseException(parseExceptionMessage(result.getResponseBody())); case HttpResponseCode.CONFLICT: // received 409 Conflict, throw exception instead of parsing ForceRestartException from 200 response body throw new ForceRestartException(parseExceptionMessage(result.getResponseBody())); case HttpResponseCode.GONE: // received 410 Gone, throw exception instead of parsing ForceDisconnectException from 200 response body + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.FORCED_DISCONNECT); throw new ForceDisconnectException(parseExceptionMessage(result.getResponseBody())); default: // response is bad (neither 200 nor 202), throw generic HttpError exception + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, AgentHealth.Status.HTTP_ERROR, + Integer.toString(result.getStatusCode()), method); logger.log(Level.FINER, "Connection http status code: {0}", result.getStatusCode()); throw HttpError.create(result.getStatusCode(), request.getURL().getHost(), data.length); } } + private void reportUnhealthyStatusToSuperAgent(AgentHealth.Status status, String ... additionalInfo) { + if (isSuperAgentEnabled) { + SuperAgentIntegrationUtils.reportUnhealthyStatus(healthDataChangeListeners, status, additionalInfo); + } + } + private String parseExceptionMessage(String responseBody) { try { JSONParser parser = new JSONParser(); @@ -805,4 +828,9 @@ public void requestEnded() { MetricNames.SUPPORTABILITY_AGENT_ENDPOINT_DURATION + " " + method); } } + + @Override + public void registerHealthDataChangeListener(HealthDataChangeListener listener) { + healthDataChangeListeners.add(listener); + } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/AgentTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/AgentTest.java index 3bc18edd14..6c332735b0 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/AgentTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/AgentTest.java @@ -1,8 +1,15 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ package com.newrelic.agent; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.ConfigService; import com.newrelic.agent.config.ConfigServiceFactory; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; import com.newrelic.agent.logging.AgentLogManager; import com.newrelic.agent.logging.IAgentLogger; import com.newrelic.agent.service.ServiceFactory; @@ -49,6 +56,7 @@ public void test_JRockitMessage() throws NoSuchFieldException, IllegalAccessExce public void test_emptyLicense() { try (MockedStatic csfMock = Mockito.mockStatic(ConfigServiceFactory.class)) { AgentConfig agentConfig = Mockito.mock(AgentConfig.class); + Mockito.when(agentConfig.getSuperAgentIntegrationConfig()).thenReturn(Mockito.mock(SuperAgentIntegrationConfig.class)); Mockito.when(agentConfig.getLicenseKey()).thenReturn(null); ConfigService configService = Mockito.mock(ConfigService.class); Mockito.when(configService.getDefaultAgentConfig()).thenReturn(agentConfig); @@ -64,6 +72,7 @@ public void test_emptyLicense() { public void test_agentDisabled2() { try (MockedStatic csfMock = Mockito.mockStatic(ConfigServiceFactory.class)) { AgentConfig agentConfig = Mockito.mock(AgentConfig.class); + Mockito.when(agentConfig.getSuperAgentIntegrationConfig()).thenReturn(Mockito.mock(SuperAgentIntegrationConfig.class)); Mockito.when(agentConfig.getLicenseKey()).thenReturn("licenseKey"); Mockito.when(agentConfig.isAgentEnabled()).thenReturn(false); ConfigService configService = Mockito.mock(ConfigService.class); diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/HarvestServiceTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/HarvestServiceTest.java index ddad7ee749..f340bf141d 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/HarvestServiceTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/HarvestServiceTest.java @@ -22,6 +22,7 @@ import com.newrelic.agent.stats.StatsService; import com.newrelic.agent.stats.StatsServiceImpl; import com.newrelic.agent.stats.StatsWork; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.api.agent.Logger; import org.junit.After; import org.junit.Assert; @@ -222,6 +223,11 @@ public String getApplicationName() { return "test"; } + @Override + public HealthDataProducer getHttpDataSenderAsHealthDataProducer() { + return null; + } + } private static class MergeStatsWork implements StatsWork { diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/MockCoreService.java b/newrelic-agent/src/test/java/com/newrelic/agent/MockCoreService.java index e29a5733f7..e6136c100a 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/MockCoreService.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/MockCoreService.java @@ -14,12 +14,14 @@ import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.service.ServiceManager; import com.newrelic.agent.service.ServiceManagerImpl; +import com.newrelic.agent.superagent.HealthDataChangeListener; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.api.agent.Logger; import org.mockito.Mockito; import static org.mockito.Mockito.mock; -public class MockCoreService extends AbstractService implements CoreService { +public class MockCoreService extends AbstractService implements CoreService, HealthDataProducer { private InstrumentationProxy instrumentation = null; public MockCoreService() { @@ -72,4 +74,7 @@ public boolean isEnabled() { return true; } + @Override + public void registerHealthDataChangeListener(HealthDataChangeListener listener) { + } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java b/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java index 07f1be300f..5894f04050 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java @@ -19,6 +19,7 @@ import com.newrelic.agent.service.analytics.TransactionEvent; import com.newrelic.agent.sql.SqlTrace; import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.superagent.HealthDataProducer; import com.newrelic.agent.trace.TransactionTrace; import java.util.ArrayList; @@ -248,6 +249,11 @@ public void sendErrorData(List tracedErrors) throws Exception { this.errorTracesSeen.addAndGet(tracedErrors.size()); } + @Override + public HealthDataProducer getHttpDataSenderAsHealthDataProducer() { + return null; + } + // Mock only public void setSendAnalyticsEventsException(Exception e) { this.ex = e; 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..37a3bec94b --- /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 = "123456789012"; + 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 = "123456789012"; + 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 = "123456789012"; + 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 = "123456789012"; + + 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/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 new file mode 100644 index 0000000000..89d974059c --- /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 = "123456789012"; + 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 = "123456789012"; + 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-agent/src/test/java/com/newrelic/agent/config/SuperAgentIntegrationConfigTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/SuperAgentIntegrationConfigTest.java new file mode 100644 index 0000000000..1ed32e45c5 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/SuperAgentIntegrationConfigTest.java @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.config; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class SuperAgentIntegrationConfigTest { + @Test + public void superAgentConfig_withValidProperties_createsValidConfig() { + Map superAgentConfigProps = new HashMap<>(); + Map healthConfigProps = new HashMap<>(); + superAgentConfigProps.put("fleet_id", "12345"); + + healthConfigProps.put("delivery_location", "file:///foo/bar"); + healthConfigProps.put("frequency", 5); + superAgentConfigProps.put("health", healthConfigProps); + + SuperAgentIntegrationConfig config = new SuperAgentIntegrationConfigImpl(superAgentConfigProps); + assertEquals("12345", config.getFleetId()); + assertEquals(5, config.getHealthReportingFrequency()); + assertEquals("file", config.getHealthClientType()); + assertEquals("/foo/bar", config.getHealthDeliveryLocation().getPath()); + } + + @Test + public void superAgentConfig_withInvalidLocation_nullsFleetId() { + Map superAgentConfigProps = new HashMap<>(); + Map healthConfigProps = new HashMap<>(); + superAgentConfigProps.put("fleet_id", "12345"); + + healthConfigProps.put("delivery_location", ""); + superAgentConfigProps.put("health", healthConfigProps); + + SuperAgentIntegrationConfig config = new SuperAgentIntegrationConfigImpl(superAgentConfigProps); + assertNull(config.getFleetId()); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/AgentHealthTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/AgentHealthTest.java new file mode 100644 index 0000000000..144166ece6 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/AgentHealthTest.java @@ -0,0 +1,63 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AgentHealthTest { + @Test + public void isHealthy_returnsTrue_whenStatusIsHealthy() { + // Newly constructed AgentHealth object should be healthy + AgentHealth agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + assertTrue(agentHealth.isHealthy()); + } + + @Test + public void isHealthy_returnsFalse_whenStatusIsNotHealthy() { + AgentHealth agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + agentHealth.setUnhealthyStatus(AgentHealth.Status.INVALID_LICENSE); + assertFalse(agentHealth.isHealthy()); + } + + @Test + public void classGetters_returnCorrectValues() { + long startTime = SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos(); + AgentHealth agentHealth = new AgentHealth(startTime); + agentHealth.setUnhealthyStatus(AgentHealth.Status.INVALID_LICENSE); + + assertEquals(startTime, agentHealth.getStartTimeNanos()); + assertEquals("NR-APM-001", agentHealth.getLastError()); + assertEquals("Invalid license key (HTTP status code 401)", agentHealth.getCurrentStatus()); + } + + @Test + public void setHealthyStatus_setsStatusToHealthy_whenLastStatusCategoryMatches() { + AgentHealth agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + agentHealth.setUnhealthyStatus(AgentHealth.Status.INVALID_LICENSE); + agentHealth.setHealthyStatus(AgentHealth.Category.CONFIG); + assertTrue(agentHealth.isHealthy()); + } + + @Test + public void setHealthyStatus_ignoresSetStatusToHealthy_whenLastStatusCategoryDoesNotMatch() { + AgentHealth agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + agentHealth.setUnhealthyStatus(AgentHealth.Status.INVALID_LICENSE); + agentHealth.setHealthyStatus(AgentHealth.Category.HARVEST); + assertFalse(agentHealth.isHealthy()); + } + + @Test + public void getDescription_returnsCorrectDescription_whenAdditionalInfoIsProvided() { + AgentHealth agentHealth = new AgentHealth(SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos()); + agentHealth.setUnhealthyStatus(AgentHealth.Status.HTTP_ERROR, "404", "whatever"); + assertEquals("HTTP error response code [404] received from New Relic while sending data type [whatever]", agentHealth.getCurrentStatus()); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentHealthUnitTestClient.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentHealthUnitTestClient.java new file mode 100644 index 0000000000..babf71451e --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentHealthUnitTestClient.java @@ -0,0 +1,21 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +public class SuperAgentHealthUnitTestClient implements SuperAgentIntegrationHealthClient { + + private AgentHealth agentHealth; + + @Override + public void sendHealthMessage(AgentHealth agentHealth) { + this.agentHealth = agentHealth; + } + + public AgentHealth getAgentHealth() { + return agentHealth; + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactoryTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactoryTest.java new file mode 100644 index 0000000000..2c280d2243 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationClientFactoryTest.java @@ -0,0 +1,50 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.config.SuperAgentIntegrationConfig; +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SuperAgentIntegrationClientFactoryTest { + private final String URI_TEST_STRING = "file://" + System.getProperty("user.dir") + "/health.yml"; + private SuperAgentIntegrationConfig mockConfig; + + @Before + public void setup() { + mockConfig = mock(SuperAgentIntegrationConfig.class); + } + + @Test + public void createHealthClient_withInvalidType_returnsNoOpClient() { + SuperAgentIntegrationHealthClient client = SuperAgentIntegrationClientFactory.createHealthClient(mockConfig); + assertTrue(client instanceof SuperAgentHealthNoOpClient); + } + + @Test + public void createHealthClient_withFileType_returnsFileBasedClient() throws URISyntaxException { + URI uri = new URI(URI_TEST_STRING); + when(mockConfig.getHealthDeliveryLocation()).thenReturn(uri); + when(mockConfig.getHealthClientType()).thenReturn("file"); + SuperAgentIntegrationHealthClient client = SuperAgentIntegrationClientFactory.createHealthClient(mockConfig); + assertTrue(client instanceof SuperAgentIntegrationHealthFileBasedClient); + } + + @Test + public void createHealthClient_withNoOpType_returnsNoOpClient() { + when(mockConfig.getHealthClientType()).thenReturn("noop"); + SuperAgentIntegrationHealthClient client = SuperAgentIntegrationClientFactory.createHealthClient(mockConfig); + assertTrue(client instanceof SuperAgentHealthNoOpClient); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClientTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClientTest.java new file mode 100644 index 0000000000..6665adf00f --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationHealthFileBasedClientTest.java @@ -0,0 +1,97 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.config.SuperAgentIntegrationConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SuperAgentIntegrationHealthFileBasedClientTest { + private static URI HEALTH_FILE_LOCATION = null; + + static { + try { + HEALTH_FILE_LOCATION = new URI("file://" + System.getProperty("user.dir") + "/health.yml"); + } catch (URISyntaxException e) { + // ignored + } + } + + private SuperAgentIntegrationConfig mockConfig; + + @Before + public void setup() { + mockConfig = mock(SuperAgentIntegrationConfig.class); + } + + @After + public void cleanup() { + File healthFile = new File(HEALTH_FILE_LOCATION); + if (healthFile.exists()) { + healthFile.delete(); + } + } + + @Test + public void sendHealthMessage_withValidConfig_createsHealthFile() throws IOException { + when(mockConfig.getHealthDeliveryLocation()).thenReturn(HEALTH_FILE_LOCATION); + SuperAgentIntegrationHealthFileBasedClient client = new SuperAgentIntegrationHealthFileBasedClient(mockConfig); + + long startTime = SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos(); + AgentHealth agentHealth = new AgentHealth(startTime); + + client.sendHealthMessage(agentHealth); + File yamlFile = new File(HEALTH_FILE_LOCATION); + Yaml yaml = new Yaml(); + InputStream is = Files.newInputStream(yamlFile.toPath()); + Map parsedYaml = yaml.load(is); + assertTrue((boolean)parsedYaml.get("healthy")); + assertEquals(startTime, parsedYaml.get("start_time_unix_nano")); + assertEquals("Healthy", parsedYaml.get("status")); + assertNotNull(parsedYaml.get("status_time_unix_nano")); + assertNull(parsedYaml.get("last_error")); + } + + @Test + public void sendHealthMessage_withUnhealthyAgentInstance_createsHealthFileWithLastError() throws IOException { + when(mockConfig.getHealthDeliveryLocation()).thenReturn(HEALTH_FILE_LOCATION); + SuperAgentIntegrationHealthFileBasedClient client = new SuperAgentIntegrationHealthFileBasedClient(mockConfig); + + long startTime = SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos(); + AgentHealth agentHealth = new AgentHealth(startTime); + agentHealth.setUnhealthyStatus(AgentHealth.Status.INVALID_LICENSE); + + client.sendHealthMessage(agentHealth); + File yamlFile = new File(HEALTH_FILE_LOCATION); + Yaml yaml = new Yaml(); + InputStream is = Files.newInputStream(yamlFile.toPath()); + Map parsedYaml = yaml.load(is); + assertFalse((boolean)parsedYaml.get("healthy")); + assertEquals(startTime, parsedYaml.get("start_time_unix_nano")); + assertEquals("Invalid license key (HTTP status code 401)", parsedYaml.get("status")); + assertNotNull(parsedYaml.get("status_time_unix_nano")); + assertEquals("NR-APM-001", parsedYaml.get("last_error")); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationServiceTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationServiceTest.java new file mode 100644 index 0000000000..b626d9c04a --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationServiceTest.java @@ -0,0 +1,117 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.MockServiceManager; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; +import com.newrelic.agent.service.ServiceFactory; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SuperAgentIntegrationServiceTest { + AgentConfig mockAgentConfig; + SuperAgentIntegrationConfig mockSuperAgentIntegrationConfig; + AgentHealth mockAgentHealth; + + @Before + public void before() { + mockAgentConfig = mock(AgentConfig.class); + mockSuperAgentIntegrationConfig = mock(SuperAgentIntegrationConfig.class); + mockAgentHealth = mock(AgentHealth.class); + + MockServiceManager manager = new MockServiceManager(); + ServiceFactory.setServiceManager(manager); + + when(mockAgentConfig.getSuperAgentIntegrationConfig()).thenReturn(mockSuperAgentIntegrationConfig); + } + + @Test + public void constructor_createsAgentHealth_withHealthyStatus() throws Exception { + when(mockSuperAgentIntegrationConfig.getHealthReportingFrequency()).thenReturn(1); + when(mockSuperAgentIntegrationConfig.isEnabled()).thenReturn(true); + SuperAgentHealthUnitTestClient healthClient = new SuperAgentHealthUnitTestClient(); + SuperAgentIntegrationService service = new SuperAgentIntegrationService(healthClient, mockAgentConfig); + service.doStart(); + Thread.sleep(2100); + + assertTrue(healthClient.getAgentHealth().isHealthy()); + assertEquals("Healthy", healthClient.getAgentHealth().getCurrentStatus()); + service.doStop(); + } + + @Test + public void onUnhealthyStatus_updatesAgentHealthToUnhealthy() throws Exception { + when(mockSuperAgentIntegrationConfig.getHealthReportingFrequency()).thenReturn(1); + when(mockSuperAgentIntegrationConfig.isEnabled()).thenReturn(true); + SuperAgentHealthUnitTestClient healthClient = new SuperAgentHealthUnitTestClient(); + SuperAgentIntegrationService service = new SuperAgentIntegrationService(healthClient, mockAgentConfig); + service.onUnhealthyStatus(AgentHealth.Status.GC_CIRCUIT_BREAKER, "1", "2"); + service.doStart(); + Thread.sleep(2100); + + assertFalse(healthClient.getAgentHealth().isHealthy()); + assertEquals("Garbage collection circuit breaker triggered: Percent free memory 1; GC CPU time: 2", healthClient.getAgentHealth().getCurrentStatus()); + + service.doStop(); + } + + @Test + public void onHealthyStatus_updatesAgentHealthToHealthy() throws Exception { + when(mockSuperAgentIntegrationConfig.getHealthReportingFrequency()).thenReturn(1); + when(mockSuperAgentIntegrationConfig.isEnabled()).thenReturn(true); + SuperAgentHealthUnitTestClient healthClient = new SuperAgentHealthUnitTestClient(); + SuperAgentIntegrationService service = new SuperAgentIntegrationService(healthClient, mockAgentConfig); + service.onUnhealthyStatus(AgentHealth.Status.GC_CIRCUIT_BREAKER, "1", "2"); + service.doStart(); + Thread.sleep(2100); + + assertFalse(healthClient.getAgentHealth().isHealthy()); + assertEquals("Garbage collection circuit breaker triggered: Percent free memory 1; GC CPU time: 2", healthClient.getAgentHealth().getCurrentStatus()); + + Thread.sleep(1100); + + service.onHealthyStatus(AgentHealth.Category.CIRCUIT_BREAKER); + assertTrue(healthClient.getAgentHealth().isHealthy()); + assertEquals("Healthy", healthClient.getAgentHealth().getCurrentStatus()); + + service.doStop(); + } + + @Test + public void doStop_writesShutdownHealthStatus() throws Exception { + when(mockSuperAgentIntegrationConfig.getHealthReportingFrequency()).thenReturn(1); + when(mockSuperAgentIntegrationConfig.isEnabled()).thenReturn(true); + SuperAgentHealthUnitTestClient healthClient = new SuperAgentHealthUnitTestClient(); + SuperAgentIntegrationService service = new SuperAgentIntegrationService(healthClient, mockAgentConfig); + service.doStart(); + service.doStop(); + + assertFalse(healthClient.getAgentHealth().isHealthy()); + assertEquals("NR-APM-099", healthClient.getAgentHealth().getLastError()); + assertEquals("Agent has shutdown", healthClient.getAgentHealth().getCurrentStatus()); + } + + + @Test + public void doStart_ignoresStartCommand_whenEnabledIsFalse() throws Exception { + when(mockSuperAgentIntegrationConfig.isEnabled()).thenReturn(false); + SuperAgentHealthUnitTestClient healthClient = new SuperAgentHealthUnitTestClient(); + SuperAgentIntegrationService service = new SuperAgentIntegrationService(healthClient, mockAgentConfig); + service.doStart(); + Thread.sleep(2100); + + assertNull(healthClient.getAgentHealth()); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtilsTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtilsTest.java new file mode 100644 index 0000000000..cfca2fa74e --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/SuperAgentIntegrationUtilsTest.java @@ -0,0 +1,51 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.newrelic.agent.superagent; + +import com.newrelic.agent.MockServiceManager; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.SuperAgentIntegrationConfig; +import com.newrelic.agent.service.ServiceFactory; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SuperAgentIntegrationUtilsTest { + private HealthDataChangeListener mockListener; + + @Before + public void before() { + mockListener = mock(HealthDataChangeListener.class); + + MockServiceManager manager = new MockServiceManager(); + ServiceFactory.setServiceManager(manager); + } + + @Test + public void getPseudoCurrentTimeNanos_returnsTimeInNanos() { + long testTime = System.currentTimeMillis(); + long time = SuperAgentIntegrationUtils.getPseudoCurrentTimeNanos(); + assertTrue(time >= testTime * 1000000); + } + + @Test + public void reportUnhealthyStatus_updatesHealthListeners() { + SuperAgentIntegrationUtils.reportUnhealthyStatus(Collections.singletonList(mockListener), AgentHealth.Status.GC_CIRCUIT_BREAKER); + verify(mockListener).onUnhealthyStatus(AgentHealth.Status.GC_CIRCUIT_BREAKER); + } + + @Test + public void reportHealthyStatus_updatesHealthListeners() { + SuperAgentIntegrationUtils.reportHealthyStatus(Collections.singletonList(mockListener), AgentHealth.Category.HARVEST); + verify(mockListener).onHealthyStatus(AgentHealth.Category.HARVEST); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/superagent/UnitTestHealthDataChangeListener.java b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/UnitTestHealthDataChangeListener.java new file mode 100644 index 0000000000..16b10c6534 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/superagent/UnitTestHealthDataChangeListener.java @@ -0,0 +1,16 @@ +package com.newrelic.agent.superagent; + +public class UnitTestHealthDataChangeListener implements HealthDataChangeListener { + private AgentHealth.Status lastStatus = null; + private AgentHealth.Category lastCategory = null; + + @Override + public void onUnhealthyStatus(AgentHealth.Status newStatus, String... additionalInfo) { + lastStatus = newStatus; + } + + @Override + public void onHealthyStatus(AgentHealth.Category... category) { + lastCategory = category[0]; + } +} 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..1454dd42ca --- /dev/null +++ b/newrelic-api/src/main/java/com/newrelic/api/agent/Cloud.java @@ -0,0 +1,56 @@ +/* + * + * * 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. + *

+ *

+ * 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 + */ + 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)}. + *

+ *

+ * 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 + */ + 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");