Skip to content

Commit

Permalink
Merge pull request #2050 from newrelic/ecs-fargate-update
Browse files Browse the repository at this point in the history
ECS Fargate Docker id updates
  • Loading branch information
jtduffy authored Sep 25, 2024
2 parents 73e1b81 + 7271111 commit 8cb150e
Showing 7 changed files with 101 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -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,44 +51,33 @@ 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";

private static final Pattern VALID_CONTAINER_ID = Pattern.compile("^[0-9a-f]{64}$");
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;
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> ip, Integer logicalProcessorCt, String dockerId, String bootId,
CloudData cloudData, Future<Long> ramFuture, UtilizationConfig configData, KubernetesData kubernetesData) {
this(host, fullHost, ip, logicalProcessorCt, dockerId, bootId, cloudData, getTotalRamMibFromFuture(ramFuture), configData, kubernetesData);
public UtilizationData(String host, String fullHost, ArrayList<String> ip, Integer logicalProcessorCt, String dockerId, String ecsFargateDockerContainerId,
String bootId, CloudData cloudData, Future<Long> 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<String> ip, Integer logicalProcessorCt, String dockerId, String bootId,
CloudData cloudData, Long ram, UtilizationConfig configData, KubernetesData kubernetesData) {
public UtilizationData(String host, String fullHost, ArrayList<String> 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<String, Object> map() {
vendors.put(DOCKER, docker);
}

if (ecsFargateDockerContainerId != null) {
Map<String, String> ecs = new HashMap<>();
ecs.put(ECS_ID_KEY, ecsFargateDockerContainerId);
vendors.put(ECS, ecs);
}

if (!vendors.isEmpty()) {
data.put(VENDORS_KEY, vendors);
}
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ public class UtilizationService extends AbstractService {
private final ArrayList<String> ipAddress;
private final String bootId;
private final String dockerContainerId;
private final String ecsFargateDockerContainerId;
private final int processorCount;
private final Future<Long> 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<UtilizationData> {
@@ -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);
}
}

Original file line number Diff line number Diff line change
@@ -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) {
Original file line number Diff line number Diff line change
@@ -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()));
}

Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> map = data.map();
Map<String, Object> vendorMap = (Map<String, Object>) map.get("vendors");
Map<String, Object> ecsMap = (Map<String, Object>) 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<String, Object> map = data.map();
Map<String, Object> vendorMap = (Map<String, Object>) map.get("vendors");
Map<String, Object> ecsMap = (Map<String, Object>) vendorMap.get("ecs");
String ecsId = (String) ecsMap.get("ecsDockerId");

Map<String, Object> dockerMap = (Map<String, Object>) vendorMap.get("docker");
String dockerId = (String) dockerMap.get("id");

Assert.assertEquals("ecs1234567890", ecsId);
Assert.assertEquals("dockerid09876", dockerId);
}
}
Original file line number Diff line number Diff line change
@@ -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();

0 comments on commit 8cb150e

Please sign in to comment.