From 72711116a39e4b8f84dd97d0369681cd9b674e13 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Wed, 11 Sep 2024 14:44:06 -0400 Subject: [PATCH] ECS container id fetch --- .../agent/utilization/DockerData.java | 57 +++++++++++-------- .../agent/utilization/UtilizationData.java | 21 +++++-- .../agent/utilization/UtilizationService.java | 16 ++++-- .../agent/utilization/DockerDataTest.java | 4 +- .../UtilizationCrossAgentTests.java | 5 +- .../utilization/UtilizationDataTest.java | 40 +++++++++++-- .../utilization/UtilizationServiceTest.java | 2 +- 7 files changed, 101 insertions(+), 44 deletions(-) diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java index 71c03cf8fc..8457e92e63 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/DockerData.java @@ -9,6 +9,7 @@ import com.google.common.annotations.VisibleForTesting; import com.newrelic.agent.Agent; +import com.newrelic.agent.config.internal.SystemEnvironmentFacade; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; @@ -39,9 +40,8 @@ * We should grab the "cpu" line. The long id number is the number we want. * * For AWS ECS (fargate and non-fargate) we check the metadata returned from the URL defined in either the - * v3 or v4 metadata URL. These checks are only made if the cgroup files don't return anything and the - * metadata URL(s) are present in the target env variables. The docker id returned in the metadata JSON response - * is a 32-digit hex followed by a 10-digit number in the "DockerId" key. + * v3 or v4 metadata URL. These checks are only made if the metadata URL(s) are present in the target env variables. + * The docker id returned in the metadata JSON response is a 32-digit hex followed by a 10-digit number in the "DockerId" key. * * In either case, this is the full docker id, not the short id that appears when you run a "docker ps". */ @@ -51,7 +51,7 @@ public class DockerData { private static final String FILE_WITH_CONTAINER_ID_V2 = "/proc/self/mountinfo"; private static final String CPU = "cpu"; - private static final String AWS_ECS_METADATA_V3_ENV_VAR = "ECS_CONTAINER_METADATA_URI"; + private static final String AWS_ECS_METADATA_UNVERSIONED_ENV_VAR = "ECS_CONTAINER_METADATA_URI"; private static final String AWS_ECS_METADATA_V4_ENV_VAR = "ECS_CONTAINER_METADATA_URI_V4"; private static final String FARGATE_DOCKER_ID_KEY = "DockerId"; @@ -59,36 +59,25 @@ public class DockerData { private static final Pattern DOCKER_CONTAINER_STRING_V1 = Pattern.compile("^.*[^0-9a-f]+([0-9a-f]{64,}).*"); private static final Pattern DOCKER_CONTAINER_STRING_V2 = Pattern.compile(".*/docker/containers/([0-9a-f]{64,}).*"); - public String getDockerContainerId(boolean isLinux) { + public String getDockerContainerIdForEcsFargate(boolean isLinux) { if (isLinux) { String result; - //try to get the container id from the v2 location - File containerIdFileV2 = new File(FILE_WITH_CONTAINER_ID_V2); - result = getDockerIdFromFile(containerIdFileV2, CGroup.V2); - if (result != null) { - return result; - } - //try to get container id from the v1 location - File containerIdFileV1 = new File(FILE_WITH_CONTAINER_ID_V1); - result = getDockerIdFromFile(containerIdFileV1, CGroup.V1); - if (result != null) { - return result; - } - - // Try v4 ESC Fargate metadata call, then finally v3 + // Try v4 ESC Fargate metadata call, then fallback to the un-versioned call String fargateUrl = null; try { fargateUrl = System.getenv(AWS_ECS_METADATA_V4_ENV_VAR); if (fargateUrl != null) { + Agent.LOG.log(Level.INFO, "Attempting to fetch ECS Fargate container id from URL (v4): {0}", fargateUrl); result = retrieveDockerIdFromFargateMetadata(new AwsFargateMetadataFetcher(fargateUrl)); if (result != null) { return result; } } - fargateUrl = System.getenv(AWS_ECS_METADATA_V3_ENV_VAR); + fargateUrl = System.getenv(AWS_ECS_METADATA_UNVERSIONED_ENV_VAR); if (fargateUrl != null) { + Agent.LOG.log(Level.INFO, "Attempting to fetch ECS Fargate container id from URL (unversioned): {0}", fargateUrl); return retrieveDockerIdFromFargateMetadata(new AwsFargateMetadataFetcher(fargateUrl)); } } catch (MalformedURLException e) { @@ -99,6 +88,27 @@ public String getDockerContainerId(boolean isLinux) { return null; } + public String getDockerContainerIdFromCGroups(boolean isLinux) { + if (isLinux) { + String result; + //try to get the container id from the v2 location + File containerIdFileV2 = new File(FILE_WITH_CONTAINER_ID_V2); + result = getDockerIdFromFile(containerIdFileV2, CGroup.V2); + if (result != null) { + return result; + } + + //try to get container id from the v1 location + File containerIdFileV1 = new File(FILE_WITH_CONTAINER_ID_V1); + result = getDockerIdFromFile(containerIdFileV1, CGroup.V1); + if (result != null) { + return result; + } + } + + return null; + } + String getDockerIdFromFile(File mountInfoFile, CGroup cgroup) { if (mountInfoFile.exists() && mountInfoFile.canRead()) { try { @@ -197,7 +207,7 @@ private boolean checkAndGetMatch(Pattern p, StringBuilder result, String segment @VisibleForTesting String retrieveDockerIdFromFargateMetadata(AwsFargateMetadataFetcher awsFargateMetadataFetcher) { String dockerId = null; - StringBuffer jsonBlob = new StringBuffer(); + StringBuilder jsonBlob = new StringBuilder(); try { try (BufferedReader reader = new BufferedReader(new InputStreamReader(awsFargateMetadataFetcher.openStream()))) { @@ -209,10 +219,11 @@ String retrieveDockerIdFromFargateMetadata(AwsFargateMetadataFetcher awsFargateM JSONObject jsonObject = (JSONObject) new JSONParser().parse(jsonBlob.toString()); dockerId = (String) jsonObject.get(FARGATE_DOCKER_ID_KEY); + Agent.LOG.log(Level.INFO, "ECS Fargate container id: {0} ", dockerId); } catch (IOException e) { - Agent.LOG.log(Level.FINEST, "Error opening input stream retrieving AWS Fargate metadata"); + Agent.LOG.log(Level.WARNING, "Error opening input stream retrieving AWS Fargate metadata"); } catch (ParseException e) { - Agent.LOG.log(Level.FINEST, "Error parsing JSON blob for AWS Fargate metadata"); + Agent.LOG.log(Level.WARNING, "Error parsing JSON blob for AWS Fargate metadata"); } return dockerId; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationData.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationData.java index 400be73c33..8d7ac9242e 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationData.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationData.java @@ -29,7 +29,9 @@ public class UtilizationData { private static final String VENDORS_KEY = "vendors"; private static final String BOOT_ID = "boot_id"; private static final String DOCKER = "docker"; + private static final String ECS = "ecs"; private static final String DOCKER_ID_KEY = "id"; + private static final String ECS_ID_KEY = "ecsDockerId"; private static final String CONFIG_KEY = "config"; private static final String KUBERNETES = "kubernetes"; @@ -39,23 +41,26 @@ public class UtilizationData { private final ArrayList ipAddress; private final Integer logicalProcessorCount; private final String dockerContainerId; + private final String ecsFargateDockerContainerId; private final String bootId; private final Long totalRamMib; private final UtilizationConfig dataConfig; private final KubernetesData kubernetesData; - public UtilizationData(String host, String fullHost, ArrayList ip, Integer logicalProcessorCt, String dockerId, String bootId, - CloudData cloudData, Future ramFuture, UtilizationConfig configData, KubernetesData kubernetesData) { - this(host, fullHost, ip, logicalProcessorCt, dockerId, bootId, cloudData, getTotalRamMibFromFuture(ramFuture), configData, kubernetesData); + public UtilizationData(String host, String fullHost, ArrayList ip, Integer logicalProcessorCt, String dockerId, String ecsFargateDockerContainerId, + String bootId, CloudData cloudData, Future ramFuture, UtilizationConfig configData, KubernetesData kubernetesData) { + this(host, fullHost, ip, logicalProcessorCt, dockerId, ecsFargateDockerContainerId, bootId, cloudData, + getTotalRamMibFromFuture(ramFuture), configData, kubernetesData); } - public UtilizationData(String host, String fullHost, ArrayList ip, Integer logicalProcessorCt, String dockerId, String bootId, - CloudData cloudData, Long ram, UtilizationConfig configData, KubernetesData kubernetesData) { + public UtilizationData(String host, String fullHost, ArrayList ip, Integer logicalProcessorCt, String dockerId, String ecsFargateDockerContainerId, + String bootId, CloudData cloudData, Long ram, UtilizationConfig configData, KubernetesData kubernetesData) { this.hostname = host; this.fullHostName = fullHost; this.ipAddress = ip; this.logicalProcessorCount = Integer.valueOf(0).equals(logicalProcessorCt) ? null : logicalProcessorCt; this.dockerContainerId = dockerId; + this.ecsFargateDockerContainerId = ecsFargateDockerContainerId; this.bootId = bootId; this.cloudData = cloudData; this.totalRamMib = Long.valueOf(0).equals(ram) ? null : ram; @@ -116,6 +121,12 @@ public Map map() { vendors.put(DOCKER, docker); } + if (ecsFargateDockerContainerId != null) { + Map ecs = new HashMap<>(); + ecs.put(ECS_ID_KEY, ecsFargateDockerContainerId); + vendors.put(ECS, ecs); + } + if (!vendors.isEmpty()) { data.put(VENDORS_KEY, vendors); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationService.java b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationService.java index dddee34ec9..f80cbe740e 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/utilization/UtilizationService.java @@ -49,6 +49,7 @@ public class UtilizationService extends AbstractService { private final ArrayList ipAddress; private final String bootId; private final String dockerContainerId; + private final String ecsFargateDockerContainerId; private final int processorCount; private final Future totalRamInMibFuture; private final UtilizationConfig configData; @@ -93,12 +94,13 @@ public UtilizationService() { isLinux = isLinuxOs(); bootId = DataFetcher.getBootId(); dockerContainerId = detectDocker ? getDockerContainerId() : null; + ecsFargateDockerContainerId = detectAws ? getEcsFargateDockerContainerId() : null; processorCount = DataFetcher.getLogicalProcessorCount(); totalRamInMibFuture = executor.submit(DataFetcher.getTotalRamInMibCallable()); configData = UtilizationConfig.createFromConfigService(); kubernetesData = getKubernetesData(); - utilizationData = new UtilizationData(hostName, fullHostName, ipAddress, processorCount, dockerContainerId, bootId, null, totalRamInMibFuture, - configData, kubernetesData); + utilizationData = new UtilizationData(hostName, fullHostName, ipAddress, processorCount, dockerContainerId, ecsFargateDockerContainerId, + bootId, null, totalRamInMibFuture, configData, kubernetesData); } @Override @@ -190,7 +192,11 @@ KubernetesData getKubernetesData() { * Do not call DockerData.getDockerContainerId(boolean, String) directly, call this method instead. */ String getDockerContainerId() { - return getDockerData().getDockerContainerId(isLinux); + return getDockerData().getDockerContainerIdFromCGroups(isLinux); + } + + String getEcsFargateDockerContainerId() { + return getDockerData().getDockerContainerIdForEcsFargate(isLinux); } class UtilizationTask implements Callable { @@ -227,8 +233,8 @@ private UtilizationData doUpdateUtilizationData() { } } - return new UtilizationData(hostName, fullHostName, ipAddress, processorCount, dockerContainerId, bootId, foundData, totalRamInMibFuture, configData, - kubernetesData); + return new UtilizationData(hostName, fullHostName, ipAddress, processorCount, dockerContainerId, ecsFargateDockerContainerId, bootId, + foundData, totalRamInMibFuture, configData, kubernetesData); } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java index f71728ebf4..7c4cbe07da 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/DockerDataTest.java @@ -39,7 +39,7 @@ public class DockerDataTest { @Test public void testGetDockerIdNotLinux() { - Assert.assertNull(dockerData.getDockerContainerId(false)); + Assert.assertNull(dockerData.getDockerContainerIdFromCGroups(false)); } @Test public void testCheckLineAndGetIdValidV2() { @@ -367,7 +367,7 @@ public void retrieveDockerIdFromFargateMetadata_withInputStreamException_returns @Test public void getDockerContainerId_withNoDockerIdSource_returnsNull() { - Assert.assertNull(dockerData.getDockerContainerId(true)); + Assert.assertNull(dockerData.getDockerContainerIdFromCGroups(true)); } private void processFile(File file, String answer, CGroup cgroup) { diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationCrossAgentTests.java b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationCrossAgentTests.java index 317129e9ed..ca848e8578 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationCrossAgentTests.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationCrossAgentTests.java @@ -158,6 +158,7 @@ private void runTest(JSONObject jsonTest, String type) throws ParseException { // no cross-agent tests for docker yet. String containerId = null; + String ecsFargateDockerId = null; CloudData data = null; @@ -210,8 +211,8 @@ private void runTest(JSONObject jsonTest, String type) throws ParseException { addresses.addAll(ipAddress); } - UtilizationData utilizationData = new UtilizationData(hostname, fullHostname, addresses, logical_processors, containerId, boot_id, data, total_ram_mib, - UtilizationConfig.createFromConfigService(), KubernetesData.extractKubernetesValues(systemPropertyProvider)); + UtilizationData utilizationData = new UtilizationData(hostname, fullHostname, addresses, logical_processors, containerId, ecsFargateDockerId, + boot_id, data, total_ram_mib, UtilizationConfig.createFromConfigService(), KubernetesData.extractKubernetesValues(systemPropertyProvider)); Assert.assertEquals("cross agent test '" + testname + "' failed.", expectedOutput, toJSONObject(utilizationData.map())); } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationDataTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationDataTest.java index 94892cb184..74e5170079 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationDataTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationDataTest.java @@ -30,7 +30,7 @@ public static void before() throws Exception { @Test public void testUtilizationDataVersion() { UtilizationConfig utilConfig = new UtilizationConfig(null, null, null); - UtilizationData data = new UtilizationData(null, null, null, 0, null, invalidBootId, awsCloudData, 0L, utilConfig, + UtilizationData data = new UtilizationData(null, null, null, 0, null, null, invalidBootId, awsCloudData, 0L, utilConfig, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertTrue(map.containsKey("metadata_version")); @@ -39,7 +39,7 @@ public void testUtilizationDataVersion() { @Test public void testUtilizationKeysInvalidBootId() { - UtilizationData data = new UtilizationData(null, null, null, 0, null, invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, + UtilizationData data = new UtilizationData(null, null, null, 0, null, null, invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertTrue(map.containsKey("metadata_version")); @@ -51,7 +51,7 @@ public void testUtilizationKeysInvalidBootId() { @Test public void testUtilizationKeysValidBootId() { - UtilizationData data = new UtilizationData(null, null, null, 0, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, + UtilizationData data = new UtilizationData(null, null, null, 0, null, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertTrue(map.containsKey("metadata_version")); @@ -64,7 +64,7 @@ public void testUtilizationKeysValidBootId() { @Test public void testUtilizationKeysValidEntityIdentity() { UtilizationData data = new UtilizationData("newrelic", "newrelic.com", new ArrayList<>(Arrays.asList("1.2.3.4")), - 0, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); + 0, null, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertTrue(map.containsKey("metadata_version")); Assert.assertTrue(map.containsKey("logical_processors")); @@ -78,7 +78,7 @@ public void testUtilizationKeysValidEntityIdentity() { @Test public void testUtilizationKeysInValidEntityIdentity() { UtilizationData data = new UtilizationData("newrelic", "newrelic", new ArrayList<>(Arrays.asList("1.2.3.4")), - 0, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); + 0, null, null, validBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertTrue(map.containsKey("hostname")); Assert.assertFalse(map.containsKey("full_hostname")); @@ -87,10 +87,38 @@ public void testUtilizationKeysInValidEntityIdentity() { @Test public void testNoData() { - UtilizationData data = new UtilizationData(null, null, null, 0, null, invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, + UtilizationData data = new UtilizationData(null, null, null, 0, null, null, invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, KubernetesData.EMPTY_KUBERNETES_DATA); Map map = data.map(); Assert.assertNull(map.get("total_ram_mib")); Assert.assertNull(map.get("logical_processors")); } + + @Test + public void map_producesCorrectJson_withEcsFargateContainerId() { + UtilizationData data = new UtilizationData(null, null, null, 0, null, "ecs1234567890", invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, + KubernetesData.EMPTY_KUBERNETES_DATA); + Map map = data.map(); + Map vendorMap = (Map) map.get("vendors"); + Map ecsMap = (Map) vendorMap.get("ecs"); + String id = (String) ecsMap.get("ecsDockerId"); + + Assert.assertEquals("ecs1234567890", id); + } + + @Test + public void map_producesCorrectJson_withBothDockerIdandEcsFargateContainerId() { + UtilizationData data = new UtilizationData(null, null, null, 0, "dockerid09876", "ecs1234567890", invalidBootId, awsCloudData, 0L, UtilizationConfig.EMPTY_DATA, + KubernetesData.EMPTY_KUBERNETES_DATA); + Map map = data.map(); + Map vendorMap = (Map) map.get("vendors"); + Map ecsMap = (Map) vendorMap.get("ecs"); + String ecsId = (String) ecsMap.get("ecsDockerId"); + + Map dockerMap = (Map) vendorMap.get("docker"); + String dockerId = (String) dockerMap.get("id"); + + Assert.assertEquals("ecs1234567890", ecsId); + Assert.assertEquals("dockerid09876", dockerId); + } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationServiceTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationServiceTest.java index 6d1cd4328c..904f00db86 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationServiceTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/utilization/UtilizationServiceTest.java @@ -79,7 +79,7 @@ public void testDockerCheckDockerConfigEnabled() { UtilizationService utilizationService = Mockito.spy(new UtilizationService()); DockerData dockerData = mock(DockerData.class); - when(dockerData.getDockerContainerId(false)).thenReturn("f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920"); + when(dockerData.getDockerContainerIdFromCGroups(false)).thenReturn("f96c541a87e1376f25461f1386cb60208cea35750eac1e24e11566f078715920"); when(utilizationService.getDockerData()).thenReturn(dockerData); UtilizationData oldData = utilizationService.utilizationData; UtilizationData newData = utilizationService.updateUtilizationData();